package cn.iocoder.yudao.module.erp.service.sale; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; 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.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; import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum; import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService; import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService; import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi; import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO; import cn.iocoder.yudao.module.crm.api.contract.CrmContractApi; import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderPageReqVO; import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderSaveReqVO; import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderImportExcelVO; import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderImportRespVO; import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOrderDO; import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOrderItemDO; import cn.iocoder.yudao.module.erp.dal.mysql.sale.ErpSaleOrderItemMapper; import cn.iocoder.yudao.module.erp.dal.mysql.sale.ErpSaleOrderMapper; import cn.iocoder.yudao.module.erp.dal.redis.no.ErpNoRedisDAO; import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus; import cn.iocoder.yudao.module.erp.api.sale.enums.ErpSaleOrderOutStatusEnum; import cn.iocoder.yudao.module.erp.service.finance.ErpAccountService; import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi; import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO; import cn.iocoder.yudao.module.mes.api.mps.MesProMpsApi; import cn.iocoder.yudao.module.mes.api.mps.dto.MesProMpsCreateReqDTO; import cn.iocoder.yudao.module.mes.api.mps.dto.MesProMpsRespDTO; import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import jakarta.annotation.Resource; import org.flowable.engine.repository.ProcessDefinition; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.*; /** * ERP 销售订单 Service 实现类 * * @author 超级管理员 */ @Service @Validated @Slf4j public class ErpSaleOrderServiceImpl implements ErpSaleOrderService { /** * MPS 状态:已取消(对应 MES 模块 MesProMpsStatusEnum.CANCELED 的值 3) */ private static final Integer MPS_STATUS_CANCELED = 3; /** * BPM 销售订单审批分类编码 */ private static final String SALE_ORDER_APPROVE_CATEGORY_CODE = "sale_order_approve"; /** * BPM 销售订单审批流程定义 Key(提交后进入协同办公审批) */ private static final String SALE_ORDER_APPROVE_PROCESS_DEFINITION_KEY = "sale_order_approve"; @Resource private ErpSaleOrderMapper saleOrderMapper; @Resource private ErpSaleOrderItemMapper saleOrderItemMapper; @Resource private ErpNoRedisDAO noRedisDAO; @Resource private CrmCustomerApi customerApi; @Resource private CrmContractApi contractApi; @Resource private ErpAccountService accountService; @Resource private AdminUserApi adminUserApi; @Resource private MdmItemApi mdmItemApi; @Resource private MesProMpsApi mpsApi; @Resource private StorageAttachmentApi storageAttachmentApi; @Resource private BpmProcessInstanceApi processInstanceApi; @Resource private BpmCategoryService bpmCategoryService; @Resource private BpmProcessDefinitionService bpmProcessDefinitionService; @Override @Transactional(rollbackFor = Exception.class) public Long createSaleOrder(ErpSaleOrderSaveReqVO createReqVO) { // 1.1 校验订单项的有效性 List saleOrderItems = validateSaleOrderItems(createReqVO.getItems()); // 1.2 校验客户(使用 CRM 客户 API) customerApi.validateCustomer(createReqVO.getCustomerId()); // 1.3 校验结算账户 if (createReqVO.getAccountId() != null) { accountService.validateAccount(createReqVO.getAccountId()); } // 1.4 校验销售人员 if (createReqVO.getSaleUserId() != null) { adminUserApi.validateUser(createReqVO.getSaleUserId()); } // 1.5 生成订单号,并校验唯一性 String no = noRedisDAO.generate(ErpNoRedisDAO.SALE_ORDER_NO_PREFIX); if (saleOrderMapper.selectByNo(no) != null) { throw exception(SALE_ORDER_NO_EXISTS); } // 2.1 插入订单(创建后为未提交状态,需手动提交审批到协同办公) ErpSaleOrderDO saleOrder = BeanUtils.toBean(createReqVO, ErpSaleOrderDO.class, in -> in .setNo(no).setStatus(ErpAuditStatus.DRAFT.getStatus()) .setDepositPrice(ObjectUtil.defaultIfNull(createReqVO.getDepositPrice(), BigDecimal.ZERO)) .setOutCount(BigDecimal.ZERO).setOutStatus(ErpSaleOrderOutStatusEnum.NOT_OUT.getStatus())); calculateTotalPrice(saleOrder, saleOrderItems); saleOrderMapper.insert(saleOrder); // 2.2 插入订单项 saleOrderItems.forEach(o -> o.setOrderId(saleOrder.getId())); saleOrderItemMapper.insertBatch(saleOrderItems); // 2.3 绑定附件 storageAttachmentApi.bindAttachments("file", "erp_sale_order", saleOrder.getId(), createReqVO.getBlobIds()); return saleOrder.getId(); } @Override @Transactional(rollbackFor = Exception.class) public void updateSaleOrder(ErpSaleOrderSaveReqVO updateReqVO) { // 1.1 校验存在 ErpSaleOrderDO saleOrder = validateSaleOrderExists(updateReqVO.getId()); // 1.2 校验状态:只有未提交或已驳回的销售订单才能修改 if (!ErpAuditStatus.DRAFT.getStatus().equals(saleOrder.getStatus()) && !ErpAuditStatus.REJECT.getStatus().equals(saleOrder.getStatus())) { throw exception(SALE_ORDER_UPDATE_FAIL_APPROVE, saleOrder.getNo()); } // 1.3 校验客户(使用 CRM 客户 API) customerApi.validateCustomer(updateReqVO.getCustomerId()); // 1.3 校验结算账户 if (updateReqVO.getAccountId() != null) { accountService.validateAccount(updateReqVO.getAccountId()); } // 1.4 校验销售人员 if (updateReqVO.getSaleUserId() != null) { adminUserApi.validateUser(updateReqVO.getSaleUserId()); } // 1.5 校验订单项的有效性 List saleOrderItems = validateSaleOrderItems(updateReqVO.getItems()); // 2.1 更新订单 ErpSaleOrderDO updateObj = BeanUtils.toBean(updateReqVO, ErpSaleOrderDO.class); updateObj.setDepositPrice(ObjectUtil.defaultIfNull(updateReqVO.getDepositPrice(), BigDecimal.ZERO)); calculateTotalPrice(updateObj, saleOrderItems); saleOrderMapper.updateById(updateObj); // 2.2 更新订单项 updateSaleOrderItemList(updateReqVO.getId(), saleOrderItems); // 2.3 更新附件 storageAttachmentApi.updateAttachments("file", "erp_sale_order", updateReqVO.getId(), updateReqVO.getBlobIds()); } @Override @Transactional(rollbackFor = Exception.class) public ErpSaleOrderImportRespVO importSaleOrderList(List importList) { ErpSaleOrderImportRespVO respVO = ErpSaleOrderImportRespVO.builder() .createList(new ArrayList<>()) .updateList(new ArrayList<>()) .failureList(new LinkedHashMap<>()) .build(); if (CollUtil.isEmpty(importList)) { return respVO; } // 1. 预热“客户名称 -> 客户编号”映射(客户主数据在 CRM 模块) Map customerIdMap = new HashMap<>(); List customers = customerApi.getCustomerSimpleList(); if (CollUtil.isNotEmpty(customers)) { customers.forEach(customer -> { if (StrUtil.isNotBlank(customer.getName())) { customerIdMap.putIfAbsent(customer.getName().trim(), customer.getId()); } }); } // 2. 按“销售单编号”分组:同编号多行 = 一张订单的多条明细;编号为空则每行独立成单 Map> orderGroups = new LinkedHashMap<>(); for (int i = 0; i < importList.size(); i++) { ErpSaleOrderImportExcelVO row = importList.get(i); if (row == null || (StrUtil.isBlank(row.getCustomerName()) && StrUtil.isBlank(row.getProductCode()) && StrUtil.isBlank(row.getNo()))) { continue; // 跳过空行 } String key = StrUtil.isNotBlank(row.getNo()) ? row.getNo().trim() : "__row_" + (i + 2); orderGroups.computeIfAbsent(key, k -> new ArrayList<>()).add(row); } // 3. 逐个订单导入(单张订单失败不影响其它) for (Map.Entry> entry : orderGroups.entrySet()) { List rows = entry.getValue(); String label = StrUtil.isNotBlank(rows.get(0).getNo()) ? rows.get(0).getNo().trim() : "第 " + entry.getKey().substring("__row_".length()) + " 行"; try { ErpSaleOrderDO saleOrder = createSaleOrderByImport(rows, customerIdMap); respVO.getCreateList().add(saleOrder.getNo()); } catch (Exception ex) { respVO.getFailureList().put(label, resolveImportErrorMessage(ex)); } } return respVO; } /** * 依据导入数据创建一张销售订单(含明细)。 * * 注意:此处不调用 submitSaleOrder,因此不会创建 BPM 工作流;订单状态固定为“未提交(0)”, * 可在列表中继续编辑或提交审批。 */ private ErpSaleOrderDO createSaleOrderByImport(List rows, Map customerIdMap) { // 1. 客户(取该组首行) ErpSaleOrderImportExcelVO head = rows.get(0); if (StrUtil.isBlank(head.getCustomerName())) { throw new IllegalArgumentException("客户名称不能为空"); } Long customerId = customerIdMap.get(head.getCustomerName().trim()); if (customerId == null) { throw new IllegalArgumentException("客户不存在:" + head.getCustomerName()); } customerApi.validateCustomer(customerId); // 2. 明细 List items = new ArrayList<>(); for (ErpSaleOrderImportExcelVO row : rows) { if (StrUtil.isBlank(row.getProductCode())) { throw new IllegalArgumentException("产品编码不能为空"); } CommonResult itemResult = mdmItemApi.getItemByCode(row.getProductCode().trim()); MdmItemRespDTO item = itemResult == null ? null : itemResult.getData(); if (item == null) { throw new IllegalArgumentException("产品不存在:" + row.getProductCode()); } if (row.getCount() == null) { throw new IllegalArgumentException("产品[" + row.getProductCode() + "]的数量不能为空"); } if (item.getUnitMeasureId() == null) { throw new IllegalArgumentException("产品[" + row.getProductCode() + "]未配置计量单位,请先在物料中维护"); } ErpSaleOrderSaveReqVO.Item itemVO = new ErpSaleOrderSaveReqVO.Item(); itemVO.setProductId(item.getId()); itemVO.setProductUnitId(item.getUnitMeasureId()); // 单价:未填写时取物料的销售价(与页面选产品后的默认值一致) itemVO.setProductPrice(row.getProductPrice() != null ? row.getProductPrice() : ObjectUtil.defaultIfNull(item.getSalesPrice(), BigDecimal.ZERO)); itemVO.setCount(row.getCount()); itemVO.setTaxPercent(row.getTaxPercent()); itemVO.setNeedProduction(parseNeedProduction(row.getNeedProduction())); itemVO.setRemark(row.getRemark()); items.add(itemVO); } // 3. 单号:填了就用(校验唯一),没填自动生成 String no = StrUtil.isNotBlank(head.getNo()) ? head.getNo().trim() : noRedisDAO.generate(ErpNoRedisDAO.SALE_ORDER_NO_PREFIX); if (saleOrderMapper.selectByNo(no) != null) { throw new IllegalArgumentException("销售单编号已存在:" + no); } // 4. 组装主表:导入后固定为“未提交(0)”状态 ErpSaleOrderDO saleOrder = new ErpSaleOrderDO(); saleOrder.setNo(no); saleOrder.setCustomerId(customerId); saleOrder.setContractNo(head.getContractNo()); saleOrder.setOrderTime(parseImportOrderTime(head.getOrderTime())); saleOrder.setRemark(head.getRemark()); saleOrder.setStatus(ErpAuditStatus.DRAFT.getStatus()); saleOrder.setDepositPrice(BigDecimal.ZERO); saleOrder.setOutCount(BigDecimal.ZERO); saleOrder.setOutStatus(ErpSaleOrderOutStatusEnum.NOT_OUT.getStatus()); List itemDOs = validateSaleOrderItems(items); calculateTotalPrice(saleOrder, itemDOs); saleOrderMapper.insert(saleOrder); itemDOs.forEach(item -> item.setOrderId(saleOrder.getId())); saleOrderItemMapper.insertBatch(itemDOs); return saleOrder; } /** * 解析“是否需要生产”:留空默认“是(1)”,填“否/0/不需要”则为“否(0)” */ private Integer parseNeedProduction(String text) { if (StrUtil.isBlank(text)) { return 1; // 与页面新增明细的默认值一致 } String value = text.trim(); boolean no = "否".equals(value) || "0".equals(value) || "不需要".equals(value) || "false".equalsIgnoreCase(value) || "no".equalsIgnoreCase(value); return no ? 0 : 1; } /** * 解析导入的下单时间,兼容多种常见格式;为空取当前时间 */ private LocalDateTime parseImportOrderTime(String text) { if (StrUtil.isBlank(text)) { return LocalDateTime.now(); } String value = text.trim(); for (String pattern : new String[]{"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy/MM/dd HH:mm:ss"}) { try { return LocalDateTime.parse(value, DateTimeFormatter.ofPattern(pattern)); } catch (Exception ignored) { // 继续尝试下一个格式 } } for (String pattern : new String[]{"yyyy-MM-dd", "yyyy/M/d"}) { try { return LocalDate.parse(value, DateTimeFormatter.ofPattern(pattern)).atStartOfDay(); } catch (Exception ignored) { // 继续尝试下一个格式 } } throw new IllegalArgumentException("下单时间格式不正确:" + text + ",示例 2026-01-01 10:00:00"); } /** * 提取导入失败原因(ServiceException 取业务提示) */ private String resolveImportErrorMessage(Exception ex) { if (ex instanceof cn.iocoder.yudao.framework.common.exception.ServiceException serviceException) { return serviceException.getMessage(); } return StrUtil.blankToDefault(ex.getMessage(), "导入失败"); } private void calculateTotalPrice(ErpSaleOrderDO saleOrder, List saleOrderItems) { saleOrder.setTotalCount(getSumValue(saleOrderItems, ErpSaleOrderItemDO::getCount, BigDecimal::add)); saleOrder.setTotalProductPrice(getSumValue(saleOrderItems, ErpSaleOrderItemDO::getTotalPrice, BigDecimal::add, BigDecimal.ZERO)); saleOrder.setTotalTaxPrice(getSumValue(saleOrderItems, ErpSaleOrderItemDO::getTaxPrice, BigDecimal::add, BigDecimal.ZERO)); saleOrder.setTotalPrice(saleOrder.getTotalProductPrice().add(saleOrder.getTotalTaxPrice())); // 计算优惠价格 if (saleOrder.getDiscountPercent() == null) { saleOrder.setDiscountPercent(BigDecimal.ZERO); } saleOrder.setDiscountPrice(MoneyUtils.priceMultiplyPercent(saleOrder.getTotalPrice(), saleOrder.getDiscountPercent())); saleOrder.setTotalPrice(saleOrder.getTotalPrice().subtract(saleOrder.getDiscountPrice())); } @Override @Transactional(rollbackFor = Exception.class) public void submitSaleOrder(Long id, String processDefinitionKey, Long userId) { // 1. 校验存在 ErpSaleOrderDO saleOrder = validateSaleOrderExists(id); // 2. 校验状态:只有未提交或已驳回的销售订单才能提交 if (!ErpAuditStatus.DRAFT.getStatus().equals(saleOrder.getStatus()) && !ErpAuditStatus.REJECT.getStatus().equals(saleOrder.getStatus())) { throw exception(SALE_ORDER_SUBMIT_FAIL_STATUS, saleOrder.getNo()); } // 3. 校验订单项不能为空 List items = saleOrderItemMapper.selectListByOrderId(id); if (CollUtil.isEmpty(items)) { throw exception(SALE_ORDER_SUBMIT_FAIL_ITEM_EMPTY, saleOrder.getNo()); } // 4. 创建 BPM 流程实例(审批在协同办公完成) String processInstanceId = processInstanceApi.createProcessInstance(userId, new BpmProcessInstanceCreateReqDTO() .setProcessDefinitionKey(processDefinitionKey) .setBusinessKey(String.valueOf(id))); // 5. 更新销售订单状态为审批中,记录流程实例编号 saleOrderMapper.updateById(new ErpSaleOrderDO() .setId(id) .setStatus(ErpAuditStatus.APPROVING.getStatus()) .setProcessInstanceId(processInstanceId)); } @Override @Transactional(rollbackFor = Exception.class) public void updateSaleOrderAuditStatus(Long id, Integer bpmResult) { // 1. 校验存在 ErpSaleOrderDO saleOrder = validateSaleOrderExists(id); if (!ErpAuditStatus.APPROVING.getStatus().equals(saleOrder.getStatus())) { log.warn("[updateSaleOrderAuditStatus] 销售订单({}) 不处于审批中状态,跳过", id); return; } // 2. 根据审批结果更新状态 Integer newStatus = convertBpmResultToAuditStatus(bpmResult); if (newStatus != null) { saleOrderMapper.updateById(new ErpSaleOrderDO() .setId(id).setStatus(newStatus)); } // 3. 审核通过时,为需要生产的明细创建 MPS if (ErpAuditStatus.APPROVE.getStatus().equals(newStatus)) { createMpsForSaleOrder(id, saleOrder); } } @Override public List> getSaleOrderApproveProcessDefinitionList() { // 1. 校验分类和流程定义是否存在 validateSaleOrderApproveCategoryAndProcessDefinition(); // 2. 获取分类下的流程定义信息 List definitionInfoList = bpmProcessDefinitionService .getProcessDefinitionInfoListByCategory(SALE_ORDER_APPROVE_CATEGORY_CODE); if (CollUtil.isEmpty(definitionInfoList)) { return Collections.emptyList(); } // 3. 遍历获取流程定义详情,过滤激活状态,保留最新版本 Map latestVersionMap = new HashMap<>(); for (BpmProcessDefinitionInfoDO info : definitionInfoList) { ProcessDefinition pd = bpmProcessDefinitionService.getProcessDefinition(info.getProcessDefinitionId()); if (pd == null || pd.isSuspended()) { continue; } ProcessDefinition existing = latestVersionMap.get(pd.getKey()); if (existing == null || pd.getVersion() > existing.getVersion()) { latestVersionMap.put(pd.getKey(), pd); } } // 4. 返回流程定义列表 List> result = new ArrayList<>(); for (ProcessDefinition pd : latestVersionMap.values()) { Map item = new HashMap<>(); item.put("id", pd.getId()); item.put("key", pd.getKey()); item.put("name", pd.getName()); result.add(item); } return result; } /** * 校验销售订单审批分类是否存在 */ private void validateSaleOrderApproveCategoryAndProcessDefinition() { // 1. 校验分类是否存在 List categories = bpmCategoryService.getCategoryListByCode( Collections.singletonList(SALE_ORDER_APPROVE_CATEGORY_CODE)); if (CollUtil.isEmpty(categories)) { throw exception(SALE_ORDER_BPM_CATEGORY_NOT_EXISTS); } } /** * 转换 BPM 审批结果为审核状态 */ private Integer convertBpmResultToAuditStatus(Integer bpmResult) { if (BpmTaskStatusEnum.APPROVE.getStatus().equals(bpmResult)) { return ErpAuditStatus.APPROVE.getStatus(); } else if (BpmTaskStatusEnum.REJECT.getStatus().equals(bpmResult)) { return ErpAuditStatus.REJECT.getStatus(); } else if (BpmTaskStatusEnum.CANCEL.getStatus().equals(bpmResult)) { return ErpAuditStatus.CANCEL.getStatus(); } return null; } /** * 为销售订单中需要生产的明细创建 MPS * * @param orderId 销售订单编号 * @param saleOrder 销售订单 */ private void createMpsForSaleOrder(Long orderId, ErpSaleOrderDO saleOrder) { List items = saleOrderItemMapper.selectListByOrderId(orderId); if (CollUtil.isEmpty(items)) { return; } int mpsCount = 0; for (ErpSaleOrderItemDO item : items) { if (item.getNeedProduction() == null || item.getNeedProduction() != 1) { continue; } // 避免审批/反审批重复操作导致同一个销售订单号产生多条 MPS MesProMpsRespDTO existingMps = mpsApi.getMpsBySaleOrderItemId(item.getId()); if (existingMps != null && ObjectUtil.notEqual(existingMps.getStatus(), MPS_STATUS_CANCELED)) { log.info("[createMpsForSaleOrder] 销售订单明细({}) 已存在 MPS({}),状态={},跳过创建", item.getId(), existingMps.getId(), existingMps.getStatus()); continue; } try { Long mpsId = createMpsForSaleOrderItem(saleOrder, item); if (mpsId != null) { mpsCount++; // 更新销售订单明细的 MPS 关联 saleOrderItemMapper.updateById(new ErpSaleOrderItemDO() .setId(item.getId()) .setMpsId(mpsId)); } } catch (Exception e) { log.error("[createMpsForSaleOrder] 创建 MPS 失败,saleOrderItemId={}", item.getId(), e); } } log.info("[createMpsForSaleOrder] 销售订单({}) 审核完成,创建 MPS {} 条", orderId, mpsCount); } /** * 为销售订单明细创建 MPS */ private Long createMpsForSaleOrderItem(ErpSaleOrderDO saleOrder, ErpSaleOrderItemDO item) { // 获取 MDM 物料信息(用于获取产品名称) String productName = null; try { MdmItemRespDTO mdmItem = mdmItemApi.getItem(item.getProductId()).getCheckedData(); if (mdmItem != null) { productName = mdmItem.getName(); } } catch (Exception e) { log.warn("[createMpsForSaleOrderItem] 获取 MDM 物料信息失败,productId={}", item.getProductId(), e); } MesProMpsCreateReqDTO createReqDTO = new MesProMpsCreateReqDTO(); createReqDTO.setSaleOrderId(saleOrder.getId()); createReqDTO.setSaleOrderNo(saleOrder.getNo()); createReqDTO.setSaleOrderItemId(item.getId()); createReqDTO.setMdmItemId(item.getProductId()); createReqDTO.setProductName(productName); createReqDTO.setQuantity(item.getCount()); createReqDTO.setClientId(saleOrder.getCustomerId()); createReqDTO.setRequestDate(formatRequestDate(saleOrder.getOrderTime())); createReqDTO.setRemark("自动生成,来自销售订单:" + saleOrder.getNo()); Long mpsId = mpsApi.createMps(createReqDTO); log.info("[createMpsForSaleOrderItem] MPS 创建成功,mpsId={}, saleOrderItemId={}", mpsId, item.getId()); return mpsId; } /** * 格式化需求日期 */ private String formatRequestDate(LocalDateTime dateTime) { if (dateTime == null) { return null; } return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); } @Override @Transactional(rollbackFor = Exception.class) public void cancelSaleOrder(Long id) { // 1.1 校验存在 ErpSaleOrderDO saleOrder = validateSaleOrderExists(id); // 1.2 校验状态:只有已审核的订单才能作废 if (!ErpAuditStatus.APPROVE.getStatus().equals(saleOrder.getStatus())) { throw exception(SALE_ORDER_CANCEL_FAIL_NOT_APPROVE, saleOrder.getNo()); } // 1.3 存在销售出库单,无法作废 BigDecimal outCount = saleOrder.getOutCount() != null ? saleOrder.getOutCount() : BigDecimal.ZERO; if (outCount.compareTo(BigDecimal.ZERO) > 0) { throw exception(SALE_ORDER_CANCEL_FAIL_EXISTS_OUT); } // 2. 更新状态为已作废 int updateCount = saleOrderMapper.updateByIdAndStatus(id, saleOrder.getStatus(), new ErpSaleOrderDO().setStatus(ErpAuditStatus.CANCEL.getStatus())); if (updateCount == 0) { throw exception(SALE_ORDER_CANCEL_FAIL); } // 3. 同步更新关联合同状态为已作废 if (saleOrder.getContractId() != null) { contractApi.updateContractStatus(saleOrder.getContractId(), 50).getCheckedData(); // 50 = 已作废 } } @Override @Transactional(rollbackFor = Exception.class) public void enableSaleOrder(Long id) { // 1.1 校验存在 ErpSaleOrderDO saleOrder = validateSaleOrderExists(id); // 1.2 校验状态:只有已作废的订单才能重新启用 if (!ErpAuditStatus.CANCEL.getStatus().equals(saleOrder.getStatus())) { throw exception(SALE_ORDER_ENABLE_FAIL_NOT_CANCEL, saleOrder.getNo()); } // 2. 更新状态为已审核 int updateCount = saleOrderMapper.updateByIdAndStatus(id, saleOrder.getStatus(), new ErpSaleOrderDO().setStatus(ErpAuditStatus.APPROVE.getStatus())); if (updateCount == 0) { throw exception(SALE_ORDER_ENABLE_FAIL); } // 3. 同步更新关联合同状态为审核通过 if (saleOrder.getContractId() != null) { contractApi.updateContractStatus(saleOrder.getContractId(), 20).getCheckedData(); // 20 = 审核通过 } } private List validateSaleOrderItems(List list) { // 转化为 ErpSaleOrderItemDO 列表(产品验证已移除) return convertList(list, o -> BeanUtils.toBean(o, ErpSaleOrderItemDO.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())); } })); } private void updateSaleOrderItemList(Long id, List newList) { // 第一步,对比新老数据,获得添加、修改、删除的列表 List oldList = saleOrderItemMapper.selectListByOrderId(id); List> diffList = diffList(oldList, newList, // id 不同,就认为是不同的记录 (oldVal, newVal) -> oldVal.getId().equals(newVal.getId())); // 第二步,批量添加、修改、删除 if (CollUtil.isNotEmpty(diffList.get(0))) { diffList.get(0).forEach(o -> o.setOrderId(id)); saleOrderItemMapper.insertBatch(diffList.get(0)); } if (CollUtil.isNotEmpty(diffList.get(1))) { saleOrderItemMapper.updateBatch(diffList.get(1)); } if (CollUtil.isNotEmpty(diffList.get(2))) { saleOrderItemMapper.deleteByIds(convertList(diffList.get(2), ErpSaleOrderItemDO::getId)); } } @Override public void updateSaleOrderOutCount(Long id, Map outCountMap) { List orderItems = saleOrderItemMapper.selectListByOrderId(id); // 1. 更新每个销售订单项 orderItems.forEach(item -> { BigDecimal outCount = outCountMap.getOrDefault(item.getId(), BigDecimal.ZERO); if (item.getOutCount().equals(outCount)) { return; } if (outCount.compareTo(item.getCount()) > 0) { throw exception(SALE_ORDER_ITEM_OUT_FAIL_PRODUCT_EXCEED, "产品ID:" + item.getProductId(), item.getCount()); } saleOrderItemMapper.updateById(new ErpSaleOrderItemDO().setId(item.getId()).setOutCount(outCount)); }); // 2. 更新销售订单 BigDecimal totalOutCount = getSumValue(outCountMap.values(), value -> value, BigDecimal::add, BigDecimal.ZERO); saleOrderMapper.updateById(new ErpSaleOrderDO().setId(id).setOutCount(totalOutCount)); } @Override @Transactional(rollbackFor = Exception.class) public void deleteSaleOrder(List ids) { // 1. 校验状态:只有未提交或已驳回的销售订单才能删除 List saleOrders = saleOrderMapper.selectByIds(ids); if (CollUtil.isEmpty(saleOrders)) { return; } saleOrders.forEach(saleOrder -> { if (!ErpAuditStatus.DRAFT.getStatus().equals(saleOrder.getStatus()) && !ErpAuditStatus.REJECT.getStatus().equals(saleOrder.getStatus())) { throw exception(SALE_ORDER_DELETE_FAIL_APPROVE, saleOrder.getNo()); } }); // 2. 遍历删除,并记录操作日志 saleOrders.forEach(saleOrder -> { // 2.1 删除附件 storageAttachmentApi.deleteAttachmentsByRecord("erp_sale_order", saleOrder.getId()); // 2.2 删除订单 saleOrderMapper.deleteById(saleOrder.getId()); // 2.3 删除订单项 saleOrderItemMapper.deleteByOrderId(saleOrder.getId()); }); } private ErpSaleOrderDO validateSaleOrderExists(Long id) { ErpSaleOrderDO saleOrder = saleOrderMapper.selectById(id); if (saleOrder == null) { throw exception(SALE_ORDER_NOT_EXISTS); } return saleOrder; } @Override public ErpSaleOrderDO getSaleOrder(Long id) { return saleOrderMapper.selectById(id); } @Override public ErpSaleOrderDO validateSaleOrder(Long id) { ErpSaleOrderDO saleOrder = validateSaleOrderExists(id); if (ObjectUtil.notEqual(saleOrder.getStatus(), ErpAuditStatus.APPROVE.getStatus())) { throw exception(SALE_ORDER_NOT_APPROVE); } return saleOrder; } @Override public PageResult getSaleOrderPage(ErpSaleOrderPageReqVO pageReqVO) { List permittedCustomerIds = customerApi.getPermittedCustomerIds(); return saleOrderMapper.selectPage(pageReqVO, permittedCustomerIds); } // ==================== 订单项 ==================== @Override public List getSaleOrderItemListByOrderId(Long orderId) { return saleOrderItemMapper.selectListByOrderId(orderId); } @Override public List getSaleOrderItemListByOrderIds(Collection orderIds) { if (CollUtil.isEmpty(orderIds)) { return Collections.emptyList(); } return saleOrderItemMapper.selectListByOrderIds(orderIds); } @Override public void updateSaleOrderOutStatus(Long orderId) { // 1. 获取销售订单 ErpSaleOrderDO order = validateSaleOrderExists(orderId); // 2. 获取销售订单明细 List items = saleOrderItemMapper.selectListByOrderId(orderId); if (CollUtil.isEmpty(items)) { return; } // 3. 计算出库状态 int outStatus = calculateOutStatus(items); // 4. 更新销售订单出库状态 saleOrderMapper.updateById(new ErpSaleOrderDO() .setId(orderId).setOutStatus(outStatus)); } /** * 计算出库状态 * * @param items 销售订单明细列表 * @return 出库状态 */ private int calculateOutStatus(List items) { boolean allOut = true; boolean anyOut = false; for (ErpSaleOrderItemDO item : items) { BigDecimal count = item.getCount() != null ? item.getCount() : BigDecimal.ZERO; BigDecimal outCount = item.getOutCount() != null ? item.getOutCount() : BigDecimal.ZERO; if (outCount.compareTo(BigDecimal.ZERO) > 0) { anyOut = true; } if (outCount.compareTo(count) < 0) { allOut = false; } } if (allOut) { return ErpSaleOrderOutStatusEnum.ALL_OUT.getStatus(); } else if (anyOut) { return ErpSaleOrderOutStatusEnum.PART_OUT.getStatus(); } else { return ErpSaleOrderOutStatusEnum.NOT_OUT.getStatus(); } } }