2 天以前 3be684af887afe4c0e45f281cc53adfef6f12b78
feat(erp): 添加销售订单生产状态跟踪功能

- 在ErpSaleOrderDO中增加productionStatus和productionFinishTime字段
- 在ErpSaleOrderRespVO中添加生产状态相关响应字段
- 实现MES模块生产完成事件发布机制
- 添加ProductionFinishedEvent事件和对应监听器
- 实现销售订单生产状态自动更新逻辑
- 提供完整的前端联调文档说明
已添加4个文件
已修改9个文件
680 ■■■■■ 文件已修改
docs/sale_order_production_status.md 118 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mes_item_id_migration.sql 303 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleOrderDO.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/listener/ProductionFinishedListener.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/event/ProductionFinishedEvent.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/mps/MesProMpsApi.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/item/MesMdItemApiImpl.java 24 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/mps/MesProMpsApiImpl.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemServiceImpl.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsService.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsServiceImpl.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sale_order_production_status.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,118 @@
# é”€å”®è®¢å•生产状态 - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- é”€å”®è®¢å•列表页
- é”€å”®è®¢å•详情页
## ä¸šåŠ¡æµç¨‹
```
销售订单审核通过 â†’ åˆ›å»º MPS â†’ ä¸‹å‘ MPS ç”Ÿæˆç”Ÿäº§å·¥å• â†’ ç”Ÿäº§æŠ¥å·¥ â†’ è´¨æ£€(如需要) â†’ ç”Ÿäº§å®Œæˆ â†’ æ›´æ–°é”€å”®è®¢å•生产状态
```
**生产完成判断条件**:
1. å·²ç”Ÿäº§æ•°é‡ >= è®¡åˆ’数量
2. æ‰€æœ‰æŠ¥å·¥å•状态为 FINISHED(确保质检完成)
## API
### é”€å”®è®¢å•查询接口(已有,响应新增字段)
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /erp/sale-order/page | é”€å”®è®¢å•分页查询 |
| GET | /erp/sale-order/get | é”€å”®è®¢å•详情查询 |
**响应字段新增:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| productionStatus | Integer | ç”Ÿäº§çŠ¶æ€ï¼š0-未完成,1-已完成 |
| productionFinishTime | DateTime | ç”Ÿäº§å®Œæˆæ—¶é—´ |
**响应示例:**
```json
{
  "code": 0,
  "data": {
    "id": 1024,
    "no": "XS20260718001",
    "status": 2,
    "customerId": 1001,
    "customerName": "测试客户",
    "productionStatus": 1,
    "productionFinishTime": "2026-07-18 16:30:00",
    "needProduction": 1,
    "canCreateSalesNotice": true,
    "items": [...]
  }
}
```
## å­—段展示规则
### åˆ—表页
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| productionStatus | åˆ—表 | æ ‡ç­¾å½¢å¼ï¼š0=未完成(灰色),1=已完成(绿色) |
| productionFinishTime | åˆ—表(可选) | ä»…当 productionStatus=1 æ—¶å±•示 |
### è¯¦æƒ…页
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| productionStatus | åŸºæœ¬ä¿¡æ¯åŒºåŸŸ | æ ‡ç­¾å½¢å¼å±•示 |
| productionFinishTime | åŸºæœ¬ä¿¡æ¯åŒºåŸŸ | ä»…当 productionStatus=1 æ—¶å±•示 |
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| ç”Ÿäº§çŠ¶æ€åˆå§‹å€¼ | é”€å”®è®¢å•审核通过时,productionStatus é»˜è®¤ä¸º 0(未完成) |
| ç”Ÿäº§çŠ¶æ€æ›´æ–°æ—¶æœº | å½“所有关联的生产订单完成时,系统自动更新为 1(已完成) |
| ç”Ÿäº§å®Œæˆåˆ¤æ–­ | å·²ç”Ÿäº§æ•°é‡ >= è®¡åˆ’数量 ä¸” æ‰€æœ‰æŠ¥å·¥å•已完成(含质检完成) |
| å‘货通知单生成 | å½“ productionStatus=1 æ—¶ï¼ŒcanCreateSalesNotice ä¸º true |
## å±•示逻辑说明
| æ¡ä»¶ | å±•示内容 |
|------|----------|
| needProduction !== 1(无需生产) | æ˜¾ç¤º"无需生产"或"-" |
| productionStatus === 0 | æ˜¾ç¤ºç°è‰²æ ‡ç­¾"未完成" |
| productionStatus === 1 | æ˜¾ç¤ºç»¿è‰²æ ‡ç­¾"已完成",并展示完成时间 |
## åˆ—顺序建议
生产状态列建议放在"出库状态"列之后。
## ç­›é€‰æ¡ä»¶ï¼ˆå¯é€‰ï¼‰
如需按生产状态筛选,可新增下拉筛选:
- å…¨éƒ¨
- æœªå®Œæˆ
- å·²å®Œæˆ
注意:筛选时仅查询 `needProduction=1` çš„订单。
## æ³¨æ„äº‹é¡¹
1. **状态联动**:`productionStatus` ä¸Ž `canCreateSalesNotice` å…³è”,生产未完成时无法生成发货通知单
2. **无需生产的订单**:`needProduction !== 1` çš„订单,`productionStatus` å­—段为 null,前端应显示"无需生产"或不显示
3. **实时性**:生产状态由后端自动更新,前端刷新页面即可看到最新状态
## å®Œæ•´æµç¨‹ç¤ºä¾‹
1. ç”¨æˆ·åˆ›å»ºé”€å”®è®¢å•(明细勾选"需要生产")
2. é”€å”®è®¢å•审核通过 â†’ ç³»ç»Ÿè‡ªåŠ¨åˆ›å»º MPS
3. MPS ä¸‹å‘ â†’ ç”Ÿæˆç”Ÿäº§å·¥å•
4. ç”Ÿäº§æŠ¥å·¥ â†’ æŠ¥å·¥å®¡æ‰¹
   - å…³é”®å·¥åº+需检验:报工状态→待检验→质检完成→已完成
   - å…¶ä»–工序:报工审批后直接→已完成
