云
2 小时以前 c215f6c20445b973ff730dcdf4206426992da81d
feat(mes): 完善质检流程功能

- 在 mes_qc_oqc 表增加 workflow_status 字段用于跟踪质检流程状态
- 新增 mes_qc_oqc_action 表记录质检操作流程日志
- 实现出货质检(OQC)的复检、销售部审批、公司质检审批完整流程
- 添加销售质检相关的API接口包括复检、审批和流程记录查询
- 实现种子质检复检和多级审批流程功能
- 在追溯服务中根据工作流状态计算质检结果显示
- 完善过程质检(IPQC)校验逻辑确保数据一致性
- 优化数据库查询支持按来源单据类型筛选
- 添加单元测试验证质检流程的各种场景
已添加12个文件
已修改18个文件
832 ■■■■■ 文件已修改
docs/production_quality_flow_20261019.md 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/mes_seed_quality_workflow_20261019.sql 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/MesQcOqcController.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcApprovalReqVO.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcPageReqVO.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcRespVO.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcRetestReqVO.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/MesQcSeedQualityController.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityApprovalReqVO.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityBatchApprovalReqVO.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityPageReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityRespVO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityRetestReqVO.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/oqc/MesQcOqcActionDO.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/oqc/MesQcOqcDO.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/seedquality/MesQcSeedQualityActionDO.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/seedquality/MesQcSeedQualityDO.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/oqc/MesQcOqcActionMapper.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/oqc/MesQcOqcMapper.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedQualityActionMapper.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedQualityMapper.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ipqc/MesQcIpqcServiceImpl.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcService.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImpl.java 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityService.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityServiceImpl.java 164 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/trace/MesProTracePublicServiceImpl.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImplTest.java 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/SeedQualityWorkflowTest.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/test/resources/sql/create_tables.sql 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/production_quality_flow_20261019.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
# ç”Ÿäº§è´¨æ£€æµç¨‹ä¸Žèœå•映射
依据用户提供的生产流程图和当前 `system_menu`,保留已有质量管理菜单,新增“销售质检”入口。
| æµç¨‹å›¾èŠ‚ç‚¹ | ç³»ç»Ÿå…¥å£ | å®žçް |
| --- | --- | --- |
| 48/100 è¢‹æ‰«ç å…¥åº“、生成托盘码 | ä¸€è¢‹ä¸€ç  / ä»“储物流 | æ—¢æœ‰è¢‹ç ã€æ‰˜ç›˜ç ã€æ‰¹æ¬¡ç  |
| åŠ å·¥éƒ¨åˆæ£€ | è´¨é‡ç®¡ç† / ç§å­è´¨æ£€ | å¯æŒ‰è¢‹ã€æ‰˜ç›˜ã€æ‰¹æ¬¡æäº¤ï¼›æ¯è¢‹ç‹¬ç«‹è®°å½• |
| åˆæ£€ä¸åˆæ ¼ã€å¤æ£€ | è´¨é‡ç®¡ç† / è´¨æ£€è®°å½• | ä¿ç•™æš‚存库存,记录复检结果与历次指标 |
| åŠ å·¥éƒ¨é¢†å¯¼å®¡æ‰¹ | è´¨é‡ç®¡ç† / è´¨æ£€è®°å½• | å•条或同状态批量审批,可驳回重检 |
| å…¬å¸è´¨æ£€å®¡æ‰¹ | è´¨é‡ç®¡ç† / è´¨æ£€è®°å½• | å•条或同状态批量审批;通过后才迁移或报废库存 |
| é”€å”®éƒ¨è´¨æ£€ | è´¨é‡ç®¡ç† / é”€å”®è´¨æ£€ | ç‹¬ç«‹èœå•,复用既有 OQC é”€å”®å‡ºåº“单检验单 |
| é”€å”®å¤æ£€ä¸Žå®¡æ‰¹ | è´¨é‡ç®¡ç† / é”€å”®è´¨æ£€ | ä¸åˆæ ¼å…ˆå¤æ£€ï¼Œé”€å”®éƒ¨ä¸Žå…¬å¸è´¨æ£€å®¡æ‰¹åŽå›žå†™å‡ºåº“质量状态 |
| ä¸åˆæ ¼å“ç•™ç—•评审 | è´¨é‡ç®¡ç† / ä¸åˆæ ¼å“å¤„理 | æ—¢æœ‰ OQC/IPQC è‡ªåŠ¨åˆ›å»º NCR |
| æ­£å¼å…¥åº“、出库、扫码追溯 | ä»“储物流 / å‡ºåº“流通 / è¿½æº¯ | å®¡æ‰¹æ”¾è¡ŒåŽè¢‹ç  `qc_status=20`;未放行不能扫码出库 |
## ç§å­è´¨æ£€çŠ¶æ€
- 10 å¾…复检:初检失败或审批驳回;库存仍在暂存库。
- 20 å¾…加工部审批:初检或复检合格。
- 30 å¾…公司质检审批:加工部同意放行。
- 40 å·²æ”¾è¡Œï¼šå…¬å¸è´¨æ£€åŒæ„ï¼Œäº‹åŠ¡æ€§åœ°å°†è¯¥è¢‹ç”±æš‚å­˜åº“ç§»å…¥åˆæ ¼åº“ã€‚
- 50 å¾…加工部不合格评审:复检不合格。
- 60 å¾…公司不合格审批:加工部同意报废。
- 70 å·²æŠ¥åºŸï¼šå…¬å¸è´¨æ£€åŒæ„ï¼Œäº‹åŠ¡æ€§åœ°ç§»é™¤è¯¥è¢‹æš‚å­˜åº“å­˜ã€‚
审批驳回回到 10。每次初检、复检和审批写入 `mes_qc_seed_quality_action`,包含操作人、时间、意见和检验指标快照。批量审批在同一数据库事务内执行,任一袋库存流转失败则全部回滚。
销售质检的流程状态与种子质检相同,其中 20/50 ç”±é”€å”®éƒ¨å®¡æ‰¹ï¼›40 è¡¨ç¤ºå‡ºè´§æ£€éªŒé€šè¿‡å¹¶å›žå†™é”€å”®å‡ºåº“单行,70 è¡¨ç¤ºä¸åˆæ ¼å®¡æ‰¹å®Œæˆå¹¶å›žå†™ä¸ºä¸åˆæ ¼ï¼ŒåŒæ—¶åˆ›å»º NCR。初检与复检不回写出库单质量结果,因此审批前仍处于待检状态。复检前后数量及审批意见写入 `mes_qc_oqc_action`。
## éƒ¨ç½²é¡ºåº
1. å¤‡ä»½æ•°æ®åº“,执行 `sql/mysql/mes_seed_quality_workflow_20261019.sql`。脚本是一次性迁移;历史已流转的合格/不合格记录分别标记为 40/70,避免重复库存事务。
2. éƒ¨ç½²åŽç«¯ã€å‰ç«¯ã€‚前端构建产物在 `mom-pro2-before/dist.zip`。
3. ä¸ºåŠ å·¥éƒ¨ä¸Žå…¬å¸è´¨æ£€è§’è‰²åˆ†åˆ«æŽˆäºˆ `mes:qc-seed-quality:process-approve`、`mes:qc-seed-quality:company-approve`;复检人员授予 `mes:qc-seed-quality:retest`。为销售部与公司质检角色分别授予 `mes:qc-oqc:sales-approve`、`mes:qc-oqc:company-approve`;销售复检人员授予 `mes:qc-oqc:retest`。核对“销售质检”菜单角色权限。
“销售质检”与原“出货检验”共用 OQC æ£€éªŒå•及既有出货检验表单,但有独立菜单与专门的复检、审批操作。原“过程检验(IPQC)”仍服务生产任务和报工,未并入按袋码流转的种子质检审批链。
sql/mysql/mes_seed_quality_workflow_20261019.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,78 @@
-- ç”Ÿäº§è´¨æ£€ï¼šåˆæ£€ã€å¤æ£€ã€åŠ å·¥éƒ¨å®¡æ‰¹ã€å…¬å¸è´¨æ£€å®¡æ‰¹åŽæ‰æ”¾è¡Œåº“å­˜ã€‚
-- å…ˆéƒ¨ç½²æ•°æ®åº“变更,再部署对应后端与前端;历史合格记录已完成库存流转。
ALTER TABLE mes_qc_seed_quality
  ADD COLUMN workflow_status tinyint NOT NULL DEFAULT 40 COMMENT '10待复检 20待加工部审批 30待公司质检审批 40已放行 50待加工部不合格评审 60待公司不合格审批 70已报废',
  ADD COLUMN qualified_warehouse_id bigint DEFAULT NULL COMMENT '审批通过后的目标合格库',
  ADD COLUMN retest_result tinyint DEFAULT NULL COMMENT '复检结果 1合格 2不合格',
  ADD COLUMN retested_at datetime DEFAULT NULL COMMENT '复检时间',
  ADD COLUMN process_approved_at datetime DEFAULT NULL COMMENT '加工部审批时间',
  ADD COLUMN company_approved_at datetime DEFAULT NULL COMMENT '公司质检审批时间';
