package cn.iocoder.yudao.module.erp.service.purchase; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.invoice.ErpPurchaseInvoiceOrderReqVO; import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.invoice.ErpPurchaseInvoicePageReqVO; import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.invoice.ErpPurchaseInvoiceSaveReqVO; import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInvoiceDO; import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInvoiceOrderDO; import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderDO; import cn.iocoder.yudao.module.erp.dal.mysql.finance.ErpFinancePaymentMapper; import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseInvoiceMapper; import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseInvoiceOrderMapper; import cn.iocoder.yudao.module.erp.dal.redis.no.ErpNoRedisDAO; import cn.iocoder.yudao.module.srm.api.supplier.SrmSupplierApi; import cn.iocoder.yudao.module.srm.api.supplier.dto.SrmSupplierRespDTO; import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getSumValue; import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.*; /** * ERP 采购来票 Service 实现类 * * @author 超级管理员 */ @Service @Validated public class ErpPurchaseInvoiceServiceImpl implements ErpPurchaseInvoiceService { /** * 来票附件的业务记录类型,对应 {@link cn.iocoder.yudao.module.system.enums.storage.StorageRecordTypeEnum#ERP_PURCHASE_INVOICE} */ public static final String PURCHASE_INVOICE_RECORD_TYPE = "erp_purchase_invoice"; @Resource private ErpPurchaseInvoiceMapper invoiceMapper; @Resource private ErpPurchaseInvoiceOrderMapper invoiceOrderMapper; @Resource private ErpFinancePaymentMapper financePaymentMapper; @Resource private ErpNoRedisDAO noRedisDAO; @Resource private ErpPurchaseOrderService purchaseOrderService; @Resource private SrmSupplierApi srmSupplierApi; @Resource private StorageAttachmentApi storageAttachmentApi; @Override @Transactional(rollbackFor = Exception.class) public Long createPurchaseInvoice(ErpPurchaseInvoiceSaveReqVO createReqVO) { // 1.1 校验采购订单、分摊明细(分摊合计 = 来票金额) List orders = validateOrderAllocations(createReqVO); // 1.2 校验每张订单的分摊金额不超过该订单剩余未开票金额 validateAllocationPriceExceedsLimit(createReqVO, orders); // 1.3 确定来票编号(前端填写则用自定义单号,留空则自动生成),并校验唯一性 String customNo = StrUtil.trim(createReqVO.getNo()); String no = StrUtil.isNotBlank(customNo) ? customNo : noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_INVOICE_NO_PREFIX); if (invoiceMapper.selectByNo(no) != null) { throw exception(PURCHASE_INVOICE_NO_EXISTS); } // 2.1 插入来票(创建后即为有效状态,无需审核);主订单、供应商取分摊明细第一张 ErpPurchaseInvoiceDO invoice = BeanUtils.toBean(createReqVO, ErpPurchaseInvoiceDO.class).setNo(no); invoiceMapper.insert(invoice); // 2.2 插入采购订单分摊明细 insertOrderAllocations(invoice.getId(), createReqVO); // 2.3 绑定发票附件 if (CollUtil.isNotEmpty(createReqVO.getBlobIds())) { storageAttachmentApi.bindAttachments("file", PURCHASE_INVOICE_RECORD_TYPE, invoice.getId(), createReqVO.getBlobIds()); } return invoice.getId(); } /** * 校验采购订单分摊明细,并把主订单、供应商回写到 reqVO 中 * * @return 分摊明细中的采购订单列表(顺序与明细一致) */ private List validateOrderAllocations(ErpPurchaseInvoiceSaveReqVO reqVO) { List allocations = reqVO.getOrderAllocations(); if (CollUtil.isEmpty(allocations)) { throw exception(PURCHASE_INVOICE_ALLOCATION_EMPTY); } // 1. 校验订单不重复、必须审核通过 Set orderIds = new HashSet<>(); List orders = new ArrayList<>(); for (int i = 0; i < allocations.size(); i++) { ErpPurchaseInvoiceOrderReqVO allocation = allocations.get(i); if (!orderIds.add(allocation.getPurchaseOrderId())) { throw new ServiceException(PURCHASE_INVOICE_ALLOCATION_ORDER_DUPLICATE.getCode(), StrUtil.format("第 {} 行的采购订单重复选择,同一张来票内每张采购订单只能出现一次,请删除重复行后重试", i + 1)); } if (allocation.getPrice() == null || allocation.getPrice().compareTo(BigDecimal.ZERO) < 0) { throw new ServiceException(PURCHASE_INVOICE_ALLOCATION_PRICE_INVALID.getCode(), StrUtil.format("第 {} 行的分摊金额不能为空且不能小于 0,请填写该订单本次分摊到的来票金额", i + 1)); } orders.add(purchaseOrderService.validatePurchaseOrder(allocation.getPurchaseOrderId())); } // 1.2 校验所有采购订单必须是同一供应商(来票对应一个供应商) validateSameSupplier(orders); // 2. 校验分摊合计不能大于来票金额(允许小于:未分摊部分可后续编辑来票时再补充采购订单) BigDecimal allocationTotal = allocations.stream().map(ErpPurchaseInvoiceOrderReqVO::getPrice) .reduce(BigDecimal.ZERO, BigDecimal::add); if (allocationTotal.compareTo(reqVO.getPrice()) > 0) { throw new ServiceException(PURCHASE_INVOICE_ALLOCATION_SUM_MISMATCH.getCode(), StrUtil.format("来票金额 {} 元,各订单分摊合计 {} 元,分摊合计不能大于来票金额(超出 {} 元)。" + "请减少订单的分摊金额,或提高来票金额后重试;未分摊的部分可以留待以后再编辑来票补充采购订单。", reqVO.getPrice().stripTrailingZeros().toPlainString(), allocationTotal.stripTrailingZeros().toPlainString(), allocationTotal.subtract(reqVO.getPrice()).stripTrailingZeros().toPlainString())); } // 3. 主订单、供应商取分摊明细第一张(一张来票涉及多个供应商时,列表/付款沿用第一家) reqVO.setPurchaseOrderId(allocations.get(0).getPurchaseOrderId()); reqVO.setSupplierId(orders.get(0).getSupplierId()); return orders; } /** * 校验分摊明细中的采购订单是否属于同一供应商:来票只能对应一个供应商 */ private void validateSameSupplier(List orders) { ErpPurchaseOrderDO firstOrder = orders.get(0); for (int i = 1; i < orders.size(); i++) { ErpPurchaseOrderDO order = orders.get(i); if (ObjectUtil.notEqual(order.getSupplierId(), firstOrder.getSupplierId())) { throw new ServiceException(PURCHASE_INVOICE_ALLOCATION_SUPPLIER_MISMATCH.getCode(), StrUtil.format("第 {} 行的采购订单[{}]供应商为[{}],与第 1 行采购订单[{}]的供应商[{}]不一致。" + "一张来票只能关联同一供应商的采购订单,请移除该订单,或改选该供应商的其他采购订单后重试。", i + 1, order.getNo(), getSupplierName(order.getSupplierId()), firstOrder.getNo(), getSupplierName(firstOrder.getSupplierId()))); } } } private String getSupplierName(Long supplierId) { SrmSupplierRespDTO supplier = srmSupplierApi.getSupplier(supplierId).getCheckedData(); return supplier == null ? "ID=" + supplierId : supplier.getName(); } private void validateAllocationPriceExceedsLimit(ErpPurchaseInvoiceSaveReqVO reqVO, List orders) { List allocations = reqVO.getOrderAllocations(); for (int i = 0; i < allocations.size(); i++) { ErpPurchaseInvoiceOrderReqVO allocation = allocations.get(i); ErpPurchaseOrderDO order = orders.get(i); // 1. 该订单其他来票已占用的金额(编辑时排除本张来票自身的分摊) List existed = invoiceOrderMapper.selectListByPurchaseOrderId(order.getId()); if (reqVO.getId() != null) { existed.removeIf(row -> ObjectUtil.equal(row.getInvoiceId(), reqVO.getId())); } BigDecimal invoicedPrice = getSumValue(existed, ErpPurchaseInvoiceOrderDO::getPrice, BigDecimal::add, BigDecimal.ZERO); BigDecimal remainPrice = order.getTotalPrice().subtract(invoicedPrice); // 2. 校验金额是否超过 if (allocation.getPrice().compareTo(remainPrice) > 0) { throw new ServiceException(PURCHASE_INVOICE_CREATE_FAIL_PRICE_EXCEEDS_LIMIT.getCode(), StrUtil.format("采购订单[{}]的分摊金额超出该订单未开票金额:订单金额 {} 元,其他来票已占用 {} 元," + "剩余未开票 {} 元,本次分摊 {} 元。请调整该订单的分摊金额,或改选其他采购订单后重试。", order.getNo(), order.getTotalPrice().stripTrailingZeros().toPlainString(), invoicedPrice.stripTrailingZeros().toPlainString(), remainPrice.stripTrailingZeros().toPlainString(), allocation.getPrice().stripTrailingZeros().toPlainString())); } } } private void insertOrderAllocations(Long invoiceId, ErpPurchaseInvoiceSaveReqVO reqVO) { List rows = new ArrayList<>(); for (int i = 0; i < reqVO.getOrderAllocations().size(); i++) { ErpPurchaseInvoiceOrderReqVO allocation = reqVO.getOrderAllocations().get(i); rows.add(ErpPurchaseInvoiceOrderDO.builder().invoiceId(invoiceId) .purchaseOrderId(allocation.getPurchaseOrderId()) .price(allocation.getPrice()).sort(i).build()); } rows.forEach(invoiceOrderMapper::insert); } @Override @Transactional(rollbackFor = Exception.class) public void updatePurchaseInvoice(ErpPurchaseInvoiceSaveReqVO updateReqVO) { Assert.notNull(updateReqVO.getId(), "来票编号不能为空"); // 1.1 校验存在 validatePurchaseInvoiceExists(updateReqVO.getId()); // 1.2 校验采购订单、分摊明细、金额上限 List orders = validateOrderAllocations(updateReqVO); validateAllocationPriceExceedsLimit(updateReqVO, orders); // 2. 更新来票 ErpPurchaseInvoiceDO updateObj = BeanUtils.toBean(updateReqVO, ErpPurchaseInvoiceDO.class); invoiceMapper.updateById(updateObj); // 3. 重建采购订单分摊明细 invoiceOrderMapper.deleteByInvoiceId(updateReqVO.getId()); insertOrderAllocations(updateReqVO.getId(), updateReqVO); // 4. 增量更新发票附件 storageAttachmentApi.updateAttachments("file", PURCHASE_INVOICE_RECORD_TYPE, updateReqVO.getId(), updateReqVO.getBlobIds()); } @Override @Transactional(rollbackFor = Exception.class) public void deletePurchaseInvoice(Long id) { // 1.1 校验存在 validatePurchaseInvoiceExists(id); // 1.2 已有有效付款单关联的来票,不允许删除(保持来票与付款的关联完整性) Map paidMap = financePaymentMapper.selectPaymentPriceMapByInvoiceId(List.of(id)); if (paidMap.getOrDefault(id, BigDecimal.ZERO).compareTo(BigDecimal.ZERO) > 0) { throw exception(PURCHASE_INVOICE_DELETE_FAIL_HAS_PAYMENT); } // 2.1 删除发票附件 storageAttachmentApi.deleteAttachmentsByRecord(PURCHASE_INVOICE_RECORD_TYPE, id); // 2.2 删除来票 invoiceMapper.deleteById(id); // 2.3 删除采购订单分摊明细(释放订单的已来票金额) invoiceOrderMapper.deleteByInvoiceId(id); } private ErpPurchaseInvoiceDO validatePurchaseInvoiceExists(Long id) { ErpPurchaseInvoiceDO invoice = invoiceMapper.selectById(id); if (invoice == null) { throw exception(PURCHASE_INVOICE_NOT_EXISTS); } return invoice; } @Override public ErpPurchaseInvoiceDO getPurchaseInvoice(Long id) { return invoiceMapper.selectById(id); } @Override public List getPurchaseInvoiceList(Collection ids) { if (CollUtil.isEmpty(ids)) { return List.of(); } return invoiceMapper.selectByIds(ids); } @Override public PageResult getPurchaseInvoicePage(ErpPurchaseInvoicePageReqVO pageReqVO) { return invoiceMapper.selectPage(pageReqVO); } @Override public List getPurchaseInvoiceListByOrderId(Long orderId) { List allocations = invoiceOrderMapper.selectListByPurchaseOrderId(orderId); if (CollUtil.isEmpty(allocations)) { return Collections.emptyList(); } return invoiceMapper.selectByIds(convertSet(allocations, ErpPurchaseInvoiceOrderDO::getInvoiceId)); } @Override public List getPurchaseInvoiceListBySupplierId(Long supplierId) { return invoiceMapper.selectListBySupplierId(supplierId); } @Override public Map getPurchaseInvoicePriceSumMapByOrderIds(Collection orderIds, Long excludeInvoiceId) { if (CollUtil.isEmpty(orderIds)) { return Collections.emptyMap(); } List allocations = invoiceOrderMapper.selectListByPurchaseOrderIds(orderIds); if (excludeInvoiceId != null) { // 编辑来票时把本张来票已分摊的金额退回订单 allocations.removeIf(row -> ObjectUtil.equal(row.getInvoiceId(), excludeInvoiceId)); } return allocations.stream().collect(Collectors.groupingBy(ErpPurchaseInvoiceOrderDO::getPurchaseOrderId, Collectors.mapping(ErpPurchaseInvoiceOrderDO::getPrice, Collectors.reducing(BigDecimal.ZERO, BigDecimal::add)))); } @Override public List getPurchaseInvoiceOrderListByInvoiceId(Long invoiceId) { return invoiceOrderMapper.selectListByInvoiceId(invoiceId); } @Override public List getPurchaseInvoiceOrderListByInvoiceIds(Collection invoiceIds) { if (CollUtil.isEmpty(invoiceIds)) { return Collections.emptyList(); } return invoiceOrderMapper.selectListByInvoiceIds(invoiceIds); } @Override public List getPurchaseInvoiceOrderListByPurchaseOrderIds(Collection purchaseOrderIds) { if (CollUtil.isEmpty(purchaseOrderIds)) { return Collections.emptyList(); } return invoiceOrderMapper.selectListByPurchaseOrderIds(purchaseOrderIds); } }