5. æ‰€æœ‰æŠ¥å·¥å®Œæˆ â†’ ç³»ç»Ÿè‡ªåŠ¨æ›´æ–°é”€å”®è®¢å• productionStatus=1
6. ç”¨æˆ·åˆ·æ–°é”€å”®è®¢å•页面,看到"生产完成"状态
7. ç”¨æˆ·å¯ç”Ÿæˆå‘货通知单
sql/mes_item_id_migration.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,303 @@
-- ============================================================
-- MES ç‰©æ–™ ID è¿ç§»è„šæœ¬
-- ç›®çš„:将 mes_md_item.id æ›´æ–°ä¸ºä¸Ž mdm_item.id ä¸€è‡´
-- æ‰§è¡Œå‰è¯·å¤‡ä»½æ•°æ®åº“!
-- ============================================================
-- 1. åˆ›å»ºä¸´æ—¶æ˜ å°„表
DROP TEMPORARY TABLE IF EXISTS tmp_item_id_mapping;
CREATE TEMPORARY TABLE tmp_item_id_mapping (
    old_id BIGINT NOT NULL,
    new_id BIGINT NOT NULL,
    code VARCHAR(64),
    PRIMARY KEY (old_id)
);
-- 2. æ’入需要迁移的物料映射数据
INSERT INTO tmp_item_id_mapping (old_id, new_id, code)
SELECT id, mdm_item_id, code
FROM mes_md_item
WHERE mdm_item_id IS NOT NULL AND id != mdm_item_id;
-- æŸ¥çœ‹éœ€è¦è¿ç§»çš„æ•°æ®
SELECT * FROM tmp_item_id_mapping;
-- 3. ç¦ç”¨å¤–键检查(避免外键约束阻止更新)
SET FOREIGN_KEY_CHECKS = 0;
-- 4. æ›´æ–°æ‰€æœ‰å¼•用 mes_md_item.id çš„表
-- æ³¨æ„ï¼šä»¥ä¸‹è¯­å¥ä¼šæ›´æ–°æ‰€æœ‰ item_id å­—段
-- 4.1 åŸºç¡€æ•°æ®è¡¨
UPDATE mes_md_item_batch_config t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_md_product_bom t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_md_product_sip t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_md_product_sop t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
-- 4.2 ç”Ÿäº§ç›¸å…³è¡¨
UPDATE mes_pro_card t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_pro_feedback t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_pro_route_product t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_pro_route_product_bom t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_pro_task t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_pro_task_issue t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_pro_work_order_bom t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
-- 4.3 è´¨é‡ç›¸å…³è¡¨
UPDATE mes_qc_indicator_result t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_qc_ipqc t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_qc_iqc t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_qc_oqc t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_qc_rqc t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_qc_template_item t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
-- 4.4 ä»“库管理表 - å…¥åº“
UPDATE mes_wm_arrival_notice_line t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_wm_item_receipt_detail t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_wm_item_receipt_line t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_wm_misc_receipt_detail t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_wm_misc_receipt_line t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_wm_outsource_receipt_detail t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_wm_outsource_receipt_line t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_wm_product_receipt_detail t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_wm_product_receipt_line t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
-- 4.5 ä»“库管理表 - å‡ºåº“
UPDATE mes_wm_item_consume_detail t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_wm_item_consume_line t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_wm_misc_issue_detail t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_wm_misc_issue_line t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_wm_outsource_issue_detail t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_wm_outsource_issue_line t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_wm_product_issue_detail t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_wm_product_issue_line t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_wm_return_issue_detail t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_wm_return_issue_line t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
-- 4.6 ä»“库管理表 - é”€å”®/退货
UPDATE mes_wm_product_sales_detail t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_wm_product_sales_line t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_wm_return_sales_detail t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_wm_return_sales_line t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_wm_return_vendor_detail t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_wm_return_vendor_line t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_wm_sales_notice_line t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
-- 4.7 ä»“库管理表 - å…¶ä»–
UPDATE mes_wm_batch t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_wm_material_stock t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_wm_package_line t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_wm_sn t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_wm_stock_reserve t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_wm_stock_taking_plan_param t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_wm_stock_taking_task_line t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_wm_stock_taking_task_result t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_wm_transaction t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_wm_transfer_detail t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_wm_transfer_line t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
-- 4.8 ç”Ÿäº§äº§å‡ºè¡¨
UPDATE mes_wm_product_produce_detail t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
UPDATE mes_wm_product_produce_line t
JOIN tmp_item_id_mapping m ON t.item_id = m.old_id
SET t.item_id = m.new_id;
-- 5. æ›´æ–°ä¸»è¡¨ mes_md_item çš„ id
-- ç”±äºŽ id æ˜¯ä¸»é”®ï¼Œéœ€è¦å…ˆåˆ é™¤æ—§è®°å½•再插入新记录
-- åˆ›å»ºä¸´æ—¶è¡¨ä¿å­˜å®Œæ•´æ•°æ®
DROP TEMPORARY TABLE IF EXISTS tmp_mes_md_item;
CREATE TEMPORARY TABLE tmp_mes_md_item AS
SELECT * FROM mes_md_item;
-- åˆ é™¤éœ€è¦è¿ç§»çš„记录
DELETE FROM mes_md_item WHERE id IN (SELECT old_id FROM tmp_item_id_mapping);
-- ä½¿ç”¨æ–° ID é‡æ–°æ’å…¥
INSERT INTO mes_md_item (
    id, mdm_item_id, code, name, specification, unit_measure_id, item_type_id,
    status, safe_stock_flag, min_stock, max_stock, high_value, batch_flag, remark,
    creator, create_time, updater, update_time, deleted
)
SELECT
    m.new_id AS id,  -- ä½¿ç”¨ MDM ç‰©æ–™ ID
    t.mdm_item_id,
    t.code, t.name, t.specification, t.unit_measure_id, t.item_type_id,
    t.status, t.safe_stock_flag, t.min_stock, t.max_stock, t.high_value, t.batch_flag, t.remark,
    t.creator, t.create_time, t.updater, t.update_time, t.deleted