CREATE INDEX idx_mes_seed_quality_workflow ON mes_qc_seed_quality (workflow_status, inspected_at);
-- æ—§æ•°æ®å·²ç»æŒ‰æ—§æµç¨‹æ”¾è¡Œæˆ–报废,不能重新审批或重复扣库存。
UPDATE mes_qc_seed_quality SET workflow_status = CASE WHEN result = 1 THEN 40 WHEN result = 2 THEN 70 ELSE 10 END;
CREATE TABLE IF NOT EXISTS mes_qc_seed_quality_action (
  id bigint NOT NULL AUTO_INCREMENT,
  quality_id bigint NOT NULL,
  action varchar(32) NOT NULL COMMENT 'INITIAL/RETEST/PROCESS_APPROVE/COMPANY_APPROVE/REJECT',
  result tinyint DEFAULT NULL,
  opinion varchar(500) DEFAULT NULL,
  indicators_json text DEFAULT NULL,
  operator_id bigint DEFAULT NULL,
  create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id),
  KEY idx_quality_time (quality_id, create_time)
) ENGINE=InnoDB COMMENT='种子质检流程操作留痕';
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
SELECT '复检', 'mes:qc-seed-quality:retest', 3, 3, id, '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM system_menu WHERE parent_id = 5500 AND path = 'seed-quality' AND deleted = b'0'
  AND NOT EXISTS (SELECT 1 FROM system_menu m WHERE m.parent_id = system_menu.id AND m.permission = 'mes:qc-seed-quality:retest' AND m.deleted = b'0');
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
SELECT '加工部审批', 'mes:qc-seed-quality:process-approve', 3, 4, id, '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM system_menu WHERE parent_id = 5500 AND path = 'seed-quality' AND deleted = b'0'
  AND NOT EXISTS (SELECT 1 FROM system_menu m WHERE m.parent_id = system_menu.id AND m.permission = 'mes:qc-seed-quality:process-approve' AND m.deleted = b'0');
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
SELECT '公司质检审批', 'mes:qc-seed-quality:company-approve', 3, 5, id, '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM system_menu WHERE parent_id = 5500 AND path = 'seed-quality' AND deleted = b'0'
  AND NOT EXISTS (SELECT 1 FROM system_menu m WHERE m.parent_id = system_menu.id AND m.permission = 'mes:qc-seed-quality:company-approve' AND m.deleted = b'0');
-- ç‹¬ç«‹é”€å”®è´¨æ£€å…¥å£ï¼›ä¿ç•™åŽŸâ€œå‡ºè´§æ£€éªŒâ€èœå•ï¼Œå¤ç”¨å…¶å·²å®žçŽ°çš„é”€å”®å‡ºåº“å•æ£€éªŒèƒ½åŠ›ã€‚
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
SELECT '销售质检', '', 2, 8, 5500, 'sales-quality', 'ep:document-checked', 'mes/qc/salesQuality/index', 'MesQcSalesQuality', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM system_menu WHERE parent_id = 5500 AND path = 'sales-quality' AND deleted = b'0');
-- é”€å”®è´¨æ£€å¤æ£€åŠä¸¤çº§å®¡æ‰¹ï¼›åŽ†å²å·²å®Œæˆå‡ºè´§æ£€éªŒå•ç»´æŒæ—¢æœ‰æ”¾è¡Œç»“æžœã€‚
ALTER TABLE mes_qc_oqc
  ADD COLUMN workflow_status tinyint NOT NULL DEFAULT 0 COMMENT '0草稿 10待复检 20待销售部审批 30待公司质检审批 40已放行 50待销售部不合格评审 60待公司不合格审批 70已拒绝出货';
CREATE INDEX idx_mes_oqc_workflow ON mes_qc_oqc (workflow_status, id);
UPDATE mes_qc_oqc SET workflow_status = CASE
  WHEN status = 0 THEN 0
  WHEN check_result = 1 THEN 40
  ELSE 70 END;
