feat(mes): 生产计划审批改走协同办公,修复工单领料与产成品入库校验
- MPS 提交审核改为启动 BPM 流程(mes-pro-mps-audit),页面内「审核」接口与按钮废弃,审批结果由监听器回写;支持全局免审批直接通过,流程未部署时给出可执行提示
- 工单辅料预览:无工序或投料不在末道工序时回退按全部投料行汇总单耗,避免有投料却显示「暂无辅料需求」
- 产成品入库执行上架前校验每行上架明细合计与收货数量一致,提示具体行、双方数量与下一步动作
- 物料需求补品牌信息;SQL 脚本增加 process_instance_id 字段并清理旧「审核」按钮权限
Co-Authored-By: Claude Code <noreply@anthropic.com>
| | |
| | | -- ============================================= |
| | | -- 计åæç¨ï¼MPSï¼å®¡æ ¸æµ + è¾
æéè´ åçº§èæ¬ |
| | | -- 1) mes_pro_mps å¢å ï¼è®¡åå¼å§æ¥æãå®¡æ ¸ä¿¡æ¯ãéè´ç³è¯·åå
³è |
| | | -- 1) mes_pro_mps å¢å ï¼è®¡åå¼å§æ¥æãå®¡æ ¸ä¿¡æ¯ãéè´ç³è¯·åå
³èãå®¡æ¹æµç¨å®ä¾ç¼å· |
| | | -- 2) åå
¸ mes_pro_mps_status å¢å ï¼4-å®¡æ ¸ä¸ã5-å·²å®¡æ ¸ |
| | | -- 3) èå/æé®ï¼æäº¤å®¡æ ¸ãå®¡æ ¸ãçæéè´ç³è¯·å |
| | | -- 3) èå/æé®ï¼æäº¤å®¡æ ¸ãçæéè´ç³è¯·åï¼é¡µé¢å
ãå®¡æ ¸ãæé®å·²åºå¼ï¼å®¡æ¹ç»ä¸å¨åååå
¬å®æï¼ |
| | | -- 4) å®¡æ¹æµç¨ï¼éé¨ç½²æµç¨å®ä¹ mes-pro-mps-auditï¼è§ docs/sql/mes_pro_mps_audit_20261102.bpmnï¼ |
| | | -- å¯é夿§è¡ï¼ä¸å
å« tenant_idï¼é¡¹ç®ä¸ä½¿ç¨å¤ç§æ·ï¼ |
| | | -- ============================================= |
| | | START TRANSACTION; |
| | |
| | | 'SELECT 1'); |
| | | PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; |
| | | |
| | | -- å®¡æ¹æµç¨å®ä¾ç¼å·ï¼æäº¤å®¡æ ¸åè¿å
¥åååå
¬å®¡æ¹ï¼å®¡æ¹ç»æååæ¬è¡¨ |
| | | SET @exists = (SELECT COUNT(*) FROM information_schema.columns |
| | | WHERE table_schema = DATABASE() AND table_name = 'mes_pro_mps' AND column_name = 'process_instance_id'); |
| | | SET @sql = IF(@exists = 0, |
| | | 'ALTER TABLE mes_pro_mps ADD COLUMN process_instance_id varchar(64) DEFAULT NULL COMMENT ''BPM æµç¨å®ä¾ç¼å·'' AFTER purchase_request_id', |
| | | 'SELECT 1'); |
| | | PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; |
| | | |
| | | -- ============================================= |
| | | -- 2. åå
¸æ°æ®ï¼MPS ç¶æå¢å å®¡æ ¸ä¸(4)ãå·²å®¡æ ¸(5) |
| | | -- ============================================= |
| | |
| | | WHERE NOT EXISTS (SELECT 1 FROM system_dict_data WHERE dict_type = 'mes_pro_mps_status' AND value = '5' AND deleted = b'0'); |
| | | |
| | | -- ============================================= |
| | | -- 3. æé®æéï¼æäº¤å®¡æ ¸ / å®¡æ ¸ / çæéè´ç³è¯·å |
| | | -- 3. æé®æéï¼æäº¤å®¡æ ¸ / çæéè´ç³è¯·å |
| | | -- ============================================= |
| | | SET @mps_page = (SELECT id FROM system_menu WHERE deleted = b'0' AND permission = 'mes:pro-mps:query' ORDER BY id LIMIT 1); |
| | | |
| | | -- 3.0 æ¸
çæ§ç页é¢å
ãå®¡æ ¸ãæé®ï¼å®¡æ¹å·²æ¹ä¸ºå¨åååå
¬å®æï¼ |
| | | DELETE FROM system_role_menu |
| | | WHERE menu_id IN (SELECT id FROM (SELECT id FROM system_menu WHERE permission = 'mes:pro-mps:audit' AND deleted = b'0') t); |
| | | DELETE FROM system_menu WHERE permission = 'mes:pro-mps:audit' AND deleted = b'0'; |
| | | |
| | | -- 3.1 æäº¤å®¡æ ¸ |
| | | SET @btn = (SELECT id FROM system_menu WHERE deleted = b'0' AND permission = 'mes:pro-mps:submit' ORDER BY id LIMIT 1); |
| | |
| | | SELECT @next, 'ç产计å-æäº¤å®¡æ ¸', 'mes:pro-mps:submit', 3, 20, @mps_page, '', '', '', '', 0, b'1', b'0', b'1', '1', NOW(), '1', NOW(), b'0' |
| | | FROM DUAL WHERE @btn IS NULL AND @mps_page IS NOT NULL; |
| | | |
| | | -- 3.2 å®¡æ ¸ |
| | | SET @btn = (SELECT id FROM system_menu WHERE deleted = b'0' AND permission = 'mes:pro-mps:audit' ORDER BY id LIMIT 1); |
| | | SET @next = (SELECT IFNULL(MAX(id), 0) + 1 FROM system_menu); |
| | | INSERT INTO system_menu(id, name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted) |
| | | SELECT @next, 'ç产计å-å®¡æ ¸', 'mes:pro-mps:audit', 3, 21, @mps_page, '', '', '', '', 0, b'1', b'0', b'1', '1', NOW(), '1', NOW(), b'0' |
| | | FROM DUAL WHERE @btn IS NULL AND @mps_page IS NOT NULL; |
| | | |
| | | -- 3.3 çæéè´ç³è¯·å |
| | | -- 3.2 çæéè´ç³è¯·å |
| | | SET @btn = (SELECT id FROM system_menu WHERE deleted = b'0' AND permission = 'mes:pro-mps:generate-purchase' ORDER BY id LIMIT 1); |
| | | SET @next = (SELECT IFNULL(MAX(id), 0) + 1 FROM system_menu); |
| | | INSERT INTO system_menu(id, name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted) |
| | |
| | | FROM DUAL WHERE @btn IS NULL AND @mps_page IS NOT NULL; |
| | | |
| | | -- ============================================= |
| | | -- 4. ææï¼è¶
级管çåç´æ¥æ¥æå
¨é¨èåï¼æ¤å¤ä»
è¡¥æ®éè§è²æéææï¼ç¤ºä¾ï¼ç产计åå·²ææ¥è¯¢æéçè§è²ï¼ |
| | | -- 4. åååå
¬æµç¨åç±»ï¼ç产计å审æ¹ï¼æµç¨å®ä¹ mes-pro-mps-audit é¨ç½²åå½å±è¯¥åç±»ï¼ |
| | | -- ============================================= |
| | | INSERT INTO bpm_category(name, code, description, status, sort, creator, create_time, updater, update_time, deleted) |
| | | SELECT 'ç产计å审æ¹', 'mes_pro_mps_approve', 'MES ç产计å审æ¹', 0, 72, '1', NOW(), '1', NOW(), b'0' |
| | | FROM DUAL |
| | | WHERE NOT EXISTS (SELECT 1 FROM bpm_category WHERE code = 'mes_pro_mps_approve' AND deleted = b'0'); |
| | | |
| | | -- ============================================= |
| | | -- 5. åååå
¬å®¡æ¹è¯¦æ
ï¼ç»å®ç产计åä¸å¡è¯¦æ
ç»ä»¶ï¼å®¡æ¹äººå¯æ¥çè®¡åæ°æ® |
| | | -- ============================================= |
| | | INSERT INTO bpm_business_attach(process_definition_key, business_type, component_path, status, remark, |
| | | creator, create_time, updater, update_time, deleted) |
| | | SELECT 'mes-pro-mps-audit', 'mes_pro_mps', '/mes/pro/mps/detail/approve/index.vue', 0, |
| | | 'ç产计å审æ¹å±ç¤ºç产计å详æ
', '1', NOW(), '1', NOW(), b'0' |
| | | FROM DUAL |
| | | WHERE NOT EXISTS (SELECT 1 FROM bpm_business_attach |
| | | WHERE process_definition_key = 'mes-pro-mps-audit' AND deleted = b'0'); |
| | | |
| | | -- ============================================= |
| | | -- 6. ææï¼è¶
级管çåç´æ¥æ¥æå
¨é¨èåï¼æ¤å¤ä»
è¡¥æ®éè§è²æéææï¼ç¤ºä¾ï¼ç产计åå·²ææ¥è¯¢æéçè§è²ï¼ |
| | | -- ============================================= |
| | | INSERT INTO system_role_menu(role_id, menu_id, creator, create_time, updater, update_time, deleted) |
| | | SELECT DISTINCT rm.role_id, m.id, '1', NOW(), '1', NOW(), b'0' |
| | | FROM system_role_menu rm |
| | | JOIN system_menu m ON m.permission IN ('mes:pro-mps:submit', 'mes:pro-mps:audit', 'mes:pro-mps:generate-purchase') |
| | | JOIN system_menu m ON m.permission IN ('mes:pro-mps:submit', 'mes:pro-mps:generate-purchase') |
| | | AND m.deleted = b'0' |
| | | JOIN system_menu parent ON parent.id = rm.menu_id AND parent.permission = 'mes:pro-mps:query' AND parent.deleted = b'0' |
| | | WHERE rm.deleted = b'0' |
| | |
| | | import cn.iocoder.yudao.framework.common.util.collection.MapUtils; |
| | | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| | | import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.pro.mps.vo.MesProMpsAuditReqVO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.pro.mps.vo.MesProMpsPageReqVO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.pro.mps.vo.MesProMpsRespVO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.pro.mps.vo.MesProMpsSaveReqVO; |
| | |
| | | @PreAuthorize("@ss.hasPermission('mes:pro-mps:submit')") |
| | | public CommonResult<Boolean> submitMps(@RequestParam("id") Long id) { |
| | | mpsService.submitMps(id); |
| | | return success(true); |
| | | } |
| | | |
| | | @PostMapping("/audit") |
| | | @Operation(summary = "å®¡æ ¸ç产主计å") |
| | | @PreAuthorize("@ss.hasPermission('mes:pro-mps:audit')") |
| | | public CommonResult<Boolean> auditMps(@Valid @RequestBody MesProMpsAuditReqVO reqVO) { |
| | | mpsService.auditMps(reqVO); |
| | | return success(true); |
| | | } |
| | | |
| | |
| | | return success(Collections.emptyList()); |
| | | } |
| | | |
| | | // 2. è·åå·¥åå·¥åºå表ï¼å«åå·¥åºçæææç»ä¸äº§åºåï¼ |
| | | List<MesProWorkOrderProcessDO> processList = workOrderProcessService |
| | | .getWorkOrderProcessListByWorkOrderId(workOrderId); |
| | | if (CollUtil.isEmpty(processList)) { |
| | | |
| | | return success(Collections.emptyList()); |
| | | } |
| | | |
| | | // 3. è·åå·¥å BOM è¡ï¼åå·¥åºçææè®°å½ï¼æ processId åç»ä½¿ç¨ï¼ |
| | | // 2. è·åå·¥å BOM è¡ï¼åå·¥åºçææè®°å½ï¼åèå£å¾ï¼ |
| | | List<MesProWorkOrderBomDO> bomList = workOrderBomService.getWorkOrderBomListByWorkOrderId(workOrderId); |
| | | if (CollUtil.isEmpty(bomList)) { |
| | | return success(Collections.emptyList()); |
| | | } |
| | | |
| | | // 3. è·åå·¥åå·¥åºå表ï¼å«åå·¥åºçæææç»ä¸äº§åºåï¼ |
| | | List<MesProWorkOrderProcessDO> processList = workOrderProcessService |
| | | .getWorkOrderProcessListByWorkOrderId(workOrderId); |
| | | |
| | | // 4. éè¿äº§åID忥工èºè·¯çº¿ï¼å·¥å routeId å¯è½ä¸ºç©ºï¼ |
| | | Long routeId = null; |
| | |
| | | } |
| | | |
| | | // 5. æå·¥åºäº§åºé¾éå±å±å¼ BOMï¼å¾å°å¶åç©æï¼æ¯å使åçç¨éï¼ |
| | | Map<Long, BigDecimal> leafItems = buildWorkOrderItems(processList, bomList, routeId); |
| | | Map<Long, BigDecimal> leafItems = CollUtil.isEmpty(processList) |
| | | ? Collections.emptyMap() |
| | | : buildWorkOrderItems(processList, bomList, routeId); |
| | | // 5.1 åéï¼å·¥å没æå·¥åºï¼ææææªæå¨æåä¸éå·¥åºä¸ï¼ä¾å¦ææé
ç½®å¨é¦éé
æå·¥åºï¼æ¶ï¼ |
| | | // éå±å±å¼ä¼å¾å°ç©ºç»æãæ¤æ¶æå
¨é¨ææè¡æ±æ»åèï¼ä¸ãæå·¥åçæé¢æåãçåæ°å£å¾ä¿æä¸è´ï¼ |
| | | // å¦åå·¥åæææææã颿ä¹é¢è¿äºï¼æå°é¢æåå´æ¾ç¤ºãææ è¾
æéæ±ãã |
| | | if (CollUtil.isEmpty(leafItems)) { |
| | | Map<Long, BigDecimal> allBomItems = new LinkedHashMap<>(); |
| | | for (MesProWorkOrderBomDO bom : bomList) { |
| | | if (bom.getItemId() == null || bom.getQuantity() == null) { |
| | | continue; |
| | | } |
| | | allBomItems.merge(bom.getItemId(), bom.getQuantity(), BigDecimal::add); |
| | | } |
| | | leafItems = allBomItems; |
| | | } |
| | | if (CollUtil.isEmpty(leafItems)) { |
| | | return success(Collections.emptyList()); |
| | | } |
| | |
| | | */ |
| | | private String auditRemark; |
| | | |
| | | /** |
| | | * BPM æµç¨å®ä¾ç¼å· |
| | | * |
| | | * æäº¤å®¡æ ¸åå¨åååå
¬å®¡æ¹ï¼å®¡æ¹ç»æéè¿çå¬å¨åå |
| | | */ |
| | | private String processInstanceId; |
| | | |
| | | // ========== éè´åå
³è ========== |
| | | |
| | | /** |
| | |
| | | ErrorCode PRO_MPS_HAS_WORK_ORDER = new ErrorCode(1_040_509_007, "MPS å·²å
³èç产工åï¼æ æ³å é¤"); |
| | | ErrorCode PRO_MPS_MES_PRODUCT_NOT_EXISTS = new ErrorCode(1_040_509_008, "MES 产åä¸åå¨ï¼æ æ³å建 MPSï¼äº§åï¼{}"); |
| | | ErrorCode PRO_MPS_WORK_ORDER_HAS_FEEDBACK = new ErrorCode(1_040_509_009, "该计åå
³èçç产订åå·²ææ¥å·¥è®°å½ï¼æ æ³åæ¶ï¼"); |
| | | ErrorCode PRO_MPS_NOT_AUDITING = new ErrorCode(1_040_509_010, "åªæå®¡æ ¸ä¸ç¶æç MPS æè½å®¡æ ¸"); |
| | | ErrorCode PRO_MPS_NOT_APPROVED = new ErrorCode(1_040_509_011, "åªæå·²å®¡æ ¸ç¶æç MPS æè½æ§è¡æ¤æä½ï¼è¯·å
æäº¤å®¡æ ¸"); |
| | | ErrorCode PRO_MPS_AUDIT_REMARK_REQUIRED = new ErrorCode(1_040_509_012, "驳åå®¡æ ¸æ¶å¿
须填å驳ååå "); |
| | | ErrorCode PRO_MPS_NO_MATERIAL_SHORTAGE = new ErrorCode(1_040_509_013, "该计åæéç©æåºåå
è¶³ï¼æ éçæéè´å"); |
| | | ErrorCode PRO_MPS_PROCESS_NOT_DEPLOYED = new ErrorCode(1_040_509_014, "æäº¤å®¡æ ¸å¤±è´¥ï¼ç产计åå®¡æ¹æµç¨(mes-pro-mps-audit)å°æªé¨ç½²ï¼è¯·è系管çåå¨ãåååå
¬ - æµç¨æ¨¡åãä¸é¨ç½²è¯¥æµç¨åéæ°æäº¤"); |
| | | ErrorCode PRO_MPS_NO_MATERIAL_BOM = new ErrorCode(1_040_509_015, "ç产计å{}ç产åã{}ãå°æªç»´æ¤äº§å BOM æå·¥èºè·¯çº¿ææ BOMï¼æ æ³è®¡ç®ç©æç¼ºå£ï¼è¯·å
å°ãç产åºç¡æ°æ® - ç©æBOMç»´æ¤ãæãç产åºç¡æ°æ® - å·¥èºè·¯çº¿ãä¸é
ç½®ææååçæéè´ç³è¯·å"); |
| | | |
| | | // ========== MES è´¨é管ç-è´¨æ£æ¹æ¡ï¼1-040-600-000ï¼ ========== |
| | | ErrorCode QC_TEMPLATE_NOT_EXISTS = new ErrorCode(1_040_600_000, "è´¨æ£æ¹æ¡ä¸åå¨"); |
| | |
| | | } |
| | | continue; |
| | | } |
| | | // é¡¶çº§äº§åæ²¡æä»»ä½ä¸çº§ BOM æ¶ï¼è¯´æå°æªç»´æ¤ BOMï¼ä¸è½æäº§åèªèº«å½æè¾
æ |
| | | if (entry.getKey().equals(productId)) { |
| | | continue; |
| | | } |
| | | // æ ä¸çº§ BOMï¼è§ä¸ºå¶åç©æï¼å¤è´ä»¶ï¼ |
| | | leafItems.merge(entry.getKey(), qty, BigDecimal::add); |
| | | } |
| | |
| | | /** |
| | | * æäº¤å®¡æ ¸ |
| | | * |
| | | * èç¨¿ç¶æ â å®¡æ ¸ä¸ |
| | | * èç¨¿ç¶æ â å®¡æ ¸ä¸ï¼å¹¶å¯å¨ BPM æµç¨ï¼å®¡æ¹å¨åååå
¬å®æ |
| | | * |
| | | * @param id MPS ç¼å· |
| | | */ |
| | | void submitMps(Long id); |
| | | |
| | | /** |
| | | * å®¡æ ¸ MPS |
| | | * å¤ç BPM 审æ¹ç»æï¼åååå
¬å®¡æ¹å®æååè°ï¼ |
| | | * |
| | | * å®¡æ ¸ä¸ â å·²å®¡æ ¸ï¼éè¿ï¼/ è稿ï¼é©³åï¼ |
| | | * |
| | | * @param reqVO å®¡æ ¸ä¿¡æ¯ |
| | | * @param id MPS ç¼å· |
| | | * @param bpmResult BPM 审æ¹ç»æ |
| | | * @param reason å®¡æ¹æè§ï¼é©³ååå ï¼ |
| | | * @param auditUserId å®¡æ ¸äººç¼å· |
| | | */ |
| | | void auditMps(@Valid cn.iocoder.yudao.module.mes.controller.admin.pro.mps.vo.MesProMpsAuditReqVO reqVO); |
| | | void handleBpmProcessResult(Long id, Integer bpmResult, String reason, Long auditUserId); |
| | | |
| | | /** |
| | | * è·å¾è®¡åçè¾
æéæ±æç»ï¼å«åºåãå¨éãæ¯å¦å
è¶³ï¼ |
| | |
| | | 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.MesProMpsAuditReqVO; |
| | | 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; |
| | |
| | | */ |
| | | private static final String MPS_CODE_RULE = "MPS"; |
| | | |
| | | /** |
| | | * MPS å®¡æ¹æµç¨å®ä¹ Keyï¼BPM æµç¨æ¨¡åä¸é
ç½®ï¼ |
| | | */ |
| | | private static final String MPS_APPROVE_PROCESS_DEFINITION_KEY = "mes-pro-mps-audit"; |
| | | |
| | | @Resource |
| | | private MesProMpsMapper mpsMapper; |
| | | @Resource |
| | |
| | | private ErpPurchaseOrderApi purchaseOrderApi; |
| | | @Resource |
| | | private ErpPurchaseRequestApi purchaseRequestApi; |
| | | @Resource |
| | | private BpmProcessInstanceApi processInstanceApi; |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | |
| | | // æ ¡éªç¼ç å¯ä¸ |
| | | validateMpsCodeUnique(updateReqVO.getId(), updateReqVO.getCode()); |
| | | |
| | | // æ´æ°ï¼å®¡æ ¸ä¸ç计å被修æ¹åéè¦éæ°æäº¤å®¡æ ¸ |
| | | // æ´æ°ï¼ä»
è稿å¯ç¼è¾ï¼ |
| | | MesProMpsDO updateObj = BeanUtils.toBean(updateReqVO, MesProMpsDO.class); |
| | | if (MesProMpsStatusEnum.AUDITING.getStatus().equals(mps.getStatus())) { |
| | | updateObj.setStatus(MesProMpsStatusEnum.DRAFT.getStatus()); |
| | | } |
| | | mpsMapper.updateById(updateObj); |
| | | } |
| | | |
| | |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void submitMps(Long id) { |
| | | MesProMpsDO mps = validateMpsExists(id); |
| | | if (!MesProMpsStatusEnum.DRAFT.getStatus().equals(mps.getStatus()) |
| | | && !MesProMpsStatusEnum.AUDITING.getStatus().equals(mps.getStatus())) { |
| | | // åªæè稿å¯ä»¥æäº¤å®¡æ ¸ï¼å®¡æ ¸ä¸ç计åéçå¾
åååå
¬å®¡æ¹ç»æ |
| | | 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())); |
| | | .setStatus(MesProMpsStatusEnum.AUDITING.getStatus()) |
| | | .setProcessInstanceId(processInstanceId)); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void auditMps(MesProMpsAuditReqVO reqVO) { |
| | | MesProMpsDO mps = validateMpsExists(reqVO.getId()); |
| | | 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())) { |
| | | throw exception(PRO_MPS_NOT_AUDITING); |
| | | log.warn("[handleBpmProcessResult] ç产计å({}) ä¸å¤äºå®¡æ ¸ä¸ç¶æï¼å½åç¶æï¼{}", id, mps.getStatus()); |
| | | return; |
| | | } |
| | | boolean approved = Boolean.TRUE.equals(reqVO.getApproved()); |
| | | if (!approved && StrUtil.isBlank(reqVO.getAuditRemark())) { |
| | | throw exception(PRO_MPS_AUDIT_REMARK_REQUIRED); |
| | | |
| | | // 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, "审æ¹é©³å"))); |
| | | } |
| | | mpsMapper.updateById(new MesProMpsDO() |
| | | .setId(reqVO.getId()) |
| | | .setStatus(approved ? MesProMpsStatusEnum.APPROVED.getStatus() |
| | | : MesProMpsStatusEnum.DRAFT.getStatus()) |
| | | .setAuditUserId(SecurityFrameworkUtils.getLoginUserId()) |
| | | .setAuditTime(LocalDateTime.now()) |
| | | .setAuditRemark(approved |
| | | ? StrUtil.blankToDefault(reqVO.getAuditRemark(), "å®¡æ ¸éè¿") |
| | | : reqVO.getAuditRemark())); |
| | | } |
| | | |
| | | @Override |
| | |
| | | if (!MesProMpsStatusEnum.APPROVED.getStatus().equals(mps.getStatus())) { |
| | | throw exception(PRO_MPS_NOT_APPROVED); |
| | | } |
| | | // 1. åªå¯¹ç¼ºå£ç©æçæéè´æç»ï¼ç¼ºå£ = éæ± - å¯ç¨åºå - å¨ééè´ |
| | | // 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 : getMpsItemList(id)) { |
| | | for (MesProWorkOrderItemRespVO item : materialItems) { |
| | | if (Boolean.TRUE.equals(item.getStockSufficient())) { |
| | | continue; |
| | | } |
| | |
| | | if (CollUtil.isEmpty(shortageItems)) { |
| | | throw exception(PRO_MPS_NO_MATERIAL_SHORTAGE); |
| | | } |
| | | // 2. çæéè´ç³è¯·åï¼èç¨¿ï¼æ ä¾åºåï¼åç»ç±éè´äººåè¡¥å
å¹¶ææµç¨æäº¤ï¼ |
| | | // 3. çæéè´ç³è¯·åï¼èç¨¿ï¼æ ä¾åºåï¼åç»ç±éè´äººåè¡¥å
å¹¶ææµç¨æäº¤ï¼ |
| | | ErpPurchaseRequestCreateReqDTO createReqDTO = new ErpPurchaseRequestCreateReqDTO() |
| | | .setRequestTime(LocalDateTime.now()) |
| | | .setRequestReason("ç产计å " + mps.getCode() + " è¾
æéè´") |
| | | .setRemark("ç±ç产计åèªå¨çæï¼å
± " + shortageItems.size() + " é¡¹ç©æåºåä¸è¶³") |
| | | .setItems(shortageItems); |
| | | Long purchaseRequestId = purchaseRequestApi.createPurchaseRequest(createReqDTO).getCheckedData(); |
| | | // 3. ååéè´ç³è¯·åç¼å·ï¼ä¾¿äºä»è®¡å追溯 |
| | | // 4. ååéè´ç³è¯·åç¼å·ï¼ä¾¿äºä»è®¡å追溯 |
| | | mpsMapper.updateById(new MesProMpsDO().setId(id).setPurchaseRequestId(purchaseRequestId)); |
| | | log.info("[generatePurchaseRequest] 计å({}) çæéè´ç³è¯·å({})ï¼æç» {} 项", mps.getCode(), purchaseRequestId, shortageItems.size()); |
| | | return purchaseRequestId; |
| | |
| | | } |
| | | |
| | | /** |
| | | * æ ¡éª MPS åå¨ + å¯ç¼è¾ç¶æï¼æªå¼å§ãå®¡æ ¸ä¸ï¼ |
| | | * æ ¡éª MPS åå¨ + å¯ç¼è¾ç¶æï¼ä»
è稿ï¼å®¡æ ¸ä¸ç计åéçå¾
åååå
¬å®¡æ¹ï¼ |
| | | */ |
| | | private MesProMpsDO validateMpsExistsAndEditable(Long id) { |
| | | MesProMpsDO mps = validateMpsExists(id); |
| | | if (!MesProMpsStatusEnum.DRAFT.getStatus().equals(mps.getStatus()) |
| | | && !MesProMpsStatusEnum.AUDITING.getStatus().equals(mps.getStatus())) { |
| | | if (!MesProMpsStatusEnum.DRAFT.getStatus().equals(mps.getStatus())) { |
| | | throw exception(PRO_MPS_NOT_DRAFT); |
| | | } |
| | | return mps; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.mes.service.pro.mps.listener; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent; |
| | | import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEventListener; |
| | | import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants; |
| | | import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService; |
| | | import cn.iocoder.yudao.module.mes.service.pro.mps.MesProMpsService; |
| | | import jakarta.annotation.Resource; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.flowable.task.api.history.HistoricTaskInstance; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | import java.util.Comparator; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * ç产主计åï¼MPSï¼å®¡æ¹ç¶æçå¬å¨ |
| | | * |
| | | * çå¬ BPM æµç¨å®ä¾ç¶æåæ´äºä»¶ï¼æåååå
¬ç审æ¹ç»æååå° MPS ç¶æ |
| | | * |
| | | * @author è¶
级管çå |
| | | */ |
| | | @Component |
| | | @Slf4j |
| | | public class MesProMpsStatusListener extends BpmProcessInstanceStatusEventListener { |
| | | |
| | | /** |
| | | * MPS å®¡æ¹æµç¨å®ä¹ Key |
| | | */ |
| | | private static final String BPM_PROCESS_DEFINITION_KEY = "mes-pro-mps-audit"; |
| | | |
| | | @Resource |
| | | private MesProMpsService mpsService; |
| | | |
| | | @Resource |
| | | private BpmTaskService bpmTaskService; |
| | | |
| | | @Override |
| | | protected String getProcessDefinitionKey() { |
| | | return BPM_PROCESS_DEFINITION_KEY; |
| | | } |
| | | |
| | | @Override |
| | | protected void onEvent(BpmProcessInstanceStatusEvent event) { |
| | | Long mpsId; |
| | | try { |
| | | mpsId = Long.parseLong(event.getBusinessKey()); |
| | | } catch (NumberFormatException e) { |
| | | log.warn("[onEvent] businessKey({}) 䏿¯ææçç产计åç¼å·", event.getBusinessKey()); |
| | | return; |
| | | } |
| | | String reason = event.getReason(); |
| | | Long auditUserId = null; |
| | | if (event.getId() != null) { |
| | | // å½ event.getReason() 为空æ¶ï¼å°è¯ä»æåä¸ä¸ªå®æçä»»å¡ä¸è·åå®¡æ¹æè§ä¸å®¡æ¹äºº |
| | | List<HistoricTaskInstance> tasks = bpmTaskService.getTaskListByProcessInstanceId(event.getId(), false); |
| | | if (CollUtil.isNotEmpty(tasks)) { |
| | | HistoricTaskInstance lastTask = tasks.stream() |
| | | .filter(task -> task.getEndTime() != null) |
| | | // æé¤å¤å®ä¾æç¾æ»¡è¶³æ¡ä»¶åï¼è¢«ç³»ç»èªå¨åæ¶çå
å¼ä»»å¡ï¼deleteReason éç©ºï¼ |
| | | .filter(task -> task.getDeleteReason() == null) |
| | | .max(Comparator.comparing(HistoricTaskInstance::getEndTime)) |
| | | .orElse(null); |
| | | if (lastTask != null) { |
| | | if (StrUtil.isEmpty(reason) && lastTask.getTaskLocalVariables() != null) { |
| | | reason = (String) lastTask.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_REASON); |
| | | } |
| | | if (StrUtil.isNotEmpty(lastTask.getAssignee())) { |
| | | try { |
| | | auditUserId = Long.parseLong(lastTask.getAssignee()); |
| | | } catch (NumberFormatException ignored) { |
| | | log.warn("[onEvent] 审æ¹äºº({}) 䏿¯ææçç¨æ·ç¼å·", lastTask.getAssignee()); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | log.info("[onEvent] æ¶å°BPM审æ¹äºä»¶ï¼mpsId={}, status={}, reason={}, auditUserId={}", |
| | | mpsId, event.getStatus(), reason, auditUserId); |
| | | mpsService.handleBpmProcessResult(mpsId, event.getStatus(), reason, auditUserId); |
| | | } |
| | | |
| | | } |
| | |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import cn.hutool.core.util.ObjUtil; |
| | | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| | | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| | | import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; |
| | | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| | |
| | | import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants; |
| | | import cn.iocoder.yudao.module.mes.enums.wm.MesWmProductReceiptStatusEnum; |
| | | import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO; |
| | | import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService; |
| | | import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService; |
| | | import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService; |
| | | import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO; |
| | |
| | | |
| | | import java.math.BigDecimal; |
| | | 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; |
| | | import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*; |
| | |
| | | private MesWmTransactionService wmTransactionService; |
| | | @Resource |
| | | private MesProWorkOrderService workOrderService; |
| | | @Resource |
| | | private MesMdItemService itemService; |
| | | @Resource |
| | | private MesWmWarehouseService warehouseService; |
| | | @Resource |
| | |
| | | throw exception(WM_PRODUCT_RECPT_STATUS_ERROR); |
| | | } |
| | | |
| | | // æ ¡éªä¸æ¶æç»å·²è¦çæ¯ä¸è¡æ¶è´§æ°éï¼ç¶æä¸æ¦æ¨è¿å°ãå¾
æ§è¡å
¥åºãï¼æç»å°±ä¸è½åç¼è¾ï¼ |
| | | // è¥æ¤æ¶æç»ä¸ºç©ºææ°éä¸è¶³ï¼åæ®å°æ°¸è¿æ æ³æ§è¡å
¥åºï¼ä¼è¢«å¡æ»å¨å¾
æ§è¡å
¥åºï¼ |
| | | validateProductReceiptStockDetail(receipt); |
| | | |
| | | // æ§è¡ä¸æ¶ï¼å¾
䏿¶ â å¾
å
¥åºï¼ |
| | | productReceiptMapper.updateById(new MesWmProductReceiptDO() |
| | | .setId(id).setStatus(MesWmProductReceiptStatusEnum.APPROVED.getStatus())); |
| | | } |
| | | |
| | | /** |
| | | * æ ¡éªæ§è¡ä¸æ¶åç䏿¶æç»ï¼æ¯ä¸è¡çæ¶è´§æ°éé½å¿
须被该è¡ç䏿¶æç»å计è¦ç |
| | | * |
| | | * æç¤ºæãå
·ä½è¡ + åæ¹æ°é + ä¸ä¸æ¥å¨ä½ãç»ç»ï¼é¿å
åªæ¥ä¸å¥ç¬¼ç»ç"æ°éä¸å¹é
" |
| | | */ |
| | | private void validateProductReceiptStockDetail(MesWmProductReceiptDO receipt) { |
| | | List<MesWmProductReceiptLineDO> lines = productReceiptLineService.getProductReceiptLineListByRecptId(receipt.getId()); |
| | | if (CollUtil.isEmpty(lines)) { |
| | | throw new ServiceException(WM_PRODUCT_RECPT_NO_LINE.getCode(), |
| | | String.format("æ¶è´§å[%s]æ²¡ææ¶è´§è¡ï¼æ æ³æ§è¡ä¸æ¶ï¼è¯·å
å¨åæ®ä¸æ·»å æ¶è´§è¡ï¼ç©æä¸æ¶è´§æ°éï¼ååæ§è¡ä¸æ¶ã", |
| | | receipt.getCode())); |
| | | } |
| | | Map<Long, MesMdItemDO> itemMap = itemService.getItemMap(lines.stream() |
| | | .map(MesWmProductReceiptLineDO::getItemId).filter(Objects::nonNull).collect(Collectors.toSet())); |
| | | for (int i = 0; i < lines.size(); i++) { |
| | | MesWmProductReceiptLineDO line = lines.get(i); |
| | | String lineLabel = String.format("第 %d è¡[%s]", i + 1, formatItemLabel(line.getItemId(), itemMap)); |
| | | if (line.getBatchCode() != null) { |
| | | lineLabel += String.format("ï¼æ¹æ¬¡[%s]ï¼", line.getBatchCode()); |
| | | } |
| | | BigDecimal lineQuantity = line.getQuantity() == null ? BigDecimal.ZERO : line.getQuantity(); |
| | | List<MesWmProductReceiptDetailDO> details = |
| | | productReceiptDetailService.getProductReceiptDetailListByLineId(line.getId()); |
| | | // æç»ä¸ºç©ºæ¶ç´æ¥æç¤º"å»ç¹ä¸æ¶"ï¼æ¯æç¤º"å计 0 ä¸ 20 ä¸ä¸è´"æ´æç¡® |
| | | if (CollUtil.isEmpty(details)) { |
| | | throw new ServiceException(WM_PRODUCT_RECPT_NO_DETAIL.getCode(), |
| | | String.format("æ¶è´§å[%s]%sè¿æ²¡æä¸æ¶æç»ï¼æ¶è´§æ°é[%s]ï¼ï¼è¯·å
å¨è¯¥è¡ç¹ã䏿¶ãï¼å¡«åå
¥åºä»åºä¸ä¸æ¶æ°éï¼" |
| | | + "å计填满 %s ååæ§è¡ä¸æ¶ã", receipt.getCode(), lineLabel, |
| | | formatQuantity(lineQuantity), formatQuantity(lineQuantity))); |
| | | } |
| | | BigDecimal detailQuantity = CollectionUtils.getSumValue(details, |
| | | MesWmProductReceiptDetailDO::getQuantity, BigDecimal::add, BigDecimal.ZERO); |
| | | if (detailQuantity.compareTo(lineQuantity) != 0) { |
| | | BigDecimal diff = lineQuantity.subtract(detailQuantity); |
| | | String action = diff.signum() > 0 |
| | | ? String.format("è¿å·®[%s]ï¼è¯·å¨è¯¥è¡ç¹ã䏿¶ãè¡¥ç»ç¼ºå°ç䏿¶æç»", formatQuantity(diff)) |
| | | : String.format("å¤åº[%s]ï¼è¯·å é¤æè°æ´è¯¥è¡å¤ä½ç䏿¶æç»", formatQuantity(diff.negate())); |
| | | throw new ServiceException(WM_PRODUCT_RECPT_DETAIL_QUANTITY_MISMATCH.getCode(), |
| | | String.format("æ¶è´§å[%s]%s䏿¶æç»å计[%s]䏿¶è´§æ°é[%s]ä¸ä¸è´ï¼%sï¼è°æ´ä¸è´ååæ§è¡ä¸æ¶ã", |
| | | receipt.getCode(), lineLabel, formatQuantity(detailQuantity), |
| | | formatQuantity(lineQuantity), action)); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * æ°éæ¾ç¤ºï¼å»ææ°æ®åº scale 带åºçå¤ä½ 0ï¼20.000000 â 20ï¼ |
| | | */ |
| | | private String formatQuantity(BigDecimal quantity) { |
| | | if (quantity == null) { |
| | | return "0"; |
| | | } |
| | | return quantity.stripTrailingZeros().toPlainString(); |
| | | } |
| | | |
| | | /** |
| | | * ç©ææ¾ç¤ºåï¼ä¼å
ãåç§°/ç¼ç ãï¼æ¥ä¸å°æ¶åé为 IDï¼é¿å
æè£¸ ID ç´æ¥æç»ç¨æ· |
| | | */ |
| | | private String formatItemLabel(Long itemId, Map<Long, MesMdItemDO> itemMap) { |
| | | MesMdItemDO item = itemId == null ? null : itemMap.get(itemId); |
| | | if (item == null) { |
| | | return itemId == null ? "ç©æ[æªéæ©]" : String.format("ç©æ[ID=%s]", itemId); |
| | | } |
| | | return String.format("%s/%s", item.getName(), item.getCode()); |
| | | } |
| | | |
| | | @Override |
| | | public Boolean checkProductReceiptQuantity(Long id) { |
| | | List<MesWmProductReceiptLineDO> lines = productReceiptLineService.getProductReceiptLineListByRecptId(id); |