From 7bd10d34e8a8dc2437ec8551eb511e37634d7306 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期一, 02 十一月 2026 16:51:52 +0800
Subject: [PATCH] feat(mes): 生产计划审批改走协同办公,修复工单领料与产成品入库校验

---
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsServiceImpl.java |  426 ++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 files changed, 403 insertions(+), 23 deletions(-)

diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsServiceImpl.java
index 34ca046..8e0381f 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsServiceImpl.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsServiceImpl.java
@@ -3,20 +3,45 @@
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjUtil;
 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.framework.security.core.util.SecurityFrameworkUtils;
+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.enums.task.BpmProcessInstanceStatusEnum;
+import cn.iocoder.yudao.module.erp.api.purchase.ErpPurchaseOrderApi;
+import cn.iocoder.yudao.module.erp.api.purchase.ErpPurchaseRequestApi;
+import cn.iocoder.yudao.module.erp.api.purchase.dto.ErpPurchaseOrderItemRespDTO;
+import cn.iocoder.yudao.module.erp.api.purchase.dto.ErpPurchaseRequestCreateReqDTO;
 import cn.iocoder.yudao.module.mes.controller.admin.pro.mps.vo.MesProMpsPageReqVO;
 import cn.iocoder.yudao.module.mes.controller.admin.pro.mps.vo.MesProMpsSaveReqVO;
+import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.MesProWorkOrderItemRespVO;
 import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.MesProWorkOrderSaveReqVO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.pro.mps.MesProMpsDO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.pro.mps.MesProMpsMergeItemDO;
 import cn.iocoder.yudao.module.mes.dal.mysql.pro.mps.MesProMpsMapper;
 import cn.iocoder.yudao.module.mes.dal.mysql.pro.mps.MesProMpsMergeItemMapper;
+import cn.iocoder.yudao.module.mes.dal.mysql.pro.feedback.MesProFeedbackMapper;
+import cn.iocoder.yudao.module.mes.dal.mysql.pro.workorder.MesProWorkOrderMapper;
+import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductBomDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
 import cn.iocoder.yudao.module.mes.enums.pro.MesProMpsStatusEnum;
+import cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackStatusEnum;
 import cn.iocoder.yudao.module.mes.enums.pro.MesProWorkOrderSourceTypeEnum;
+import cn.iocoder.yudao.module.mes.enums.pro.MesProWorkOrderStatusEnum;
 import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
+import cn.iocoder.yudao.module.mes.service.pro.material.MesProMaterialRequirementService;
 import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
+import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductService;
+import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProcessService;
 import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
+import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderProcessService;
+import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.process.MesProWorkOrderProcessSaveReqVO;
 import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
 import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
@@ -27,9 +52,15 @@
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
@@ -37,7 +68,7 @@
 /**
  * MES 鐢熶骇涓昏鍒掞紙MPS锛塖ervice 瀹炵幇绫�
  *
- * @author 鑺嬮亾婧愮爜
+ * @author 瓒呯骇绠$悊鍛�
  */
 @Service
 @Validated
@@ -48,6 +79,11 @@
      * MPS 缂栫爜瑙勫垯
      */
     private static final String MPS_CODE_RULE = "MPS";
+
+    /**
+     * MPS 瀹℃壒娴佺▼瀹氫箟 Key锛圔PM 娴佺▼妯″瀷涓厤缃級
+     */
+    private static final String MPS_APPROVE_PROCESS_DEFINITION_KEY = "mes-pro-mps-audit";
 
     @Resource
     private MesProMpsMapper mpsMapper;
@@ -61,6 +97,24 @@
     private MesMdAutoCodeRecordService autoCodeRecordService;
     @Resource
     private CrmCustomerApi customerApi;
+    @Resource
+    private MesProFeedbackMapper feedbackMapper;
+    @Resource
+    private MesProWorkOrderMapper workOrderMapper;
+    @Resource
+    private MesProRouteProductService routeProductService;
+    @Resource
+    private MesProRouteProcessService routeProcessService;
+    @Resource
+    private MesProWorkOrderProcessService workOrderProcessService;
+    @Resource
+    private MesProMaterialRequirementService materialRequirementService;
+    @Resource
+    private ErpPurchaseOrderApi purchaseOrderApi;
+    @Resource
+    private ErpPurchaseRequestApi purchaseRequestApi;
+    @Resource
+    private BpmProcessInstanceApi processInstanceApi;
 
     @Override
     @Transactional(rollbackFor = Exception.class)
