云
2026-10-27 ffca46b39d25439a06a27cafda2ec523a3fb67cf
feat(mes): 增加盘号字段支持和优化报工质检流程

- 在多个数据库表中新增 reel_no 字段以支持盘号追踪
- 重构报工流程,将报工与质检分离,报工审批后生成待检产出
- 新增批量逐条下发MPS功能,禁用合并下发功能
- 添加多个错误码常量用于盘号校验和MPS状态控制
- 在报工相关接口中增加盘号字段支持
- 优化报工审批逻辑,移除工序检验标识对产出入库的控制
- 修复报工质检回调逻辑,确保质量数量正确回填
- 添加报工时间字段支持,可在快速报工时指定报工时间
- 增强MPS下发权限控制,仅销售订单创建人可下发生产计划
已添加8个文件
已修改62个文件
1325 ■■■■■ 文件已修改
production-rework-legacy-code.txt 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/20261027_00_preflight.sql 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/20261027_01_reel_fields_safe.sql 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/20261027_02_query_permissions_preview.sql 243 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/20261027_sales_reel_no.sql 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/sale/dto/ErpSaleOrderCreateReqDTO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/sale/dto/ErpSaleOrderItemRespDTO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/sale/dto/ErpSaleOrderRespDTO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderItemRespVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderSaveReqVO.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleOrderItemDO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/mps/dto/MesProMpsCreateReqDTO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/mps/dto/MesProMpsRespDTO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/mps/MesProMpsApiImpl.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/feedback/MesProFeedbackController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/feedback/vo/MesProFeedbackBatchCreateReqVO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/feedback/vo/MesProFeedbackQuickCreateReqVO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/feedback/vo/MesProFeedbackRespVO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/mps/MesProMpsController.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/mps/vo/MesProMpsRespVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/mps/vo/MesProMpsSaveReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/MesProWorkOrderRespVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/MesProWorkOrderSaveReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ipqc/vo/MesQcIpqcRespVO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcRespVO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcSaveReqVO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/pendinginspect/vo/MesQcPendingInspectRespVO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/batch/vo/MesWmBatchGenerateReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/MesWmMaterialStockController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/vo/MesWmMaterialStockRespVO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/productproduce/vo/MesWmProductProduceLineRespVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/mps/MesProMpsDO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/workorder/MesProWorkOrderDO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/ipqc/MesQcIpqcDO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/oqc/MesQcOqcDO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/batch/MesWmBatchDO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/productproduce/MesWmProductProduceDetailDO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/productproduce/MesWmProductProduceLineDO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/feedback/MesProFeedbackMapper.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/mps/MesProMpsMapper.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/batch/MesWmBatchMapper.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackService.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsService.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsServiceImpl.java 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskService.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskServiceImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ipqc/MesQcIpqcServiceImpl.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImpl.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/pendinginspect/MesQcPendingInspectServiceImpl.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/batch/MesWmBatchService.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/batch/MesWmBatchServiceImpl.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productproduce/MesWmProductProduceService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productproduce/MesWmProductProduceServiceImpl.java 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesDetailServiceImpl.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesServiceImpl.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmSalesReelGuard.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/MesWmTransactionServiceImpl.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImplTest.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/ipqc/MesQcIpqcServiceImplTest.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcAutoCreateTest.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImplTest.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/wm/batch/MesWmBatchServiceImplTest.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/wm/productproduce/MesWmProductProduceServiceImplTest.java 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmSalesReelGuardTest.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/wm/transaction/MesWmTransactionReelSafetyTest.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/test/resources/sql/create_tables.sql 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
production-rework-legacy-code.txt
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
# ç”Ÿäº§æ”¹é€ ï¼šæ—§é€»è¾‘与兼容点
此清单记录已核实的当前代码行为,不代表相关功能已经停用。改造完成后应更新状态,禁止将历史兼容逻辑误当成新业务入口。
待办:工资计算公式尚未与客户确认;本次不实施工资计算,待确认工序、设备、工时定额和辅助工时的计算口径后再改造。
盘号追溯:新生产产出即使物料未启用普通批次管理,也会为非空盘号创建内部追溯批次,库存仍按批次分开;库存页面和拣货选择器显示盘号,销售拣货及出库时核对销售明细盘号。历史无盘号库存保留空值,不自动推测或拆分;销售明细有盘号时,这类旧库存不能直接作为匹配库存出库,需先核实并处理历史数据。内部追溯批次受现有库位批次混放规则约束,正式使用前需验证现场库位配置。
| ä½ç½® | å½“前状态 | æ”¹é€ æ³¨æ„äº‹é¡¹ |
| --- | --- | --- |
| `MesProMpsController.mergeAndIssueMps` å’Œ `MesProMpsServiceImpl.mergeAndIssueMps` | å·²åœç”¨ï¼šæ—§å…¥å£è¿”回停用提示 | æ–°æŽ¥å£é€æ¡æ‰¹é‡ä¸‹å‘;历史合并工单及明细仍需保留查询和取消兼容。 |
| `MesProMpsServiceImpl.generateMergeBatchNo` | å·²åœç”¨ï¼Œæ— è°ƒç”¨ | æ—§åˆå¹¶ä¸‹å‘使用的批次号生成方法,不用于新单据。 |
| `MesProMpsServiceImpl.cancelMps` ä¸­å…±äº«å·¥å•处理 | åŽ†å²å…¼å®¹é€»è¾‘ï¼Œå½“å‰æœ‰æ•ˆ | å³ä½¿æ–°è®¡åˆ’不再合并下发,也不能直接删除,否则可能影响历史合并单取消。 |
| `MesProFeedbackServiceImpl` æŒ‰å·¥åºæ£€éªŒæ ‡è¯†ç›´æŽ¥å…¥åº“的旧分支 | å·²ç§»é™¤ | æ–°æŠ¥å·¥äº§å‡ºç»Ÿä¸€å¾…检;旧工序标识和历史记录暂保留兼容,人工检验需要明确关联报工产出行。 |
| å‰ç«¯ç”Ÿäº§æŠ¥å·¥è¡¨å•按工序标识切换检验逻辑 | å·²ç§»é™¤ | æŠ¥å·¥è¡¨å•按统一待检规则提交,不再读取工序检验标识。 |
| `MesQcIpqcServiceImpl` æŒ‰ä»»åŠ¡è‡ªåŠ¨é€‰å–ç¬¬ä¸€æ¡å¾…æ£€æŠ¥å·¥çš„æ—§é€»è¾‘ | å·²ç§»é™¤ | ç‹¬ç«‹æ£€éªŒä¸æŽ¨æµ‹æ¥æºï¼Œå…³è”报工时必须明确指定来源行,避免多次报工或不同盘号串单。 |
| `MesProFeedbackDO.lotNumber` | é¢„留生产批号字段,不能认定为盘号 | ç›˜å·å”¯ä¸€èŒƒå›´å’Œè¿½æº¯è§„则确认后再决定是否复用,避免覆盖既有批号语义。 |
| `MesProRouteController` å·¥è‰ºè·¯çº¿å¯¼å…¥æŽ¥å£ | å½“前有效 | å·²æœ‰æ™®é€šå¯¼å…¥å’Œå®Œæ•´è·¯çº¿å¯¼å…¥ï¼Œåº”先验证模板,再确定是否需要扩展。 |
sql/mysql/20261027_00_preflight.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,13 @@
-- 2026-10-27 æ‰§è¡Œå‰æ£€æŸ¥ï¼ˆåªè¯»ï¼‰
-- å…ˆåœ¨å®¢æˆ·ç«¯é€‰æ‹©ç›®æ ‡æ•°æ®åº“。
SELECT DATABASE() AS selected_database, VERSION() AS mysql_version;
-- åº”有 8 å¼ ä¸šåŠ¡è¡¨ï¼›ç¼ºå¤±çš„è¡¨è¯·å‹¿ç»§ç»­æ‰§è¡Œå­—æ®µè„šæœ¬ã€‚
SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN ('erp_sale_order_items', 'mes_pro_mps', 'mes_pro_work_order', 'mes_qc_ipqc', 'mes_qc_oqc', 'mes_wm_product_produce_line', 'mes_wm_product_produce_detail', 'mes_wm_batch') ORDER BY TABLE_NAME;
-- åˆ—出已存在的盘号字段;预期均为 varchar(128)、允许 NULL。
SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_COMMENT FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN ('erp_sale_order_items', 'mes_pro_mps', 'mes_pro_work_order', 'mes_qc_ipqc', 'mes_qc_oqc', 'mes_wm_product_produce_line', 'mes_wm_product_produce_detail', 'mes_wm_batch') AND COLUMN_NAME = 'reel_no' ORDER BY TABLE_NAME;
-- æŸ¥è¯¢æƒé™æ¸…理涉及删除按钮和角色授权,请在确认前端兼容后再单独评估执行。
-- æ‰¹é‡ä¸‹å‘沿用单条下发权限;检查两种按钮及其状态。
SELECT id, name, permission, type, status, deleted FROM system_menu
WHERE permission IN ('mes:pro-mps:issue', 'mes:pro-mps:merge-issue')
ORDER BY permission, id;
sql/mysql/20261027_01_reel_fields_safe.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,55 @@
-- 2026-10-27 æœ¬æ¬¡æ”¹é€ ï¼šç›˜å·å­—段(MySQL)
-- å…ˆé€‰æ‹©ç›®æ ‡æ•°æ®åº“并执行 20261027_00_preflight.sql,核对库名、备份和现有字段类型。
-- æœ¬è„šæœ¬åªåœ¨å­—æ®µä¸å­˜åœ¨æ—¶æ·»åŠ å­—æ®µï¼›é‡å¤æ‰§è¡Œä¼šè·³è¿‡å·²å­˜åœ¨å­—æ®µã€‚
-- å·²æœ‰åŒåå­—段但类型或用途不同,不会自动修正,请先核对。
-- MySQL ALTER TABLE ä¼šéšå¼æäº¤ï¼›å¤±è´¥åŽéœ€æ£€æŸ¥å®žé™…执行结果再重试。
-- erp_sale_order_items.reel_no
SET @reel_ddl = IF(EXISTS (SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'erp_sale_order_items' AND COLUMN_NAME = 'reel_no'), 'SELECT ''erp_sale_order_items.reel_no å·²å­˜åœ¨ï¼Œè·³è¿‡'' AS message', 'ALTER TABLE `erp_sale_order_items` ADD COLUMN `reel_no` varchar(128) DEFAULT NULL COMMENT ''销售明细盘号''');
PREPARE reel_stmt FROM @reel_ddl;
EXECUTE reel_stmt;
DEALLOCATE PREPARE reel_stmt;
-- mes_pro_mps.reel_no
SET @reel_ddl = IF(EXISTS (SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mes_pro_mps' AND COLUMN_NAME = 'reel_no'), 'SELECT ''mes_pro_mps.reel_no å·²å­˜åœ¨ï¼Œè·³è¿‡'' AS message', 'ALTER TABLE `mes_pro_mps` ADD COLUMN `reel_no` varchar(128) DEFAULT NULL COMMENT ''销售明细盘号快照''');
PREPARE reel_stmt FROM @reel_ddl;
EXECUTE reel_stmt;
DEALLOCATE PREPARE reel_stmt;
-- mes_pro_work_order.reel_no
SET @reel_ddl = IF(EXISTS (SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mes_pro_work_order' AND COLUMN_NAME = 'reel_no'), 'SELECT ''mes_pro_work_order.reel_no å·²å­˜åœ¨ï¼Œè·³è¿‡'' AS message', 'ALTER TABLE `mes_pro_work_order` ADD COLUMN `reel_no` varchar(128) DEFAULT NULL COMMENT ''销售明细盘号快照''');
PREPARE reel_stmt FROM @reel_ddl;
EXECUTE reel_stmt;
DEALLOCATE PREPARE reel_stmt;
-- mes_qc_ipqc.reel_no
SET @reel_ddl = IF(EXISTS (SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mes_qc_ipqc' AND COLUMN_NAME = 'reel_no'), 'SELECT ''mes_qc_ipqc.reel_no å·²å­˜åœ¨ï¼Œè·³è¿‡'' AS message', 'ALTER TABLE `mes_qc_ipqc` ADD COLUMN `reel_no` varchar(128) DEFAULT NULL COMMENT ''销售明细盘号快照''');
PREPARE reel_stmt FROM @reel_ddl;
EXECUTE reel_stmt;
DEALLOCATE PREPARE reel_stmt;
-- mes_qc_oqc.reel_no
SET @reel_ddl = IF(EXISTS (SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mes_qc_oqc' AND COLUMN_NAME = 'reel_no'), 'SELECT ''mes_qc_oqc.reel_no å·²å­˜åœ¨ï¼Œè·³è¿‡'' AS message', 'ALTER TABLE `mes_qc_oqc` ADD COLUMN `reel_no` varchar(128) DEFAULT NULL COMMENT ''销售明细盘号快照''');
PREPARE reel_stmt FROM @reel_ddl;
EXECUTE reel_stmt;
DEALLOCATE PREPARE reel_stmt;
-- mes_wm_product_produce_line.reel_no
SET @reel_ddl = IF(EXISTS (SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mes_wm_product_produce_line' AND COLUMN_NAME = 'reel_no'), 'SELECT ''mes_wm_product_produce_line.reel_no å·²å­˜åœ¨ï¼Œè·³è¿‡'' AS message', 'ALTER TABLE `mes_wm_product_produce_line` ADD COLUMN `reel_no` varchar(128) DEFAULT NULL COMMENT ''销售明细盘号快照''');
PREPARE reel_stmt FROM @reel_ddl;
EXECUTE reel_stmt;
DEALLOCATE PREPARE reel_stmt;
-- mes_wm_product_produce_detail.reel_no
SET @reel_ddl = IF(EXISTS (SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mes_wm_product_produce_detail' AND COLUMN_NAME = 'reel_no'), 'SELECT ''mes_wm_product_produce_detail.reel_no å·²å­˜åœ¨ï¼Œè·³è¿‡'' AS message', 'ALTER TABLE `mes_wm_product_produce_detail` ADD COLUMN `reel_no` varchar(128) DEFAULT NULL COMMENT ''销售明细盘号快照''');
PREPARE reel_stmt FROM @reel_ddl;
EXECUTE reel_stmt;
DEALLOCATE PREPARE reel_stmt;
-- mes_wm_batch.reel_no
SET @reel_ddl = IF(EXISTS (SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mes_wm_batch' AND COLUMN_NAME = 'reel_no'), 'SELECT ''mes_wm_batch.reel_no å·²å­˜åœ¨ï¼Œè·³è¿‡'' AS message', 'ALTER TABLE `mes_wm_batch` ADD COLUMN `reel_no` varchar(128) DEFAULT NULL COMMENT ''生产追溯盘号''');
PREPARE reel_stmt FROM @reel_ddl;
EXECUTE reel_stmt;
DEALLOCATE PREPARE reel_stmt;
SET @reel_ddl = NULL;
sql/mysql/20261027_02_query_permissions_preview.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,243 @@
-- åªè¯»é¢„览:查询权限清理脚本将影响的按钮和角色授权数量。
-- æ³¨æ„ï¼šå®Œæ•´å‰ç«¯ä»å¼•用其中多种权限,请勿据此直接执行删除脚本。
SELECT m.id, m.name, m.permission, m.parent_id, m.deleted,
       COUNT(rm.id) AS role_menu_rows
FROM system_menu m
LEFT JOIN system_role_menu rm ON rm.menu_id = m.id
WHERE m.type = 3 AND m.permission IN (
    'aftersales:problem:query',
    'aftersales:return:query',
    'aftersales:ticket:query',
    'aftersales:visit:query',
    'ai:api-key:query',
    'ai:knowledge:query',
    'ai:model:query',
    'bi:chart-config:query',
    'bi:dashboard:query',
    'bi:data-source-config:query',
    'bpm:business-attach:query',
    'bpm:category:query',
    'bpm:form:query',
    'bpm:model:query',
    'bpm:oa-leave:query',
    'bpm:oa-notice:query',
    'bpm:process-auto-approve:query',
    'bpm:process-definition:query',
    'bpm:process-expression:query',
    'bpm:process-instance-cc:query',
    'bpm:process-listener:query',
    'bpm:task:query',
    'bpm:user-group:query',
    'crm:business-status:query',
    'crm:business:query',
    'crm:clue:query',
    'crm:contact:query',
    'crm:contract-config:query',
    'crm:contract:query',
    'crm:customer-limit-config:query',
    'crm:customer-pool-config:query',
    'crm:customer:query',
    'crm:invoice:query',
    'crm:receivable-plan:query',
    'crm:receivable:query',
    'crm:sale-quotation:query',
    'crm:sales-target:query',
    'crm:statistics-customer:query',
    'crm:statistics-funnel:query',
    'crm:statistics-performance:query',
    'crm:statistics-portrait:query',
    'crm:statistics-rank:query',
    'energy:alert:query',
    'energy:analysis:query',
    'energy:record:query',
    'erp:account:query',
    'erp:finance-payment:query',
    'erp:finance-receipt:query',
    'erp:purchase-invoice:query',
    'erp:purchase-order:query',
    'erp:purchase-request:query',
    'erp:sale-order:query',
    'erp:statistics:query',
    'erp:supplier:query',
    'finance:account-subject:query',
    'finance:fixed-asset:query',
    'finance:intangible-asset:query',
    'finance:ledger:query',
    'finance:statistics:query',
    'finance:voucher:query',
    'hrm:attendance-exception:query',
    'hrm:attendance-holiday:query',
    'hrm:attendance-record:query',
    'hrm:attendance-rule:query',
    'hrm:attendance-summary:query',
    'hrm:employee-contract:query',
    'hrm:employee-schedule:query',
    'hrm:employee-social-security:query',
    'hrm:employee:query',
    'hrm:leave-type-config:query',
    'hrm:leave:query',
    'hrm:overtime:query',
    'hrm:resignation:query',
    'hrm:salary-calculation:query',
    'hrm:salary-payment:query',
    'hrm:salary-structure:query',
    'hrm:shift:query',
    'hrm:social-security-scheme:query',
    'hrm:social-security:query',
    'hrm:tax-rate-config:query',
    'hrm:transfer:query',
    'im:manager:channel-material:query',
    'im:manager:channel-message:query',
    'im:manager:channel:query',
    'im:manager:face-pack-item:query',
    'im:manager:face-pack:query',
    'im:manager:face-user-item:query',
    'im:manager:friend-request:query',
    'im:manager:friend:query',
    'im:manager:group-request:query',
    'im:manager:group:query',
    'im:manager:message:query',
    'im:manager:rtc:query',
    'im:manager:sensitive-word:query',
    'im:manager:statistics:query',
    'infra:api-access-log:query',
    'infra:api-error-log:query',
    'infra:codegen:query',
    'infra:config:query',
    'infra:data-source-config:query',
    'infra:demo01-contact:query',
    'infra:demo02-category:query',
    'infra:demo03-student:query',
    'infra:file-config:query',
    'infra:file:query',
    'infra:job:query',
    'mdm:brand:query',
    'mdm:item-category:query',
    'mdm:item:query',
    'mdm:unit:query',
    'mdm:warehouse:query',
    'member:config:query',
    'member:experience-record:query',
    'member:group:query',
    'member:level-record:query',
    'member:level:query',
    'member:tag:query',
    'member:user:query',
    'mes:cal-holiday:query',
    'mes:cal-plan:query',
    'mes:cal-team-shift:query',
    'mes:cal-team:query',
    'mes:dv-check-plan:query',
    'mes:dv-check-record:query',
    'mes:dv-machinery-type:query',
    'mes:dv-machinery:query',
    'mes:dv-mainten-record:query',
    'mes:dv-metering-check:query',
    'mes:dv-metering:query',
    'mes:dv-repair:query',
    'mes:dv-spare:query',
    'mes:dv-subject:query',
    'mes:home:query',
    'mes:md-item-type:query',
    'mes:md-item:query',
    'mes:md-unit-measure:query',
    'mes:md-workshop:query',
    'mes:md-workstation:query',
    'mes:pd-archive:query',
    'mes:pd-document-audit:query',
    'mes:pd-document:query',
    'mes:pd-process-param-template:query',
    'mes:pd-process-param:query',
    'mes:pd-project:query',
    'mes:pro-andon-config:query',
    'mes:pro-andon-record:query',
    'mes:pro-card:query',
    'mes:pro-feedback:query',
    'mes:pro-mps:query',
    'mes:pro-process:query',
    'mes:pro-route:query',
    'mes:pro-task-issue:query',
    'mes:pro-task:query',
    'mes:pro-work-order:query',
    'mes:pro-workrecord:query',
    'mes:qc-defect:query',
    'mes:qc-indicator:query',
    'mes:qc-inspection-ledger:query',
    'mes:qc-ipqc:query',
    'mes:qc-iqc:query',
    'mes:qc-ncr:query',
    'mes:qc-oqc:query',
    'mes:qc-pending-inspect:query',
    'mes:qc-rqc:query',
    'mes:qc-template:query',
    'mes:tm-tool-type:query',
    'mes:tm-tool:query',
    'mes:wm-arrival-notice:query',
    'mes:wm-barcode-config:query',
    'mes:wm-barcode:query',
    'mes:wm-batch:query',
    'mes:wm-item-receipt:query',
    'mes:wm-material-stock:query',
    'mes:wm-misc-issue:query',
    'mes:wm-outsource-issue:query',
    'mes:wm-outsource-receipt:query',
    'mes:wm-package:query',
    'mes:wm-product-issue:query',
    'mes:wm-product-produce:query',
    'mes:wm-product-receipt:query',
    'mes:wm-product-sales:query',
    'mes:wm-return-issue:query',
    'mes:wm-return-sales:query',
    'mes:wm-return-vendor:query',
    'mes:wm-sales-notice:query',
    'mes:wm-sn:query',
    'mes:wm-stock-taking-plan:query',
    'mes:wm-stock-taking-task:query',
    'mes:wm-transaction:query',
    'mes:wm-transfer:query',
    'mes:wm-warehouse:query',
    'mes:wm:misc-receipt:query',
    'point:record:query',
    'point:sign-in-config:query',
    'point:sign-in-record:query',
    'qc-report:instance:query',
    'qc-report:template:query',
    'srm:bid-award:query',
    'srm:bid-evaluation:query',
    'srm:bid-open:query',
    'srm:statistics:query',
    'srm:supplier-apply:query',
    'srm:supplier-certificate:query',
    'srm:supplier-contact:query',
    'srm:supplier-evaluation:query',
    'srm:supplier-quote:query',
    'srm:supplier:query',
    'srm:tender-bid:query',
    'srm:tender-config:query',
    'srm:tender-project:query',
    'system:dept:query',
    'system:dict:query',
    'system:login-log:query',
    'system:mail-account:query',
    'system:mail-log:query',
    'system:mail-template:query',
    'system:menu:query',
    'system:notice:query',
    'system:notify-message:query',
    'system:notify-template:query',
    'system:oauth2-client:query',
    'system:operate-log:query',
    'system:post:query',
    'system:role:query',
    'system:sms-channel:query',
    'system:sms-log:query',
    'system:sms-template:query',
    'system:social-client:query',
    'system:social-user:query',
    'system:storage-attachment:query',
    'system:tenant-package:query',
    'system:tenant:query',
    'system:user:query'
)
GROUP BY m.id, m.name, m.permission, m.parent_id, m.deleted
ORDER BY m.permission, m.id;
sql/mysql/20261027_sales_reel_no.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,10 @@
-- ç›˜å·ç”±é”€å”®è®¢å•产品明细手工录入,计划及工单保存下发时的快照。
-- æ‰§è¡Œå‰ç¡®è®¤å„列尚不存在并备份;历史记录保留 NULL,不推测盘号。
ALTER TABLE `erp_sale_order_items` ADD COLUMN `reel_no` varchar(128) DEFAULT NULL COMMENT '销售明细盘号';
ALTER TABLE `mes_pro_mps` ADD COLUMN `reel_no` varchar(128) DEFAULT NULL COMMENT '销售明细盘号快照';
ALTER TABLE `mes_pro_work_order` ADD COLUMN `reel_no` varchar(128) DEFAULT NULL COMMENT '销售明细盘号快照';
ALTER TABLE `mes_qc_ipqc` ADD COLUMN `reel_no` varchar(128) DEFAULT NULL COMMENT '销售明细盘号快照';
ALTER TABLE `mes_qc_oqc` ADD COLUMN `reel_no` varchar(128) DEFAULT NULL COMMENT '销售明细盘号快照';
ALTER TABLE `mes_wm_product_produce_line` ADD COLUMN `reel_no` varchar(128) DEFAULT NULL COMMENT '销售明细盘号快照';
ALTER TABLE `mes_wm_product_produce_detail` ADD COLUMN `reel_no` varchar(128) DEFAULT NULL COMMENT '销售明细盘号快照';
ALTER TABLE `mes_wm_batch` ADD COLUMN `reel_no` varchar(128) DEFAULT NULL COMMENT '生产追溯盘号';
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/sale/dto/ErpSaleOrderCreateReqDTO.java
@@ -46,6 +46,9 @@
        @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
        private Long productId;
        @Schema(description = "销售产品明细手工录入的盘号")
        private String reelNo;
        @Schema(description = "产品单位编号", example = "1")
        private Long productUnitId;
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/sale/dto/ErpSaleOrderItemRespDTO.java
@@ -18,6 +18,9 @@
    @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long productId;
    @Schema(description = "销售产品明细手工录入的盘号")
    private String reelNo;
    @Schema(description = "产品单位编号", example = "1")
    private Long productUnitId;
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/sale/dto/ErpSaleOrderRespDTO.java
@@ -10,6 +10,9 @@
    @Schema(description = "订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long id;
    @Schema(description = "销售订单创建人用户编号")
    private String creator;
    @Schema(description = "销售订单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "SO001")
    private String no;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderItemRespVO.java
@@ -21,6 +21,9 @@
    @Schema(description = "物料编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long productId;
    @Schema(description = "销售产品明细手工录入的盘号")
    private String reelNo;
    @Schema(description = "物料名称", example = "产品A")
    private String productName;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java
@@ -135,6 +135,9 @@
        @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
        private Long productId;
        @Schema(description = "销售产品明细手工录入的盘号")
        private String reelNo;
        @Schema(description = "产品单位单位", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
        private Long productUnitId;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderSaveReqVO.java
@@ -2,6 +2,7 @@
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.math.BigDecimal;
@@ -60,6 +61,10 @@
        @NotNull(message = "产品编号不能为空")
        private Long productId;
        @Schema(description = "销售产品明细手工录入的盘号")
        @Size(max = 128, message = "盘号不能超过128个字符")
        private String reelNo;
        @Schema(description = "产品单位单位", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
        @NotNull(message = "产品单位单位不能为空")
        private Long productUnitId;
@@ -82,4 +87,4 @@
    }
}
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleOrderItemDO.java
@@ -40,6 +40,9 @@
     * å…³è” {@link cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO#getId()}
     */
    private Long productId;
    /** é”€å”®äº§å“æ˜Žç»†æ‰‹å·¥å½•入的盘号快照。 */
    private String reelNo;
    /**
     * è®¡é‡å•位编号
     *
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java
@@ -568,6 +568,7 @@
        createReqDTO.setSaleOrderId(saleOrder.getId());
        createReqDTO.setSaleOrderNo(saleOrder.getNo());
        createReqDTO.setSaleOrderItemId(item.getId());
        createReqDTO.setReelNo(item.getReelNo());
        createReqDTO.setMdmItemId(item.getProductId());
        createReqDTO.setProductName(productName);
        createReqDTO.setQuantity(item.getCount());
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/mps/dto/MesProMpsCreateReqDTO.java
@@ -23,6 +23,9 @@
    @Schema(description = "销售订单明细ID", example = "200")
    private Long saleOrderItemId;
    @Schema(description = "销售明细手工填写的盘号")
    private String reelNo;
    @Schema(description = "MDM ç‰©æ–™ID(销售订单明细的 productId)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
    private Long mdmItemId;
@@ -41,4 +44,4 @@
    @Schema(description = "备注", example = "自动生成")
    private String remark;
}
}
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/mps/dto/MesProMpsRespDTO.java
@@ -15,6 +15,9 @@
@Data
public class MesProMpsRespDTO {
    @Schema(description = "盘号")
    private String reelNo;
    @Schema(description = "MPS ç¼–号", example = "1024")
    private Long id;
@@ -66,4 +69,4 @@
    @Schema(description = "合并批次号", example = "MERGE-001")
    private String mergeBatchNo;
}
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/mps/MesProMpsApiImpl.java
@@ -39,7 +39,8 @@
                createReqDTO.getQuantity(),
                createReqDTO.getClientId(),
                createReqDTO.getRequestDate(),
                createReqDTO.getRemark()
                createReqDTO.getRemark(),
                createReqDTO.getReelNo()
        );
    }
@@ -117,4 +118,4 @@
        return mpsService.checkAndFinishMps(mpsId);
    }
}
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/feedback/MesProFeedbackController.java
@@ -331,7 +331,7 @@
                    findAndThen(processCheckMap, vo.getProcessId(), vo::setCheckFlag));
            // å·¥å•
            findAndThen(workOrderMap, vo.getWorkOrderId(), wo ->
                    vo.setWorkOrderCode(wo.getCode()).setWorkOrderName(wo.getName()));
                    vo.setWorkOrderCode(wo.getCode()).setWorkOrderName(wo.getName()).setReelNo(wo.getReelNo()));
            // ä»»åŠ¡
            findAndThen(taskMap, vo.getTaskId(), task ->
                    vo.setTaskCode(task.getCode()));
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/feedback/vo/MesProFeedbackBatchCreateReqVO.java
@@ -6,6 +6,7 @@
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - MES æ‰¹é‡æŠ¥å·¥ Request VO")
@@ -20,6 +21,9 @@
    @NotNull(message = "报工数量不能为空")
    private BigDecimal feedbackQuantity;
    @Schema(description = "报工时间(应用到所有任务),不填时使用当前时间")
    private LocalDateTime feedbackTime;
    @Schema(description = "备注", example = "批量报工")
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/feedback/vo/MesProFeedbackQuickCreateReqVO.java
@@ -5,6 +5,7 @@
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - MES å¿«é€ŸæŠ¥å·¥ Request VO")
@@ -19,6 +20,9 @@
    @NotNull(message = "报工数量不能为空")
    private BigDecimal feedbackQuantity;
    @Schema(description = "报工时间,不填时使用当前时间")
    private LocalDateTime feedbackTime;
    @Schema(description = "工艺参数值")
    private List<MesProFeedbackSaveReqVO.ParamValue> paramValues;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/feedback/vo/MesProFeedbackRespVO.java
@@ -79,6 +79,10 @@
    @ExcelProperty("工单编码")
    private String workOrderCode;
    @Schema(description = "销售明细盘号")
    @ExcelProperty("盘号")
    private String reelNo;
    @Schema(description = "工单名称", example = "博世螺丝刀")
    @ExcelProperty("工单名称")
    private String workOrderName;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/mps/MesProMpsController.java
@@ -34,15 +34,24 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.ArrayList;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_MPS_MERGE_ISSUE_DISABLED;
@Tag(name = "管理后台 - MES ç”Ÿäº§ä¸»è®¡åˆ’")
@RestController
@RequestMapping("/mes/pro-mps")
@Validated
@Slf4j
public class MesProMpsController {
    @Resource
@@ -120,7 +129,36 @@
    @Operation(summary = "合并下发生产主计划")
    @PreAuthorize("@ss.hasPermission('mes:pro-mps:merge-issue')")
    public CommonResult<Long> mergeAndIssueMps(@RequestBody List<Long> mpsIds) {
        return success(mpsService.mergeAndIssueMps(mpsIds));
        // æ—§å…¥å£ä¿ç•™ç”¨äºŽæ˜Žç¡®æç¤ºæ—§å®¢æˆ·ç«¯å‡çº§ï¼Œç¦æ­¢ç»•过新的逐条下发权限校验。
        throw exception(PRO_MPS_MERGE_ISSUE_DISABLED);
    }
    @PostMapping("/batch-issue")
    @Operation(summary = "批量逐条下发生产主计划")
    @PreAuthorize("@ss.hasPermission('mes:pro-mps:issue')")
    public CommonResult<List<BatchIssueResult>> batchIssueMps(@RequestBody @NotEmpty List<@NotNull Long> ids) {
        List<BatchIssueResult> results = new ArrayList<>();
        for (Long id : ids) {
            BatchIssueResult result = new BatchIssueResult();
            result.setMpsId(id);
            try {
                result.setWorkOrderId(mpsService.issueMps(id));
            } catch (ServiceException ex) {
                result.setError(ex.getMessage());
            } catch (Exception ex) {
                log.error("[batchIssueMps] ä¸‹å‘失败,mpsId={}", id, ex);
                result.setError("下发失败,请联系管理员查看日志");
            }
            results.add(result);
        }
        return success(results);
    }
    @Data
    public static class BatchIssueResult {
        private Long mpsId;
        private Long workOrderId;
        private String error;
    }
    @PostMapping("/cancel")
@@ -159,4 +197,4 @@
        });
    }
}
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/mps/vo/MesProMpsRespVO.java
@@ -46,6 +46,9 @@
    @Schema(description = "产品编号", example = "100")
    private Long productId;
    @Schema(description = "销售产品明细手工录入的盘号")
    private String reelNo;
    @Schema(description = "产品编码", example = "P-001")
    @ExcelProperty("产品编码")
    private String productCode;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/mps/vo/MesProMpsSaveReqVO.java
@@ -35,6 +35,9 @@
    @NotNull(message = "产品不能为空")
    private Long productId;
    @Schema(description = "销售产品明细手工录入的盘号")
    private String reelNo;
    @Schema(description = "计划生产数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
    @NotNull(message = "计划生产数量不能为空")
    private BigDecimal quantity;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/MesProWorkOrderRespVO.java
@@ -45,6 +45,9 @@
    @Schema(description = "产品编号", example = "100")
    private Long productId;
    @Schema(description = "销售产品明细手工录入的盘号")
    private String reelNo;
    @Schema(description = "产品名称", example = "电路板 A")
    @ExcelProperty("产品名称")
    private String productName;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/MesProWorkOrderSaveReqVO.java
@@ -38,6 +38,9 @@
    @NotNull(message = "产品不能为空")
    private Long productId;
    @Schema(description = "销售产品明细手工录入的盘号")
    private String reelNo;
    @Schema(description = "生产数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
    @NotNull(message = "生产数量不能为空")
    private BigDecimal quantity;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ipqc/vo/MesQcIpqcRespVO.java
@@ -55,6 +55,10 @@
    @Schema(description = "生产工单 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
    private Long workOrderId;
    @Schema(description = "销售明细盘号")
    @ExcelProperty("盘号")
    private String reelNo;
    @Schema(description = "工单编号", example = "WO20250101001")
    @ExcelProperty("工单编号")
    private String workOrderCode;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcRespVO.java
@@ -45,6 +45,10 @@
    @Schema(description = "来源单据编号", example = "DOC20250101001")
    private String sourceDocCode;
    @Schema(description = "盘号")
    @ExcelProperty("盘号")
    private String reelNo;
    // ========== å®¢æˆ· ==========
    @Schema(description = "客户 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcSaveReqVO.java
@@ -35,6 +35,10 @@
    @Schema(description = "来源单据行 ID", example = "300")
    private Long sourceLineId;
    @Schema(description = "盘号;关联销售出库时由销售明细带入")
    @Size(max = 128, message = "盘号不能超过128个字符")
    private String reelNo;
    // ========== å®¢æˆ· ==========
    @Schema(description = "客户 ID,可为空", example = "10")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/pendinginspect/vo/MesQcPendingInspectRespVO.java
@@ -59,6 +59,9 @@
    @Schema(description = "生产工单 ID", example = "10")
    private Long workOrderId;
    @Schema(description = "生产工单对应的销售明细盘号")
    private String reelNo;
    @Schema(description = "工作站 ID", example = "20")
    private Long workstationId;
@@ -84,4 +87,4 @@
    @Schema(description = "记录时间")
    private LocalDateTime recordTime;
}
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/batch/vo/MesWmBatchGenerateReqVO.java
@@ -24,6 +24,9 @@
     */
    private String batchCode;
    /** ç”Ÿäº§æ¥æºç›˜å·ï¼›éžç©ºæ—¶å³ä½¿ç‰©æ–™æœªå¯ç”¨æ™®é€šæ‰¹æ¬¡ç®¡ç†ä¹Ÿä¿ç•™è¿½æº¯æ‰¹æ¬¡ã€‚ */
    private String reelNo;
    /**
     * ç”Ÿäº§æ—¥æœŸ
     */
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/MesWmMaterialStockController.java
@@ -60,6 +60,9 @@
public class MesWmMaterialStockController {
    @Resource
    private cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService batchService;
    @Resource
    private MesWmMaterialStockService materialStockService;
    @Resource
    private MdmItemApi mdmItemApi;
@@ -181,8 +184,11 @@
        Map<Long, SrmSupplierRespDTO> vendorMap = srmSupplierApi.getSupplierList(
                convertSet(list, MesWmMaterialStockDO::getVendorId)).getCheckedData().stream()
                .collect(Collectors.toMap(SrmSupplierRespDTO::getId, s -> s));
        Map<Long, cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO> batchMap =
                batchService.getBatchMap(convertSet(list, MesWmMaterialStockDO::getBatchId));
        // 2. æž„建结果
        return BeanUtils.toBean(list, MesWmMaterialStockRespVO.class, vo -> {
            MapUtils.findAndThen(batchMap, vo.getBatchId(), batch -> vo.setReelNo(batch.getReelNo()));
            MapUtils.findAndThen(itemMap, vo.getItemId(), item -> {
                vo.setItemCode(item.getCode()).setItemName(item.getName()).setSpecification(item.getSpecification());
                MapUtils.findAndThen(unitMeasureMap, item.getUnitMeasureId(),
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/vo/MesWmMaterialStockRespVO.java
@@ -46,6 +46,10 @@
    @ExcelProperty("批次号")
    private String batchCode;
    @Schema(description = "生产追溯盘号;历史库存可能为空")
    @ExcelProperty("盘号")
    private String reelNo;
    @Schema(description = "仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long warehouseId;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/productproduce/vo/MesWmProductProduceLineRespVO.java
@@ -43,6 +43,9 @@
    @Schema(description = "批次号", example = "BATCH20260101001")
    private String batchCode;
    @Schema(description = "销售明细盘号")
    private String reelNo;
    @Schema(description = "有效期")
    private LocalDateTime expireDate;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/mps/MesProMpsDO.java
@@ -73,6 +73,9 @@
     */
    private Long productId;
    /** é”€å”®äº§å“æ˜Žç»†æ‰‹å·¥å½•入的盘号快照。 */
    private String reelNo;
    /**
     * è®¡åˆ’生产数量
     */
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/workorder/MesProWorkOrderDO.java
@@ -72,6 +72,9 @@
     * å…³è” {@link MesMdItemDO#getId()}
     */
    private Long productId;
    /** é”€å”®äº§å“æ˜Žç»†æ‰‹å·¥å½•入的盘号快照。 */
    private String reelNo;
    /**
     * ç”Ÿäº§æ•°é‡
     */
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/ipqc/MesQcIpqcDO.java
@@ -94,6 +94,9 @@
     * å…³è” {@link MesProWorkOrderDO#getId()}
     */
    private Long workOrderId;
    /** åˆ›å»ºæ£€éªŒå•时从生产工单带入的销售盘号快照。 */
    private String reelNo;
    /**
     * ç”Ÿäº§ä»»åŠ¡ ID
     *
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/oqc/MesQcOqcDO.java
@@ -75,6 +75,9 @@
     */
    private String sourceDocCode;
    /** é”€å”®æ˜Žç»†ç›˜å·å¿«ç…§ï¼›ç‹¬ç«‹æ£€éªŒå¯ç”±è´¨æ£€äººå‘˜å¡«å†™ã€‚ */
    private String reelNo;
    // ========== å®¢æˆ· ==========
    /**
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/batch/MesWmBatchDO.java
@@ -28,6 +28,9 @@
@AllArgsConstructor
public class MesWmBatchDO extends BaseDO {
    /** é”€å”®æ˜Žç»†ç›˜å·ï¼Œéžç©ºæ—¶æ‰¹æ¬¡æ‰¿æ‹…盘号库存追溯。 */
    private String reelNo;
    /**
     * æ‰¹æ¬¡ID
     */
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/productproduce/MesWmProductProduceDetailDO.java
@@ -62,6 +62,9 @@
     * æ‰¹æ¬¡å·
     */
    private String batchCode;
    /** ä»Žç”Ÿäº§äº§å‡ºè¡Œç»§æ‰¿çš„盘号快照。 */
    private String reelNo;
    /**
     * ä»“库 ID
     *
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/productproduce/MesWmProductProduceLineDO.java
@@ -62,6 +62,9 @@
     * æ‰¹æ¬¡å·
     */
    private String batchCode;
    /** ä»Žç”Ÿäº§å·¥å•带入的销售明细盘号快照。 */
    private String reelNo;
    /**
     * è¿‡æœŸæ—¥æœŸ
     */
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/feedback/MesProFeedbackMapper.java
@@ -21,6 +21,11 @@
@Mapper
public interface MesProFeedbackMapper extends BaseMapperX<MesProFeedbackDO> {
    default MesProFeedbackDO selectByIdForUpdate(Long id) {
        return selectOne(new LambdaQueryWrapperX<MesProFeedbackDO>()
                .eq(MesProFeedbackDO::getId, id).last("FOR UPDATE"));
    }
    default PageResult<MesProFeedbackDO> selectPage(MesProFeedbackPageReqVO reqVO) {
        // å½“按任务编码模糊查询时,需要 JOIN ä»»åŠ¡è¡¨
        if (StrUtil.isNotBlank(reqVO.getTaskCode())) {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/mps/MesProMpsMapper.java
@@ -23,6 +23,11 @@
@Mapper
public interface MesProMpsMapper extends BaseMapperX<MesProMpsDO> {
    default MesProMpsDO selectByIdForUpdate(Long id) {
        return selectOne(new LambdaQueryWrapperX<MesProMpsDO>()
                .eq(MesProMpsDO::getId, id).last("FOR UPDATE"));
    }
    default PageResult<MesProMpsDO> selectPage(MesProMpsPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesProMpsDO>()
                .likeIfPresent(MesProMpsDO::getCode, reqVO.getCode())
@@ -95,4 +100,4 @@
                .setSql("quantity_produced = IFNULL(quantity_produced, 0) + " + incrQuantity));
    }
}
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/batch/MesWmBatchMapper.java
@@ -95,6 +95,11 @@
    default MesWmBatchDO selectFirst(MesWmBatchDO batch) {
        LambdaQueryWrapper<MesWmBatchDO> query = new LambdaQueryWrapper<>();
        query.eq(MesWmBatchDO::getItemId, batch.getItemId());
        if (ObjUtil.isNull(batch.getReelNo())) {
            query.isNull(MesWmBatchDO::getReelNo);
        } else {
            query.eq(MesWmBatchDO::getReelNo, batch.getReelNo());
        }
        if (ObjUtil.isNull(batch.getVendorId())) {
            query.isNull(MesWmBatchDO::getVendorId);
        } else {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -60,6 +60,7 @@
    ErrorCode WM_BATCH_QUALITY_STATUS_REQUIRED = new ErrorCode(1_040_717_013, "批次配置要求质量状态不能为空");
    ErrorCode WM_BATCH_NOT_EXISTS = new ErrorCode(1_040_717_014, "批次不存在");
    ErrorCode WM_BATCH_ITEM_MISMATCH = new ErrorCode(1_040_717_015, "批次不属于当前物料");
    ErrorCode WM_BATCH_REEL_MISMATCH = new ErrorCode(1_040_717_018, "所选批次的盘号与生产订单盘号不一致");
    ErrorCode WM_BATCH_CLIENT_MISMATCH = new ErrorCode(1_040_717_016, "批次不属于当前客户");
    ErrorCode WM_BATCH_VENDOR_MISMATCH = new ErrorCode(1_040_717_017, "批次不属于当前供应商");
@@ -363,6 +364,8 @@
    ErrorCode PRO_MPS_NOT_ISSUED = new ErrorCode(1_040_509_002, "只有已下发状态的 MPS æ‰èƒ½æ‰§è¡Œæ­¤æ“ä½œ");
    ErrorCode PRO_MPS_ALREADY_ISSUED = new ErrorCode(1_040_509_003, "MPS å·²ä¸‹å‘,无法修改");
    ErrorCode PRO_MPS_MERGE_AT_LEAST_TWO = new ErrorCode(1_040_509_004, "合并下发至少需要选择两条 MPS");
    ErrorCode PRO_MPS_ISSUE_FORBIDDEN = new ErrorCode(1_040_509_010, "仅销售订单创建人或超级管理员可以下发生产计划");
    ErrorCode PRO_MPS_MERGE_ISSUE_DISABLED = new ErrorCode(1_040_509_011, "合并下发已停用,请使用批量下发");
    ErrorCode PRO_MPS_MERGE_PRODUCT_MISMATCH = new ErrorCode(1_040_509_005, "合并下发的 MPS å¿…须是相同产品");
    ErrorCode PRO_MPS_CODE_DUPLICATE = new ErrorCode(1_040_509_006, "MPS ç¼–码已存在");
    ErrorCode PRO_MPS_HAS_WORK_ORDER = new ErrorCode(1_040_509_007, "MPS å·²å…³è”生产工单,无法删除");
@@ -419,6 +422,7 @@
    ErrorCode QC_IPQC_SOURCE_LINE_NOT_BELONG = new ErrorCode(1_040_604_009, "来源单据行不属于该报工单");
    ErrorCode QC_IPQC_SOURCE_LINE_REQUIRED = new ErrorCode(1_040_604_010, "来源单据类型为报工时,来源产出行 ID ä¸èƒ½ä¸ºç©º");
    ErrorCode QC_IPQC_SOURCE_LINE_NOT_PENDING = new ErrorCode(1_040_604_011, "来源产出行不是待检验状态");
    ErrorCode QC_IPQC_WORK_ORDER_STATUS_INVALID = new ErrorCode(1_040_604_012, "仅已确认或已完成的生产工单可创建过程检验");
    // ========== MES è´¨é‡ç®¡ç†-过程检验行(1-040-604-100) ==========
    ErrorCode QC_IPQC_LINE_NOT_EXISTS = new ErrorCode(1_040_604_100, "过程检验行不存在");
@@ -597,6 +601,8 @@
    // ========== MES ä»“库管理-生产入库单(1-040-709-000) ==========
    ErrorCode WM_PRODUCT_PRODUCE_NOT_EXISTS = new ErrorCode(1_040_709_000, "生产入库单不存在");
    ErrorCode WM_PRODUCT_PRODUCE_STATUS_INVALID = new ErrorCode(1_040_709_001, "生产入库单状态不正确,无法执行该操作");
    ErrorCode WM_PRODUCT_PRODUCE_QUALITY_PENDING = new ErrorCode(1_040_709_006, "生产产出尚未检验,不能入库");
    ErrorCode WM_PRODUCT_PRODUCE_INSPECTION_INVALID = new ErrorCode(1_040_709_007, "检验结果与待检产出不匹配");
    ErrorCode WM_PRODUCT_PRODUCE_NO_LINE = new ErrorCode(1_040_709_002, "生产入库单至少需要一条行数据");
    ErrorCode WM_PRODUCT_PRODUCE_DETAIL_QUANTITY_MISMATCH = new ErrorCode(1_040_709_003, "生产入库单行数量与明细数量不一致");
    ErrorCode WM_PRODUCT_PRODUCE_CANCEL_NOT_ALLOWED = new ErrorCode(1_040_709_004, "生产入库单已完成或已取消,无法取消");
@@ -732,6 +738,8 @@
    ErrorCode WM_PRODUCT_SALES_LINE_QUANTITY_INVALID = new ErrorCode(1_040_714_014, "出库数量必须大于 0");
    ErrorCode WM_PRODUCT_SALES_DETAIL_LINE_NOT_MATCH = new ErrorCode(1_040_714_015, "拣货明细不属于指定的销售出库单");
    ErrorCode WM_PRODUCT_SALES_DETAIL_ITEM_MISMATCH = new ErrorCode(1_040_714_016, "拣货明细的物料与销售出库单行的物料不一致");
    ErrorCode WM_PRODUCT_SALES_REEL_MISMATCH = new ErrorCode(1_040_714_034, "所选库存盘号与销售明细盘号不一致,请重新选择可追溯库存");
    ErrorCode WM_PRODUCT_SALES_REEL_SOURCE_MISSING = new ErrorCode(1_040_714_035, "销售订单明细不存在,无法核对库存盘号");
    ErrorCode WM_PRODUCT_SALES_LINE_SALES_NOTICE_LINE_REQUIRED = new ErrorCode(1_040_714_017,
            "出库单关联了发货通知单,必须选择发货通知单行");
    ErrorCode WM_PRODUCT_SALES_LINE_SALES_NOTICE_LINE_NOT_ALLOWED = new ErrorCode(1_040_714_018,
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackService.java
@@ -99,13 +99,7 @@
     * <ol>
     *   <li>校验报工单状态和数量</li>
     *   <li>生成物料消耗单,扣减线边库物料</li>
     *   <li>根据工序配置(关键工序 & æ˜¯å¦éœ€è¦æ£€éªŒï¼‰åˆ†æƒ…况处理:
     *     <ul>
     *       <li><b>关键工序 + éœ€è¦æ£€éªŒ</b>:生成产出单(质量状态=待检验),报工单状态 â†’ å¾…检验,等待质检完成回调</li>
     *       <li><b>关键工序 + æ— éœ€æ£€éªŒ</b>:生成产出单(按合格/不合格拆行),执行产品入库,更新任务/工单已生产数量,报工单状态 â†’ å·²å®Œæˆ</li>
     *       <li><b>非关键工序</b>:不生成产出单,不更新任务/工单数量,报工单状态 â†’ å·²å®Œæˆ</li>
     *     </ul>
     *   </li>
     *   <li>生成待检产出,累计生产数量;人工质检完成后回写质量数量并处理入库</li>
     * </ol>
     *
     * @param reqVO å®¡æ‰¹è¯·æ±‚(含报工单编号及可选批次属性)
@@ -133,7 +127,7 @@
     * IPQC å®ŒæˆåŽå›žè°ƒï¼šå®ŒæˆæŠ¥å·¥å•并更新任务/工单进度
     *
     * <p>由 IPQC æ£€éªŒå®Œæˆæ—¶è°ƒç”¨ï¼Œå°†æŠ¥å·¥å•状态从待检验改为已完成,
     * å¹¶æ ¹æ®æ£€éªŒç»“果回写合格/不合格/废品数量,同时更新任务/工单的已生产数量。
     * å¹¶æ ¹æ®æ£€éªŒç»“果回写合格/不合格/废品数量;已生产数量在报工审批时累计。
     *
     * @param feedbackId          æŠ¥å·¥è®°å½• ID
     * @param sourceLineId        æ¥æºäº§å‡ºè¡Œ ID(用于直接定位待检行)
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java
@@ -308,27 +308,10 @@
        // 1.3 æ ¡éªŒä»»åŠ¡æœªå®Œæˆ
        taskService.validateTaskNotFinished(feedback.getTaskId());
        // 2.1 æŸ¥è¯¢å·¥åºçš„关键工序标识 + æ£€éªŒæ ‡è¯†ï¼ˆéœ€åœ¨ uncheckQuantity æ ¡éªŒä¹‹å‰ï¼Œå› ä¸ºè´¨æ£€å·¥åºå…è®¸ uncheckQuantity > 0)
        // å·¥åºæ£€éªŒæ ‡è¯†ä¸å†æŽ§åˆ¶äº§å‡ºå…¥åº“:所有新报工产出均需人工质检。
        MesProRouteProcessDO routeProcess = routeProcessService.getRouteProcessByRouteIdAndProcessId(
                feedback.getRouteId(), feedback.getProcessId());
        boolean keyFlag = routeProcess != null && Boolean.TRUE.equals(routeProcess.getKeyFlag());
        boolean checkFlag = routeProcess != null && Boolean.TRUE.equals(routeProcess.getCheckFlag());
        // 2.2 éžè´¨æ£€å·¥åºï¼šåˆæ ¼å“/不良品数量由质检确定;本工序不经过质检,报工未填写时默认全部为合格品
        if (!checkFlag
                && feedback.getQualifiedQuantity() == null
                && feedback.getUnqualifiedQuantity() == null) {
            feedback.setQualifiedQuantity(feedback.getFeedbackQuantity());
            feedback.setUnqualifiedQuantity(BigDecimal.ZERO);
            feedbackMapper.updateById(new MesProFeedbackDO().setId(feedback.getId())
                    .setQualifiedQuantity(feedback.getFeedbackQuantity())
                    .setUnqualifiedQuantity(BigDecimal.ZERO));
        }
        // 2.3 éžè´¨æ£€å·¥åºï¼šä»æœ‰å¾…检数量时不能审批(质检工序的 uncheckQuantity > 0 æ˜¯æ­£å¸¸çŠ¶æ€ï¼Œä¸åšæ‹¦æˆªï¼‰
        if (!checkFlag
                && feedback.getUncheckQuantity() != null
                && feedback.getUncheckQuantity().compareTo(BigDecimal.ZERO) > 0) {
            throw exception(PRO_FEEDBACK_UNCHECK_QUANTITY_EXISTS, feedback.getUncheckQuantity());
        }
        // 3. ç‰©æ–™æ¶ˆè€—:倒冲工序按本工序投料 BOM è‡ªåŠ¨æ‰£æ–™ï¼›éžå€’å†²å·¥åºè·³è¿‡ï¼ˆç‰©æ–™èµ°æ‰‹å·¥é¢†æ–™/投料)
        boolean backflushFlag = routeProcess != null && Boolean.TRUE.equals(routeProcess.getBackflushFlag());
@@ -347,10 +330,14 @@
            }
        }
        // 5. å…ˆæ›´æ–°æŠ¥å·¥çŠ¶æ€ä¸ºå·²å®Œæˆï¼ˆç¡®ä¿ checkAndFinishMps æ£€æŸ¥æ—¶å½“前报工已标记完成)
        // æŠ¥å·¥ä¸Žè´¨æ£€åˆ†å¼€ï¼šæŠ¥å·¥å®¡æ‰¹å®Œæˆå³ç´¯è®¡äº§é‡ï¼Œè´¨é‡æ•°é‡ç”±è´¨æ£€å›žå¡«ã€‚
        feedbackMapper.updateById(new MesProFeedbackDO().setId(reqVO.getId())
                .setStatus(MesProFeedbackStatusEnum.FINISHED.getStatus())
                .setUncheckQuantity(BigDecimal.ZERO));
                .setStatus(MesProFeedbackStatusEnum.UNCHECK.getStatus())
                .setQualifiedQuantity(BigDecimal.ZERO)
                .setUnqualifiedQuantity(BigDecimal.ZERO)
                .setUncheckQuantity(feedback.getFeedbackQuantity()));
        feedback.setQualifiedQuantity(BigDecimal.ZERO).setUnqualifiedQuantity(BigDecimal.ZERO);
        productProduceService.generateProductProduce(feedback, true, reqVO);
        // 6. æ ¹æ®æ˜¯å¦éœ€è¦æ£€éªŒï¼Œå†³å®šäº§å‡ºå’Œå…¥åº“方式
        // 6.2 è°ƒæ•´ åªè¦å®¡æ‰¹æˆåŠŸç›´æŽ¥æ›´æ–°
@@ -365,23 +352,8 @@
                checkAndPublishProductionFinished(feedback.getWorkOrderId());
        }
        if (checkFlag) {
            // 6.1 éœ€è¦æ£€éªŒï¼šç”Ÿæˆäº§å‡ºå•(质量状态=待检验),更新报工状态为待检验,等质检完成回调后再入库
            // å®Œæˆæ—¶å›žè°ƒè§ï¼šMesQcIpqcServiceImpl#finishIpqc â†’ splitPendingAndFinishProduce + updateProFeedbackWhenIpqcFinish
            productProduceService.generateProductProduce(feedback, true, reqVO);
            feedbackMapper.updateById(new MesProFeedbackDO().setId(reqVO.getId())
                    .setStatus(MesProFeedbackStatusEnum.UNCHECK.getStatus()));
            equipmentService.afterApproved(feedback);
            return false;
        } else {
            // 6.2 æ— éœ€æ£€éªŒï¼šç”Ÿæˆäº§å‡ºå•(按合格/不合格拆行),直接完成入库
            MesWmProductProduceDO produce = productProduceService.generateProductProduce(feedback, false, reqVO);
            productProduceService.finishProductProduce(produce.getId());
        }
        equipmentService.afterApproved(feedback);
        return true; // å·²å®Œæˆ
        return false; // äº§å‡ºç­‰å¾…质检,报工产量已累计
    }
    /**
@@ -397,28 +369,9 @@
     * @param feedback æŠ¥å·¥å•
     */
    private void updateTaskAndWorkOrderByFeedback(MesProFeedbackDO feedback) {
        // 1. æŸ¥è¯¢è¯¥æŠ¥å·¥å•关联的所有产出单行,按质量状态聚合数量
        BigDecimal qualifiedQty = BigDecimal.ZERO;
        BigDecimal unqualifiedQty = BigDecimal.ZERO;
        List<MesWmProductProduceLineDO> lines = produceLineService.getProductProduceLineListByFeedbackId(feedback.getId());
        for (MesWmProductProduceLineDO line : lines) {
            if (ObjUtil.equal(line.getQualityStatus(), MesWmQualityStatusEnum.FAIL.getStatus())) {
                unqualifiedQty = unqualifiedQty.add(line.getQuantity());
            }
            if (ObjUtil.equal(line.getQualityStatus(), MesWmQualityStatusEnum.PASS.getStatus())) {
                qualifiedQty = qualifiedQty.add(line.getQuantity());
            }
        }
        // 1.1 å…œåº•:关键非质检工序审批时产出单行尚未生成,直接采用报工单上的合格/不良数量
        //     ï¼ˆéžè´¨æ£€å·¥åºé»˜è®¤å…¨éƒ¨ä¸ºåˆæ ¼å“ï¼›è´¨æ£€å·¥åºçš„合格/不良在质检回调后按产出行聚合)
        if (CollUtil.isEmpty(lines)) {
            qualifiedQty = ObjectUtil.defaultIfNull(feedback.getQualifiedQuantity(), BigDecimal.ZERO);
            unqualifiedQty = ObjectUtil.defaultIfNull(feedback.getUnqualifiedQuantity(), BigDecimal.ZERO);
        }
        //   2. æ›´æ–°ä»»åŠ¡çš„å·²ç”Ÿäº§/合格/不合格数量
        // æŠ¥å·¥å®¡æ‰¹åªç´¯è®¡äº§é‡ï¼›åˆæ ¼ä¸Žä¸åˆæ ¼æ•°é‡åœ¨è´¨æ£€å®Œæˆæ—¶ç´¯è®¡ã€‚
        taskService.updateProducedQuantity(feedback.getTaskId(),
                feedback.getFeedbackQuantity(), qualifiedQty, unqualifiedQty);
                feedback.getFeedbackQuantity(), BigDecimal.ZERO, BigDecimal.ZERO);
        // 3. æ›´æ–°è®¢å•的已生产数量
        // æ–°å¢žé€»è¾‘:只有在该笔订单最后一个工序才更新生产订单的已生产数量
        MesProWorkOrderProcessDO lastProcess = workOrderProcessService.getLastWorkOrderProcessByWorkOrderId(feedback.getWorkOrderId());
@@ -443,12 +396,16 @@
    private void checkAndPublishProductionFinished(Long workOrderId) {
        // 1. èŽ·å–å·¥å•å…³è”çš„ MPS
        MesProMpsRespDTO mps = mpsApi.getMpsByWorkOrderId(workOrderId);
        if (mps == null || mps.getSaleOrderId() == null) {
        if (mps == null || mps.getSaleOrderId() == null
                || ObjUtil.equal(mps.getStatus(), cn.iocoder.yudao.module.mes.enums.pro.MesProMpsStatusEnum.FINISHED.getStatus())
                || ObjUtil.equal(mps.getStatus(), cn.iocoder.yudao.module.mes.enums.pro.MesProMpsStatusEnum.CANCELED.getStatus())) {
            return; // éžé”€å”®è®¢å•来源的工单,跳过
        }
        // 2. æ£€æŸ¥å¹¶è‡ªåŠ¨å®Œæˆ MPS(如果满足条件)
        mpsApi.checkAndFinishMps(mps.getId());
        if (!Boolean.TRUE.equals(mpsApi.checkAndFinishMps(mps.getId()))) {
            return;
        }
        // 3. æ£€æŸ¥é”€å”®è®¢å•所有生产是否完成
        if (mpsApi.checkSaleOrderProductionFinished(mps.getSaleOrderId())) {
@@ -515,7 +472,7 @@
            throw exception(PRO_FEEDBACK_ROUTE_PROCESS_INVALID);
        }
        // 4.1 æ ¡éªŒæ•°é‡ï¼šæ— è®ºæ˜¯å¦éœ€è¦æ£€éªŒï¼ŒæŠ¥å·¥åªéœ€å¡«æŠ¥å·¥æ•°é‡ï¼Œä¸”å¿…é¡» > 0
        // åˆæ ¼å“/不良品数量由质检确定(非质检工序在审批时默认全部为合格品)
        // åˆæ ¼å“/不良品数量由质检确定。
        if (reqVO.getFeedbackQuantity() == null
                || reqVO.getFeedbackQuantity().compareTo(BigDecimal.ZERO) <= 0) {
            throw exception(PRO_FEEDBACK_QUANTITY_MUST_POSITIVE);
@@ -626,7 +583,7 @@
                .setItemId(task.getItemId())
                .setFeedbackQuantity(reqVO.getFeedbackQuantity())
                .setScheduledQuantity(task.getQuantity())
                .setFeedbackTime(LocalDateTime.now())
                .setFeedbackTime(reqVO.getFeedbackTime() != null ? reqVO.getFeedbackTime() : LocalDateTime.now())
                .setStatus(MesProFeedbackStatusEnum.APPROVING.getStatus())
                .setRemark(reqVO.getRemark());
        equipmentService.validateSubmission(feedback);
@@ -639,7 +596,8 @@
        BigDecimal produced = task.getProducedQuantity() == null ? BigDecimal.ZERO : task.getProducedQuantity();
        BigDecimal inTransit = BigDecimal.ZERO;
        for (MesProFeedbackDO fb : getFeedbackListByTaskId(task.getId())) {
            if (!ObjUtil.equal(fb.getStatus(), MesProFeedbackStatusEnum.FINISHED.getStatus())) {
            if (!ObjUtil.equal(fb.getStatus(), MesProFeedbackStatusEnum.FINISHED.getStatus())
                    && !ObjUtil.equal(fb.getStatus(), MesProFeedbackStatusEnum.UNCHECK.getStatus())) {
                inTransit = inTransit.add(fb.getFeedbackQuantity() == null ? BigDecimal.ZERO : fb.getFeedbackQuantity());
            }
        }
@@ -662,6 +620,7 @@
                MesProFeedbackQuickCreateReqVO singleReq = new MesProFeedbackQuickCreateReqVO();
                singleReq.setTaskId(taskId);
                singleReq.setFeedbackQuantity(reqVO.getFeedbackQuantity());
                singleReq.setFeedbackTime(reqVO.getFeedbackTime());
                singleReq.setRemark(reqVO.getRemark());
                MesProFeedbackQuickCreateRespVO resp = quickCreateFeedback(singleReq);
                results.add(new MesProFeedbackBatchCreateRespVO.BatchResult()
@@ -689,7 +648,10 @@
                                                BigDecimal qualifiedQty, BigDecimal unqualifiedQty,
                                                BigDecimal laborScrapQty, BigDecimal materialScrapQty, BigDecimal otherScrapQty) {
        // 1. æ ¡éªŒæŠ¥å·¥å•存在且为待检验状态
        MesProFeedbackDO feedback = validateFeedbackExists(feedbackId);
        MesProFeedbackDO feedback = feedbackMapper.selectByIdForUpdate(feedbackId);
        if (feedback == null) {
            throw exception(PRO_FEEDBACK_NOT_EXISTS);
        }
        if (ObjUtil.notEqual(feedback.getStatus(), MesProFeedbackStatusEnum.UNCHECK.getStatus())) {
            throw exception(PRO_FEEDBACK_NOT_UNCHECK);
        }
@@ -707,8 +669,11 @@
                .setOtherScrapQuantity(otherScrapQty)
                .setStatus(MesProFeedbackStatusEnum.FINISHED.getStatus()));
        // 4. æ›´æ–°ä»»åŠ¡/工单的已生产数量
        // 4. äº§é‡å·²åœ¨æŠ¥å·¥å®¡æ‰¹æ—¶ç´¯è®¡ï¼Œæ­¤å¤„只回填质量数量。
        feedback.setQualifiedQuantity(qualifiedQty).setUnqualifiedQuantity(unqualifiedQty);
        taskService.updateQualityQuantity(feedback.getTaskId(),
                ObjectUtil.defaultIfNull(qualifiedQty, BigDecimal.ZERO),
                ObjectUtil.defaultIfNull(unqualifiedQty, BigDecimal.ZERO));
        //5.检查并发布生产完成事件
        checkAndPublishProductionFinished(feedback.getWorkOrderId());
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsService.java
@@ -34,7 +34,7 @@
     */
    Long createMps(Long saleOrderId, String saleOrderNo, Long saleOrderItemId,
                   Long mdmItemId, String productName, BigDecimal quantity, Long clientId,
                   String requestDate, String remark);
                   String requestDate, String remark, String reelNo);
    /**
     * åˆ›å»º MPS
@@ -73,6 +73,7 @@
     * @param mpsIds MPS ç¼–号列表
     * @return ç”Ÿæˆçš„工单 ID
     */
    @Deprecated
    Long mergeAndIssueMps(List<Long> mpsIds);
    /**
@@ -176,4 +177,4 @@
     */
    Boolean checkAndFinishMps(Long mpsId);
}
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsServiceImpl.java
@@ -5,13 +5,14 @@
import cn.hutool.core.util.StrUtil;
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.erp.api.sale.ErpSaleOrderApi;
import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
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.MesProWorkOrderSaveReqVO;
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;
@@ -69,7 +70,9 @@
    @Resource
    private MesProMpsMapper mpsMapper;
    @Resource
    private MesProMpsMergeItemMapper mergeItemMapper;
    private ErpSaleOrderApi saleOrderApi;
    @Resource
    private PermissionApi permissionApi;
    @Resource
    private MesProWorkOrderService workOrderService;
    @Resource
@@ -93,7 +96,7 @@
    @Transactional(rollbackFor = Exception.class)
    public Long createMps(Long saleOrderId, String saleOrderNo, Long saleOrderItemId,
                          Long mdmItemId, String productName, BigDecimal quantity, Long clientId,
                          String requestDate, String remark) {
                          String requestDate, String remark, String reelNo) {
        // 1. æ ¹æ® MDM ç‰©æ–™ ID æŸ¥æ‰¾ MES äº§å“
        cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO mesItem = itemService.getItemByMdmItemId(mdmItemId);
        if (mesItem == null) {
@@ -121,6 +124,7 @@
        mps.setSaleOrderId(saleOrderId);
        mps.setSaleOrderNo(saleOrderNo);
        mps.setSaleOrderItemId(saleOrderItemId);
        mps.setReelNo(reelNo);
        mps.setProductId(mesItem.getId()); // ä½¿ç”¨ MES äº§å“ ID
        mps.setQuantity(quantity);
        mps.setQuantityProduced(BigDecimal.ZERO);
@@ -194,7 +198,14 @@
    @Transactional(rollbackFor = Exception.class)
    public Long issueMps(Long id) {
        // æ ¡éªŒå­˜åœ¨ + è‰ç¨¿çŠ¶æ€
        MesProMpsDO mps = validateMpsExistsAndDraft(id);
        MesProMpsDO mps = mpsMapper.selectByIdForUpdate(id);
        if (mps == null) {
            throw exception(PRO_MPS_NOT_EXISTS);
        }
        if (!MesProMpsStatusEnum.DRAFT.getStatus().equals(mps.getStatus())) {
            throw exception(PRO_MPS_NOT_DRAFT);
        }
        validateIssuePermission(mps);
        // åˆ›å»ºç”Ÿäº§å·¥å•
        Long workOrderId = createWorkOrderFromMps(mps, mps.getQuantity());
@@ -210,66 +221,30 @@
        return workOrderId;
    }
    private void validateIssuePermission(MesProMpsDO mps) {
        Long userId = SecurityFrameworkUtils.getLoginUserId();
        if (userId == null) {
            throw exception(PRO_MPS_ISSUE_FORBIDDEN);
        }
        if (permissionApi.hasAnyRoles(userId, "super_admin")) {
            return;
        }
        // æ‰‹å·¥åˆ›å»ºä¸”没有销售订单的计划以计划创建人为准。
        String creator = mps.getCreator();
        if (mps.getSaleOrderId() != null) {
            var order = saleOrderApi.getSaleOrder(mps.getSaleOrderId()).getCheckedData();
            creator = order != null ? order.getCreator() : null;
        }
        if (!String.valueOf(userId).equals(creator)) {
            throw exception(PRO_MPS_ISSUE_FORBIDDEN);
        }
    }
    /** æ—§åˆå¹¶ä¸‹å‘入口已停用;历史合并记录仍由取消和查询逻辑处理。 */
    @Deprecated
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long mergeAndIssueMps(List<Long> mpsIds) {
        // æ ¡éªŒ MPS åˆ—表
        if (CollUtil.isEmpty(mpsIds) || mpsIds.size() < 2) {
            throw exception(PRO_MPS_MERGE_AT_LEAST_TWO);
        }
        List<MesProMpsDO> mpsList = mpsMapper.selectBatchIds(mpsIds);
        if (mpsList.size() != mpsIds.size()) {
            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 (productId == null) {
                productId = mps.getProductId();
            } else if (!productId.equals(mps.getProductId())) {
                throw exception(PRO_MPS_MERGE_PRODUCT_MISMATCH);
            }
        }
        // è®¡ç®—合并数量
        BigDecimal totalQuantity = mpsList.stream()
                .map(MesProMpsDO::getQuantity)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        // å–第一条 MPS ä½œä¸ºä¸»è®°å½•,创建生产工单
        MesProMpsDO firstMps = mpsList.get(0);
        Long workOrderId = createWorkOrderFromMps(firstMps, totalQuantity);
        // ç”Ÿæˆåˆå¹¶æ‰¹æ¬¡å·
        String mergeBatchNo = generateMergeBatchNo();
        // æ›´æ–°æ‰€æœ‰ MPS çŠ¶æ€ï¼Œè®°å½•åˆå¹¶å…³ç³»
        for (MesProMpsDO mps : mpsList) {
            mpsMapper.updateById(new MesProMpsDO()
                    .setId(mps.getId())
                    .setStatus(MesProMpsStatusEnum.ISSUED.getStatus())
                    .setWorkOrderId(workOrderId)
                    .setMergeFlag(1)
                    .setMergeBatchNo(mergeBatchNo)
                    .setIssueDate(LocalDateTime.now()));
            // è®°å½•合并明细
            MesProMpsMergeItemDO mergeItem = new MesProMpsMergeItemDO();
            mergeItem.setMergeBatchNo(mergeBatchNo);
            mergeItem.setMpsId(mps.getId());
            mergeItem.setWorkOrderId(workOrderId);
            mergeItem.setQuantity(mps.getQuantity());
            mergeItemMapper.insert(mergeItem);
        }
        log.info("[mergeAndIssueMps] MPS åˆå¹¶ä¸‹å‘成功,mpsIds={}, workOrderId={}, totalQuantity={}",
                mpsIds, workOrderId, totalQuantity);
        return workOrderId;
        throw exception(PRO_MPS_MERGE_ISSUE_DISABLED);
    }
    @Override
@@ -452,12 +427,13 @@
                    mps.getId(), quantityProduced, mps.getQuantity());
            return false;
        }
        // 2. æ£€æŸ¥å·¥å•下所有报工是否完成(确保质检完成)
        // 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()));
                            .notIn(MesProFeedbackDO::getStatus, MesProFeedbackStatusEnum.FINISHED.getStatus(),
                                    MesProFeedbackStatusEnum.UNCHECK.getStatus()));
            log.debug("[isMpsFinished] MPS {} unfinished feedback count: {}", mps.getId(), unfinishedCount);
            if (unfinishedCount > 0) {
                return false;
@@ -481,9 +457,8 @@
        }
    }
    /**
     * ç”Ÿæˆåˆå¹¶æ‰¹æ¬¡å·
     */
    /** å·²åœç”¨ï¼šæ—§åˆå¹¶ä¸‹å‘批次号生成逻辑,仅供识别历史实现,不再调用。 */
    @Deprecated
    private String generateMergeBatchNo() {
        return "MERGE" + System.currentTimeMillis();
    }
@@ -555,6 +530,7 @@
        workOrderReq.setOrderSourceType(MesProWorkOrderSourceTypeEnum.ORDER.getType());
        workOrderReq.setOrderSourceCode(mps.getSaleOrderNo());
        workOrderReq.setProductId(mps.getProductId());
        workOrderReq.setReelNo(mps.getReelNo());
        workOrderReq.setQuantity(quantity);
        workOrderReq.setClientId(mps.getClientId());
        workOrderReq.setRequestDate(mps.getRequestDate() != null ? mps.getRequestDate() : LocalDateTime.now());
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskService.java
@@ -144,6 +144,9 @@
                                BigDecimal incrQualifyQuantity,
                                BigDecimal incrUnqualifyQuantity);
    /** è´¨æ£€ç»“果回填,仅更新质量数量,不修改产量或报工时间。 */
    void updateQualityQuantity(Long id, BigDecimal qualifiedQuantity, BigDecimal unqualifiedQuantity);
    /**
     * ä¸€é”®æŽ’产预览
     *
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskServiceImpl.java
@@ -478,6 +478,12 @@
    }
    @Override
    public void updateQualityQuantity(Long id, BigDecimal qualifiedQuantity, BigDecimal unqualifiedQuantity) {
        validateTaskExists(id);
        taskMapper.updateProducedQuantity(id, BigDecimal.ZERO, qualifiedQuantity, unqualifiedQuantity);
    }
    @Override
    public void updateProducedQuantity(Long id,
                                       BigDecimal incrProducedQuantity,
                                       BigDecimal incrQualifyQuantity,
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ipqc/MesQcIpqcServiceImpl.java
@@ -164,6 +164,7 @@
        // 2. æ’入主表
        MesQcIpqcDO ipqc = BeanUtils.toBean(createReqVO, MesQcIpqcDO.class)
                .setItemId(itemId)
                .setReelNo(workOrder.getReelNo())
                .setProcessId(getProcessId(itemId, ctx.workstation().getProcessId()))
                .setTemplateId(templateId)
                .setSourceDocCode(sourceDocCode)
@@ -200,14 +201,21 @@
        MesMdWorkstationDO workstation = workstationService.validateWorkstationExists(reqVO.getWorkstationId());
        adminUserApi.validateUser(reqVO.getInspectorUserId());
        // æ ¡éªŒå·¥å•存在
        MesProWorkOrderDO workOrder = workOrderService.validateWorkOrderConfirmed(reqVO.getWorkOrderId());
        MesProWorkOrderDO workOrder = workOrderService.validateWorkOrderExists(reqVO.getWorkOrderId());
        if (!Objects.equals(workOrder.getStatus(), cn.iocoder.yudao.module.mes.enums.pro.MesProWorkOrderStatusEnum.CONFIRMED.getStatus())
                && !Objects.equals(workOrder.getStatus(), cn.iocoder.yudao.module.mes.enums.pro.MesProWorkOrderStatusEnum.FINISHED.getStatus())) {
            throw exception(QC_IPQC_WORK_ORDER_STATUS_INVALID);
        }
        if (reqVO.getProcessId() != null && ObjUtil.notEqual(workstation.getProcessId(), reqVO.getProcessId())) {
            throw exception(PRO_WORKSTATION_PROCESS_MISMATCH);
        }
        // æ ¡éªŒä»»åŠ¡å…³ç³»ï¼ˆå¦‚æžœæŒ‡å®šäº†ä»»åŠ¡ï¼‰
        MesProTaskDO task = null;
        if (reqVO.getTaskId() != null) {
            task = taskService.validateTaskNotFinished(reqVO.getTaskId());
            task = taskService.validateTaskExists(reqVO.getTaskId());
            if (Objects.equals(task.getStatus(), cn.iocoder.yudao.module.mes.enums.pro.MesProTaskStatusEnum.CANCELED.getStatus())) {
                throw exception(PRO_TASK_ALREADY_FINISHED);
            }
            validateTaskRelation(task, workstation, workOrder, reqVO);
        } else {
            // æ— ä»»åŠ¡æ—¶ï¼ŒitemId ä»éœ€ä¸Žå·¥å•产品一致
@@ -295,22 +303,15 @@
        Long sourceDocId = ipqc.getSourceDocId();
        Long sourceLineId = ipqc.getSourceLineId();
        // å¦‚æžœ IPQC å•没有设置来源,尝试通过 taskId æŸ¥æ‰¾å…³è”的待检验报工
        if (sourceDocId == null && ipqc.getTaskId() != null) {
            MesProFeedbackDO feedback = feedbackService.getFeedbackListByTaskId(ipqc.getTaskId()).stream()
                    .filter(f -> MesProFeedbackStatusEnum.UNCHECK.getStatus().equals(f.getStatus()))
                    .findFirst().orElse(null);
            if (feedback != null) {
                sourceDocId = feedback.getId();
                log.info("[writeBackSourceDoc] IPQC({}) æœªè®¾ç½®æ¥æºå•据,通过 taskId({}) æ‰¾åˆ°å¾…检验报工({})",
                        ipqc.getId(), ipqc.getTaskId(), feedback.getId());
            }
        }
        if (sourceDocId == null) {
        // ç‹¬ç«‹æ£€éªŒä¸æŽ¨æµ‹å…³è”报工:同一任务可能有多次报工和不同盘号。
        if (ipqc.getSourceDocType() == null && sourceDocId == null && sourceLineId == null) {
            log.warn("[writeBackSourceDoc] IPQC({}) æ— æ³•确定来源报工,跳过回写", ipqc.getId());
            return;
        }
        if (!Objects.equals(ipqc.getSourceDocType(), MesBizTypeConstants.PRO_FEEDBACK)
                || sourceDocId == null || sourceLineId == null) {
            throw new IllegalArgumentException("过程检验的报工来源或产出行不完整");
        }
        // 2. å›žå†™æŠ¥å·¥
        feedbackService.updateProFeedbackWhenIpqcFinish(sourceDocId, sourceLineId,
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImpl.java
@@ -8,6 +8,7 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.operatelog.core.util.OperateLogUtils;
import cn.iocoder.yudao.module.erp.api.sale.ErpSaleOrderApi;
import cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.MesQcOqcImportExcelVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.MesQcOqcPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.MesQcOqcSaveReqVO;
@@ -67,6 +68,9 @@
    @Resource
    private MesQcOqcMapper oqcMapper;
    @Resource
    private ErpSaleOrderApi saleOrderApi;
    @Resource
    private MesQcTemplateItemService templateItemService;
@@ -129,6 +133,10 @@
                .setTemplateId(templateId)
                .setSourceDocCode(sourceDocCode)
                .setStatus(MesQcStatusEnum.DRAFT.getStatus());
        if (createReqVO.getSourceLineId() != null) {
            oqc.setReelNo(resolveSalesReelNo(productSalesLineService
                    .validateProductSalesLineExists(createReqVO.getSourceLineId())));
        }
        oqcMapper.insert(oqc);
        // 3. ä»Žæ¨¡æ¿æŒ‡æ ‡è‡ªåŠ¨ç”Ÿæˆæ£€éªŒè¡Œ
@@ -139,7 +147,7 @@
    @Override
    public void updateOqc(MesQcOqcSaveReqVO updateReqVO) {
        // 1.1 æ ¡éªŒå­˜åœ¨ + è‰ç¨¿çŠ¶æ€
        validateOqcStatusPrepare(updateReqVO.getId());
        MesQcOqcDO existingOqc = validateOqcStatusPrepare(updateReqVO.getId());
        // 1.2 æ ¡éªŒæ•°æ®
        validateOqcSaveData(updateReqVO);
@@ -148,6 +156,7 @@
        updateObj.setSourceDocType(null).setSourceDocId(null).setSourceLineId(null); // ä¸å…è®¸ä¿®æ”¹æ¥æºå•据
        updateObj.setTemplateId(null); // ä¸å…è®¸ä¿®æ”¹æ¨¡æ¿
        updateObj.setItemId(null); // ä¸å…è®¸ä¿®æ”¹ç‰©æ–™
        if (existingOqc.getSourceLineId() != null) updateObj.setReelNo(null); // æ¥æºç›˜å·ä¿æŒå¿«ç…§
        // ç¼–辑时允许清空客户;完成检验等局部更新仍保留原客户。
        updateObj.setClientId(null);
        oqcMapper.update(updateObj, new LambdaUpdateWrapper<MesQcOqcDO>()
@@ -385,6 +394,7 @@
        oqc.setClientId(clientId);
        oqc.setItemId(line.getItemId());
        oqc.setBatchCode(line.getBatchCode());
        oqc.setReelNo(resolveSalesReelNo(line));
        oqc.setOutQuantity(line.getQuantity());
        oqc.setCheckQuantity(BigDecimal.ZERO); // å¾…填写
        oqc.setQualifiedQuantity(BigDecimal.ZERO); // å¾…填写
@@ -403,6 +413,12 @@
        return oqc.getId();
    }
    private String resolveSalesReelNo(MesWmProductSalesLineDO line) {
        if (line == null || line.getSaleOrderItemId() == null) return null;
        var item = saleOrderApi.getSaleOrderItem(line.getSaleOrderItemId()).getCheckedData();
        return item == null ? null : item.getReelNo();
    }
    /**
     * èŽ·å–ç‰©æ–™ä¿¡æ¯ï¼ˆç¼–ç +名称)
     */
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/pendinginspect/MesQcPendingInspectServiceImpl.java
@@ -5,6 +5,7 @@
import cn.iocoder.yudao.module.mes.controller.admin.qc.pendinginspect.vo.MesQcPendingInspectPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.pendinginspect.vo.MesQcPendingInspectRespVO;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.pendinginspect.MesQcPendingInspectMapper;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@@ -19,6 +20,8 @@
    @Resource
    private MesQcPendingInspectMapper pendingInspectMapper;
    @Resource
    private MesProWorkOrderService workOrderService;
    @Override
    public PageResult<MesQcPendingInspectRespVO> getPendingInspectPage(MesQcPendingInspectPageReqVO pageReqVO) {
@@ -26,6 +29,15 @@
        IPage<MesQcPendingInspectRespVO> page = MyBatisUtils.buildPage(pageReqVO);
        pendingInspectMapper.selectQcPendingPage(page,
                pageReqVO.getSourceDocCode(), pageReqVO.getQcType(), pageReqVO.getItemId());
        var orderIds = page.getRecords().stream().map(MesQcPendingInspectRespVO::getWorkOrderId)
                .filter(java.util.Objects::nonNull).collect(java.util.stream.Collectors.toSet());
        if (!orderIds.isEmpty()) {
            var orders = workOrderService.getWorkOrderMap(orderIds);
            page.getRecords().forEach(row -> {
                var order = row.getWorkOrderId() == null ? null : orders.get(row.getWorkOrderId());
                if (order != null) row.setReelNo(order.getReelNo());
            });
        }
        // è¿”回结果
        return new PageResult<>(page.getRecords(), page.getTotal());
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/batch/MesWmBatchService.java
@@ -9,6 +9,9 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
import java.util.List;
import java.util.Collection;
import java.util.Map;
import java.util.stream.Collectors;
/**
 * æ‰¹æ¬¡ç®¡ç† Service æŽ¥å£
@@ -33,6 +36,12 @@
     */
    MesWmBatchDO getBatch(Long id);
    List<MesWmBatchDO> getBatchList(Collection<Long> ids);
    default Map<Long, MesWmBatchDO> getBatchMap(Collection<Long> ids) {
        return getBatchList(ids).stream().collect(Collectors.toMap(MesWmBatchDO::getId, batch -> batch));
    }
    /**
     * æ ¹æ®æ‰¹æ¬¡å·èŽ·å¾—æ‰¹æ¬¡
     *
@@ -47,7 +56,7 @@
     * æ ¹æ®ç‰©æ–™æ‰¹æ¬¡é…ç½®ï¼ŒæŸ¥è¯¢æˆ–生成批次记录
     *
     * @param reqVO æ‰¹æ¬¡å‚数(包含 itemId åŠå…¶ä»–可选属性)
     * @return æ‰¹æ¬¡è®°å½•(如果物料未启用批次管理则返回 null)
     * @return æ‰¹æ¬¡è®°å½•(未启用批次管理且未传盘号时返回 null)
     */
    MesWmBatchDO getOrGenerateBatchCode(MesWmBatchGenerateReqVO reqVO);
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/batch/MesWmBatchServiceImpl.java
@@ -84,11 +84,16 @@
    }
    @Override
    public List<MesWmBatchDO> getBatchList(java.util.Collection<Long> ids) {
        return CollUtil.isEmpty(ids) ? java.util.Collections.emptyList() : batchMapper.selectByIds(ids);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public MesWmBatchDO getOrGenerateBatchCode(MesWmBatchGenerateReqVO reqVO) {
        // 1.1 æŸ¥è¯¢ç‰©æ–™ä¿¡æ¯ï¼Œæ£€æŸ¥æ˜¯å¦å¯ç”¨æ‰¹æ¬¡ç®¡ç†
        MesMdmItemDO item = mdmItemService.validateItemExists(reqVO.getItemId());
        if (Boolean.FALSE.equals(item.getIsBatchManaged())) {
        if (Boolean.FALSE.equals(item.getIsBatchManaged()) && StrUtil.isBlank(reqVO.getReelNo())) {
            // æœªå¯ç”¨æ‰¹æ¬¡ç®¡ç†ï¼Œè¿”回 null
            return null;
        }
@@ -101,7 +106,8 @@
        MdmItemBatchConfigRespDTO config = itemBatchConfigApi.getItemBatchConfig(reqVO.getItemId());
        // 2. æ ¹æ®é…ç½®æ ¡éªŒå¿…填参数并清空不需要的参数
        MesWmBatchDO batch = MesWmBatchDO.builder().itemId(reqVO.getItemId()).build();
        String reelNo = StrUtil.isBlank(reqVO.getReelNo()) ? null : reqVO.getReelNo();
        MesWmBatchDO batch = MesWmBatchDO.builder().itemId(reqVO.getItemId()).reelNo(reelNo).build();
        if (config != null) {
            // ========== å¿…填校验(flag=true è¡¨ç¤ºè¯¥å­—段为必填) ==========
            // ç”Ÿäº§æ—¥æœŸ
@@ -214,6 +220,15 @@
                if (!existingBatch.getItemId().equals(reqVO.getItemId())) {
                    throw exception(WM_BATCH_ITEM_MISMATCH);
                }
                // å·²æœ‰æ‰¹æ¬¡ä¸èƒ½æ”¹å˜ç›˜å·ï¼Œé¿å…åŽ†å²åº“å­˜è¢«è¯¯æ ‡æˆ–ä¸åŒç›˜å·æ··åˆã€‚
                String existingReelNo = StrUtil.isBlank(existingBatch.getReelNo()) ? null : existingBatch.getReelNo();
                if (!ObjUtil.equal(existingReelNo, reelNo)) {
                    throw exception(WM_BATCH_REEL_MISMATCH);
                }
                if (existingReelNo != null && !ObjUtil.equal(existingBatch.getWorkOrderId(), reqVO.getWorkOrderId())) {
                    // å·²æœ‰è¿½æº¯æ‰¹æ¬¡ä¸èƒ½è¢«å¦ä¸€å·¥å•复用并覆盖来源。
                    throw exception(WM_BATCH_REEL_MISMATCH);
                }
                // æ›´æ–°æ‰¹æ¬¡å±žæ€§ï¼ˆä»…更新传入的非空值)
                updateBatchProperties(existingBatch, batch);
                return existingBatch;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productproduce/MesWmProductProduceService.java
@@ -30,7 +30,7 @@
     * æ ¹æ®æŠ¥å·¥è®°å½•,自动生成产品产出单(头 + è¡Œ + æ˜Žç»†ï¼‰
     *
     * @param feedback  æŠ¥å·¥è®°å½•
     * @param checkFlag æ˜¯å¦éœ€è¦æ£€éªŒï¼ˆtrue=待检验,false=按合格/不合格拆分行)
     * @param checkFlag æ—§å‚数,必须为 true;false çš„直接入库逻辑已停用
     * @param approveReqVO å®¡æ‰¹è¯·æ±‚(含可选批次属性,可为 null)
     * @return ç”Ÿæˆçš„产品产出单
     */
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productproduce/MesWmProductProduceServiceImpl.java
@@ -96,6 +96,13 @@
    private void createTransactionList(Long produceId, List<MesWmProductProduceLineDO> lines) {
        for (MesWmProductProduceLineDO line : lines) {
            if (MesWmQualityStatusEnum.FAIL.getStatus().equals(line.getQualityStatus())) {
                // ä¸åˆæ ¼äº§å‡ºä¿ç•™è®°å½•,交由不合格品处理,不生成合格库存流水。
                continue;
            }
            if (!MesWmQualityStatusEnum.PASS.getStatus().equals(line.getQualityStatus())) {
                throw exception(WM_PRODUCT_PRODUCE_QUALITY_PENDING);
            }
            // 1. æ ¡éªŒæ¯è¡Œæ˜Žç»†æ•°é‡ä¹‹å’Œç­‰äºŽè¡Œæ•°é‡
            List<MesWmProductProduceDetailDO> details = productProduceDetailService.getProductProduceDetailListByLineId(
                    line.getId());
@@ -121,6 +128,7 @@
    @Transactional(rollbackFor = Exception.class)
    public MesWmProductProduceDO generateProductProduce(MesProFeedbackDO feedback, boolean checkFlag,
                                                         MesProFeedbackApproveReqVO approveReqVO) {
        if (!checkFlag) throw exception(WM_PRODUCT_PRODUCE_QUALITY_PENDING);
        // 0. æŸ¥è¯¢å…³è”的工单(用于获取 clientId ç­‰ä¿¡æ¯ï¼‰
        MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(feedback.getWorkOrderId());
@@ -135,6 +143,7 @@
        // 2. èŽ·å–æˆ–ç”Ÿæˆæ‰¹æ¬¡ï¼ˆå®¡æ‰¹è¯·æ±‚ä¸­çš„æ‰¹æ¬¡å±žæ€§ä¼˜å…ˆï¼‰
        MesWmBatchGenerateReqVO batchReqVO = new MesWmBatchGenerateReqVO()
                .setItemId(feedback.getItemId())
                .setReelNo(workOrder != null ? workOrder.getReelNo() : null)
                .setProduceDate(LocalDate.now().atStartOfDay()) // æˆªæ–­åˆ°å½“天零点,确保同一天的报工生成相同批次号
                .setExpireDate(feedback.getExpireDate())
                .setWorkOrderId(feedback.getWorkOrderId())
@@ -160,68 +169,23 @@
        Long batchId = batch != null ? batch.getId() : null;
        String batchCode = batch != null ? batch.getCode() : null;
        WarehouseTarget outputTarget = resolveOutputTarget(feedback.getWorkOrderId(), feedback.getProcessId(), feedback.getItemId());
        // 3. æ ¹æ®æ˜¯å¦éœ€è¦æ£€éªŒåˆ†æ”¯å¤„理
        if (checkFlag) {
            // 3.1 éœ€è¦æ£€éªŒï¼šåˆ›å»ºä¸€æ¡è¡Œï¼ˆè´¨é‡çŠ¶æ€=待检验),不生成明细
            MesWmProductProduceLineDO line = buildProduceLine(produce, feedback, batchId, batchCode,
                    feedback.getFeedbackQuantity(), MesWmQualityStatusEnum.PENDING.getStatus());
            productProduceLineService.createProductProduceLine(line);
            // æ³¨æ„ï¼šcheckFlag=true æ—¶ä¸ç”Ÿæˆæ˜Žç»†è¡Œï¼Œç­‰ IPQC æ£€éªŒå®ŒæˆåŽç”± splitPendingAndFinishProduce æŒ‰è´¨é‡ç»“果拆行+生成明细
        } else {
            // 3.2 æ— éœ€æ£€éªŒï¼šæŒ‰åˆæ ¼å“/不合格品各生成一行 + æ˜Žç»†
            BigDecimal qualifiedQty = ObjectUtil.defaultIfNull(feedback.getQualifiedQuantity(), BigDecimal.ZERO);
            BigDecimal unqualifiedQty = ObjectUtil.defaultIfNull(feedback.getUnqualifiedQuantity(), BigDecimal.ZERO);
            // 3.2.0 å…œåº•:合格/不良数量都未填写时(报工只填报工数量),默认整单为合格品
            if (qualifiedQty.compareTo(BigDecimal.ZERO) <= 0 && unqualifiedQty.compareTo(BigDecimal.ZERO) <= 0) {
                qualifiedQty = feedback.getFeedbackQuantity();
            }
            // 3.2.1 ä¸åˆæ ¼å“è¡Œ + æ˜Žç»†
            if (unqualifiedQty.compareTo(BigDecimal.ZERO) > 0) {
                MesWmProductProduceLineDO unqualifiedLine = buildProduceLine(produce, feedback, batchId, batchCode,
                        unqualifiedQty, MesWmQualityStatusEnum.FAIL.getStatus());
                productProduceLineService.createProductProduceLine(unqualifiedLine);
                MesWmProductProduceDetailDO unqualifiedDetail = buildProduceDetail(produce, feedback, batchId, batchCode,
                        unqualifiedLine.getId(), unqualifiedQty,
                        outputTarget.warehouseId(), outputTarget.locationId(), outputTarget.areaId());
                productProduceDetailService.createProductProduceDetail(unqualifiedDetail);
            }
            // 3.2.2 åˆæ ¼å“è¡Œ + æ˜Žç»†
            if (qualifiedQty.compareTo(BigDecimal.ZERO) > 0) {
                MesWmProductProduceLineDO qualifiedLine = buildProduceLine(produce, feedback, batchId, batchCode,
                        qualifiedQty, MesWmQualityStatusEnum.PASS.getStatus());
                productProduceLineService.createProductProduceLine(qualifiedLine);
                MesWmProductProduceDetailDO qualifiedDetail = buildProduceDetail(produce, feedback, batchId, batchCode,
                        qualifiedLine.getId(), qualifiedQty,
                        outputTarget.warehouseId(), outputTarget.locationId(), outputTarget.areaId());
                productProduceDetailService.createProductProduceDetail(qualifiedDetail);
            }
        }
        // æ–°æŠ¥å·¥äº§å‡ºç»Ÿä¸€å¾…检,历史不检验即入库分支已移除。
        MesWmProductProduceLineDO line = buildProduceLine(produce, feedback, batchId, batchCode,
                feedback.getFeedbackQuantity(), MesWmQualityStatusEnum.PENDING.getStatus(),
                workOrder != null ? workOrder.getReelNo() : null);
        productProduceLineService.createProductProduceLine(line);
        return produce;
    }
    private MesWmProductProduceLineDO buildProduceLine(MesWmProductProduceDO produce, MesProFeedbackDO feedback,
                                                       Long batchId, String batchCode,
                                                       BigDecimal quantity, Integer qualityStatus) {
                                                       BigDecimal quantity, Integer qualityStatus, String reelNo) {
        return MesWmProductProduceLineDO.builder()
                .produceId(produce.getId()).feedbackId(feedback.getId())
                .itemId(feedback.getItemId()).quantity(quantity)
                .batchId(batchId).batchCode(batchCode)
                .batchId(batchId).batchCode(batchCode).reelNo(reelNo)
                .expireDate(feedback.getExpireDate()).lotNumber(feedback.getLotNumber())
                .qualityStatus(qualityStatus)
                .build();
    }
    private MesWmProductProduceDetailDO buildProduceDetail(MesWmProductProduceDO produce, MesProFeedbackDO feedback,
                                                           Long batchId, String batchCode,
                                                           Long lineId, BigDecimal quantity,
                                                           Long warehouseId, Long locationId, Long areaId) {
        return MesWmProductProduceDetailDO.builder()
                .produceId(produce.getId()).lineId(lineId)
                .itemId(feedback.getItemId()).quantity(quantity)
                .batchId(batchId).batchCode(batchCode)
                .warehouseId(warehouseId).locationId(locationId).areaId(areaId)
                .build();
    }
@@ -256,6 +220,14 @@
        }
        // 2. é€šè¿‡ sourceLineId ç›´æŽ¥å®šä½å¾…检验行
        MesWmProductProduceLineDO pendingLine = productProduceLineService.validateProductProduceLineExists(sourceLineId);
        if (!ObjUtil.equal(pendingLine.getProduceId(), produce.getId())
                || !ObjUtil.equal(pendingLine.getFeedbackId(), feedbackId)
                || !ObjUtil.equal(pendingLine.getQualityStatus(), MesWmQualityStatusEnum.PENDING.getStatus())
                || qualifiedQty == null || unqualifiedQty == null || pendingLine.getQuantity() == null
                || qualifiedQty.signum() < 0 || unqualifiedQty.signum() < 0
                || qualifiedQty.add(unqualifiedQty).compareTo(pendingLine.getQuantity()) != 0) {
            throw exception(WM_PRODUCT_PRODUCE_INSPECTION_INVALID);
        }
        WarehouseTarget outputTarget = resolveOutputTarget(produce.getWorkOrderId(), produce.getProcessId(), pendingLine.getItemId());
        // 3A. æƒ…况一:存在不合格品数量,需要拆分行
@@ -265,14 +237,14 @@
                    .produceId(produce.getId()).feedbackId(feedbackId)
                    .itemId(pendingLine.getItemId()).quantity(unqualifiedQty)
                    .batchId(pendingLine.getBatchId()).batchCode(pendingLine.getBatchCode())
                    .expireDate(pendingLine.getExpireDate()).lotNumber(pendingLine.getLotNumber())
                    .expireDate(pendingLine.getExpireDate()).lotNumber(pendingLine.getLotNumber()).reelNo(pendingLine.getReelNo())
                    .qualityStatus(MesWmQualityStatusEnum.FAIL.getStatus())
                    .build();
            productProduceLineService.createProductProduceLine(unqualifiedLine);
            productProduceDetailService.createProductProduceDetail(MesWmProductProduceDetailDO.builder()
                    .produceId(produce.getId()).lineId(unqualifiedLine.getId())
                    .itemId(pendingLine.getItemId()).quantity(unqualifiedQty)
                    .batchId(pendingLine.getBatchId()).batchCode(pendingLine.getBatchCode())
                    .batchId(pendingLine.getBatchId()).batchCode(pendingLine.getBatchCode()).reelNo(pendingLine.getReelNo())
                    .warehouseId(outputTarget.warehouseId()).locationId(outputTarget.locationId()).areaId(outputTarget.areaId())
                    .build());
            // 3A.2 åˆæ ¼å“ï¼šå¤ç”¨åŽŸå¾…æ£€è¡Œï¼Œæ›´æ–°æ•°é‡å’ŒçŠ¶æ€ + æ˜Žç»†
@@ -283,7 +255,7 @@
                productProduceDetailService.createProductProduceDetail(MesWmProductProduceDetailDO.builder()
                        .produceId(produce.getId()).lineId(pendingLine.getId())
                        .itemId(pendingLine.getItemId()).quantity(qualifiedQty)
                        .batchId(pendingLine.getBatchId()).batchCode(pendingLine.getBatchCode())
                        .batchId(pendingLine.getBatchId()).batchCode(pendingLine.getBatchCode()).reelNo(pendingLine.getReelNo())
                        .warehouseId(outputTarget.warehouseId()).locationId(outputTarget.locationId()).areaId(outputTarget.areaId())
                        .build());
            }
@@ -294,7 +266,7 @@
            productProduceDetailService.createProductProduceDetail(MesWmProductProduceDetailDO.builder()
                    .produceId(produce.getId()).lineId(pendingLine.getId())
                    .itemId(pendingLine.getItemId()).quantity(pendingLine.getQuantity())
                    .batchId(pendingLine.getBatchId()).batchCode(pendingLine.getBatchCode())
                    .batchId(pendingLine.getBatchId()).batchCode(pendingLine.getBatchCode()).reelNo(pendingLine.getReelNo())
                    .warehouseId(outputTarget.warehouseId()).locationId(outputTarget.locationId()).areaId(outputTarget.areaId())
                    .build());
        }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesDetailServiceImpl.java
@@ -40,6 +40,8 @@
    private MesWmMaterialStockService materialStockService;
    @Resource
    private MesWmWarehouseAreaService warehouseAreaService;
    @Resource
    private MesWmSalesReelGuard salesReelGuard;
    @Override
    public Long createProductSalesDetail(MesWmProductSalesDetailSaveReqVO createReqVO) {
@@ -126,6 +128,7 @@
        materialStockService.validateSelectedStock(
                reqVO.getMaterialStockId(), reqVO.getItemId(), reqVO.getBatchId(), reqVO.getBatchCode(),
                reqVO.getWarehouseId(), reqVO.getLocationId(), reqVO.getAreaId(), reqVO.getQuantity());
        salesReelGuard.validate(line, reqVO.getBatchId());
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesServiceImpl.java
@@ -79,6 +79,8 @@
    @Resource
    private ErpSaleOrderApi saleOrderApi;
    @Resource
    private MesWmSalesReelGuard salesReelGuard;
    @Override
    public Long createProductSales(MesWmProductSalesSaveReqVO createReqVO) {
@@ -214,6 +216,7 @@
            }
            // æ ¡éªŒåº“存充足并创建占用
            for (MesWmProductSalesDetailDO detail : details) {
                salesReelGuard.validate(line, detail.getBatchId());
                materialStockService.validateSelectedStock(
                        detail.getMaterialStockId(),
                        detail.getItemId(),
@@ -343,6 +346,13 @@
    private void createTransactionList(MesWmProductSalesDO sales) {
        List<MesWmProductSalesDetailDO> details = productSalesDetailService.getProductSalesDetailListBySalesId(sales.getId());
        for (MesWmProductSalesDetailDO detail : details) {
            MesWmProductSalesLineDO line = productSalesLineService.getProductSalesLine(detail.getLineId());
            if (line == null || !ObjUtil.equal(line.getSalesId(), sales.getId())) {
                throw exception(WM_PRODUCT_SALES_DETAIL_LINE_NOT_MATCH);
            }
            salesReelGuard.validate(line, detail.getBatchId());
        }
        wmTransactionService.createTransactionList(convertList(details, detail -> new MesWmTransactionSaveReqDTO()
                .setType(MesWmTransactionTypeEnum.OUT.getType()).setItemId(detail.getItemId())
                .setQuantity(detail.getQuantity().negate()) // å‡ºåº“数量为负数
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmSalesReelGuard.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.mes.service.wm.productsales;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.erp.api.sale.ErpSaleOrderApi;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesLineDO;
import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
/** æŒ‰é”€å”®æ˜Žç»†ç›˜å·æ ¡éªŒå®žé™…选中的库存批次;无盘号的历史单据保留原有流程。 */
@Service
public class MesWmSalesReelGuard {
    @Resource private ErpSaleOrderApi saleOrderApi;
    @Resource private MesWmBatchService batchService;
    public void validate(MesWmProductSalesLineDO line, Long batchId) {
        if (line.getSaleOrderItemId() == null) return;
        var item = saleOrderApi.getSaleOrderItem(line.getSaleOrderItemId()).getCheckedData();
        if (item == null) throw exception(WM_PRODUCT_SALES_REEL_SOURCE_MISSING);
        if (StrUtil.isBlank(item.getReelNo())) return;
        var batch = batchId == null ? null : batchService.getBatch(batchId);
        if (batch == null || !item.getReelNo().equals(batch.getReelNo())) {
            throw exception(WM_PRODUCT_SALES_REEL_MISMATCH);
        }
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/MesWmTransactionServiceImpl.java
@@ -96,7 +96,8 @@
            if (ObjUtil.notEqual(materialStock.getItemId(), reqDTO.getItemId())
                    || ObjUtil.notEqual(materialStock.getWarehouseId(), reqDTO.getWarehouseId())
                    || ObjUtil.notEqual(materialStock.getLocationId(), reqDTO.getLocationId())
                    || ObjUtil.notEqual(materialStock.getAreaId(), reqDTO.getAreaId())) {
                    || ObjUtil.notEqual(materialStock.getAreaId(), reqDTO.getAreaId())
                    || ObjUtil.notEqual(materialStock.getBatchId(), reqDTO.getBatchId())) {
                throw new ServiceException(WM_MATERIAL_STOCK_SELECTION_MISMATCH.getCode(),
                        "该库存记录已变动(可能被移库/合并/拆分),与出库明细不一致,无法扣减,请退回重新拣货");
            }
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImplTest.java
@@ -159,6 +159,7 @@
        assertEquals(0, otherScrapQty.compareTo(updatedFeedback.getOtherScrapQuantity()));
        // äº§é‡å·²ç»åœ¨æŠ¥å·¥å®¡æ‰¹æ—¶ç´¯è®¡ï¼Œè´¨æ£€å›žè°ƒä¸èƒ½é‡å¤ç´¯è®¡ã€‚
        verify(taskService).updateQualityQuantity(taskId, qualifiedQty, unqualifiedQty);
        verify(taskService, never()).updateProducedQuantity(anyLong(), any(), any(), any());
        verify(workOrderService, never()).updateProducedQuantity(anyLong(), any());
    }
@@ -267,35 +268,26 @@
        when(routeProcessService.getRouteProcessByRouteIdAndProcessId(routeId, processId))
                .thenReturn(routeProcess);
        // mock: äº§å“äº§å‡ºå•
        MesWmProductProduceDO produce = randomPojo(MesWmProductProduceDO.class);
        when(productProduceService.generateProductProduce(any(), eq(false), any())).thenReturn(produce);
        // mock: äº§å‡ºè¡Œï¼ˆç”¨äºŽ updateTaskAndWorkOrderByFeedback)
        // å…³é”®æœ«é“工序累计工单产量,质量数量等待检验。
        when(workOrderProcessService.getLastWorkOrderProcessByWorkOrderId(feedback.getWorkOrderId()))
                .thenReturn(new MesProWorkOrderProcessDO().setProcessId(processId));
        MesWmProductProduceLineDO qualifiedLine = MesWmProductProduceLineDO.builder()
                .quantity(BigDecimal.valueOf(80))
                .qualityStatus(MesWmQualityStatusEnum.PASS.getStatus()).build();
        when(produceLineService.getProductProduceLineListByFeedbackId(feedback.getId()))
                .thenReturn(ListUtil.of(qualifiedLine));
        // è°ƒç”¨
        MesProFeedbackApproveReqVO reqVO = new MesProFeedbackApproveReqVO();
        reqVO.setId(feedback.getId());
        boolean result = feedbackService.approveFeedback(reqVO);
        // æ–­è¨€ 1:返回 true(已完成)
        assertTrue(result);
        assertFalse(result);
        // æ–­è¨€ 2:状态为已完成,uncheckQuantity æ¸…é›¶
        // æœªç»æ£€éªŒçš„æŠ¥å·¥ä¸å…è®¸ç›´æŽ¥å…¥åº“。
        MesProFeedbackDO updated = feedbackMapper.selectById(feedback.getId());
        assertEquals(MesProFeedbackStatusEnum.FINISHED.getStatus(), updated.getStatus());
        assertEquals(0, BigDecimal.ZERO.compareTo(updated.getUncheckQuantity()));
        assertEquals(MesProFeedbackStatusEnum.UNCHECK.getStatus(), updated.getStatus());
        assertEquals(0, feedback.getFeedbackQuantity().compareTo(updated.getUncheckQuantity()));
        assertEquals(0, BigDecimal.ZERO.compareTo(updated.getQualifiedQuantity()));
        // æ–­è¨€ 3:调用了产出单生成 + å…¥åº“ + ä»»åŠ¡/工单更新
        verify(productProduceService).generateProductProduce(any(), eq(false), any());
        verify(productProduceService).finishProductProduce(produce.getId());
        verify(productProduceService).generateProductProduce(any(), eq(true), any());
        verify(productProduceService, never()).finishProductProduce(anyLong());
        verify(taskService).updateProducedQuantity(eq(feedback.getTaskId()),
                any(BigDecimal.class), any(BigDecimal.class), any(BigDecimal.class));
        verify(workOrderService).updateProducedQuantity(eq(feedback.getWorkOrderId()),
@@ -364,24 +356,21 @@
                .keyFlag(false).checkFlag(false).build();
        when(routeProcessService.getRouteProcessByRouteIdAndProcessId(routeId, processId))
                .thenReturn(routeProcess);
        MesWmProductProduceDO produce = randomPojo(MesWmProductProduceDO.class);
        when(productProduceService.generateProductProduce(any(), eq(false), any())).thenReturn(produce);
        // è°ƒç”¨
        MesProFeedbackApproveReqVO reqVO = new MesProFeedbackApproveReqVO();
        reqVO.setId(feedback.getId());
        boolean result = feedbackService.approveFeedback(reqVO);
        // æ–­è¨€ 1:返回 true(已完成)
        assertTrue(result);
        assertFalse(result);
        // æ–­è¨€ 2:状态为已完成
        MesProFeedbackDO updated = feedbackMapper.selectById(feedback.getId());
        assertEquals(MesProFeedbackStatusEnum.FINISHED.getStatus(), updated.getStatus());
        assertEquals(MesProFeedbackStatusEnum.UNCHECK.getStatus(), updated.getStatus());
        // éžå…³é”®å·¥åºä¹Ÿç”Ÿæˆäº§å‡ºå¹¶ç´¯è®¡ä»»åŠ¡è¿›åº¦ï¼Œä½†ä¸ç´¯è®¡å·¥å•æ€»äº§é‡ã€‚
        verify(productProduceService).generateProductProduce(any(), eq(false), any());
        verify(productProduceService).finishProductProduce(produce.getId());
        verify(productProduceService).generateProductProduce(any(), eq(true), any());
        verify(productProduceService, never()).finishProductProduce(anyLong());
        verify(taskService).updateProducedQuantity(eq(feedback.getTaskId()), any(), any(), any());
        verify(workOrderService, never()).updateProducedQuantity(anyLong(), any());
    }
@@ -407,15 +396,12 @@
        when(routeProcessService.getRouteProcessByRouteIdAndProcessId(routeId, processId))
                .thenReturn(routeProcess);
        // è°ƒç”¨ï¼Œåº”该抛异常
        // å¾…检数量不再阻止非检验标识工序报工。
        MesProFeedbackApproveReqVO reqVO = new MesProFeedbackApproveReqVO();
        reqVO.setId(feedback.getId());
        assertThrows(Exception.class, () ->
                feedbackService.approveFeedback(reqVO));
        // æ–­è¨€ï¼šä¸åº”该执行任何后续操作
        verify(productProduceService, never()).generateProductProduce(any(), anyBoolean(), any());
        verify(itemConsumeService, never()).generateItemConsume(any());
        assertFalse(feedbackService.approveFeedback(reqVO));
        verify(productProduceService).generateProductProduce(any(), eq(true), any());
        verify(productProduceService, never()).finishProductProduce(anyLong());
    }
    @Test
@@ -450,7 +436,7 @@
        // è´¨æ£€å·¥åºæ— è®ºæ˜¯å¦å…³é”®å·¥åºï¼Œéƒ½è¿›å…¥å¾…检验。
        MesProFeedbackDO updated = feedbackMapper.selectById(feedback.getId());
        assertEquals(MesProFeedbackStatusEnum.UNCHECK.getStatus(), updated.getStatus());
        assertEquals(0, BigDecimal.ZERO.compareTo(updated.getUncheckQuantity()));
        assertEquals(0, feedback.getFeedbackQuantity().compareTo(updated.getUncheckQuantity()));
        verify(productProduceService).generateProductProduce(any(), eq(true), any());
        verify(productProduceService, never()).finishProductProduce(anyLong());
@@ -601,9 +587,11 @@
        MesProFeedbackQuickCreateReqVO req = new MesProFeedbackQuickCreateReqVO();
        req.setTaskId(curTaskId);
        req.setFeedbackQuantity(BigDecimal.TEN);
        req.setFeedbackTime(java.time.LocalDateTime.of(2026, 10, 20, 9, 30));
        MesProFeedbackQuickCreateRespVO resp = feedbackService.quickCreateFeedback(req);
        assertNotNull(resp);
        assertNotNull(resp.getFeedbackId());
        assertEquals(req.getFeedbackTime(), feedbackMapper.selectById(resp.getFeedbackId()).getFeedbackTime());
        verify(autoCodeRecordService).generateAutoCode(anyString());
    }
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/ipqc/MesQcIpqcServiceImplTest.java
@@ -49,6 +49,11 @@
@Import(MesQcIpqcServiceImpl.class)
public class MesQcIpqcServiceImplTest extends BaseDbUnitTest {
    @MockitoBean
    private cn.iocoder.yudao.module.mes.service.qc.imports.MesQcImportHelper qcImportHelper;
    @MockitoBean
    private cn.iocoder.yudao.module.mes.service.qc.ncr.MesQcNcrService ncrService;
    @Resource
    private MesQcIpqcServiceImpl ipqcService;
@@ -272,12 +277,14 @@
        MesProWorkOrderDO workOrder = new MesProWorkOrderDO();
        workOrder.setId(workOrderId);
        workOrder.setProductId(productId);
        // ç”Ÿäº§å®ŒæˆåŽä»å¯æ‰‹å·¥è¡¥å½•质检。
        workOrder.setStatus(cn.iocoder.yudao.module.mes.enums.pro.MesProWorkOrderStatusEnum.FINISHED.getStatus());
        when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
        when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
        // mock æ¨¡æ¿åŒ¹é…
        MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
        templateItem.setTemplateId(templateId);
        when(templateItemService.getRequiredTemplateByItemIdAndType(eq(productId), anyInt())).thenReturn(templateItem);
        when(templateItemService.getTemplateItemList(eq(productId), anyInt(), isNull(), isNull())).thenReturn(java.util.List.of(templateItem));
        // mock å·¥è‰ºè·¯çº¿
        MesProRouteProductDO routeProduct = new MesProRouteProductDO();
        routeProduct.setRouteId(routeId);
@@ -322,6 +329,7 @@
        MesProWorkOrderDO workOrder = new MesProWorkOrderDO();
        workOrder.setId(workOrderId);
        workOrder.setProductId(productId);
        workOrder.setStatus(cn.iocoder.yudao.module.mes.enums.pro.MesProWorkOrderStatusEnum.CONFIRMED.getStatus());
        when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
        when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
        // mock ä»»åŠ¡
@@ -330,11 +338,11 @@
        task.setWorkOrderId(workOrderId);
        task.setWorkstationId(workstationId);
        task.setItemId(productId);
        when(taskService.validateTaskNotFinished(taskId)).thenReturn(task);
        when(taskService.validateTaskExists(taskId)).thenReturn(task);
        // mock æ¨¡æ¿åŒ¹é…
        MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
        templateItem.setTemplateId(templateId);
        when(templateItemService.getRequiredTemplateByItemIdAndType(eq(productId), anyInt())).thenReturn(templateItem);
        when(templateItemService.getTemplateItemList(eq(productId), anyInt(), isNull(), isNull())).thenReturn(java.util.List.of(templateItem));
        // è°ƒç”¨
        Long ipqcId = ipqcService.createIpqc(reqVO);
@@ -375,12 +383,13 @@
        MesProWorkOrderDO workOrder = new MesProWorkOrderDO();
        workOrder.setId(workOrderId);
        workOrder.setProductId(productId);
        workOrder.setStatus(cn.iocoder.yudao.module.mes.enums.pro.MesProWorkOrderStatusEnum.CONFIRMED.getStatus());
        when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
        when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
        // mock æ¨¡æ¿åŒ¹é…
        MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
        templateItem.setTemplateId(templateId);
        when(templateItemService.getRequiredTemplateByItemIdAndType(eq(productId), anyInt())).thenReturn(templateItem);
        when(templateItemService.getTemplateItemList(eq(productId), anyInt(), isNull(), isNull())).thenReturn(java.util.List.of(templateItem));
        // mock å·¥è‰ºè·¯çº¿å­˜åœ¨
        MesProRouteProductDO routeProduct = new MesProRouteProductDO();
        routeProduct.setRouteId(routeId);
@@ -428,11 +437,12 @@
        MesProWorkOrderDO workOrder = new MesProWorkOrderDO();
        workOrder.setId(workOrderId);
        workOrder.setProductId(productId);
        workOrder.setStatus(cn.iocoder.yudao.module.mes.enums.pro.MesProWorkOrderStatusEnum.CONFIRMED.getStatus());
        when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
        when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
        MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
        templateItem.setTemplateId(templateId);
        when(templateItemService.getRequiredTemplateByItemIdAndType(eq(productId), anyInt())).thenReturn(templateItem);
        when(templateItemService.getTemplateItemList(eq(productId), anyInt(), isNull(), isNull())).thenReturn(java.util.List.of(templateItem));
        when(feedbackService.validateFeedbackExists(feedbackId)).thenReturn(new MesProFeedbackDO());
        // è°ƒç”¨ï¼Œæ–­è¨€å¼‚常
@@ -470,11 +480,12 @@
        MesProWorkOrderDO workOrder = new MesProWorkOrderDO();
        workOrder.setId(workOrderId);
        workOrder.setProductId(productId);
        workOrder.setStatus(cn.iocoder.yudao.module.mes.enums.pro.MesProWorkOrderStatusEnum.CONFIRMED.getStatus());
        when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
        when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
        MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
        templateItem.setTemplateId(templateId);
        when(templateItemService.getRequiredTemplateByItemIdAndType(eq(productId), anyInt())).thenReturn(templateItem);
        when(templateItemService.getTemplateItemList(eq(productId), anyInt(), isNull(), isNull())).thenReturn(java.util.List.of(templateItem));
        when(feedbackService.validateFeedbackExists(feedbackId)).thenReturn(new MesProFeedbackDO());
        // mock:产出行存在但 feedbackId ä¸åŒ¹é…
        MesWmProductProduceLineDO line = new MesWmProductProduceLineDO();
@@ -518,11 +529,12 @@
        MesProWorkOrderDO workOrder = new MesProWorkOrderDO();
        workOrder.setId(workOrderId);
        workOrder.setProductId(productId);
        workOrder.setStatus(cn.iocoder.yudao.module.mes.enums.pro.MesProWorkOrderStatusEnum.CONFIRMED.getStatus());
        when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
        when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
        MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
        templateItem.setTemplateId(templateId);
        when(templateItemService.getRequiredTemplateByItemIdAndType(eq(productId), anyInt())).thenReturn(templateItem);
        when(templateItemService.getTemplateItemList(eq(productId), anyInt(), isNull(), isNull())).thenReturn(java.util.List.of(templateItem));
        when(feedbackService.validateFeedbackExists(feedbackId)).thenReturn(new MesProFeedbackDO());
        // mock:产出行存在且归属正确,但不是 PENDING çŠ¶æ€
        MesWmProductProduceLineDO line = new MesWmProductProduceLineDO();
@@ -566,11 +578,12 @@
        MesProWorkOrderDO workOrder = new MesProWorkOrderDO();
        workOrder.setId(workOrderId);
        workOrder.setProductId(productId);
        workOrder.setStatus(cn.iocoder.yudao.module.mes.enums.pro.MesProWorkOrderStatusEnum.CONFIRMED.getStatus());
        when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
        when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
        MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
        templateItem.setTemplateId(templateId);
        when(templateItemService.getRequiredTemplateByItemIdAndType(eq(productId), anyInt())).thenReturn(templateItem);
        when(templateItemService.getTemplateItemList(eq(productId), anyInt(), isNull(), isNull())).thenReturn(java.util.List.of(templateItem));
        MesProFeedbackDO feedback = new MesProFeedbackDO();
        feedback.setId(feedbackId);
        feedback.setCode("FB-001");
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcAutoCreateTest.java
@@ -1,6 +1,9 @@
package cn.iocoder.yudao.module.mes.service.qc.oqc;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.oqc.MesQcOqcDO;
import cn.iocoder.yudao.module.erp.api.sale.ErpSaleOrderApi;
import cn.iocoder.yudao.module.erp.api.sale.dto.ErpSaleOrderItemRespDTO;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesLineDO;
@@ -28,6 +31,7 @@
    @Mock private MesQcTemplateItemService templates;
    @Mock private MesQcOqcLineService lines;
    @Mock private MesWmProductSalesService sales;
    @Mock private ErpSaleOrderApi saleOrderApi;
    @Test
    void generatedInspectionUsesHistoricalShipmentDateAndPreservesPendingQuantities() {
@@ -36,7 +40,10 @@
                .thenReturn(new MesQcTemplateItemDO().setTemplateId(20L));
        when(sales.getProductSales(30L)).thenReturn(new MesWmProductSalesDO().setSalesDate(date));
        MesWmProductSalesLineDO line = new MesWmProductSalesLineDO().setId(40L)
                .setItemId(10L).setQuantity(BigDecimal.valueOf(200)).setBatchCode("B001");
                .setItemId(10L).setSaleOrderItemId(60L).setQuantity(BigDecimal.valueOf(200)).setBatchCode("B001");
        ErpSaleOrderItemRespDTO saleItem = new ErpSaleOrderItemRespDTO();
        saleItem.setReelNo("PAN-001");
        when(saleOrderApi.getSaleOrderItem(60L)).thenReturn(CommonResult.success(saleItem));
        service.createOqcFromProductSales(30L, line, "SL001", 50L);
        ArgumentCaptor<MesQcOqcDO> captor = ArgumentCaptor.forClass(MesQcOqcDO.class);
        verify(mapper).insert(captor.capture());
@@ -48,5 +55,21 @@
        assertEquals(BigDecimal.ZERO, row.getQualifiedQuantity());
        assertEquals(30L, row.getSourceDocId());
        assertEquals(40L, row.getSourceLineId());
        assertEquals("PAN-001", row.getReelNo());
    }
    @Test
    void historicalShipmentWithoutSaleItemDoesNotGuessReel() {
        when(templates.getRequiredTemplateByItemIdAndType(10L, 3))
                .thenReturn(new MesQcTemplateItemDO().setTemplateId(20L));
        MesWmProductSalesLineDO line = new MesWmProductSalesLineDO().setId(40L)
                .setItemId(10L).setQuantity(BigDecimal.ONE);
        service.createOqcFromProductSales(30L, line, "SL001", 50L);
        ArgumentCaptor<MesQcOqcDO> captor = ArgumentCaptor.forClass(MesQcOqcDO.class);
        verify(mapper).insert(captor.capture());
        assertNull(captor.getValue().getReelNo());
        verifyNoInteractions(saleOrderApi);
    }
}
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImplTest.java
@@ -33,6 +33,17 @@
@Import(MesQcOqcServiceImpl.class)
public class MesQcOqcServiceImplTest extends BaseDbUnitTest {
    @MockitoBean
    private cn.iocoder.yudao.module.erp.api.sale.ErpSaleOrderApi saleOrderApi;
    @MockitoBean
    private cn.iocoder.yudao.module.mdm.api.item.MdmItemApi mdmItemApi;
    @MockitoBean
    private cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi customerApi;
    @MockitoBean
    private cn.iocoder.yudao.module.mes.service.qc.imports.MesQcImportHelper qcImportHelper;
    @MockitoBean
    private cn.iocoder.yudao.module.mes.service.qc.ncr.MesQcNcrService ncrService;
    @Resource
    private MesQcOqcServiceImpl oqcService;
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/wm/batch/MesWmBatchServiceImplTest.java
@@ -3,6 +3,9 @@
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchGenerateReqVO;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeDetailDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceDO;
@@ -30,6 +33,8 @@
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_BATCH_REEL_MISMATCH;
/**
 * {@link MesWmBatchServiceImpl} çš„单元测试
@@ -38,6 +43,13 @@
 */
@Import(MesWmBatchServiceImpl.class)
public class MesWmBatchServiceImplTest extends BaseDbUnitTest {
    @MockitoBean
    private cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService mdmItemService;
    @MockitoBean
    private cn.iocoder.yudao.module.mdm.api.item.MdmItemBatchConfigApi mdmBatchConfigApi;
    @MockitoBean
    private cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi customerApi;
    @Resource
    private MesWmBatchServiceImpl batchService;
@@ -67,6 +79,48 @@
    @MockitoBean
    private MesWmBarcodeService barcodeService;
    @Test
    public void testNonBatchManagedItemUsesDistinctTraceBatchesForReels() {
        when(mdmItemService.validateItemExists(10L)).thenReturn(new MesMdmItemDO().setId(10L)
                .setName("产品").setIsBatchManaged(false));
        when(autoCodeRecordService.generateAutoCode(anyString())).thenReturn("TRACE-1", "TRACE-2");
        MesWmBatchGenerateReqVO first = new MesWmBatchGenerateReqVO().setItemId(10L)
                .setWorkOrderId(20L).setReelNo("PAN-1");
        MesWmBatchGenerateReqVO second = new MesWmBatchGenerateReqVO().setItemId(10L)
                .setWorkOrderId(20L).setReelNo("PAN-2");
        MesWmBatchDO a = batchService.getOrGenerateBatchCode(first);
        MesWmBatchDO b = batchService.getOrGenerateBatchCode(second);
        assertNotNull(a.getId());
        assertNotEquals(a.getId(), b.getId());
        assertEquals(a.getId(), batchService.getOrGenerateBatchCode(first).getId());
        assertEquals("PAN-2", batchMapper.selectById(b.getId()).getReelNo());
        assertNull(batchService.getOrGenerateBatchCode(new MesWmBatchGenerateReqVO().setItemId(10L)));
    }
    @Test
    public void testExistingBatchCannotBeRelabeledWithDifferentReel() {
        when(mdmItemService.validateItemExists(10L)).thenReturn(new MesMdmItemDO().setId(10L)
                .setName("产品").setIsBatchManaged(false));
        batchMapper.insert(new MesWmBatchDO().setCode("EXISTING").setItemId(10L).setReelNo("PAN-1"));
        MesWmBatchGenerateReqVO request = new MesWmBatchGenerateReqVO().setItemId(10L)
                .setBatchCode("EXISTING").setReelNo("PAN-2");
        ServiceException ex = assertThrows(ServiceException.class, () -> batchService.getOrGenerateBatchCode(request));
        assertEquals(WM_BATCH_REEL_MISMATCH.getCode(), ex.getCode());
    }
    @Test
    public void testExistingReelBatchCannotBeReassignedToAnotherOrder() {
        when(mdmItemService.validateItemExists(10L)).thenReturn(new MesMdmItemDO().setId(10L)
                .setName("产品").setIsBatchManaged(false));
        batchMapper.insert(new MesWmBatchDO().setCode("EXISTING").setItemId(10L)
                .setReelNo("PAN-1").setWorkOrderId(20L));
        MesWmBatchGenerateReqVO request = new MesWmBatchGenerateReqVO().setItemId(10L)
                .setBatchCode("EXISTING").setReelNo("PAN-1").setWorkOrderId(21L);
        ServiceException ex = assertThrows(ServiceException.class, () -> batchService.getOrGenerateBatchCode(request));
        assertEquals(WM_BATCH_REEL_MISMATCH.getCode(), ex.getCode());
    }
    // ==================== å‘前追溯 ====================
    @Test
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/wm/productproduce/MesWmProductProduceServiceImplTest.java
@@ -4,6 +4,8 @@
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceDetailDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceLineDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseAreaDO;
@@ -13,6 +15,8 @@
import cn.iocoder.yudao.module.mes.enums.wm.MesWmProductProduceStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum;
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.service.pro.route.MesProRouteProductService;
import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
@@ -53,6 +57,10 @@
    @MockitoBean
    private MesProWorkOrderService workOrderService;
    @MockitoBean
    private MesProWorkOrderProcessService workOrderProcessService;
    @MockitoBean
    private MesProRouteProductService routeProductService;
    @MockitoBean
    private MesWmBatchService batchService;
    @MockitoBean
    private MesWmTransactionService wmTransactionService;
@@ -62,6 +70,63 @@
    private MesWmWarehouseLocationService locationService;
    @MockitoBean
    private MesWmWarehouseAreaService areaService;
    @Test
    public void testGeneratePendingProduceCopiesOrderReel() {
        MesProFeedbackDO feedback = new MesProFeedbackDO().setId(2L).setWorkOrderId(3L)
                .setTaskId(4L).setItemId(5L).setFeedbackQuantity(BigDecimal.TEN);
        when(workOrderService.getWorkOrder(3L)).thenReturn(new MesProWorkOrderDO().setReelNo("PAN-001"));
        productProduceService.generateProductProduce(feedback, true, null);
        ArgumentCaptor<MesWmProductProduceLineDO> captor = ArgumentCaptor.forClass(MesWmProductProduceLineDO.class);
        verify(productProduceLineService).createProductProduceLine(captor.capture());
        assertEquals("PAN-001", captor.getValue().getReelNo());
        assertEquals(MesWmQualityStatusEnum.PENDING.getStatus(), captor.getValue().getQualityStatus());
    }
    @Test
    public void testGenerateProduceRejectsDirectInbound() {
        ServiceException ex = assertThrows(ServiceException.class,
                () -> productProduceService.generateProductProduce(new MesProFeedbackDO(), false, null));
        assertEquals(WM_PRODUCT_PRODUCE_QUALITY_PENDING.getCode(), ex.getCode());
        verifyNoInteractions(wmTransactionService);
    }
    @Test
    public void testFinishProductProduceRejectsPendingInspection() {
        MesWmProductProduceDO produce = randomPojo(MesWmProductProduceDO.class,
                o -> o.setStatus(MesWmProductProduceStatusEnum.PREPARE.getStatus()));
        productProduceMapper.insert(produce);
        MesWmProductProduceLineDO pending = MesWmProductProduceLineDO.builder()
                .id(1L).produceId(produce.getId()).quantity(BigDecimal.TEN)
                .qualityStatus(MesWmQualityStatusEnum.PENDING.getStatus()).build();
        when(productProduceLineService.getProductProduceLineListByProduceId(produce.getId()))
                .thenReturn(ListUtil.of(pending));
        ServiceException ex = assertThrows(ServiceException.class,
                () -> productProduceService.finishProductProduce(produce.getId()));
        assertEquals(WM_PRODUCT_PRODUCE_QUALITY_PENDING.getCode(), ex.getCode());
        verify(wmTransactionService, never()).createTransaction(any());
    }
    @Test
    public void testSplitPendingRejectsWrongQuantity() {
        Long feedbackId = randomLongId();
        MesWmProductProduceDO produce = randomPojo(MesWmProductProduceDO.class,
                o -> o.setFeedbackId(feedbackId).setStatus(MesWmProductProduceStatusEnum.PREPARE.getStatus()));
        productProduceMapper.insert(produce);
        MesWmProductProduceLineDO pending = MesWmProductProduceLineDO.builder()
                .id(1L).produceId(produce.getId()).feedbackId(feedbackId).quantity(BigDecimal.TEN)
                .qualityStatus(MesWmQualityStatusEnum.PENDING.getStatus()).build();
        when(productProduceLineService.validateProductProduceLineExists(1L)).thenReturn(pending);
        ServiceException ex = assertThrows(ServiceException.class,
                () -> productProduceService.splitPendingAndFinishProduce(feedbackId, 1L,
                        BigDecimal.valueOf(9), BigDecimal.ZERO));
        assertEquals(WM_PRODUCT_PRODUCE_INSPECTION_INVALID.getCode(), ex.getCode());
        verify(wmTransactionService, never()).createTransaction(any());
    }
    @Test
    public void testSplitPendingAndFinishProduce_withUnqualified() {
@@ -92,6 +157,7 @@
                .id(sourceLineId).produceId(produce.getId()).feedbackId(feedbackId)
                .itemId(itemId).quantity(BigDecimal.valueOf(100))
                .batchId(batchId).batchCode(batchCode)
                .reelNo("PAN-001")
                .qualityStatus(MesWmQualityStatusEnum.PENDING.getStatus())
                .build();
        when(productProduceLineService.validateProductProduceLineExists(sourceLineId))
@@ -132,6 +198,7 @@
        assertEquals(MesWmQualityStatusEnum.FAIL.getStatus(), createdLine.getQualityStatus());
        assertEquals(0, BigDecimal.valueOf(20).compareTo(createdLine.getQuantity()));
        assertEquals(itemId, createdLine.getItemId());
        assertEquals("PAN-001", createdLine.getReelNo());
        // æ–­è¨€ 2:原待检行 - æ›´æ–°ä¸ºåˆæ ¼å“
        ArgumentCaptor<MesWmProductProduceLineDO> updateCaptor = ArgumentCaptor.forClass(MesWmProductProduceLineDO.class);
@@ -141,14 +208,16 @@
        assertEquals(0, BigDecimal.valueOf(80).compareTo(updatedLine.getQuantity()));
        // æ–­è¨€ 3:生成了 2 æ¡æ˜Žç»†ï¼ˆä¸åˆæ ¼å“æ˜Žç»† + åˆæ ¼å“æ˜Žç»†ï¼‰
        verify(productProduceDetailService, times(2)).createProductProduceDetail(any());
        ArgumentCaptor<MesWmProductProduceDetailDO> detailCaptor = ArgumentCaptor.forClass(MesWmProductProduceDetailDO.class);
        verify(productProduceDetailService, times(2)).createProductProduceDetail(detailCaptor.capture());
        assertTrue(detailCaptor.getAllValues().stream().allMatch(d -> "PAN-001".equals(d.getReelNo())));
        // æ–­è¨€ 4:产出单状态更新为已完成
        MesWmProductProduceDO updatedProduce = productProduceMapper.selectById(produce.getId());
        assertEquals(MesWmProductProduceStatusEnum.FINISHED.getStatus(), updatedProduce.getStatus());
        // æ–­è¨€ 5:创建了库存事务
        verify(wmTransactionService, times(2)).createTransaction(any());
        // æ–­è¨€ 5:只有合格品生成库存事务,不合格品不进入合格库存
        verify(wmTransactionService, times(1)).createTransaction(any());
    }
    @Test
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmSalesReelGuardTest.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,56 @@
package cn.iocoder.yudao.module.mes.service.wm.productsales;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.erp.api.sale.ErpSaleOrderApi;
import cn.iocoder.yudao.module.erp.api.sale.dto.ErpSaleOrderItemRespDTO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesLineDO;
import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_PRODUCT_SALES_REEL_MISMATCH;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_PRODUCT_SALES_REEL_SOURCE_MISSING;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class MesWmSalesReelGuardTest {
    @InjectMocks private MesWmSalesReelGuard guard;
    @Mock private ErpSaleOrderApi saleOrderApi;
    @Mock private MesWmBatchService batchService;
    @Test
    void acceptsOnlyMatchingTracedInventory() {
        var line = new MesWmProductSalesLineDO().setSaleOrderItemId(10L);
        var item = new ErpSaleOrderItemRespDTO();
        item.setReelNo("PAN-1");
        when(saleOrderApi.getSaleOrderItem(10L)).thenReturn(CommonResult.success(item));
        when(batchService.getBatch(20L)).thenReturn(new MesWmBatchDO().setReelNo("PAN-1"));
        when(batchService.getBatch(21L)).thenReturn(new MesWmBatchDO().setReelNo("PAN-2"));
        assertDoesNotThrow(() -> guard.validate(line, 20L));
        ServiceException wrong = assertThrows(ServiceException.class, () -> guard.validate(line, 21L));
        assertEquals(WM_PRODUCT_SALES_REEL_MISMATCH.getCode(), wrong.getCode());
        assertThrows(ServiceException.class, () -> guard.validate(line, null));
    }
    @Test
    void preservesLegacyWithoutSalesItem() {
        assertDoesNotThrow(() -> guard.validate(new MesWmProductSalesLineDO(), null));
        verifyNoInteractions(saleOrderApi, batchService);
    }
    @Test
    void rejectsMissingReferencedSalesItem() {
        when(saleOrderApi.getSaleOrderItem(10L)).thenReturn(CommonResult.success(null));
        ServiceException ex = assertThrows(ServiceException.class,
                () -> guard.validate(new MesWmProductSalesLineDO().setSaleOrderItemId(10L), 20L));
        assertEquals(WM_PRODUCT_SALES_REEL_SOURCE_MISSING.getCode(), ex.getCode());
        verifyNoInteractions(batchService);
    }
}
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/wm/transaction/MesWmTransactionReelSafetyTest.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.mes.service.wm.transaction;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_MATERIAL_STOCK_SELECTION_MISMATCH;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class MesWmTransactionReelSafetyTest {
    @InjectMocks private MesWmTransactionServiceImpl service;
    @Mock private MesWmMaterialStockService stockService;
    @Test
    void rejectsSelectedStockWithDifferentBatch() {
        when(stockService.getMaterialStock(1L)).thenReturn(new MesWmMaterialStockDO().setId(1L)
                .setItemId(2L).setWarehouseId(3L).setLocationId(4L).setAreaId(5L).setBatchId(6L));
        MesWmTransactionSaveReqDTO req = new MesWmTransactionSaveReqDTO()
                .setType(MesWmTransactionTypeEnum.OUT.getType()).setQuantity(BigDecimal.ONE.negate())
                .setItemId(2L).setWarehouseId(3L).setLocationId(4L).setAreaId(5L)
                .setBatchId(7L).setBatchCode("other-batch").setMaterialStockId(1L);
        ServiceException ex = assertThrows(ServiceException.class, () -> service.createTransaction(req));
        assertEquals(WM_MATERIAL_STOCK_SELECTION_MISMATCH.getCode(), ex.getCode());
        verify(stockService, never()).updateMaterialStockQuantity(anyLong(), any(), anyBoolean());
    }
}
yudao-module-mes/src/test/resources/sql/create_tables.sql
@@ -244,6 +244,7 @@
    "source_line_id" bigint DEFAULT NULL,
    "source_doc_code" varchar(64) DEFAULT NULL,
    "work_order_id" bigint DEFAULT NULL,
    "reel_no" varchar(128) DEFAULT NULL,
    "task_id" bigint DEFAULT NULL,
    "workstation_id" bigint DEFAULT NULL,
    "process_id" bigint DEFAULT NULL,
@@ -348,6 +349,7 @@
    "quantity" decimal(12,2) DEFAULT NULL,
    "batch_id" bigint DEFAULT NULL,
    "batch_code" varchar(255) DEFAULT NULL,
    "reel_no" varchar(128) DEFAULT NULL,
    "expire_date" timestamp DEFAULT NULL,
    "lot_number" varchar(128) DEFAULT NULL,
    "quality_status" int DEFAULT NULL,
@@ -372,6 +374,7 @@
    "quantity" decimal(12,2) DEFAULT NULL,
    "batch_id" bigint DEFAULT NULL,
    "batch_code" varchar(255) DEFAULT NULL,
    "reel_no" varchar(128) DEFAULT NULL,
    "warehouse_id" bigint DEFAULT NULL,
    "location_id" bigint DEFAULT NULL,
    "area_id" bigint DEFAULT NULL,
@@ -397,6 +400,7 @@
    "source_doc_id" bigint DEFAULT NULL,
    "source_line_id" bigint DEFAULT NULL,
    "source_doc_code" varchar(64) DEFAULT NULL,
    "reel_no" varchar(128) DEFAULT NULL,
    "client_id" bigint DEFAULT NULL,
    "batch_code" varchar(64) DEFAULT NULL,
    "item_id" bigint NOT NULL,
@@ -681,6 +685,7 @@
    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
    "code" varchar(128) DEFAULT NULL,
    "item_id" bigint DEFAULT NULL,
    "reel_no" varchar(128) DEFAULT NULL,
    "produce_date" timestamp DEFAULT NULL,
    "expire_date" timestamp DEFAULT NULL,
    "receipt_date" timestamp DEFAULT NULL,