gaoluyang
6 天以前 2eeeb70e4733e7fc41635c3aa2143f13cbc43d58
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseInvoiceServiceImpl.java
@@ -3,15 +3,22 @@
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;
@@ -19,11 +26,16 @@
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;
@@ -47,6 +59,9 @@
    private ErpPurchaseInvoiceMapper invoiceMapper;
    @Resource
    private ErpPurchaseInvoiceOrderMapper invoiceOrderMapper;
    @Resource
    private ErpFinancePaymentMapper financePaymentMapper;
    @Resource
@@ -56,55 +71,140 @@
    private ErpPurchaseOrderService purchaseOrderService;
    @Resource
    private SrmSupplierApi srmSupplierApi;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createPurchaseInvoice(ErpPurchaseInvoiceSaveReqVO createReqVO) {
        // 1.1 校验关联数据存在
        validateRelationDataExists(createReqVO);
        // 1.2 校验可开票金额超过上限
        validatePurchaseInvoicePriceExceedsLimit(createReqVO);
        // 1.3 生成来票编号
        String no = noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_INVOICE_NO_PREFIX);
        // 1.1 校验采购订单、分摊明细(分摊合计 = 来票金额)
        List<ErpPurchaseOrderDO> 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 插入来票(创建后即为有效状态,无需审核)
        // 2.1 插入来票(创建后即为有效状态,无需审核);主订单、供应商取分摊明细第一张
        ErpPurchaseInvoiceDO invoice = BeanUtils.toBean(createReqVO, ErpPurchaseInvoiceDO.class).setNo(no);
        invoiceMapper.insert(invoice);
        // 2.2 绑定发票附件
        // 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();
    }
    private void validateRelationDataExists(ErpPurchaseInvoiceSaveReqVO reqVO) {
        // 校验采购订单,必须审核通过
        ErpPurchaseOrderDO purchaseOrder = purchaseOrderService.validatePurchaseOrder(reqVO.getPurchaseOrderId());
        // 校验供应商与采购订单一致(防御脏数据:来票必须属于该采购订单的供应商)
        if (reqVO.getSupplierId() != null && ObjectUtil.notEqual(purchaseOrder.getSupplierId(), reqVO.getSupplierId())) {
            throw exception(PURCHASE_INVOICE_CREATE_FAIL_SUPPLIER_MISMATCH);
    /**
     * 校验采购订单分摊明细,并把主订单、供应商回写到 reqVO 中
     *
     * @return 分摊明细中的采购订单列表(顺序与明细一致)
     */
    private List<ErpPurchaseOrderDO> validateOrderAllocations(ErpPurchaseInvoiceSaveReqVO reqVO) {
        List<ErpPurchaseInvoiceOrderReqVO> allocations = reqVO.getOrderAllocations();
        if (CollUtil.isEmpty(allocations)) {
            throw exception(PURCHASE_INVOICE_ALLOCATION_EMPTY);
        }
        reqVO.setSupplierId(purchaseOrder.getSupplierId()); // 供应商以采购订单为准
        // 1. 校验订单不重复、必须审核通过
        Set<Long> orderIds = new HashSet<>();
        List<ErpPurchaseOrderDO> 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 validatePurchaseInvoicePriceExceedsLimit(ErpPurchaseInvoiceSaveReqVO reqVO) {
        // 1. 计算剩余可开票金额,不包括 reqVO 自身(所有有效来票都占用额度,防止累计来票超过采购订单金额)
        ErpPurchaseOrderDO purchaseOrder = purchaseOrderService.getPurchaseOrder(reqVO.getPurchaseOrderId());
        List<ErpPurchaseInvoiceDO> invoices = invoiceMapper.selectListByOrderId(reqVO.getPurchaseOrderId());
        if (reqVO.getId() != null) {
            invoices.removeIf(invoice -> ObjectUtil.equal(invoice.getId(), reqVO.getId()));
    /**
     * 校验分摊明细中的采购订单是否属于同一供应商:来票只能对应一个供应商
     */
    private void validateSameSupplier(List<ErpPurchaseOrderDO> 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())));
            }
        }
        BigDecimal remainPrice = purchaseOrder.getTotalPrice().subtract(
                getSumValue(invoices, ErpPurchaseInvoiceDO::getPrice, BigDecimal::add, BigDecimal.ZERO));
        // 2. 校验金额是否超过
        if (reqVO.getPrice().compareTo(remainPrice) > 0) {
            throw exception(PURCHASE_INVOICE_CREATE_FAIL_PRICE_EXCEEDS_LIMIT, remainPrice);
    }
    private String getSupplierName(Long supplierId) {
        SrmSupplierRespDTO supplier = srmSupplierApi.getSupplier(supplierId).getCheckedData();
        return supplier == null ? "ID=" + supplierId : supplier.getName();
    }
    private void validateAllocationPriceExceedsLimit(ErpPurchaseInvoiceSaveReqVO reqVO, List<ErpPurchaseOrderDO> orders) {
        List<ErpPurchaseInvoiceOrderReqVO> allocations = reqVO.getOrderAllocations();
        for (int i = 0; i < allocations.size(); i++) {
            ErpPurchaseInvoiceOrderReqVO allocation = allocations.get(i);
            ErpPurchaseOrderDO order = orders.get(i);
            // 1. 该订单其他来票已占用的金额(编辑时排除本张来票自身的分摊)
            List<ErpPurchaseInvoiceOrderDO> 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<ErpPurchaseInvoiceOrderDO> 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
@@ -113,15 +213,19 @@
        Assert.notNull(updateReqVO.getId(), "来票编号不能为空");
        // 1.1 校验存在
        validatePurchaseInvoiceExists(updateReqVO.getId());
        // 1.2 校验关联数据 + 可开票金额
        validateRelationDataExists(updateReqVO);
        validatePurchaseInvoicePriceExceedsLimit(updateReqVO);
        // 1.2 校验采购订单、分摊明细、金额上限
        List<ErpPurchaseOrderDO> orders = validateOrderAllocations(updateReqVO);
        validateAllocationPriceExceedsLimit(updateReqVO, orders);
        // 2. 更新来票
        ErpPurchaseInvoiceDO updateObj = BeanUtils.toBean(updateReqVO, ErpPurchaseInvoiceDO.class);
        invoiceMapper.updateById(updateObj);
        // 3. 增量更新发票附件
        // 3. 重建采购订单分摊明细
        invoiceOrderMapper.deleteByInvoiceId(updateReqVO.getId());
        insertOrderAllocations(updateReqVO.getId(), updateReqVO);
        // 4. 增量更新发票附件
        storageAttachmentApi.updateAttachments("file", PURCHASE_INVOICE_RECORD_TYPE, updateReqVO.getId(), updateReqVO.getBlobIds());
    }
@@ -140,6 +244,8 @@
        storageAttachmentApi.deleteAttachmentsByRecord(PURCHASE_INVOICE_RECORD_TYPE, id);
        // 2.2 删除来票
        invoiceMapper.deleteById(id);
        // 2.3 删除采购订单分摊明细(释放订单的已来票金额)
        invoiceOrderMapper.deleteByInvoiceId(id);
    }
    private ErpPurchaseInvoiceDO validatePurchaseInvoiceExists(Long id) {
@@ -170,7 +276,11 @@
    @Override
    public List<ErpPurchaseInvoiceDO> getPurchaseInvoiceListByOrderId(Long orderId) {
        return invoiceMapper.selectListByOrderId(orderId);
        List<ErpPurchaseInvoiceOrderDO> allocations = invoiceOrderMapper.selectListByPurchaseOrderId(orderId);
        if (CollUtil.isEmpty(allocations)) {
            return Collections.emptyList();
        }
        return invoiceMapper.selectByIds(convertSet(allocations, ErpPurchaseInvoiceOrderDO::getInvoiceId));
    }
    @Override
@@ -179,13 +289,38 @@
    }
    @Override
    public Map<Long, BigDecimal> getPurchaseInvoicePriceSumMapByOrderIds(Collection<Long> orderIds) {
    public Map<Long, BigDecimal> getPurchaseInvoicePriceSumMapByOrderIds(Collection<Long> orderIds, Long excludeInvoiceId) {
        if (CollUtil.isEmpty(orderIds)) {
            return Collections.emptyMap();
        }
        List<ErpPurchaseInvoiceDO> invoices = invoiceMapper.selectListByOrderIds(orderIds);
        return invoices.stream().collect(Collectors.groupingBy(ErpPurchaseInvoiceDO::getPurchaseOrderId,
                Collectors.mapping(ErpPurchaseInvoiceDO::getPrice, Collectors.reducing(BigDecimal.ZERO, BigDecimal::add))));
        List<ErpPurchaseInvoiceOrderDO> 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<ErpPurchaseInvoiceOrderDO> getPurchaseInvoiceOrderListByInvoiceId(Long invoiceId) {
        return invoiceOrderMapper.selectListByInvoiceId(invoiceId);
    }
    @Override
    public List<ErpPurchaseInvoiceOrderDO> getPurchaseInvoiceOrderListByInvoiceIds(Collection<Long> invoiceIds) {
        if (CollUtil.isEmpty(invoiceIds)) {
            return Collections.emptyList();
        }
        return invoiceOrderMapper.selectListByInvoiceIds(invoiceIds);
    }
    @Override
    public List<ErpPurchaseInvoiceOrderDO> getPurchaseInvoiceOrderListByPurchaseOrderIds(Collection<Long> purchaseOrderIds) {
        if (CollUtil.isEmpty(purchaseOrderIds)) {
            return Collections.emptyList();
        }
        return invoiceOrderMapper.selectListByPurchaseOrderIds(purchaseOrderIds);
    }
}