yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java
@@ -7,6 +7,7 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.operatelog.core.util.OperateLogUtils;
import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
@@ -36,6 +37,7 @@
import cn.iocoder.yudao.module.srm.api.supplier.dto.SrmSupplierRespDTO;
import cn.iocoder.yudao.module.mes.api.arrivalnotice.MesArrivalNoticeApi;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import com.mzt.logapi.starter.annotation.LogRecord;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.repository.ProcessDefinition;
@@ -54,6 +56,7 @@
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.erp.enums.LogRecordConstants.*;
/**
 * ERP 采购订单 Service 实现类
@@ -78,6 +81,8 @@
    private ErpPurchaseOrderMapper purchaseOrderMapper;
    @Resource
    private ErpPurchaseOrderItemMapper purchaseOrderItemMapper;
    @Resource
    private ErpSupplierContractService supplierContractService;
    @Resource
    private ErpNoRedisDAO noRedisDAO;
@@ -122,6 +127,8 @@
        if (createReqVO.getAccountId() != null) {
            accountService.validateAccount(createReqVO.getAccountId());
        }
        // 1.3.1 校验并解析关联合同(订单必须与合同属于同一供应商)
        String contractNo = resolveContractNo(createReqVO.getContractId(), createReqVO.getSupplierId());
        // 1.4 生成订单号,并校验唯一性
        String no = noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_ORDER_NO_PREFIX);
        if (purchaseOrderMapper.selectByNo(no) != null) {
@@ -131,7 +138,8 @@
        // 2.1 插入订单(采购订单创建后为未审核状态,需要提交审批)
        ErpPurchaseOrderDO purchaseOrder = BeanUtils.toBean(createReqVO, ErpPurchaseOrderDO.class, in -> in
                .setNo(no).setStatus(ErpAuditStatus.PROCESS.getStatus())
                .setInCount(BigDecimal.ZERO).setInStatus(ErpPurchaseOrderInStatusEnum.NOT_IN.getStatus()));
                .setInCount(BigDecimal.ZERO).setInStatus(ErpPurchaseOrderInStatusEnum.NOT_IN.getStatus())
                .setContractNo(contractNo));
        calculateTotalPrice(purchaseOrder, purchaseOrderItems);
        purchaseOrderMapper.insert(purchaseOrder);
        // 2.2 插入订单项
@@ -164,9 +172,12 @@
        }
        // 1.4 校验订单项的有效性
        List<ErpPurchaseOrderItemDO> purchaseOrderItems = validatePurchaseOrderItems(updateReqVO.getItems());
        // 1.5 校验并解析关联合同
        String contractNo = resolveContractNo(updateReqVO.getContractId(), updateReqVO.getSupplierId());
        // 2.1 更新订单
        ErpPurchaseOrderDO updateObj = BeanUtils.toBean(updateReqVO, ErpPurchaseOrderDO.class);
        updateObj.setContractNo(contractNo);
        calculateTotalPrice(updateObj, purchaseOrderItems);
        purchaseOrderMapper.updateById(updateObj);
        // 2.2 更新订单项
@@ -177,6 +188,8 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    @LogRecord(type = ERP_PURCHASE_ORDER_TYPE, subType = ERP_PURCHASE_ORDER_IMPORT_SUB_TYPE, bizNo = "0",
            success = ERP_PURCHASE_ORDER_IMPORT_SUCCESS)
    public ErpPurchaseOrderImportRespVO importPurchaseOrderList(List<ErpPurchaseOrderImportExcelVO> importList,
                                                               Boolean directApprove) {
        ErpPurchaseOrderImportRespVO respVO = ErpPurchaseOrderImportRespVO.builder()
@@ -185,6 +198,8 @@
                .failureList(new LinkedHashMap<>())
                .build();
        if (CollUtil.isEmpty(importList)) {
            // 记录导入结果汇总,供 @LogRecord 的 success 文案引用
            OperateLogUtils.putImportSummary(respVO.getCreateList(), respVO.getUpdateList(), respVO.getFailureList());
            return respVO;
        }
@@ -226,6 +241,8 @@
                respVO.getFailureList().put(label, resolveImportErrorMessage(ex));
            }
        }
        // 记录导入结果汇总,供 @LogRecord 的 success 文案引用
        OperateLogUtils.putImportSummary(respVO.getCreateList(), respVO.getUpdateList(), respVO.getFailureList());
        return respVO;
    }
@@ -340,9 +357,12 @@
    private void calculateTotalPrice(ErpPurchaseOrderDO purchaseOrder, List<ErpPurchaseOrderItemDO> purchaseOrderItems) {
        purchaseOrder.setTotalCount(getSumValue(purchaseOrderItems, ErpPurchaseOrderItemDO::getCount, BigDecimal::add));
        purchaseOrder.setTotalProductPrice(getSumValue(purchaseOrderItems, ErpPurchaseOrderItemDO::getTotalPrice, BigDecimal::add, BigDecimal.ZERO));
        purchaseOrder.setTotalTaxPrice(getSumValue(purchaseOrderItems, ErpPurchaseOrderItemDO::getTaxPrice, BigDecimal::add, BigDecimal.ZERO));
        purchaseOrder.setTotalPrice(purchaseOrder.getTotalProductPrice().add(purchaseOrder.getTotalTaxPrice()));
        // 明细的 totalPrice 是「数量 × 含税单价」,即含税总价;不含税金额 = 含税总价 − 税额
        BigDecimal totalPriceWithTax = getSumValue(purchaseOrderItems, ErpPurchaseOrderItemDO::getTotalPrice, BigDecimal::add, BigDecimal.ZERO);
        BigDecimal totalTaxPrice = getSumValue(purchaseOrderItems, ErpPurchaseOrderItemDO::getTaxPrice, BigDecimal::add, BigDecimal.ZERO);
        purchaseOrder.setTotalTaxPrice(totalTaxPrice);
        purchaseOrder.setTotalPrice(totalPriceWithTax);
        purchaseOrder.setTotalProductPrice(totalPriceWithTax.subtract(totalTaxPrice));
        // 计算优惠价格
        if (purchaseOrder.getDiscountPercent() == null) {
            purchaseOrder.setDiscountPercent(BigDecimal.ZERO);
@@ -390,6 +410,16 @@
        }
        // 2. 创建 BPM 流程实例
        // 全局免审批:业务校验通过后直接执行审批通过逻辑,无需流程模型。
        if (processInstanceApi.isGlobalAutoApprove()) {
            purchaseOrderMapper.updateById(new ErpPurchaseOrderDO()
                    .setId(id)
                    .setStatus(ErpAuditStatus.APPROVING.getStatus())
                    .setProcessInstanceId(""));
            updatePurchaseOrderAuditStatus(id, cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum.APPROVE.getStatus());
            return;
        }
        String processInstanceId = processInstanceApi.createProcessInstance(userId,
                new BpmProcessInstanceCreateReqDTO()
                        .setProcessDefinitionKey(processDefinitionKey)
@@ -486,13 +516,12 @@
    private List<ErpPurchaseOrderItemDO> validatePurchaseOrderItems(List<ErpPurchaseOrderSaveReqVO.Item> list) {
        // 转化为 ErpPurchaseOrderItemDO 列表(产品验证已移除)
        return convertList(list, o -> BeanUtils.toBean(o, ErpPurchaseOrderItemDO.class, item -> {
            // 产品单价为含税单价:行总价 = 数量 × 含税单价,税额再从含税总价里倒算
            item.setTotalPrice(MoneyUtils.priceMultiply(item.getProductPrice(), item.getCount()));
            if (item.getTotalPrice() == null) {
                return;
            }
            if (item.getTaxPercent() != null) {
                item.setTaxPrice(MoneyUtils.priceMultiplyPercent(item.getTotalPrice(), item.getTaxPercent()));
            }
            item.setTaxPrice(MoneyUtils.priceCalculateTaxIncluded(item.getTotalPrice(), item.getTaxPercent()));
        }));
    }
@@ -580,6 +609,21 @@
        });
    }
    /**
     * 校验关联合同,返回冗余的合同编码;未关联合同时返回 null
     */
    private String resolveContractNo(Long contractId, Long supplierId) {
        if (contractId == null) {
            return null;
        }
        cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpSupplierContractDO contract =
                supplierContractService.validateContractExists(contractId);
        if (!Objects.equals(contract.getSupplierId(), supplierId)) {
            throw exception(SUPPLIER_CONTRACT_SUPPLIER_MISMATCH);
        }
        return contract.getCode();
    }
    private ErpPurchaseOrderDO validatePurchaseOrderExists(Long id) {
        ErpPurchaseOrderDO purchaseOrder = purchaseOrderMapper.selectById(id);
        if (purchaseOrder == null) {