FROM tmp_mes_md_item t
JOIN tmp_item_id_mapping m ON t.id = m.old_id;
-- 6. é‡æ–°å¯ç”¨å¤–键检查
SET FOREIGN_KEY_CHECKS = 1;
-- 7. æ¸…理临时表
DROP TEMPORARY TABLE IF EXISTS tmp_item_id_mapping;
DROP TEMPORARY TABLE IF EXISTS tmp_mes_md_item;
-- 8. éªŒè¯è¿ç§»ç»“æžœ
SELECT id, mdm_item_id, code, name
FROM mes_md_item
WHERE mdm_item_id IS NOT NULL;
-- æ£€æŸ¥æ˜¯å¦è¿˜æœ‰ id != mdm_item_id çš„记录
SELECT COUNT(*) AS need_migration_count
FROM mes_md_item
WHERE mdm_item_id IS NOT NULL AND id != mdm_item_id;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java
@@ -115,6 +115,14 @@
    @Schema(description = "是否有需要生产的明细(0=否 1=是)", example = "1")
    private Integer needProduction;
    // ========== ç”Ÿäº§å…³è” ==========
    @Schema(description = "生产状态:0-未完成,1-已完成", example = "1")
    private Integer productionStatus;
    @Schema(description = "生产完成时间")
    private LocalDateTime productionFinishTime;
    @Data
    public static class Item {
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleOrderDO.java
@@ -128,4 +128,16 @@
     */
    private Integer outStatus;
    // ========== ç”Ÿäº§å…³è” ==========
    /**
     * ç”Ÿäº§çŠ¶æ€ï¼š0-未完成,1-已完成
     */
    private Integer productionStatus;
    /**
     * ç”Ÿäº§å®Œæˆæ—¶é—´
     */
    private LocalDateTime productionFinishTime;
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/listener/ProductionFinishedListener.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
package cn.iocoder.yudao.module.erp.service.sale.listener;
import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOrderDO;
import cn.iocoder.yudao.module.erp.dal.mysql.sale.ErpSaleOrderMapper;
import cn.iocoder.yudao.module.mes.api.event.ProductionFinishedEvent;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
/**
 * ç”Ÿäº§å®Œæˆäº‹ä»¶ç›‘听器
 *
 * ç›‘听 MES æ¨¡å—发布的 ProductionFinishedEvent,更新销售订单的生产状态
 *
 * @author èŠ‹é“æºç 
 */
@Component
@Slf4j
public class ProductionFinishedListener {
    @Resource
    private ErpSaleOrderMapper saleOrderMapper;
    @EventListener
    @Async
    public void onProductionFinished(ProductionFinishedEvent event) {
        log.info("[onProductionFinished] æ”¶åˆ°ç”Ÿäº§å®Œæˆäº‹ä»¶ï¼Œé”€å”®è®¢å•ID: {}", event.getSaleOrderId());
        // æ›´æ–°é”€å”®è®¢å•的生产状态
        saleOrderMapper.updateById(new ErpSaleOrderDO()
                .setId(event.getSaleOrderId())
                .setProductionStatus(1)
                .setProductionFinishTime(event.getFinishTime()));
        log.info("[onProductionFinished] é”€å”®è®¢å• {} ç”Ÿäº§çŠ¶æ€å·²æ›´æ–°ä¸ºå®Œæˆ", event.getSaleOrderId());
    }
}
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/event/ProductionFinishedEvent.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.mes.api.event;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
 * ç”Ÿäº§å®Œæˆäº‹ä»¶
 *
 * å½“销售订单关联的所有生产订单完成时发布此事件
 *
 * @author èŠ‹é“æºç 
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ProductionFinishedEvent {
    /**
     * é”€å”®è®¢å•ID
     */
    private Long saleOrderId;
    /**
     * å®Œæˆæ—¶é—´
     */
    private LocalDateTime finishTime;
}
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/mps/MesProMpsApi.java
@@ -56,4 +56,24 @@
     */
    Boolean checkCanCreateSalesNotice(List<Long> saleOrderItemIds);
    /**
     * æ£€æŸ¥é”€å”®è®¢å•的所有 MPS æ˜¯å¦å·²å®Œæˆç”Ÿäº§
     *
     * å®Œæˆæ¡ä»¶ï¼š
     * 1. å·²ç”Ÿäº§æ•°é‡ >= è®¡åˆ’数量
     * 2. æ‰€æœ‰æŠ¥å·¥å•状态为 FINISHED(确保质检完成)
     *
     * @param saleOrderId é”€å”®è®¢å•ID
     * @return true=全部完成,false=未全部完成
     */
    Boolean checkSaleOrderProductionFinished(Long saleOrderId);
    /**
     * æ ¹æ®å·¥å•ID获取 MPS
     *
     * @param workOrderId å·¥å•ID
     * @return MPS ä¿¡æ¯
     */
    MesProMpsRespDTO getMpsByWorkOrderId(Long workOrderId);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/item/MesMdItemApiImpl.java
@@ -43,8 +43,12 @@
    @Override
    public Long createItem(MesMdItemSyncReqDTO syncReqDTO) {
        log.info("[createItem] åŒæ­¥åˆ›å»º MES ç‰©æ–™äº§å“ï¼Œcode={}", syncReqDTO.getCode());
        log.info("[createItem] åŒæ­¥åˆ›å»º MES ç‰©æ–™äº§å“ï¼Œcode={}, mdmItemId={}", syncReqDTO.getCode(), syncReqDTO.getMdmItemId());
        MesMdItemSaveReqVO saveReqVO = buildSaveReqVO(syncReqDTO);
        // ä½¿ç”¨ MDM ç‰©æ–™ ID ä½œä¸º MES ç‰©æ–™ ID
        if (syncReqDTO.getMdmItemId() != null) {
            saveReqVO.setId(syncReqDTO.getMdmItemId());
        }
        Long itemId = itemService.createItem(saveReqVO);
        // åŒæ­¥æ‰¹æ¬¡é…ç½®
//        saveBatchConfig(itemId, syncReqDTO);
@@ -53,10 +57,22 @@
    @Override
    public void updateItem(MesMdItemSyncReqDTO syncReqDTO) {
        log.info("[updateItem] åŒæ­¥æ›´æ–° MES ç‰©æ–™äº§å“ï¼Œcode={}", syncReqDTO.getCode());
        log.info("[updateItem] åŒæ­¥æ›´æ–° MES ç‰©æ–™äº§å“ï¼Œcode={}, mdmItemId={}", syncReqDTO.getCode(), syncReqDTO.getMdmItemId());
        MesMdItemSaveReqVO saveReqVO = buildSaveReqVO(syncReqDTO);
        // æŒ‰ code åŒ¹é…ï¼šå­˜åœ¨åˆ™æ›´æ–°ï¼Œä¸å­˜åœ¨åˆ™åˆ›å»ºï¼ˆupsert)
        MesMdItemDO existItem = itemService.getItemByCode(syncReqDTO.getCode());
        // ä½¿ç”¨ MDM ç‰©æ–™ ID ä½œä¸º MES ç‰©æ–™ ID
        if (syncReqDTO.getMdmItemId() != null) {
            saveReqVO.setId(syncReqDTO.getMdmItemId());
        }
        // æŒ‰ ID æˆ– code åŒ¹é…ï¼šå­˜åœ¨åˆ™æ›´æ–°ï¼Œä¸å­˜åœ¨åˆ™åˆ›å»ºï¼ˆupsert)
        MesMdItemDO existItem = null;
        // ä¼˜å…ˆæŒ‰ ID æŸ¥æ‰¾
        if (syncReqDTO.getMdmItemId() != null) {
            existItem = itemService.getItem(syncReqDTO.getMdmItemId());
        }
        // å¦‚果按 ID æ‰¾ä¸åˆ°ï¼Œå†æŒ‰ code æŸ¥æ‰¾
        if (existItem == null) {
            existItem = itemService.getItemByCode(syncReqDTO.getCode());
        }
        Long itemId;
        if (existItem == null) {
            itemId = itemService.createItem(saveReqVO);
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/mps/MesProMpsApiImpl.java
@@ -95,4 +95,15 @@
        return true;
    }
    @Override
    public Boolean checkSaleOrderProductionFinished(Long saleOrderId) {
        return mpsService.checkSaleOrderProductionFinished(saleOrderId);
    }
    @Override
    public MesProMpsRespDTO getMpsByWorkOrderId(Long workOrderId) {
        MesProMpsDO mps = mpsService.getMpsByWorkOrderId(workOrderId);
        return BeanUtils.toBean(mps, MesProMpsRespDTO.class);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemServiceImpl.java
@@ -77,6 +77,16 @@
        // æ ¡éªŒæ•°æ®
        validateItemSaveData(createReqVO);
        // å¦‚果指定了 ID,检查该 ID æ˜¯å¦å·²å­˜åœ¨ï¼ˆç”¨äºŽ MDM åŒæ­¥æ—¶ä¿æŒ ID ä¸€è‡´ï¼‰
        if (createReqVO.getId() != null) {
            MesMdItemDO existItem = itemMapper.selectById(createReqVO.getId());
            if (existItem != null) {
                // ID å·²å­˜åœ¨ï¼Œè½¬ä¸ºæ›´æ–°
                updateItem(createReqVO);
                return createReqVO.getId();
            }
        }
        // æ’å…¥
        MesMdItemDO item = BeanUtils.toBean(createReqVO, MesMdItemDO.class);
        item.setStatus(CommonStatusEnum.DISABLE.getStatus()); // é»˜è®¤ç¦ç”¨ã€‚信息完成后,再启用
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java
@@ -26,7 +26,11 @@
import cn.iocoder.yudao.module.mes.service.wm.itemconsume.MesWmItemConsumeService;
import cn.iocoder.yudao.module.mes.service.wm.productproduce.MesWmProductProduceLineService;
import cn.iocoder.yudao.module.mes.service.wm.productproduce.MesWmProductProduceService;
import cn.iocoder.yudao.module.mes.api.mps.MesProMpsApi;
import cn.iocoder.yudao.module.mes.api.mps.dto.MesProMpsRespDTO;
import cn.iocoder.yudao.module.mes.api.event.ProductionFinishedEvent;
import jakarta.annotation.Resource;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -67,6 +71,10 @@
    private MesWmProductProduceService productProduceService;
    @Resource
    private MesWmProductProduceLineService produceLineService;
    @Resource
    private MesProMpsApi mpsApi;
    @Resource
    private ApplicationEventPublisher eventPublisher;
    @Override
    public Long createFeedback(MesProFeedbackSaveReqVO createReqVO) {
@@ -247,6 +255,25 @@
        // 3. æ›´æ–°å·¥å•的已生产数量
        workOrderService.updateProducedQuantity(feedback.getWorkOrderId(),
                feedback.getFeedbackQuantity());
        // 4. æ£€æŸ¥å¹¶å‘布生产完成事件
        checkAndPublishProductionFinished(feedback.getWorkOrderId());
    }
    /**
     * æ£€æŸ¥å¹¶å‘布生产完成事件
     */
    private void checkAndPublishProductionFinished(Long workOrderId) {
        // 1. èŽ·å–å·¥å•å…³è”çš„ MPS
        MesProMpsRespDTO mps = mpsApi.getMpsByWorkOrderId(workOrderId);
        if (mps == null || mps.getSaleOrderId() == null) {
            return; // éžé”€å”®è®¢å•来源的工单,跳过
        }
        // 2. æ£€æŸ¥é”€å”®è®¢å•所有生产是否完成
        if (mpsApi.checkSaleOrderProductionFinished(mps.getSaleOrderId())) {
            eventPublisher.publishEvent(new ProductionFinishedEvent(mps.getSaleOrderId(), java.time.LocalDateTime.now()));
        }
    }
    // ==================== æ ¡éªŒæ–¹æ³• ====================
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsService.java
@@ -145,4 +145,24 @@
     */
    void updateProducedQuantity(Long id, BigDecimal incrQuantity);
    /**
     * æ£€æŸ¥é”€å”®è®¢å•的所有 MPS æ˜¯å¦å·²å®Œæˆç”Ÿäº§
     *
     * å®Œæˆæ¡ä»¶ï¼š
     * 1. å·²ç”Ÿäº§æ•°é‡ >= è®¡åˆ’数量
     * 2. æ‰€æœ‰æŠ¥å·¥å•状态为 FINISHED(确保质检完成)
     *
     * @param saleOrderId é”€å”®è®¢å•ID
     * @return true=全部完成,false=未全部完成
     */
    Boolean checkSaleOrderProductionFinished(Long saleOrderId);
    /**
     * æ ¹æ®å·¥å•ID获取 MPS
     *
     * @param workOrderId å·¥å•ID
     * @return MPS
     */
    MesProMpsDO getMpsByWorkOrderId(Long workOrderId);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsServiceImpl.java
@@ -12,7 +12,10 @@
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.dataobject.pro.feedback.MesProFeedbackDO;
import cn.iocoder.yudao.module.mes.enums.pro.MesProMpsStatusEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackStatusEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProWorkOrderSourceTypeEnum;
import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
@@ -61,6 +64,8 @@
    private MesMdAutoCodeRecordService autoCodeRecordService;
    @Resource
    private CrmCustomerApi customerApi;
    @Resource
    private MesProFeedbackMapper feedbackMapper;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -331,6 +336,57 @@
        mpsMapper.updateProducedQuantity(id, incrQuantity);
    }
    @Override
    public Boolean checkSaleOrderProductionFinished(Long saleOrderId) {
        if (saleOrderId == null) {
            return false;
        }
        // 1. æŸ¥è¯¢è¯¥é”€å”®è®¢å•所有 MPS(通过 saleOrderId å…³è”)
        List<MesProMpsDO> mpsList = mpsMapper.selectList(
                MesProMpsDO::getSaleOrderId, saleOrderId);
        if (CollUtil.isEmpty(mpsList)) {
            // æ—  MPS å…³è”,视为完成
            return true;
        }
        // 2. æ£€æŸ¥æ¯ä¸ª MPS æ˜¯å¦å®Œæˆ
        for (MesProMpsDO mps : mpsList) {
            if (!isMpsFinished(mps)) {
                return false;
            }
        }
        return true;
    }
    @Override
    public MesProMpsDO getMpsByWorkOrderId(Long workOrderId) {
        if (workOrderId == null) {
            return null;
        }
        return mpsMapper.selectOne(MesProMpsDO::getWorkOrderId, workOrderId);
    }
    /**
     * åˆ¤æ–­å•个 MPS æ˜¯å¦å®Œæˆ
     */
    private boolean isMpsFinished(MesProMpsDO mps) {
        // 1. å·²ç”Ÿäº§æ•°é‡ >= è®¡åˆ’数量
        BigDecimal quantityProduced = ObjUtil.defaultIfNull(mps.getQuantityProduced(), BigDecimal.ZERO);
        if (quantityProduced.compareTo(mps.getQuantity()) < 0) {
            return false;
        }
        // 2. æ£€æŸ¥å·¥å•下所有报工是否完成(确保质检完成)
        if (mps.getWorkOrderId() != null) {
            Long unfinishedCount = feedbackMapper.selectCount(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<MesProFeedbackDO>()
                            .eq(MesProFeedbackDO::getWorkOrderId, mps.getWorkOrderId())
                            .ne(MesProFeedbackDO::getStatus, MesProFeedbackStatusEnum.FINISHED.getStatus()));
            if (unfinishedCount > 0) {
                return false;
            }
        }
        return true;
    }
    // ========== ç§æœ‰æ–¹æ³• ==========
    /**