gaoluyang
3 天以前 d39e4f7d839167c16425086dfa2f8747bccfdff9
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseInvoiceServiceImpl.java
@@ -3,12 +3,19 @@
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.ErpPurchaseInvoiceItemReceiptRespVO;
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.ErpPurchaseOrderDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderItemDO;
import cn.iocoder.yudao.module.mes.api.itemreceipt.MesWmItemReceiptApi;
import cn.iocoder.yudao.module.mes.api.itemreceipt.dto.MesWmItemReceiptLineRespDTO;
import cn.iocoder.yudao.module.mes.api.itemreceipt.dto.MesWmItemReceiptRespDTO;
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.redis.no.ErpNoRedisDAO;
@@ -23,6 +30,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -56,7 +64,20 @@
    private ErpPurchaseOrderService purchaseOrderService;
    @Resource
    private MesWmItemReceiptApi mesWmItemReceiptApi;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    /**
     * MES 入库单已完成状态,见 MesWmItemReceiptStatusEnum.FINISHED
     */
    private static final Integer ITEM_RECEIPT_STATUS_FINISHED = 4;
    /**
     * MES 入库单已取消状态,见 MesWmItemReceiptStatusEnum.CANCELED
     */
    private static final Integer ITEM_RECEIPT_STATUS_CANCELED = 5;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -73,6 +94,7 @@
        // 2.1 插入来票(创建后即为有效状态,无需审核)
        ErpPurchaseInvoiceDO invoice = BeanUtils.toBean(createReqVO, ErpPurchaseInvoiceDO.class).setNo(no);
        applyItemReceiptCode(invoice);
        invoiceMapper.insert(invoice);
        // 2.2 绑定发票附件