@@ -114,6 +168,13 @@
     public Long createMps(MesProMpsSaveReqVO createReqVO) {
         // 鏍¢獙浜у搧瀛樺湪
         itemService.validateItemExists(createReqVO.getProductId());
+        // 璁¢噺鍗曚綅鏈~鍐欐椂锛岄粯璁ゅ彇浜у搧鍗曚綅
+        if (createReqVO.getUnitMeasureId() == null) {
+            MesMdItemDO product = itemService.getItem(createReqVO.getProductId());
+            if (product != null) {
+                createReqVO.setUnitMeasureId(product.getUnitMeasureId());
+            }
+        }
 
         // 濡傛灉鏈~鍐欑紪鐮侊紝鑷姩鐢熸垚
         if (StrUtil.isBlank(createReqVO.getCode())) {
@@ -137,13 +198,13 @@
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void updateMps(MesProMpsSaveReqVO updateReqVO) {
-        // 鏍¢獙瀛樺湪 + 鑽夌鐘舵��
-        validateMpsExistsAndDraft(updateReqVO.getId());
+        // 鏍¢獙瀛樺湪 + 鍙紪杈戠姸鎬侊紙鏈紑濮嬨�佸鏍镐腑锛�
+        MesProMpsDO mps = validateMpsExistsAndEditable(updateReqVO.getId());
 
         // 鏍¢獙缂栫爜鍞竴
         validateMpsCodeUnique(updateReqVO.getId(), updateReqVO.getCode());
 
-        // 鏇存柊
+        // 鏇存柊锛堜粎鑽夌鍙紪杈戯級
         MesProMpsDO updateObj = BeanUtils.toBean(updateReqVO, MesProMpsDO.class);
         mpsMapper.updateById(updateObj);
     }
@@ -151,8 +212,8 @@
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void deleteMps(Long id) {
-        // 鏍¢獙瀛樺湪 + 鑽夌鐘舵��
-        MesProMpsDO mps = validateMpsExistsAndDraft(id);
+        // 鏍¢獙瀛樺湪 + 鍙紪杈戠姸鎬侊紙鏈紑濮嬨�佸鏍镐腑锛�
+        MesProMpsDO mps = validateMpsExistsAndEditable(id);
 
         // 鏍¢獙鏄惁宸插叧鑱斿伐鍗�
         if (mps.getWorkOrderId() != null) {
@@ -165,9 +226,141 @@
 
     @Override
     @Transactional(rollbackFor = Exception.class)
+    public void submitMps(Long id) {
+        MesProMpsDO mps = validateMpsExists(id);
+        // 鍙湁鑽夌鍙互鎻愪氦瀹℃牳锛涘鏍镐腑鐨勮鍒掗渶绛夊緟鍗忓悓鍔炲叕瀹℃壒缁撴灉
+        if (!MesProMpsStatusEnum.DRAFT.getStatus().equals(mps.getStatus())) {
+            throw exception(PRO_MPS_NOT_DRAFT);
+        }
+        // 鍏ㄥ眬鍏嶅鎵癸細涓嶅仛 BPM 娴佺▼锛岀洿鎺ユ寜瀹℃壒閫氳繃澶勭悊
+        if (processInstanceApi.isGlobalAutoApprove()) {
+            mpsMapper.updateById(new MesProMpsDO()
+                    .setId(id)
+                    .setStatus(MesProMpsStatusEnum.AUDITING.getStatus())
+                    .setProcessInstanceId(""));
+            handleBpmProcessResult(id, BpmProcessInstanceStatusEnum.APPROVE.getStatus(), "鍏ㄥ眬鍏嶅鎵硅嚜鍔ㄩ�氳繃", null);
+            return;
+        }
+        // 鍚姩 BPM 娴佺▼锛屽鎵瑰湪鍗忓悓鍔炲叕瀹屾垚锛岀粨鏋滅敱鐩戝惉鍣ㄥ洖鍐�
+        String processInstanceId;
+        try {
+            processInstanceId = processInstanceApi.createProcessInstance(SecurityFrameworkUtils.getLoginUserId(),
+                    new BpmProcessInstanceCreateReqDTO()
+                            .setProcessDefinitionKey(MPS_APPROVE_PROCESS_DEFINITION_KEY)
+                            .setBusinessKey(String.valueOf(id)));
+        } catch (ServiceException ex) {
+            // 娴佺▼鏈儴缃叉椂 BPM 鍙姏銆屾祦绋嬪畾涔変笉瀛樺湪銆嶏紝琛ヤ笂鍙墽琛岀殑鎻愮ず
+            if (StrUtil.contains(ex.getMessage(), "娴佺▼瀹氫箟涓嶅瓨鍦�")) {
+                log.warn("[submitMps] 鐢熶骇璁″垝瀹℃壒娴佺▼({}) 鏈儴缃诧紝mpsId={}", MPS_APPROVE_PROCESS_DEFINITION_KEY, id);
+                throw exception(PRO_MPS_PROCESS_NOT_DEPLOYED);
+            }
+            throw ex;
+        }
+        mpsMapper.updateById(new MesProMpsDO()
+                .setId(id)
+                .setStatus(MesProMpsStatusEnum.AUDITING.getStatus())
+                .setProcessInstanceId(processInstanceId));
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void handleBpmProcessResult(Long id, Integer bpmResult, String reason, Long auditUserId) {
+        // 1. 鏌旀�ф牎楠岋細璁″垝瀛樺湪涓斿浜庡鏍镐腑
+        MesProMpsDO mps = mpsMapper.selectById(id);
+        if (mps == null) {
+            log.warn("[handleBpmProcessResult] 鐢熶骇璁″垝({}) 涓嶅瓨鍦�", id);
+            return;
+        }
+        if (!MesProMpsStatusEnum.AUDITING.getStatus().equals(mps.getStatus())) {
+            log.warn("[handleBpmProcessResult] 鐢熶骇璁″垝({}) 涓嶅浜庡鏍镐腑鐘舵�侊紝褰撳墠鐘舵�侊細{}", id, mps.getStatus());
+            return;
+        }
+
+        // 2. 鏍规嵁瀹℃壒缁撴灉鏇存柊鐘舵��
+        if (BpmProcessInstanceStatusEnum.APPROVE.getStatus().equals(bpmResult)) {
+            mpsMapper.updateById(new MesProMpsDO()
+                    .setId(id)
+                    .setStatus(MesProMpsStatusEnum.APPROVED.getStatus())
+                    .setAuditUserId(auditUserId)
+                    .setAuditTime(LocalDateTime.now())
+                    .setAuditRemark(StrUtil.blankToDefault(reason, "瀹℃牳閫氳繃")));
+        } else if (BpmProcessInstanceStatusEnum.REJECT.getStatus().equals(bpmResult)
+                || BpmProcessInstanceStatusEnum.CANCEL.getStatus().equals(bpmResult)) {
+            mpsMapper.updateById(new MesProMpsDO()
+                    .setId(id)
+                    .setStatus(MesProMpsStatusEnum.DRAFT.getStatus())
+                    .setAuditUserId(auditUserId)
+                    .setAuditTime(LocalDateTime.now())
+                    .setAuditRemark(StrUtil.blankToDefault(reason, "瀹℃壒椹冲洖")));
+        }
+    }
+
+    @Override
+    public List<MesProWorkOrderItemRespVO> getMpsItemList(Long id) {
+        MesProMpsDO mps = validateMpsExists(id);
+        // 鎸変骇鍝� BOM锛堜紭鍏堝伐鑹鸿矾绾� BOM锛夊睍寮�鍙跺瓙鐗╂枡锛屽苟鎸夎鍒掓暟閲忔姌绠楅渶姹傦紱
+        // 璁″垝灏氭湭鐢熸垚宸ュ崟锛屾棤鏉ユ簮涓烘湰璁″垝鐨勫湪閫旈噰璐紝鐩存帴鎸夊彲鐢ㄥ簱瀛樺垽鏂槸鍚﹀厖瓒�
+        return materialRequirementService.getItemListByProduct(mps.getProductId(), mps.getQuantity());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long generatePurchaseRequest(Long id) {
+        MesProMpsDO mps = validateMpsExists(id);
+        if (!MesProMpsStatusEnum.APPROVED.getStatus().equals(mps.getStatus())) {
+            throw exception(PRO_MPS_NOT_APPROVED);
+        }
+        // 1. 鐗╂枡闇�姹備负绌鸿鏄庝骇鍝佹湭缁存姢浠讳綍 BOM锛岀洿鎺ユ彁绀猴紝閬垮厤璇姤鈥滃簱瀛樺厖瓒斥��
+        List<MesProWorkOrderItemRespVO> materialItems = getMpsItemList(id);
+        if (CollUtil.isEmpty(materialItems)) {
+            MesMdItemDO product = itemService.getItem(mps.getProductId());
+            throw exception(PRO_MPS_NO_MATERIAL_BOM, mps.getCode(),
+                    product != null ? product.getName() : String.valueOf(mps.getProductId()));
+        }
+        // 2. 鍙缂哄彛鐗╂枡鐢熸垚閲囪喘鏄庣粏锛氱己鍙� = 闇�姹� - 鍙敤搴撳瓨 - 鍦ㄩ�旈噰璐�
+        List<ErpPurchaseRequestCreateReqDTO.Item> shortageItems = new ArrayList<>();
+        for (MesProWorkOrderItemRespVO item : materialItems) {
+            if (Boolean.TRUE.equals(item.getStockSufficient())) {
+                continue;
+            }
+            BigDecimal supply = nullToZero(item.getStockQuantity())
+                    .add(nullToZero(item.getPurchaseOrderQuantity()));
+            BigDecimal shortage = nullToZero(item.getQuantity()).subtract(supply);
+            if (shortage.compareTo(BigDecimal.ZERO) <= 0) {
+                continue;
+            }
+            MesMdItemDO itemDO = itemService.getItem(item.getItemId());
+            if (itemDO == null) {
+                continue;
+            }
+            shortageItems.add(new ErpPurchaseRequestCreateReqDTO.Item()
+                    .setProductId(item.getItemId())
+                    .setProductUnitId(itemDO.getUnitMeasureId())
+                    .setCount(shortage)
+                    .setDemandTime(mps.getRequestDate() != null ? mps.getRequestDate().toLocalDate() : null)
+                    .setRemark("鐢熶骇璁″垝 " + mps.getCode() + " 搴撳瓨缂哄彛 " + shortage.stripTrailingZeros().toPlainString()));
+        }
+        if (CollUtil.isEmpty(shortageItems)) {
+            throw exception(PRO_MPS_NO_MATERIAL_SHORTAGE);
+        }
+        // 3. 鐢熸垚閲囪喘鐢宠鍗曪紙鑽夌锛屾棤渚涘簲鍟嗭紝鍚庣画鐢遍噰璐汉鍛樿ˉ鍏呭苟鎸夋祦绋嬫彁浜わ級
+        ErpPurchaseRequestCreateReqDTO createReqDTO = new ErpPurchaseRequestCreateReqDTO()
+                .setRequestTime(LocalDateTime.now())
+                .setRequestReason("鐢熶骇璁″垝 " + mps.getCode() + " 杈呮枡閲囪喘")
+                .setRemark("鐢辩敓浜ц鍒掕嚜鍔ㄧ敓鎴愶紝鍏� " + shortageItems.size() + " 椤圭墿鏂欏簱瀛樹笉瓒�")
+                .setItems(shortageItems);
+        Long purchaseRequestId = purchaseRequestApi.createPurchaseRequest(createReqDTO).getCheckedData();
+        // 4. 鍥炲啓閲囪喘鐢宠鍗曠紪鍙凤紝渚夸簬浠庤鍒掕拷婧�
+        mpsMapper.updateById(new MesProMpsDO().setId(id).setPurchaseRequestId(purchaseRequestId));
+        log.info("[generatePurchaseRequest] 璁″垝({}) 鐢熸垚閲囪喘鐢宠鍗�({})锛屾槑缁� {} 椤�", mps.getCode(), purchaseRequestId, shortageItems.size());
+        return purchaseRequestId;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
     public Long issueMps(Long id) {
-        // 鏍¢獙瀛樺湪 + 鑽夌鐘舵��
-        MesProMpsDO mps = validateMpsExistsAndDraft(id);
+        // 鏍¢獙瀛樺湪 + 宸插鏍哥姸鎬�
+        MesProMpsDO mps = validateMpsExistsAndApproved(id);
 
         // 鍒涘缓鐢熶骇宸ュ崟
         Long workOrderId = createWorkOrderFromMps(mps, mps.getQuantity());
@@ -196,11 +389,11 @@
             throw exception(PRO_MPS_NOT_EXISTS);
         }
 
-        // 鏍¢獙锛氬繀椤绘槸鐩稿悓浜у搧銆佽崏绋跨姸鎬�
+        // 鏍¢獙锛氬繀椤绘槸鐩稿悓浜у搧銆佸凡瀹℃牳鐘舵��
         Long productId = null;
         for (MesProMpsDO mps : mpsList) {
-            if (!MesProMpsStatusEnum.DRAFT.getStatus().equals(mps.getStatus())) {
-                throw exception(PRO_MPS_NOT_DRAFT);
+            if (!MesProMpsStatusEnum.APPROVED.getStatus().equals(mps.getStatus())) {
+                throw exception(PRO_MPS_NOT_APPROVED);
             }
             if (productId == null) {
                 productId = mps.getProductId();
@@ -256,14 +449,32 @@
             throw exception(PRO_MPS_NOT_ISSUED);
         }
 
-        // 绾ц仈鍙栨秷鍏宠仈鐨勭敓浜у伐鍗�
+        // 绾ц仈鍙栨秷鍏宠仈鐨勭敓浜у伐鍗曪紙璺熼殢宸ュ崟褰撳墠鐘舵�侊紝涓斾笉璇激鍚堝苟涓嬪彂鐨勫叡浜伐鍗曪級
         if (mps.getWorkOrderId() != null) {
-            try {
-                workOrderService.cancelWorkOrder(mps.getWorkOrderId());
-                log.info("[cancelMps] 绾ц仈鍙栨秷鐢熶骇宸ュ崟锛寃orkOrderId={}", mps.getWorkOrderId());
-            } catch (Exception e) {
-                // 宸ュ崟鍙兘宸插畬鎴愭垨宸插彇娑堬紝璁板綍鏃ュ織浣嗕笉闃绘 MPS 鍙栨秷
-                log.warn("[cancelMps] 鍙栨秷鐢熶骇宸ュ崟澶辫触锛寃orkOrderId={}锛屽師鍥狅細{}", mps.getWorkOrderId(), e.getMessage());
+            // 鏂板閫昏緫锛氳嫢璇ュ伐鍗曟湁鎶ュ伐璁板綍锛屽垯涓嶅厑璁稿彇娑� MPS
+            Long feedbackCount = feedbackMapper.selectCount(MesProFeedbackDO::getWorkOrderId, mps.getWorkOrderId());
+            if (feedbackCount != null && feedbackCount > 0) {
+                throw exception(PRO_MPS_WORK_ORDER_HAS_FEEDBACK);
+            }
+
+            // 1. 缁熻璇ュ伐鍗曡澶氬皯鏉� MPS 寮曠敤锛氬悎骞朵笅鍙戠殑澶氭潯 MPS 鍏变韩鍚屼竴涓伐鍗�
+            Long refCount = mpsMapper.selectCount(MesProMpsDO::getWorkOrderId, mps.getWorkOrderId());
+            if (refCount != null && refCount > 1) {
+                // 鍏变韩宸ュ崟锛氬彧鍙栨秷褰撳墠 MPS锛屼笉鍔ㄥ伐鍗�
+                log.info("[cancelMps] 鐢熶骇宸ュ崟({}) 琚� {} 鏉� MPS 鍏辩敤锛岃烦杩囩骇鑱斿彇娑堝伐鍗�",
+                        mps.getWorkOrderId(), refCount);
+            } else {
+                MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(mps.getWorkOrderId());
+                if (workOrder != null
+                        && MesProWorkOrderStatusEnum.CONFIRMED.getStatus().equals(workOrder.getStatus())) {
+                    // 鍙湁"宸茬‘璁�"鐘舵�佺殑宸ュ崟鎵嶉渶瑕佺骇鑱斿彇娑�
+                    workOrderService.cancelWorkOrder(mps.getWorkOrderId());
+                    log.info("[cancelMps] 绾ц仈鍙栨秷鐢熶骇宸ュ崟锛寃orkOrderId={}", mps.getWorkOrderId());
+                } else {
+                    // 宸ュ崟宸叉槸鑽夌/宸插畬鎴�/宸插彇娑堬紝鏃犻渶绾ц仈鍙栨秷锛岀洿鎺ュ彇娑� MPS
+                    log.info("[cancelMps] 鐢熶骇宸ュ崟({}) 鐘舵�佷负 {}锛屾棤闇�绾ц仈鍙栨秷",
+                            mps.getWorkOrderId(), workOrder != null ? workOrder.getStatus() : "null");
+                }
             }
         }
 
@@ -303,9 +514,7 @@
 
     @Override
     public PageResult<MesProMpsDO> getMpsPage(MesProMpsPageReqVO pageReqVO) {
-        // 鑾峰彇鐢ㄦ埛鏈夋潈闄愮殑瀹㈡埛 ID 鍒楄〃
-        List<Long> permittedClientIds = customerApi.getPermittedCustomerIds();
-        return mpsMapper.selectPage(pageReqVO, permittedClientIds);
+        return mpsMapper.selectPage(pageReqVO, (List<Long>) null);
     }
 
     @Override
@@ -329,6 +538,98 @@
     @Override
     public void updateProducedQuantity(Long id, BigDecimal incrQuantity) {
         mpsMapper.updateProducedQuantity(id, incrQuantity);
+    }
+
+    @Override
+    public Boolean checkSaleOrderProductionFinished(Long saleOrderId) {
+        if (saleOrderId == null) {
+            return false;
+        }
+        // 1. 鏌ヨ璇ラ攢鍞鍗曟墍鏈� MPS锛堥�氳繃 saleOrderId 鍏宠仈锛�
+        List<MesProMpsDO> mpsList = mpsMapper.selectList(
+                MesProMpsDO::getSaleOrderId, saleOrderId);
+        if (CollUtil.isEmpty(mpsList)) {
+            // 鏃� MPS 鍏宠仈锛岃涓哄畬鎴�
+            return true;
+        }
+        // 2. 妫�鏌ユ瘡涓� MPS 鏄惁瀹屾垚
+        for (MesProMpsDO mps : mpsList) {
+            if (!isMpsFinished(mps)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public MesProMpsDO getMpsByWorkOrderId(Long workOrderId) {
+        if (workOrderId == null) {
+            return null;
+        }
+        return mpsMapper.selectOne(MesProMpsDO::getWorkOrderId, workOrderId);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean checkAndFinishMps(Long mpsId) {
+        if (mpsId == null) {
+            log.warn("[checkAndFinishMps] mpsId is null");
+            return false;
+        }
+        MesProMpsDO mps = mpsMapper.selectById(mpsId);
+        if (mps == null) {
+            log.warn("[checkAndFinishMps] MPS not found, mpsId={}", mpsId);
+            return false;
+        }
+        // 宸插畬鎴愭垨宸插彇娑堬紝鐩存帴杩斿洖 true
+        if (MesProMpsStatusEnum.FINISHED.getStatus().equals(mps.getStatus())
+                || MesProMpsStatusEnum.CANCELED.getStatus().equals(mps.getStatus())) {
+            log.info("[checkAndFinishMps] MPS {} already finished or canceled, status={}", mpsId, mps.getStatus());
+            return true;
+        }
+        // 妫�鏌ユ槸鍚︽弧瓒冲畬鎴愭潯浠�
+        boolean finished = isMpsFinished(mps);
+        log.info("[checkAndFinishMps] MPS {} check result: {}, quantity={}, produced={}, workOrderId={}",
+                mpsId, finished, mps.getQuantity(), mps.getQuantityProduced(), mps.getWorkOrderId());
+        if (finished) {
+            // 鑷姩瀹屾垚 MPS
+            mpsMapper.updateById(new MesProMpsDO()
+                    .setId(mpsId)
+                    .setStatus(MesProMpsStatusEnum.FINISHED.getStatus())
+                    .setFinishDate(LocalDateTime.now()));
+            log.info("[checkAndFinishMps] MPS {} auto finished", mpsId);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * 鍒ゆ柇鍗曚釜 MPS 鏄惁瀹屾垚
+     */
+    private boolean isMpsFinished(MesProMpsDO mps) {
+        // 宸插彇娑堢殑 MPS 瑙嗕负缁堟�侊紝涓嶅啀闃诲鐢熶骇瀹屾垚鍒ゆ柇锛堜笌 checkCanCreateSalesNotice 鐨勫彛寰勪繚鎸佷竴鑷达級
+        if (MesProMpsStatusEnum.CANCELED.getStatus().equals(mps.getStatus())) {
+            return true;
+        }
+        // 1. 宸茬敓浜ф暟閲� >= 璁″垝鏁伴噺
+        BigDecimal quantityProduced = ObjUtil.defaultIfNull(mps.getQuantityProduced(), BigDecimal.ZERO);
+        if (quantityProduced.compareTo(mps.getQuantity()) < 0) {
+            log.debug("[isMpsFinished] MPS {} quantity not enough: produced={}, required={}",
+                    mps.getId(), quantityProduced, mps.getQuantity());
+            return false;
+        }
+        // 2. 妫�鏌ュ伐鍗曚笅鎵�鏈夋姤宸ユ槸鍚﹀畬鎴愶紙纭繚璐ㄦ瀹屾垚锛�
+        if (mps.getWorkOrderId() != null) {
+            Long unfinishedCount = feedbackMapper.selectCount(
+                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<MesProFeedbackDO>()
+                            .eq(MesProFeedbackDO::getWorkOrderId, mps.getWorkOrderId())
+                            .ne(MesProFeedbackDO::getStatus, MesProFeedbackStatusEnum.FINISHED.getStatus()));
+            log.debug("[isMpsFinished] MPS {} unfinished feedback count: {}", mps.getId(), unfinishedCount);
+            if (unfinishedCount > 0) {
+                return false;
+            }
+        }
+        return true;
     }
 
     // ========== 绉佹湁鏂规硶 ==========
@@ -376,6 +677,32 @@
     }
 
     /**
+     * 鏍¢獙 MPS 瀛樺湪 + 宸插鏍哥姸鎬�
+     */
+    private MesProMpsDO validateMpsExistsAndApproved(Long id) {
+        MesProMpsDO mps = validateMpsExists(id);
+        if (!MesProMpsStatusEnum.APPROVED.getStatus().equals(mps.getStatus())) {
+            throw exception(PRO_MPS_NOT_APPROVED);
+        }
+        return mps;
+    }
+
+    /**
+     * 鏍¢獙 MPS 瀛樺湪 + 鍙紪杈戠姸鎬侊紙浠呰崏绋匡紱瀹℃牳涓殑璁″垝闇�绛夊緟鍗忓悓鍔炲叕瀹℃壒锛�
+     */
+    private MesProMpsDO validateMpsExistsAndEditable(Long id) {
+        MesProMpsDO mps = validateMpsExists(id);
+        if (!MesProMpsStatusEnum.DRAFT.getStatus().equals(mps.getStatus())) {
+            throw exception(PRO_MPS_NOT_DRAFT);
+        }
+        return mps;
+    }
+
+    private static BigDecimal nullToZero(BigDecimal value) {
+        return value != null ? value : BigDecimal.ZERO;
+    }
+
+    /**
      * 鏍¢獙 MPS 缂栫爜鍞竴
      */
     private void validateMpsCodeUnique(Long id, String code) {
@@ -398,6 +725,14 @@
      * 鏍规嵁 MPS 鍒涘缓鐢熶骇宸ュ崟
      */
     private Long createWorkOrderFromMps(MesProMpsDO mps, BigDecimal quantity) {
+        // 1. 鏌ユ壘浜у搧宸ヨ壓璺嚎锛涙湭閰嶇疆鏃朵笉闃绘柇锛屾寜鍗曞伐搴忕敓浜у鐞�
+        MesProRouteProductDO routeProduct = routeProductService.getRouteProductByItemId(mps.getProductId());
+        Long routeId = routeProduct != null ? routeProduct.getRouteId() : null;
+        List<MesProRouteProcessDO> routeProcesses = routeProduct != null
+                ? routeProcessService.getRouteProcessListByProductId(mps.getProductId())
+                : Collections.emptyList();
+
+        // 3. 鍒涘缓鐢熶骇宸ュ崟
         MesProWorkOrderSaveReqVO workOrderReq = new MesProWorkOrderSaveReqVO();
         workOrderReq.setCode(generateWorkOrderCode());
         workOrderReq.setName("鐢熶骇宸ュ崟-" + mps.getCode());
@@ -412,7 +747,52 @@
 
         Long workOrderId = workOrderService.createWorkOrder(workOrderReq);
 
-        // 宸ュ崟鐩存帴纭涓哄凡纭鐘舵�侊紙璺宠繃鑽夌锛�
+        // 4. 鏇存柊宸ュ崟宸ヨ壓璺嚎锛堟湭閰嶇疆璺嚎鏃朵繚鎸佷负绌猴紝鎸夊崟宸ュ簭鐢熶骇锛�
+        if (routeId != null) {
+            workOrderMapper.updateById(new MesProWorkOrderDO().setId(workOrderId).setRouteId(routeId));
+        }
+
+        // 5. 淇濆瓨宸ュ崟宸ュ簭锛堜粎閰嶇疆浜嗗伐鑹鸿矾绾垮伐搴忔椂甯﹀叆锛屽惈鎶曟枡BOM锛�
+        if (CollUtil.isNotEmpty(routeProcesses)) {
+            Map<Long, List<MesProRouteProductBomDO>> bomMap = routeProcessService.getRouteProcessBomMapByProductId(mps.getProductId());
+            List<MesProWorkOrderProcessSaveReqVO> processList = new ArrayList<>();
+            for (MesProRouteProcessDO rp : routeProcesses) {
+                MesProWorkOrderProcessSaveReqVO process = new MesProWorkOrderProcessSaveReqVO();
+                process.setWorkOrderId(workOrderId);
+                process.setProcessId(rp.getProcessId());
+                process.setSort(rp.getSort());
+                process.setKeyFlag(rp.getKeyFlag());
+                process.setCheckFlag(rp.getCheckFlag());
+                process.setBackflushFlag(rp.getBackflushFlag());
+                if (rp.getOutputItemId() != null && !rp.getOutputItemId().isBlank()) {
+                    process.setOutputItemIds(Arrays.stream(rp.getOutputItemId().split(","))
+                            .map(String::trim).filter(s -> !s.isEmpty())
+                            .map(Long::valueOf).collect(Collectors.toList()));
+                }
+                process.setColorCode(rp.getColorCode());
+                // 甯﹀叆宸ヨ壓璺嚎浜у搧閰嶇疆鐨勬姤宸ヤ粨搴�
+                process.setWarehouseId(routeProduct.getWarehouseId());
+                process.setLocationId(routeProduct.getLocationId());
+                process.setAreaId(routeProduct.getAreaId());
+                // 甯﹀叆宸ュ簭鎶曟枡BOM
+                List<MesProRouteProductBomDO> routeBomItems = bomMap != null ? bomMap.get(rp.getProcessId()) : null;
+                if (CollUtil.isNotEmpty(routeBomItems)) {
+                    List<MesProWorkOrderProcessSaveReqVO.BomItem> bomItems = new ArrayList<>();
+                    for (MesProRouteProductBomDO bomDO : routeBomItems) {
+                        MesProWorkOrderProcessSaveReqVO.BomItem bomItem = new MesProWorkOrderProcessSaveReqVO.BomItem();
+                        bomItem.setItemId(bomDO.getItemId());
+                        bomItem.setQuantity(bomDO.getQuantity());
+                        bomItem.setRemark(bomDO.getRemark());
+                        bomItems.add(bomItem);
+                    }
+                    process.setBomItems(bomItems);
+                }
+                processList.add(process);
+            }
+            workOrderProcessService.batchSaveWorkOrderProcess(processList);
+        }
+
+        // 6. 宸ュ崟鐩存帴纭涓哄凡纭鐘舵��
         workOrderService.confirmWorkOrder(workOrderId);
 
         return workOrderId;
@@ -430,4 +810,4 @@
         }
     }
 
-}
\ No newline at end of file
+}

--
Gitblit v1.9.3