CREATE TABLE IF NOT EXISTS mes_qc_oqc_action (
  id bigint NOT NULL AUTO_INCREMENT,
  oqc_id bigint NOT NULL,
  action varchar(32) NOT NULL,
  result tinyint DEFAULT NULL,
  opinion varchar(500) DEFAULT NULL,
  quantities_json text DEFAULT NULL,
  operator_id bigint DEFAULT NULL,
  create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id),
  KEY idx_oqc_time (oqc_id, create_time)
) ENGINE=InnoDB COMMENT='销售质检复检和审批留痕';
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
SELECT '销售复检', 'mes:qc-oqc:retest', 3, 1, id, '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM system_menu WHERE parent_id = 5500 AND path = 'sales-quality' AND deleted = b'0'
  AND NOT EXISTS (SELECT 1 FROM system_menu m WHERE m.parent_id = system_menu.id AND m.permission = 'mes:qc-oqc:retest' AND m.deleted = b'0');
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
SELECT '销售部审批', 'mes:qc-oqc:sales-approve', 3, 2, id, '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM system_menu WHERE parent_id = 5500 AND path = 'sales-quality' AND deleted = b'0'
  AND NOT EXISTS (SELECT 1 FROM system_menu m WHERE m.parent_id = system_menu.id AND m.permission = 'mes:qc-oqc:sales-approve' AND m.deleted = b'0');
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
SELECT '销售公司质检审批', 'mes:qc-oqc:company-approve', 3, 3, id, '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM system_menu WHERE parent_id = 5500 AND path = 'sales-quality' AND deleted = b'0'
  AND NOT EXISTS (SELECT 1 FROM system_menu m WHERE m.parent_id = system_menu.id AND m.permission = 'mes:qc-oqc:company-approve' AND m.deleted = b'0');
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/MesQcOqcController.java
@@ -10,6 +10,8 @@
import cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.MesQcOqcPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.MesQcOqcRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.MesQcOqcSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.MesQcOqcRetestReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.MesQcOqcApprovalReqVO;
import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
@@ -83,6 +85,30 @@
        return success(true);
    }
    @PostMapping("/retest") @Operation(summary = "销售质检复检")
    @PreAuthorize("@ss.hasPermission('mes:qc-oqc:retest')")
    public CommonResult<Boolean> retestOqc(@Valid @RequestBody MesQcOqcRetestReqVO reqVO) {
        oqcService.retestOqc(reqVO);
        return success(true);
    }
    @PostMapping("/sales-approve") @Operation(summary = "销售部审批")
    @PreAuthorize("@ss.hasPermission('mes:qc-oqc:sales-approve')")
    public CommonResult<Boolean> salesApprove(@Valid @RequestBody MesQcOqcApprovalReqVO reqVO) {
        oqcService.salesApprove(reqVO.getId(), reqVO.getApproved(), reqVO.getOpinion());
        return success(true);
    }
    @PostMapping("/company-approve") @Operation(summary = "公司质检审批销售出货")
    @PreAuthorize("@ss.hasPermission('mes:qc-oqc:company-approve')")
    public CommonResult<Boolean> companyApprove(@Valid @RequestBody MesQcOqcApprovalReqVO reqVO) {
        oqcService.companyApprove(reqVO.getId(), reqVO.getApproved(), reqVO.getOpinion());
        return success(true);
    }
    @GetMapping("/actions") @Operation(summary = "销售质检流程记录")
    @PreAuthorize("@ss.hasPermission('mes:qc-oqc:query')")
    public CommonResult<List<cn.iocoder.yudao.module.mes.dal.dataobject.qc.oqc.MesQcOqcActionDO>> actions(@RequestParam Long id) {
        return success(oqcService.getActions(id));
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除出货检验单")
    @Parameter(name = "id", description = "编号", required = true)
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcApprovalReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,12 @@
package cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
public class MesQcOqcApprovalReqVO {
    @NotNull private Long id;
    @NotNull private Boolean approved;
    @NotBlank private String opinion;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcPageReqVO.java
@@ -26,5 +26,7 @@
    @Schema(description = "检测结果", example = "1")
    private Integer checkResult;
    @Schema(description = "来源单据类型")
    private Integer sourceDocType;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcRespVO.java
@@ -142,6 +142,7 @@
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    @ExcelProperty("状态")
    private Integer status;
    private Integer workflowStatus;
    @Schema(description = "备注", example = "备注")
    @ExcelProperty("备注")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcRetestReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class MesQcOqcRetestReqVO {
    @NotNull private Long id;
    @NotNull @DecimalMin("0") private BigDecimal checkQuantity;
    @NotNull @DecimalMin("0") private BigDecimal qualifiedQuantity;
    @NotNull @DecimalMin("0") private BigDecimal unqualifiedQuantity;
    @NotNull private Integer checkResult;
    private String opinion;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/MesQcSeedQualityController.java
@@ -6,6 +6,9 @@
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualitySubmitReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityApprovalReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityRetestReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityBatchApprovalReqVO;
import cn.iocoder.yudao.module.mes.service.qc.seedquality.MesQcSeedQualityService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -33,6 +36,17 @@
        return success(BeanUtils.toBean(service.getQualityPage(pageReqVO), MesQcSeedQualityRespVO.class));
    }
    @GetMapping("/indicators") @Operation(summary = "获得种子质检最新指标")
    @PreAuthorize("@ss.hasPermission('mes:qc-seed-quality:query')")
    public CommonResult<java.util.List<cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedQualityIndicatorDO>> indicators(@RequestParam Long id) {
        return success(service.getIndicatorsByQualityId(id));
    }
    @GetMapping("/actions") @Operation(summary = "获得种子质检流程留痕")
    @PreAuthorize("@ss.hasPermission('mes:qc-seed-quality:query')")
    public CommonResult<java.util.List<cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedQualityActionDO>> actions(@RequestParam Long id) {
        return success(service.getActions(id));
    }
    @PostMapping("/submit") @Operation(summary = "提交种子质检")
    @PreAuthorize("@ss.hasPermission('mes:qc-seed-quality:submit')")
    public CommonResult<Long> submit(@Valid @RequestBody MesQcSeedQualitySubmitReqVO reqVO) { return success(service.submit(reqVO)); }
@@ -42,4 +56,37 @@
    public CommonResult<java.util.Map<String, Object>> submitGroup(@Valid @RequestBody cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityGroupSubmitReqVO reqVO) {
        return success(service.submitGroup(reqVO));
    }
    @PostMapping("/retest") @Operation(summary = "种子质检复检")
    @PreAuthorize("@ss.hasPermission('mes:qc-seed-quality:retest')")
    public CommonResult<Boolean> retest(@RequestParam Long id, @Valid @RequestBody MesQcSeedQualityRetestReqVO reqVO) {
        service.retest(id, reqVO);
        return success(true);
    }
    @PostMapping("/process-approve") @Operation(summary = "加工部审批")
    @PreAuthorize("@ss.hasPermission('mes:qc-seed-quality:process-approve')")
    public CommonResult<Boolean> processApprove(@Valid @RequestBody MesQcSeedQualityApprovalReqVO reqVO) {
        service.processApprove(reqVO.getId(), reqVO.getApproved(), reqVO.getOpinion());
        return success(true);
    }
    @PostMapping("/company-approve") @Operation(summary = "公司质检审批并流转库存")
    @PreAuthorize("@ss.hasPermission('mes:qc-seed-quality:company-approve')")
    public CommonResult<Boolean> companyApprove(@Valid @RequestBody MesQcSeedQualityApprovalReqVO reqVO) {
        service.companyApprove(reqVO.getId(), reqVO.getApproved(), reqVO.getOpinion());
        return success(true);
    }
    @PostMapping("/process-approve-batch") @Operation(summary = "批量加工部审批")
    @PreAuthorize("@ss.hasPermission('mes:qc-seed-quality:process-approve')")
    public CommonResult<Boolean> processApproveBatch(@Valid @RequestBody MesQcSeedQualityBatchApprovalReqVO reqVO) {
        service.processApproveBatch(reqVO.getIds(), reqVO.getApproved(), reqVO.getOpinion());
        return success(true);
    }
    @PostMapping("/company-approve-batch") @Operation(summary = "批量公司质检审批并流转库存")
    @PreAuthorize("@ss.hasPermission('mes:qc-seed-quality:company-approve')")
    public CommonResult<Boolean> companyApproveBatch(@Valid @RequestBody MesQcSeedQualityBatchApprovalReqVO reqVO) {
        service.companyApproveBatch(reqVO.getIds(), reqVO.getApproved(), reqVO.getOpinion());
        return success(true);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityApprovalReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,12 @@
package cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
public class MesQcSeedQualityApprovalReqVO {
    @NotNull private Long id;
    @NotNull private Boolean approved;
    @NotBlank private String opinion;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityBatchApprovalReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
package cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
@Data
public class MesQcSeedQualityBatchApprovalReqVO {
    @NotEmpty private List<@NotNull Long> ids;
    @NotNull private Boolean approved;
    @NotBlank private String opinion;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityPageReqVO.java
@@ -18,4 +18,7 @@
    @Schema(description = "检验结果:1合格 2不合格")
    private Integer result;
    @Schema(description = "流程状态:10待复检 20/50待加工部 30/60待公司质检 40已放行 70已报废")
    private Integer workflowStatus;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityRespVO.java
@@ -15,6 +15,11 @@
    private Long batchId;
    private String batchCode;
    private Integer result;
    private Integer workflowStatus;
    private Integer retestResult;
    private LocalDateTime retestedAt;
    private LocalDateTime processApprovedAt;
    private LocalDateTime companyApprovedAt;
    private String reason;
    private Long inspectorId;
    private LocalDateTime inspectedAt;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualityRetestReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
package cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import java.util.List;
@Data
public class MesQcSeedQualityRetestReqVO {
    @NotBlank private String code;
    @NotEmpty @Valid private List<MesQcSeedQualitySubmitReqVO.Indicator> indicators;
    private String reason;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/oqc/MesQcOqcActionDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.qc.oqc;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("mes_qc_oqc_action")
public class MesQcOqcActionDO {
    @TableId private Long id;
    private Long oqcId;
    private String action;
    private Integer result;
    private String opinion;
    private String quantitiesJson;
    private Long operatorId;
    private LocalDateTime createTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/oqc/MesQcOqcDO.java
@@ -181,6 +181,8 @@
     * æžšä¸¾ {@link cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum}
     */
    private Integer status;
    /** 0草稿;10待复检;20待销售部审批;30待公司质检审批;40已放行;50待销售部不合格评审;60待公司不合格审批;70已拒绝出货。 */
    private Integer workflowStatus;
    /**
     * å¤‡æ³¨
     */
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/seedquality/MesQcSeedQualityActionDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("mes_qc_seed_quality_action")
public class MesQcSeedQualityActionDO {
    @TableId private Long id;
    private Long qualityId;
    private String action;
    private Integer result;
    private String opinion;
    private String indicatorsJson;
    private Long operatorId;
    private LocalDateTime createTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/seedquality/MesQcSeedQualityDO.java
@@ -17,6 +17,13 @@
    private String batchCode;
    private Integer result;
    private String reason;
    /** 10 å¾…复检;20 å¾…加工部审批;30 å¾…公司质检审批;40 å·²æ”¾è¡Œï¼›50 å¾…加工部不合格评审;60 å¾…公司不合格审批;70 å·²æŠ¥åºŸã€‚ */
    private Integer workflowStatus;
    private Long qualifiedWarehouseId;
    private Integer retestResult;
    private LocalDateTime retestedAt;
    private LocalDateTime processApprovedAt;
    private LocalDateTime companyApprovedAt;
    private Long inspectorId;
    private LocalDateTime inspectedAt;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/oqc/MesQcOqcActionMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
package cn.iocoder.yudao.module.mes.dal.mysql.qc.oqc;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.oqc.MesQcOqcActionDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface MesQcOqcActionMapper extends BaseMapperX<MesQcOqcActionDO> {
    default List<MesQcOqcActionDO> selectByOqcId(Long id) {
        return selectList(new LambdaQueryWrapperX<MesQcOqcActionDO>()
                .eq(MesQcOqcActionDO::getOqcId, id).orderByAsc(MesQcOqcActionDO::getId));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/oqc/MesQcOqcMapper.java
@@ -25,6 +25,7 @@
                .likeIfPresent(MesQcOqcDO::getBatchCode, reqVO.getBatchCode())
                .eqIfPresent(MesQcOqcDO::getItemId, reqVO.getItemId())
                .eqIfPresent(MesQcOqcDO::getCheckResult, reqVO.getCheckResult())
                .eqIfPresent(MesQcOqcDO::getSourceDocType, reqVO.getSourceDocType())
                .orderByDesc(MesQcOqcDO::getId));
    }
@@ -38,6 +39,7 @@
                .likeIfPresent(MesQcOqcDO::getBatchCode, reqVO.getBatchCode())
                .eqIfPresent(MesQcOqcDO::getItemId, reqVO.getItemId())
                .eqIfPresent(MesQcOqcDO::getCheckResult, reqVO.getCheckResult())
                .eqIfPresent(MesQcOqcDO::getSourceDocType, reqVO.getSourceDocType())
                .orderByDesc(MesQcOqcDO::getId);
        if (permittedClientIds != null) {
            if (CollUtil.isEmpty(permittedClientIds)) {
@@ -51,5 +53,8 @@
    default MesQcOqcDO selectByCode(String code) {
        return selectOne(MesQcOqcDO::getCode, code);
    }
    default MesQcOqcDO selectByIdForUpdate(Long id) {
        return selectOne(new LambdaQueryWrapperX<MesQcOqcDO>().eq(MesQcOqcDO::getId, id).last("FOR UPDATE"));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedQualityActionMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
package cn.iocoder.yudao.module.mes.dal.mysql.qc.seedquality;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedQualityActionDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface MesQcSeedQualityActionMapper extends BaseMapperX<MesQcSeedQualityActionDO> {
    default java.util.List<MesQcSeedQualityActionDO> selectByQualityId(Long qualityId) {
        return selectList(new LambdaQueryWrapperX<MesQcSeedQualityActionDO>()
                .eq(MesQcSeedQualityActionDO::getQualityId, qualityId)
                .orderByAsc(MesQcSeedQualityActionDO::getId));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedQualityMapper.java
@@ -17,6 +17,7 @@
                .likeIfPresent(MesQcSeedQualityDO::getCode, reqVO.getCode())
                .likeIfPresent(MesQcSeedQualityDO::getBatchCode, reqVO.getBatchCode())
                .eqIfPresent(MesQcSeedQualityDO::getResult, reqVO.getResult())
                .eqIfPresent(MesQcSeedQualityDO::getWorkflowStatus, reqVO.getWorkflowStatus())
                .orderByDesc(MesQcSeedQualityDO::getInspectedAt)
                .orderByDesc(MesQcSeedQualityDO::getId));
    }
@@ -34,6 +35,9 @@
    default MesQcSeedQualityDO selectByCodeForUpdate(String code) {
        return selectOne(new LambdaQueryWrapperX<MesQcSeedQualityDO>().eq(MesQcSeedQualityDO::getCode, code).last("FOR UPDATE"));
    }
    default MesQcSeedQualityDO selectByIdForUpdate(Long id) {
        return selectOne(new LambdaQueryWrapperX<MesQcSeedQualityDO>().eq(MesQcSeedQualityDO::getId, id).last("FOR UPDATE"));
    }
    default List<MesQcSeedQualityDO> selectListByBatchCode(String batchCode) {
        return selectList(new LambdaQueryWrapperX<MesQcSeedQualityDO>()
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ipqc/MesQcIpqcServiceImpl.java
@@ -18,6 +18,7 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateItemDO;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.ipqc.MesQcIpqcMapper;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcDefectLevelEnum;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcCheckResultEnum;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackStatusEnum;
@@ -215,6 +216,14 @@
        if (ipqc.getCheckResult() == null) {
            throw exception(QC_IPQC_CHECK_RESULT_EMPTY);
        }
        if (ipqc.getUnqualifiedQuantity() != null && ipqc.getUnqualifiedQuantity().compareTo(BigDecimal.ZERO) > 0
                && MesQcCheckResultEnum.QUALIFIED.getType().equals(ipqc.getCheckResult())) {
            throw new IllegalArgumentException("存在不合格品时,过程检验结论不能为合格");
        }
        if ((ipqc.getUnqualifiedQuantity() == null || ipqc.getUnqualifiedQuantity().compareTo(BigDecimal.ZERO) == 0)
                && !MesQcCheckResultEnum.QUALIFIED.getType().equals(ipqc.getCheckResult())) {
            throw new IllegalArgumentException("不合格数量为 0 æ—¶ï¼Œè¿‡ç¨‹æ£€éªŒç»“论必须为合格");
        }
        // 1.3 æ ¡éªŒè‡³å°‘存在一条检测结果
        indicatorResultService.validateIndicatorResultExistsByQcIdAndType(id, MesQcTypeEnum.IPQC.getType());
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcService.java
@@ -38,6 +38,10 @@
     * @param id ç¼–号
     */
    void finishOqc(Long id);
    void retestOqc(cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.MesQcOqcRetestReqVO reqVO);
    void salesApprove(Long id, boolean approved, String opinion);
    void companyApprove(Long id, boolean approved, String opinion);
    List<cn.iocoder.yudao.module.mes.dal.dataobject.qc.oqc.MesQcOqcActionDO> getActions(Long id);
    /**
     * åˆ é™¤å‡ºè´§æ£€éªŒå•
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImpl.java
@@ -1,14 +1,18 @@
package cn.iocoder.yudao.module.mes.service.qc.oqc;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.MesQcOqcPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.MesQcOqcSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.MesQcOqcRetestReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.defectrecord.MesQcDefectRecordDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.oqc.MesQcOqcDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.oqc.MesQcOqcActionDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateItemDO;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.oqc.MesQcOqcMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.oqc.MesQcOqcActionMapper;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcDefectLevelEnum;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum;
@@ -36,8 +40,11 @@
import java.math.RoundingMode;
import java.util.List;
import java.util.Objects;
import java.time.LocalDateTime;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
/**
@@ -49,8 +56,13 @@
@Validated
public class MesQcOqcServiceImpl implements MesQcOqcService {
    private static final int WAIT_RETEST = 10, WAIT_SALES = 20, WAIT_COMPANY = 30,
            RELEASED = 40, WAIT_SALES_FAIL = 50, WAIT_COMPANY_FAIL = 60, REJECTED = 70;
    @Resource
    private MesQcOqcMapper oqcMapper;
    @Resource
    private MesQcOqcActionMapper actionMapper;
    @Resource
    private MesQcTemplateItemService templateItemService;
@@ -133,26 +145,115 @@
    @Transactional(rollbackFor = Exception.class)
    public void finishOqc(Long id) {
        // 1.1 æ ¡éªŒå­˜åœ¨ + è‰ç¨¿çŠ¶æ€
        MesQcOqcDO oqc = validateOqcStatusPrepare(id);
        MesQcOqcDO oqc = lock(id);
        if (!MesQcStatusEnum.DRAFT.getStatus().equals(oqc.getStatus())) throw exception(QC_OQC_NOT_PREPARE);
        // 1.2 æ ¡éªŒæ£€æµ‹ç»“论必填
        if (oqc.getCheckResult() == null) {
            throw exception(QC_OQC_CHECK_RESULT_EMPTY);
        }
        if (oqc.getUnqualifiedQuantity() != null && oqc.getUnqualifiedQuantity().compareTo(BigDecimal.ZERO) > 0
                && MesQcCheckResultEnum.QUALIFIED.getType().equals(oqc.getCheckResult())) {
            throw new IllegalArgumentException("存在不合格品时,出货检验结论不能为合格");
        }
        if ((oqc.getUnqualifiedQuantity() == null || oqc.getUnqualifiedQuantity().compareTo(BigDecimal.ZERO) == 0)
                && !MesQcCheckResultEnum.QUALIFIED.getType().equals(oqc.getCheckResult())) {
            throw new IllegalArgumentException("不合格数量为 0 æ—¶ï¼Œå‡ºè´§æ£€éªŒç»“论必须为合格");
        }
        // 1.3 æ ¡éªŒè‡³å°‘存在一条检测结果
        indicatorResultService.validateIndicatorResultExistsByQcIdAndType(id, MesQcTypeEnum.OQC.getType());
        // 2. æ›´æ–°çŠ¶æ€ä¸ºå·²å®Œæˆ
        MesQcOqcDO updateObj = new MesQcOqcDO()
                .setId(id).setStatus(MesQcStatusEnum.FINISHED.getStatus());
                .setId(id).setStatus(MesQcStatusEnum.FINISHED.getStatus())
                .setWorkflowStatus(MesQcCheckResultEnum.QUALIFIED.getType().equals(oqc.getCheckResult()) ? WAIT_SALES : WAIT_RETEST);
        oqcMapper.updateById(updateObj);
        // 3. ä¸åˆæ ¼æ—¶è‡ªåŠ¨åˆ›å»º NCR
        generateNcrIfNeeded(oqc);
        // 4. å›žå†™æ¥æºå•据
        writeBackSourceDoc(oqc);
        recordAction(id, "INITIAL", oqc.getCheckResult(), null, oqc);
        // é”€å”®å‡ºåº“明细仍为待检;公司质检审批通过后才回写并允许后续出库。
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void retestOqc(MesQcOqcRetestReqVO req) {
        MesQcOqcDO oqc = requireStatus(req.getId(), WAIT_RETEST);
        if (req.getCheckQuantity().compareTo(req.getQualifiedQuantity().add(req.getUnqualifiedQuantity())) != 0)
            throw new IllegalArgumentException("检测数量必须等于合格与不合格数量之和");
        if (req.getCheckQuantity().compareTo(oqc.getOutQuantity()) > 0)
            throw new IllegalArgumentException("检测数量不能超过出货数量");
        if (!Set.of(1, 2).contains(req.getCheckResult())) throw new IllegalArgumentException("复检结论只能为合格或不合格");
        boolean passed = req.getCheckResult() == 1;
        if (passed != (req.getUnqualifiedQuantity().compareTo(BigDecimal.ZERO) == 0))
            throw new IllegalArgumentException("复检结论与不合格数量不一致");
        if (!passed && (req.getOpinion() == null || req.getOpinion().isBlank()))
            throw new IllegalArgumentException("复检不合格必须填写原因");
        MesQcOqcDO update = new MesQcOqcDO().setId(req.getId())
                .setCheckQuantity(req.getCheckQuantity()).setQualifiedQuantity(req.getQualifiedQuantity())
                .setUnqualifiedQuantity(req.getUnqualifiedQuantity()).setCheckResult(req.getCheckResult())
                .setInspectDate(LocalDateTime.now()).setInspectorUserId(getLoginUserId())
                .setWorkflowStatus(passed ? WAIT_SALES : WAIT_SALES_FAIL);
        oqcMapper.updateById(update);
        recordAction(req.getId(), "RETEST", req.getCheckResult(), req.getOpinion(), update);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void salesApprove(Long id, boolean approved, String opinion) {
        MesQcOqcDO oqc = lock(id);
        if (!Set.of(WAIT_SALES, WAIT_SALES_FAIL).contains(oqc.getWorkflowStatus()))
            throw new IllegalArgumentException("当前状态不能进行销售部审批");
        requireOpinion(opinion);
        int next = approved ? (oqc.getWorkflowStatus() == WAIT_SALES ? WAIT_COMPANY : WAIT_COMPANY_FAIL) : WAIT_RETEST;
        oqcMapper.updateById(new MesQcOqcDO().setId(id).setWorkflowStatus(next));
        recordAction(id, approved ? "SALES_APPROVE" : "SALES_REJECT", oqc.getCheckResult(), opinion, oqc);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void companyApprove(Long id, boolean approved, String opinion) {
        MesQcOqcDO oqc = lock(id);
        if (!Set.of(WAIT_COMPANY, WAIT_COMPANY_FAIL).contains(oqc.getWorkflowStatus()))
            throw new IllegalArgumentException("当前状态不能进行公司质检审批");
        requireOpinion(opinion);
        if (approved) {
            if (oqc.getWorkflowStatus() == WAIT_COMPANY_FAIL) generateNcrIfNeeded(oqc);
            writeBackSourceDoc(oqc);
        }
        int next = approved ? (oqc.getWorkflowStatus() == WAIT_COMPANY ? RELEASED : REJECTED) : WAIT_RETEST;
        oqcMapper.updateById(new MesQcOqcDO().setId(id).setWorkflowStatus(next));
        recordAction(id, approved ? "COMPANY_APPROVE" : "COMPANY_REJECT", oqc.getCheckResult(), opinion, oqc);
    }
    private MesQcOqcDO lock(Long id) {
        MesQcOqcDO oqc = oqcMapper.selectByIdForUpdate(id);
        if (oqc == null) throw exception(QC_OQC_NOT_EXISTS);
        return oqc;
    }
    private MesQcOqcDO requireStatus(Long id, int status) {
        MesQcOqcDO oqc = lock(id);
        if (!Objects.equals(oqc.getWorkflowStatus(), status))
            throw new IllegalArgumentException("销售质检状态已变化,请刷新后重试");
        return oqc;
    }
    private void requireOpinion(String opinion) {
        if (opinion == null || opinion.isBlank()) throw new IllegalArgumentException("请填写审批意见");
    }
    private void recordAction(Long id, String action, Integer result, String opinion, MesQcOqcDO snapshot) {
        MesQcOqcActionDO event = new MesQcOqcActionDO();
        event.setOqcId(id);event.setAction(action);event.setResult(result);event.setOpinion(opinion);
        java.util.Map<String, BigDecimal> quantities = new java.util.HashMap<>();
        quantities.put("checkQuantity", snapshot.getCheckQuantity());
        quantities.put("qualifiedQuantity", snapshot.getQualifiedQuantity());
        quantities.put("unqualifiedQuantity", snapshot.getUnqualifiedQuantity());
        event.setQuantitiesJson(JSONUtil.toJsonStr(quantities));
        event.setOperatorId(getLoginUserId());event.setCreateTime(LocalDateTime.now());
        actionMapper.insert(event);
    }
    @Override
    public List<MesQcOqcActionDO> getActions(Long id) { return actionMapper.selectByOqcId(id); }
    /**
     * ä¸åˆæ ¼æ—¶è‡ªåŠ¨åˆ›å»º NCR(不合格品处理单)
     *
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityService.java
@@ -15,6 +15,11 @@
    java.util.Map<String, Object> resolveScan(String code);
    java.util.Map<String, Object> resolveScan(String code, String scanType);
    java.util.Map<String, Object> submitGroup(cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityGroupSubmitReqVO reqVO);
    void retest(Long id, cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityRetestReqVO reqVO);
    void processApprove(Long id, boolean approved, String opinion);
    void companyApprove(Long id, boolean approved, String opinion);
    void processApproveBatch(List<Long> ids, boolean approved, String opinion);
    void companyApproveBatch(List<Long> ids, boolean approved, String opinion);
    PageResult<MesQcSeedQualityDO> getQualityPage(MesQcSeedQualityPageReqVO reqVO);
@@ -34,4 +39,5 @@
     * @return è´¨æ£€æŒ‡æ ‡åˆ—表
     */
    List<MesQcSeedQualityIndicatorDO> getIndicatorsByQualityId(Long qualityId);
    List<cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedQualityActionDO> getActions(Long qualityId);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityServiceImpl.java
@@ -2,6 +2,7 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityPageReqVO;
@@ -43,6 +44,7 @@
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_QUALIFIED_WAREHOUSE_NOT_CONFIGURED;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_WAREHOUSE_NOT_EXISTS;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_WAREHOUSE_TYPE_NOT_QUALIFIED;
@@ -50,7 +52,10 @@
@Service @Validated @Slf4j
public class MesQcSeedQualityServiceImpl implements MesQcSeedQualityService {
    private static final Set<String> REQUIRED = Set.of("purity", "cleanliness", "germination_rate", "moisture");
    private static final int WAIT_RETEST = 10, WAIT_PROCESS = 20, WAIT_COMPANY = 30,
            RELEASED = 40, WAIT_PROCESS_FAIL = 50, WAIT_COMPANY_FAIL = 60, SCRAPPED = 70;
    @Resource private MesQcSeedQualityMapper qualityMapper;
    @Resource private MesQcSeedQualityActionMapper actionMapper;
    @Resource private MesQcSeedQualityIndicatorMapper indicatorMapper;
    @Resource private MesProBagCodeMapper bagCodeMapper;
    @Resource private cn.iocoder.yudao.module.mes.dal.mysql.pro.pallet.MesProPalletMapper palletMapper;
@@ -197,33 +202,52 @@
        if (!MesWmWarehouseTypeEnum.QUALIFIED.getType().equals(qualifiedWarehouse.getType())) {
            throw exception(WM_WAREHOUSE_TYPE_NOT_QUALIFIED);
        }
        // è§£æžåˆæ ¼åº“默认库区/库位(取该仓库下第一个库区、其下第一个库位,未配置则为 null)
        Long qualifiedLocationId = null;
        Long qualifiedAreaId = null;
        List<MesWmWarehouseLocationDO> locations = warehouseLocationService.getWarehouseLocationList(qualifiedWarehouse.getId());
        if (CollUtil.isNotEmpty(locations)) {
            qualifiedLocationId = locations.get(0).getId();
            List<MesWmWarehouseAreaDO> areas = warehouseAreaService.getWarehouseAreaList(qualifiedLocationId);
            if (CollUtil.isNotEmpty(areas)) {
                qualifiedAreaId = areas.get(0).getId();
            }
        }
        Set<String> codes = new HashSet<>();
        boolean passed = true;
        MesQcSeedQualityDO quality = MesQcSeedQualityDO.builder().code(bag.getCode()).itemId(batch.getItemId())
                .batchCode(batch.getCode()).reason(reqVO.getReason()).result(0)
                .batchCode(batch.getCode()).batchId(batch.getId()).reason(reqVO.getReason()).result(0)
                .qualifiedWarehouseId(qualifiedWarehouse.getId()).inspectorId(getLoginUserId())
                .inspectedAt(LocalDateTime.now()).build();
        qualityMapper.insert(quality);
        for (MesQcSeedQualitySubmitReqVO.Indicator item : reqVO.getIndicators()) {
            if (!REQUIRED.contains(item.getCode()) || !codes.add(item.getCode())) throw new IllegalArgumentException("指标必须为纯度、净度、发芽率、水分且不可重复");
            if (item.getMinValue().compareTo(item.getMaxValue()) > 0 || item.getValue().compareTo(item.getMinValue()) < 0 || item.getValue().compareTo(item.getMaxValue()) > 0) passed = false;
            if (item.getMinValue().compareTo(item.getMaxValue()) > 0) throw new IllegalArgumentException("指标下限不能大于上限");
            if (item.getValue().compareTo(item.getMinValue()) < 0 || item.getValue().compareTo(item.getMaxValue()) > 0) passed = false;
            indicatorMapper.insert(MesQcSeedQualityIndicatorDO.builder().qualityId(quality.getId()).indicatorCode(item.getCode())
                    .value(item.getValue()).minValue(item.getMinValue()).maxValue(item.getMaxValue()).build());
        }
        if (!codes.equals(REQUIRED)) throw new IllegalArgumentException("四项指标不完整");
        if (!passed && StrUtil.isBlank(reqVO.getReason())) throw new IllegalArgumentException("质检不合格必须填写原因");
        quality.setResult(passed ? 1 : 2);
        quality.setWorkflowStatus(passed ? WAIT_PROCESS : WAIT_RETEST);
        qualityMapper.updateById(quality);
        recordAction(quality.getId(), "INITIAL", quality.getResult(), reqVO.getReason(), reqVO.getIndicators());
        // åˆæ£€ä»…记录结论;袋仍留在暂存库,待复检及两级审批后再放行或报废。
        return quality.getId();
    }
    private void finalizeStock(MesQcSeedQualityDO quality, boolean passed) {
        MesProBagCodeDO bag = bagCodeMapper.selectByScanCodeForUpdate(quality.getCode());
        if (bag == null || bag.getInboundEventId() == null || !Objects.equals(bag.getQcStatus(), MesProBagCodeQcStatusEnum.UNCHECKED.getStatus()))
            throw new IllegalArgumentException("袋码状态已变化,禁止重复库存流转");
        MesProBatchDO batch = proBatchService.getBatch(bag.getBatchId());
        if (batch == null) throw new IllegalArgumentException("袋码所属生产批次不存在");
        MesWmBatchDO wmBatch = mesWmBatchMapper.selectByCode(batch.getCode());
        if (wmBatch == null) throw new IllegalArgumentException("暂存库存所属仓库批次不存在");
        MesWmWarehouseDO qualifiedWarehouse = warehouseService.getWarehouse(quality.getQualifiedWarehouseId());
        if (passed && (qualifiedWarehouse == null || Boolean.TRUE.equals(qualifiedWarehouse.getFrozen())
                || !MesWmWarehouseTypeEnum.QUALIFIED.getType().equals(qualifiedWarehouse.getType())))
            throw new IllegalArgumentException("目标合格库不存在或类型已变化");
        Long qualifiedLocationId = null;
        Long qualifiedAreaId = null;
        if (passed) {
            List<MesWmWarehouseLocationDO> locations = warehouseLocationService.getWarehouseLocationList(qualifiedWarehouse.getId());
            if (CollUtil.isNotEmpty(locations)) {
                qualifiedLocationId = locations.get(0).getId();
                List<MesWmWarehouseAreaDO> areas = warehouseAreaService.getWarehouseAreaList(qualifiedLocationId);
                if (CollUtil.isNotEmpty(areas)) qualifiedAreaId = areas.get(0).getId();
            }
        }
        // æŒ‰è¢‹ç ç²’度流转库存:锁定该袋在暂存库中的活动明细,禁止按批次任意扣减
        MesWmMaterialStockBagDO stockBag = materialStockBagMapper.selectActiveByBagCode(bag.getCode());
        MesWmMaterialStockDO tempStock = resolveTempStock(bag, wmBatch, stockBag);
@@ -232,7 +256,7 @@
                .setType(MesWmTransactionTypeEnum.OUT.getType())
                .setBizType(MesBizTypeConstants.QC_SEED_QUALITY)
                .setBizId(quality.getId())
                .setBizCode(reqVO.getCode())
                .setBizCode(quality.getCode())
                .setBizLineId(quality.getId())
                .setItemId(batch.getItemId())
                .setQuantity(BigDecimal.ONE.negate())
@@ -255,7 +279,7 @@
                    .setType(MesWmTransactionTypeEnum.IN.getType())
                    .setBizType(MesBizTypeConstants.QC_SEED_QUALITY)
                    .setBizId(quality.getId())
                    .setBizCode(reqVO.getCode())
                    .setBizCode(quality.getCode())
                    .setBizLineId(quality.getId())
                    .setItemId(batch.getItemId())
                    .setQuantity(BigDecimal.ONE)
@@ -275,14 +299,113 @@
            // B2. åŒæ­¥è¿ç§»è¢‹ç æ˜Žç»†åˆ°åˆæ ¼åº“存落点
            materialStockBagService.moveIn(qualInReq, qualifiedStock, qualInTxId);
            bagCodeMapper.updateQcStatus(bag.getId(), MesProBagCodeQcStatusEnum.QUALIFIED.getStatus());
            log.info("[submit][质检合格,袋码 {} ç”±æš‚存库转合格库 {}]", bag.getCode(), qualifiedWarehouse.getName());
            log.info("[finalizeStock][审批放行,袋码 {} ç”±æš‚存库转合格库 {}]", bag.getCode(), qualifiedWarehouse.getName());
        } else {
            // C. è´¨æ£€ä¸åˆæ ¼ â†’ è¯¥è¢‹æŠ¥åºŸï¼›æš‚存出库事务已扣减暂存库存,标记袋码报废
            materialStockBagService.scrap(tempOutReq, tempOutTxId);
            bagCodeMapper.updateQcStatus(bag.getId(), MesProBagCodeQcStatusEnum.UNQUALIFIED.getStatus());
            log.info("[submit][质检不合格,袋码 {} æŠ¥åºŸ]", bag.getCode());
            log.info("[finalizeStock][审批报废,袋码 {}]", bag.getCode());
        }
        return quality.getId();
    }
    @Override @Transactional(rollbackFor = Exception.class)
    public void retest(Long id, cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityRetestReqVO req) {
        MesQcSeedQualityDO quality = requireStatus(id, WAIT_RETEST);
        if (!Objects.equals(quality.getCode(), req.getCode())) throw new IllegalArgumentException("复检袋码与原质检记录不一致");
        boolean passed = validateIndicators(req.getIndicators());
        if (!passed && StrUtil.isBlank(req.getReason())) throw new IllegalArgumentException("复检不合格必须填写原因");
        recordAction(id, "RETEST", passed ? 1 : 2, req.getReason(), req.getIndicators());
        qualityMapper.updateById(MesQcSeedQualityDO.builder().id(id).result(passed ? 1 : 2)
                .reason(passed ? "" : req.getReason().trim()).retestResult(passed ? 1 : 2).retestedAt(LocalDateTime.now())
                .inspectorId(getLoginUserId()).inspectedAt(LocalDateTime.now())
                .workflowStatus(passed ? WAIT_PROCESS : WAIT_PROCESS_FAIL).build());
        // é¡µé¢ã€å…¬å¼€è¿½æº¯è¯»å–主指标表;同步为最新一次测值,历次测值在操作留痕表保留。
        Map<String, MesQcSeedQualityIndicatorDO> existing = new HashMap<>();
        for (MesQcSeedQualityIndicatorDO indicator : indicatorMapper.selectListByQualityId(id))
            existing.put(indicator.getIndicatorCode(), indicator);
        for (MesQcSeedQualitySubmitReqVO.Indicator item : req.getIndicators()) {
            MesQcSeedQualityIndicatorDO indicator = existing.get(item.getCode());
            if (indicator == null) throw new IllegalArgumentException("质检指标记录缺失");
            indicatorMapper.updateById(MesQcSeedQualityIndicatorDO.builder().id(indicator.getId())
                    .value(item.getValue()).minValue(item.getMinValue()).maxValue(item.getMaxValue()).build());
        }
    }
    @Override @Transactional(rollbackFor = Exception.class)
    public void processApprove(Long id, boolean approved, String opinion) {
        MesQcSeedQualityDO quality = qualityMapper.selectByIdForUpdate(id);
        if (quality == null || !Set.of(WAIT_PROCESS, WAIT_PROCESS_FAIL).contains(quality.getWorkflowStatus()))
            throw new IllegalArgumentException("当前状态不能进行加工部审批");
        if (StrUtil.isBlank(opinion)) throw new IllegalArgumentException("请填写审批意见");
        int next = approved ? (quality.getWorkflowStatus() == WAIT_PROCESS ? WAIT_COMPANY : WAIT_COMPANY_FAIL) : WAIT_RETEST;
        qualityMapper.updateById(MesQcSeedQualityDO.builder().id(id).workflowStatus(next)
                .processApprovedAt(LocalDateTime.now()).build());
        recordAction(id, approved ? "PROCESS_APPROVE" : "PROCESS_REJECT", quality.getResult(), opinion, null);
    }
    @Override @Transactional(rollbackFor = Exception.class)
    public void companyApprove(Long id, boolean approved, String opinion) {
        MesQcSeedQualityDO quality = qualityMapper.selectByIdForUpdate(id);
        if (quality == null || !Set.of(WAIT_COMPANY, WAIT_COMPANY_FAIL).contains(quality.getWorkflowStatus()))
            throw new IllegalArgumentException("当前状态不能进行公司质检审批");
        if (StrUtil.isBlank(opinion)) throw new IllegalArgumentException("请填写审批意见");
        if (approved) finalizeStock(quality, quality.getWorkflowStatus() == WAIT_COMPANY);
        int next = approved ? (quality.getWorkflowStatus() == WAIT_COMPANY ? RELEASED : SCRAPPED) : WAIT_RETEST;
        qualityMapper.updateById(MesQcSeedQualityDO.builder().id(id).workflowStatus(next)
                .companyApprovedAt(LocalDateTime.now()).build());
        recordAction(id, approved ? "COMPANY_APPROVE" : "COMPANY_REJECT", quality.getResult(), opinion, null);
    }
    @Override @Transactional(rollbackFor = Exception.class)
    public void processApproveBatch(List<Long> ids, boolean approved, String opinion) {
        for (Long id : approvalIds(ids)) processApprove(id, approved, opinion);
    }
    @Override @Transactional(rollbackFor = Exception.class)
    public void companyApproveBatch(List<Long> ids, boolean approved, String opinion) {
        for (Long id : approvalIds(ids)) companyApprove(id, approved, opinion);
    }
    private List<Long> approvalIds(List<Long> ids) {
        if (ids == null || ids.isEmpty() || ids.size() > 500 || ids.stream().anyMatch(Objects::isNull))
            throw new IllegalArgumentException("请选择 1 è‡³ 500 æ¡è´¨æ£€è®°å½•");
        List<Long> sorted = ids.stream().distinct().sorted().toList();
        if (sorted.size() != ids.size()) throw new IllegalArgumentException("质检记录不能重复选择");
        return sorted;
    }
    private MesQcSeedQualityDO requireStatus(Long id, int expected) {
        MesQcSeedQualityDO quality = qualityMapper.selectByIdForUpdate(id);
        if (quality == null || !Objects.equals(quality.getWorkflowStatus(), expected))
            throw new IllegalArgumentException("质检记录状态已变化,请刷新后重试");
        return quality;
    }
    private boolean validateIndicators(List<MesQcSeedQualitySubmitReqVO.Indicator> indicators) {
        if (indicators == null || indicators.size() != REQUIRED.size()) throw new IllegalArgumentException("必须提交四项指标");
        Set<String> codes = new HashSet<>();
        boolean passed = true;
        for (MesQcSeedQualitySubmitReqVO.Indicator item : indicators) {
            if (item == null || !REQUIRED.contains(item.getCode()) || !codes.add(item.getCode())
                    || item.getValue() == null || item.getMinValue() == null || item.getMaxValue() == null)
                throw new IllegalArgumentException("指标必须为纯度、净度、发芽率、水分且不可重复");
            if (item.getMinValue().compareTo(item.getMaxValue()) > 0) throw new IllegalArgumentException("指标下限不能大于上限");
            if (item.getValue().compareTo(item.getMinValue()) < 0 || item.getValue().compareTo(item.getMaxValue()) > 0) passed = false;
        }
        return passed;
    }
    private void recordAction(Long id, String action, Integer result, String opinion,
                              List<MesQcSeedQualitySubmitReqVO.Indicator> indicators) {
        MesQcSeedQualityActionDO event = new MesQcSeedQualityActionDO();
        event.setQualityId(id);
        event.setAction(action);
        event.setResult(result);
        event.setOpinion(opinion);
        event.setIndicatorsJson(indicators == null ? null : JSONUtil.toJsonStr(indicators));
        event.setOperatorId(getLoginUserId());
        event.setCreateTime(LocalDateTime.now());
        actionMapper.insert(event);
    }
    /**
@@ -392,4 +515,9 @@
            return List.of();
        }
    }
    @Override
    public List<MesQcSeedQualityActionDO> getActions(Long qualityId) {
        return actionMapper.selectByQualityId(qualityId);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/trace/MesProTracePublicServiceImpl.java
@@ -378,7 +378,9 @@
        }
        MesQcSeedQualityDO quality = list.get(0);
        MesProTracePublicRespVO.QualityVO vo = new MesProTracePublicRespVO.QualityVO();
        vo.setResult(quality.getResult()).setResultName(qualityResultName(quality.getResult()))
        Integer finalResult = Integer.valueOf(40).equals(quality.getWorkflowStatus()) ? 1
                : Integer.valueOf(70).equals(quality.getWorkflowStatus()) ? 2 : 0;
        vo.setResult(finalResult).setResultName(finalResult == 0 ? "复检或审批中" : qualityResultName(finalResult))
                .setReason(quality.getReason()).setInspectorName(userName(quality.getInspectorId()))
                .setInspectedAt(quality.getInspectedAt());
        List<MesQcSeedQualityIndicatorDO> indicators = qcSeedQualityService.getIndicatorsByQualityId(quality.getId());
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImplTest.java
@@ -2,6 +2,7 @@
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.oqc.MesQcOqcDO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.MesQcOqcRetestReqVO;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.oqc.MesQcOqcMapper;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum;
@@ -11,6 +12,9 @@
import cn.iocoder.yudao.module.mes.service.wm.productsales.MesWmProductSalesLineService;
import cn.iocoder.yudao.module.mes.service.wm.productsales.MesWmProductSalesService;
import cn.iocoder.yudao.module.mes.service.qc.indicatorresult.MesQcIndicatorResultService;
import cn.iocoder.yudao.module.mes.service.qc.ncr.MesQcNcrService;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
@@ -55,6 +59,12 @@
    private AdminUserApi adminUserApi;
    @MockitoBean
    private MesQcIndicatorResultService indicatorResultService;
    @MockitoBean
    private MesQcNcrService ncrService;
    @MockitoBean
    private MdmItemApi mdmItemApi;
    @MockitoBean
    private CrmCustomerApi customerApi;
    @Test
    public void testFinishOqc_writeBack_productSales() {
@@ -66,6 +76,9 @@
            o.setSourceDocType(MesBizTypeConstants.WM_PRODUCT_SALES);
            o.setSourceLineId(sourceLineId);
            o.setCheckResult(checkResult);
            o.setUnqualifiedQuantity(java.math.BigDecimal.ZERO);
            o.setQualifiedQuantity(java.math.BigDecimal.TEN);
            o.setCheckQuantity(java.math.BigDecimal.TEN);
        });
        oqcMapper.insert(oqc);
@@ -75,9 +88,83 @@
        // æ–­è¨€ï¼šéªŒè¯çŠ¶æ€æ›´æ–°ä¸ºå·²å®Œæˆ
        MesQcOqcDO updatedOqc = oqcMapper.selectById(oqc.getId());
        assertEquals(MesQcStatusEnum.FINISHED.getStatus(), updatedOqc.getStatus());
        // æ–­è¨€ï¼šéªŒè¯å›žå†™é”€å”®å‡ºåº“单行
        assertEquals(20, updatedOqc.getWorkflowStatus());
        // åˆæ£€å®Œæˆä¸æ”¾è¡Œï¼›é”€å”®éƒ¨å’Œå…¬å¸è´¨æ£€å‡å®¡æ‰¹åŽæ‰å›žå†™ã€‚
        verify(productSalesLineService, never()).updateProductSalesLineWhenOqcFinish(anyLong(), anyLong(), anyInt());
        oqcService.salesApprove(oqc.getId(), true, "销售部同意");
        oqcService.companyApprove(oqc.getId(), true, "公司质检同意");
        verify(productSalesLineService).updateProductSalesLineWhenOqcFinish(
                eq(sourceLineId), eq(oqc.getId()), eq(checkResult));
    }
    @Test
    public void testFailedInitialRetestPassOnlyReleasesAfterBothApprovals() {
        Long sourceLineId = randomLongId();
        MesQcOqcDO oqc = randomPojo(MesQcOqcDO.class, o -> {
            o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
            o.setSourceDocType(MesBizTypeConstants.WM_PRODUCT_SALES);
            o.setSourceLineId(sourceLineId);
            o.setOutQuantity(java.math.BigDecimal.valueOf(100));
            o.setCheckQuantity(java.math.BigDecimal.TEN);
            o.setQualifiedQuantity(java.math.BigDecimal.valueOf(9));
            o.setUnqualifiedQuantity(java.math.BigDecimal.ONE);
            o.setCheckResult(2);
        });
        oqcMapper.insert(oqc);
        oqcService.finishOqc(oqc.getId());
        assertEquals(10, oqcMapper.selectById(oqc.getId()).getWorkflowStatus());
        verifyNoInteractions(ncrService);
        verify(productSalesLineService, never()).updateProductSalesLineWhenOqcFinish(anyLong(), anyLong(), anyInt());
        MesQcOqcRetestReqVO retest = new MesQcOqcRetestReqVO();
        retest.setId(oqc.getId());
        retest.setCheckQuantity(java.math.BigDecimal.TEN);
        retest.setQualifiedQuantity(java.math.BigDecimal.TEN);
        retest.setUnqualifiedQuantity(java.math.BigDecimal.ZERO);
        retest.setCheckResult(1);
        oqcService.retestOqc(retest);
        assertEquals(20, oqcMapper.selectById(oqc.getId()).getWorkflowStatus());
        oqcService.salesApprove(oqc.getId(), true, "销售部通过");
        assertEquals(30, oqcMapper.selectById(oqc.getId()).getWorkflowStatus());
        verify(productSalesLineService, never()).updateProductSalesLineWhenOqcFinish(anyLong(), anyLong(), anyInt());
        oqcService.companyApprove(oqc.getId(), true, "公司质检通过");
        assertEquals(40, oqcMapper.selectById(oqc.getId()).getWorkflowStatus());
        verify(productSalesLineService).updateProductSalesLineWhenOqcFinish(sourceLineId, oqc.getId(), 1);
        verifyNoInteractions(ncrService);
    }
    @Test
    public void testFailedRetestCreatesNcrOnlyAfterCompanyApproval() {
        Long sourceLineId = randomLongId();
        MesQcOqcDO oqc = randomPojo(MesQcOqcDO.class, o -> {
            o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
            o.setSourceDocType(MesBizTypeConstants.WM_PRODUCT_SALES);
            o.setSourceLineId(sourceLineId);
            o.setOutQuantity(java.math.BigDecimal.valueOf(100));
            o.setCheckQuantity(java.math.BigDecimal.TEN);
            o.setQualifiedQuantity(java.math.BigDecimal.valueOf(9));
            o.setUnqualifiedQuantity(java.math.BigDecimal.ONE);
            o.setCheckResult(2);
        });
        oqcMapper.insert(oqc);
        oqcService.finishOqc(oqc.getId());
        MesQcOqcRetestReqVO retest = new MesQcOqcRetestReqVO();
        retest.setId(oqc.getId());
        retest.setCheckQuantity(java.math.BigDecimal.TEN);
        retest.setQualifiedQuantity(java.math.BigDecimal.valueOf(8));
        retest.setUnqualifiedQuantity(java.math.BigDecimal.valueOf(2));
        retest.setCheckResult(2);
        retest.setOpinion("复检仍不合格");
        oqcService.retestOqc(retest);
        assertEquals(50, oqcMapper.selectById(oqc.getId()).getWorkflowStatus());
        verifyNoInteractions(ncrService);
        oqcService.salesApprove(oqc.getId(), true, "销售部确认不合格");
        assertEquals(60, oqcMapper.selectById(oqc.getId()).getWorkflowStatus());
        verifyNoInteractions(ncrService);
        oqcService.companyApprove(oqc.getId(), true, "公司质检确认不合格");
        assertEquals(70, oqcMapper.selectById(oqc.getId()).getWorkflowStatus());
        verify(ncrService).createNcr(any());
        verify(productSalesLineService).updateProductSalesLineWhenOqcFinish(sourceLineId, oqc.getId(), 2);
    }
    @Test
@@ -86,6 +173,9 @@
        MesQcOqcDO oqc = randomPojo(MesQcOqcDO.class, o -> {
            o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
            o.setCheckResult(1);
            o.setUnqualifiedQuantity(java.math.BigDecimal.ZERO);
            o.setQualifiedQuantity(java.math.BigDecimal.TEN);
            o.setCheckQuantity(java.math.BigDecimal.TEN);
            o.setSourceDocType(null);
            o.setSourceLineId(null);
        });
@@ -108,13 +198,17 @@
        MesQcOqcDO oqc = randomPojo(MesQcOqcDO.class, o -> {
            o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
            o.setCheckResult(1);
            o.setUnqualifiedQuantity(java.math.BigDecimal.ZERO);
            o.setQualifiedQuantity(java.math.BigDecimal.TEN);
            o.setCheckQuantity(java.math.BigDecimal.TEN);
            o.setSourceDocType(MesBizTypeConstants.WM_PRODUCT_SALES);
            o.setSourceLineId(null); // è„æ•°æ®
        });
        oqcMapper.insert(oqc);
        // è°ƒç”¨ï¼Œå¹¶æ–­è¨€å¼‚常
        assertThrows(IllegalArgumentException.class, () -> oqcService.finishOqc(oqc.getId()));
        oqcService.finishOqc(oqc.getId());
        oqcService.salesApprove(oqc.getId(), true, "销售部同意");
        assertThrows(IllegalArgumentException.class, () -> oqcService.companyApprove(oqc.getId(), true, "公司质检同意"));
    }
    @Test
@@ -123,13 +217,17 @@
        MesQcOqcDO oqc = randomPojo(MesQcOqcDO.class, o -> {
            o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
            o.setCheckResult(1);
            o.setUnqualifiedQuantity(java.math.BigDecimal.ZERO);
            o.setQualifiedQuantity(java.math.BigDecimal.TEN);
            o.setCheckQuantity(java.math.BigDecimal.TEN);
            o.setSourceDocType(99999); // æ— æ³•识别的类型
            o.setSourceLineId(randomLongId());
        });
        oqcMapper.insert(oqc);
        // è°ƒç”¨ï¼Œå¹¶æ–­è¨€å¼‚常
        assertThrows(IllegalArgumentException.class, () -> oqcService.finishOqc(oqc.getId()));
        oqcService.finishOqc(oqc.getId());
        oqcService.salesApprove(oqc.getId(), true, "销售部同意");
        assertThrows(IllegalArgumentException.class, () -> oqcService.companyApprove(oqc.getId(), true, "公司质检同意"));
    }
    @Test
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/SeedQualityWorkflowTest.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
package cn.iocoder.yudao.module.mes.service.qc.seedquality;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedQualityDO;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.seedquality.MesQcSeedQualityActionMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.seedquality.MesQcSeedQualityMapper;
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.test.util.ReflectionTestUtils;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
class SeedQualityWorkflowTest {
    private final MesQcSeedQualityServiceImpl service = new MesQcSeedQualityServiceImpl();
    private final MesQcSeedQualityMapper qualityMapper = mock(MesQcSeedQualityMapper.class);
    private final MesQcSeedQualityActionMapper actionMapper = mock(MesQcSeedQualityActionMapper.class);
    private final MesWmTransactionService transactionService = mock(MesWmTransactionService.class);
    @BeforeEach
    void setUp() {
        ReflectionTestUtils.setField(service, "qualityMapper", qualityMapper);
        ReflectionTestUtils.setField(service, "actionMapper", actionMapper);
        ReflectionTestUtils.setField(service, "wmTransactionService", transactionService);
    }
    @Test
    void failedRetestNeedsBothApprovalsBeforeDisposal() {
        when(qualityMapper.selectByIdForUpdate(1L)).thenReturn(
                MesQcSeedQualityDO.builder().id(1L).code("B1").result(2).workflowStatus(50).build(),
                MesQcSeedQualityDO.builder().id(1L).code("B1").result(2).workflowStatus(60).build());
        service.processApprove(1L, true, "确认不合格,送公司质检审批");
        verifyNoInteractions(transactionService);
        service.companyApprove(1L, false, "返回复检");
        ArgumentCaptor<MesQcSeedQualityDO> update = ArgumentCaptor.forClass(MesQcSeedQualityDO.class);
        verify(qualityMapper, times(2)).updateById(update.capture());
        assertEquals(60, update.getAllValues().get(0).getWorkflowStatus());
        assertEquals(10, update.getAllValues().get(1).getWorkflowStatus());
        verifyNoInteractions(transactionService);
    }
    @Test
    void skipsApprovalWhenStatusChanged() {
        when(qualityMapper.selectByIdForUpdate(1L)).thenReturn(
                MesQcSeedQualityDO.builder().id(1L).workflowStatus(40).build());
        assertThrows(IllegalArgumentException.class, () -> service.companyApprove(1L, true, "重复审批"));
        verify(qualityMapper, never()).updateById(any(MesQcSeedQualityDO.class));
        verifyNoInteractions(transactionService, actionMapper);
    }
}
yudao-module-mes/src/test/resources/sql/create_tables.sql
@@ -413,6 +413,7 @@
    "inspect_date" timestamp DEFAULT NULL,
    "inspector_user_id" bigint DEFAULT NULL,
    "status" tinyint NOT NULL DEFAULT 0,
    "workflow_status" tinyint NOT NULL DEFAULT 0,
    "remark" varchar(500) DEFAULT '',
    "creator" varchar(64) DEFAULT '',
    "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -423,6 +424,18 @@
    PRIMARY KEY ("id")
);
CREATE TABLE IF NOT EXISTS "mes_qc_oqc_action" (
    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
    "oqc_id" bigint NOT NULL,
    "action" varchar(32) NOT NULL,
    "result" tinyint DEFAULT NULL,
    "opinion" varchar(500) DEFAULT NULL,
    "quantities_json" varchar(4000) DEFAULT NULL,
    "operator_id" bigint DEFAULT NULL,
    "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY ("id")
);
-- ----------------------------
-- MES é€€è´§æ£€éªŒå•(RQC)
-- ----------------------------