@@ -90,6 +112,65 @@
            throw exception(PURCHASE_INVOICE_CREATE_FAIL_SUPPLIER_MISMATCH);
        }
        reqVO.setSupplierId(purchaseOrder.getSupplierId()); // 供应商以采购订单为准
        // 校验入库单,并回填入库单号快照
        validateItemReceipt(reqVO, purchaseOrder);
    }
    /**
     * 校验来票绑定的入库单:必须存在、已完成入库、属于当前采购订单和供应商
     */
    private void validateItemReceipt(ErpPurchaseInvoiceSaveReqVO reqVO, ErpPurchaseOrderDO purchaseOrder) {
        if (reqVO.getItemReceiptId() == null) {
            return; // 存量/未绑定时允许为空
        }
        MesWmItemReceiptRespDTO receipt = mesWmItemReceiptApi.getItemReceipt(reqVO.getItemReceiptId());
        if (receipt == null) {
            throw new ServiceException(PURCHASE_INVOICE_CREATE_FAIL_RECEIPT_NOT_EXISTS.getCode(),
                    "来票关联的入库单不存在:入库单 ID=" + reqVO.getItemReceiptId()
                            + "。请重新选择该采购订单下的入库单;若列表中没有可选项,请先在 MES 完成入库。");
        }
        if (!ITEM_RECEIPT_STATUS_FINISHED.equals(receipt.getStatus())) {
            throw new ServiceException(PURCHASE_INVOICE_CREATE_FAIL_RECEIPT_NOT_FINISHED.getCode(),
                    "来票关联的入库单[" + receipt.getCode() + "]当前状态为「" + itemReceiptStatusName(receipt.getStatus())
                            + "」,只有「已完成」的入库单才能来票。请先在 MES 完成入库后再选择该入库单。");
        }
        if (ObjectUtil.notEqual(receipt.getPurchaseOrderId(), reqVO.getPurchaseOrderId())) {
            throw new ServiceException(PURCHASE_INVOICE_CREATE_FAIL_RECEIPT_ORDER_MISMATCH.getCode(),
                    "所选入库单[" + receipt.getCode() + "]属于采购订单[" + StrUtil.blankToDefault(receipt.getPurchaseOrderCode(), "ID=" + receipt.getPurchaseOrderId())
                            + "],与当前选择的采购订单[" + purchaseOrder.getNo() + "]不一致。请重新选择该采购订单下的入库单,或先核对采购订单号。");
        }
        if (ObjectUtil.notEqual(receipt.getVendorId(), purchaseOrder.getSupplierId())) {
            throw new ServiceException(PURCHASE_INVOICE_CREATE_FAIL_RECEIPT_SUPPLIER_MISMATCH.getCode(),
                    "所选入库单[" + receipt.getCode() + "]的供应商(ID=" + receipt.getVendorId()
                            + ")与采购订单[" + purchaseOrder.getNo() + "]的供应商(ID=" + purchaseOrder.getSupplierId()
                            + ")不一致。请核对入库单来源,或改选该采购订单下的入库单。");
        }
    }
    /**
     * 保存入库单号快照,列表与导出直接展示,避免联表
     */
    private void applyItemReceiptCode(ErpPurchaseInvoiceDO invoice) {
        if (invoice.getItemReceiptId() == null) {
            invoice.setItemReceiptCode(null);
            return;
        }
        MesWmItemReceiptRespDTO receipt = mesWmItemReceiptApi.getItemReceipt(invoice.getItemReceiptId());
        invoice.setItemReceiptCode(receipt != null ? receipt.getCode() : null);
    }
    private String itemReceiptStatusName(Integer status) {
        if (status == null) {
            return "未知";
        }
        return switch (status) {
            case 0 -> "草稿";
            case 2 -> "待上架";
            case 3 -> "待执行入库";
            case 4 -> "已完成";
            case 5 -> "已取消";
            default -> "状态" + status;
        };
    }
    private void validatePurchaseInvoicePriceExceedsLimit(ErpPurchaseInvoiceSaveReqVO reqVO) {
@@ -101,10 +182,67 @@
        }
        BigDecimal remainPrice = purchaseOrder.getTotalPrice().subtract(
                getSumValue(invoices, ErpPurchaseInvoiceDO::getPrice, BigDecimal::add, BigDecimal.ZERO));
        // 2. 校验金额是否超过
        // 2. 校验金额是否超过采购订单可开票金额
        if (reqVO.getPrice().compareTo(remainPrice) > 0) {
            throw exception(PURCHASE_INVOICE_CREATE_FAIL_PRICE_EXCEEDS_LIMIT, remainPrice);
        }
        // 3. 绑定了入库单时,还要校验不超过入库单可开票金额(入库金额 - 该入库单已来票金额)
        if (reqVO.getItemReceiptId() != null) {
            MesWmItemReceiptRespDTO receipt = mesWmItemReceiptApi.getItemReceipt(reqVO.getItemReceiptId());
            BigDecimal receiptTotalPrice = calcItemReceiptTotalPrice(receipt);
            BigDecimal invoicedPrice = getItemReceiptInvoicedPrice(reqVO.getItemReceiptId(), reqVO.getId());
            BigDecimal receiptRemainPrice = receiptTotalPrice.subtract(invoicedPrice);
            if (reqVO.getPrice().compareTo(receiptRemainPrice) > 0) {
                throw new ServiceException(PURCHASE_INVOICE_CREATE_FAIL_RECEIPT_PRICE_EXCEEDS_LIMIT.getCode(),
                        "本次来票金额 " + reqVO.getPrice().stripTrailingZeros().toPlainString() + " 元,超过所选入库单["
                                + (receipt != null ? receipt.getCode() : "ID=" + reqVO.getItemReceiptId()) + "]的可开票金额 "
                                + receiptRemainPrice.stripTrailingZeros().toPlainString() + " 元(入库金额 "
                                + receiptTotalPrice.stripTrailingZeros().toPlainString() + " 元,已来票 "
                                + invoicedPrice.stripTrailingZeros().toPlainString()
                                + " 元)。请调整来票金额,或改选其他入库单;如需按整单开票,请先核对入库金额。");
            }
        }
    }
    /**
     * 计算入库单金额:优先取入库行金额快照,其次按行单价 × 入库数量,最后按采购订单行单价兜底
     */
    private BigDecimal calcItemReceiptTotalPrice(MesWmItemReceiptRespDTO receipt) {
        if (receipt == null || CollUtil.isEmpty(receipt.getLines())) {
            return BigDecimal.ZERO;
        }
        Map<Long, BigDecimal> orderItemPriceMap = purchaseOrderService
                .getPurchaseOrderItemListByOrderId(receipt.getPurchaseOrderId()).stream()
                .filter(item -> item.getProductPrice() != null)
                .collect(Collectors.toMap(ErpPurchaseOrderItemDO::getId, ErpPurchaseOrderItemDO::getProductPrice, (a, b) -> a));
        BigDecimal total = BigDecimal.ZERO;
        for (MesWmItemReceiptLineRespDTO line : receipt.getLines()) {
            BigDecimal amount = line.getTotalPrice();
            if (amount == null && line.getPrice() != null && line.getReceivedQuantity() != null) {
                amount = line.getPrice().multiply(line.getReceivedQuantity());
            }
            if (amount == null && line.getPurchaseOrderItemId() != null && line.getReceivedQuantity() != null) {
                BigDecimal orderItemPrice = orderItemPriceMap.get(line.getPurchaseOrderItemId());
                if (orderItemPrice != null) {
                    amount = orderItemPrice.multiply(line.getReceivedQuantity());
                }
            }
            if (amount != null) {
                total = total.add(amount);
            }
        }
        return total;
    }
    /**
     * 计算某入库单已来票金额(可按来票编号排除自身)
     */
    private BigDecimal getItemReceiptInvoicedPrice(Long itemReceiptId, Long excludeInvoiceId) {
        List<ErpPurchaseInvoiceDO> invoices = invoiceMapper.selectListByItemReceiptId(itemReceiptId);
        if (excludeInvoiceId != null) {
            invoices.removeIf(invoice -> Objects.equals(invoice.getId(), excludeInvoiceId));
        }
        return getSumValue(invoices, ErpPurchaseInvoiceDO::getPrice, BigDecimal::add, BigDecimal.ZERO);
    }
    @Override
@@ -119,6 +257,7 @@
        // 2. 更新来票
        ErpPurchaseInvoiceDO updateObj = BeanUtils.toBean(updateReqVO, ErpPurchaseInvoiceDO.class);
        applyItemReceiptCode(updateObj);
        invoiceMapper.updateById(updateObj);
        // 3. 增量更新发票附件
@@ -188,4 +327,28 @@
                Collectors.mapping(ErpPurchaseInvoiceDO::getPrice, Collectors.reducing(BigDecimal.ZERO, BigDecimal::add))));
    }
    @Override
    public List<ErpPurchaseInvoiceItemReceiptRespVO> getItemReceiptListForInvoice(Long purchaseOrderId, Long excludeInvoiceId) {
        if (purchaseOrderId == null) {
            return List.of();
        }
        List<MesWmItemReceiptRespDTO> receipts = mesWmItemReceiptApi.getItemReceiptListByPurchaseOrderId(purchaseOrderId);
        return receipts.stream()
                .filter(receipt -> !ITEM_RECEIPT_STATUS_CANCELED.equals(receipt.getStatus()))
                .map(receipt -> {
                    BigDecimal totalPrice = calcItemReceiptTotalPrice(receipt);
                    BigDecimal invoicedPrice = getItemReceiptInvoicedPrice(receipt.getId(), excludeInvoiceId);
                    return new ErpPurchaseInvoiceItemReceiptRespVO()
                            .setId(receipt.getId())
                            .setCode(receipt.getCode())
                            .setName(receipt.getName())
                            .setReceiptDate(receipt.getReceiptDate())
                            .setStatus(receipt.getStatus())
                            .setTotalPrice(totalPrice)
                            .setInvoicedPrice(invoicedPrice)
                            .setRemainInvoicePrice(totalPrice.subtract(invoicedPrice));
                })
                .toList();
    }
}