liyong
4 天以前 0f792c9b3b88dc4b1b7b306f89e257d931d2d0bd
代码提交
已添加4个文件
已修改21个文件
637 ■■■■■ 文件已修改
docs/return_issue_integration.md 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/route_process_output_item_integration.md 143 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/MesProRouteProcessController.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/process/MesProRouteProcessRespVO.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/process/MesProRouteProcessSaveReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/template/MesQcTemplateItemController.java 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnissue/MesWmReturnIssueLineController.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnissue/vo/MesWmReturnIssueSaveReqVO.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/route/MesProRouteProcessDO.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/returnissue/MesWmReturnIssueLinePageVO.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/returnissue/MesWmReturnIssueLineMapper.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/transaction/MesWmTransactionMapper.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskServiceImpl.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/template/MesQcTemplateItemServiceImpl.java 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstock/MesWmMaterialStockService.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstock/MesWmMaterialStockServiceImpl.java 21 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productissue/MesWmProductIssueDetailServiceImpl.java 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productissue/MesWmProductIssueServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnissue/MesWmReturnIssueDetailServiceImpl.java 25 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnissue/MesWmReturnIssueLineService.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnissue/MesWmReturnIssueLineServiceImpl.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnissue/MesWmReturnIssueServiceImpl.java 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/MesWmTransactionServiceImpl.java 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/resources/mapper/qc/pendinginspect/MesQcPendingInspectMapper.xml 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/resources/mapper/wm/returnissue/MesWmReturnIssueLineMapper.xml 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/return_issue_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,105 @@
# ç”Ÿäº§é€€æ–™ - å‰ç«¯è”调方案
## é—®é¢˜èƒŒæ™¯
退料明细(detail)的 `warehouseId`/`locationId`/`areaId` è¯­ä¹‰å˜æ›´ï¼š
- **变更前**:来源仓库(从库存记录带入,即虚拟线边库)
- **变更后**:上架目标仓库(退料要放回的实际仓库)
前端传虚拟线边库信息 â†’ åŽç«¯å…¥åº“也入虚拟库 â†’ å®žé™…仓库库存没恢复。
## æ¶‰åŠé¡µé¢
- ç”Ÿäº§é€€æ–™å• - æ˜Žç»†æ–°å¢ž/编辑弹窗
- ç”Ÿäº§é€€æ–™å• - ä¸Šæž¶æ“ä½œé¡µé¢
## æ ¸å¿ƒå˜æ›´
### é€€æ–™æ˜Žç»†çš„仓库字段语义
| å­—段 | å˜æ›´å‰è¯­ä¹‰ | å˜æ›´åŽè¯­ä¹‰ | è¯´æ˜Ž |
|------|-----------|-----------|------|
| `materialStockId` | è™šæ‹Ÿçº¿è¾¹åº“库存记录 ID | ä¸å˜ | ç”¨äºŽä»Žè™šæ‹Ÿçº¿è¾¹åº“出库 |
| `warehouseId` | æ¥æºä»“库(虚拟库) | **上架目标仓库(实际仓库)** | é€€æ–™å…¥åº“的目标仓库 |
| `locationId` | æ¥æºåº“区 | **上架目标库区** | é€€æ–™å…¥åº“的目标库区 |
| `areaId` | æ¥æºåº“位 | **上架目标库位** | é€€æ–™å…¥åº“的目标库位 |
### åŽç«¯å·²åšçš„兼容
后端 `createTransactionList` å·²åšå…œåº•:
- å¦‚æžœ detail çš„ `warehouseId` ä»æ˜¯è™šæ‹Ÿçº¿è¾¹åº“(旧数据/前端未改),会自动通过事务流水反查原始仓库
- å‰ç«¯æ”¹å®ŒåŽï¼Œç›´æŽ¥ä½¿ç”¨ detail çš„仓库信息作为入库目标,不再走反查
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. æ˜Žç»†æ–°å¢ž/编辑弹窗 - ä»“库选择逻辑
**当前逻辑**:选择虚拟线边库库存记录后,自动带入该库存记录的 warehouseId/locationId/areaId(虚拟库)
**改为**:
- `materialStockId`:仍从虚拟线边库库存记录获取(用于标识从哪个库存出)
- `warehouseId`/`locationId`/`areaId`:**改为让用户选择上架目标库位**(实际仓库的库位),不再自动从库存记录带入
#### é€‰æ‹©åº“存记录时
```
选择库存记录 â†’ è‡ªåŠ¨å¸¦å…¥ï¼šitemId, batchId, batchCode, materialStockId
                ä¸å†å¸¦å…¥ï¼šwarehouseId, locationId, areaId
```
#### ä¸Šæž¶ç›®æ ‡åº“位选择
新增一个库位选择器,让用户选择退料要放回的实际仓库库位:
- ä»“库下拉:过滤掉虚拟线边仓库(code = `WIP_VIRTUAL_WAREHOUSE`)
- åº“区下拉:联动仓库
- åº“位下拉:联动库区
### 2. æ•°æ®å¸¦å…¥è¯´æ˜Ž
| åœºæ™¯ | ä»Žå“ªå¸¦å…¥ | å­—段 |
|------|---------|------|
| é€‰æ‹©åº“存记录 | è™šæ‹Ÿçº¿è¾¹åº“库存 | materialStockId, itemId, batchId, batchCode |
| é€‰æ‹©ä¸Šæž¶åº“位 | ç”¨æˆ·æ‰‹åŠ¨é€‰æ‹© | warehouseId, locationId, areaId |
| é»˜è®¤ä¸Šæž¶åº“位 | é¢†æ–™å‡ºåº“时的原始仓库 | warehouseId, locationId, areaId |
**建议**:选择库存记录后,自动查询该物料的领料出库记录,将原始出库仓库作为默认上架目标库位,用户可修改。
### 3. API è¯´æ˜Ž
保存明细的接口不变:
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /mes/wm-return-issue-detail/create | æ–°å¢žæ˜Žç»† |
| PUT | /mes/wm-return-issue-detail/update | ä¿®æ”¹æ˜Žç»† |
**请求参数(同之前,但语义变更):**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| materialStockId | Long | å¦ | è™šæ‹Ÿçº¿è¾¹åº“库存记录 ID(来源) |
| warehouseId | Long | æ˜¯ | **上架目标仓库 ID**(不再是来源仓库) |
| locationId | Long | æ˜¯ | **上架目标库区 ID** |
| areaId | Long | æ˜¯ | **上架目标库位 ID** |
### 4. åŽç«¯æ ¡éªŒå˜æ›´
后端校验已调整:
- âœ… æ ¡éªŒ `materialStockId` å¯¹åº”的库存记录存在、物料一致、库存充足
- âŒ ä¸å†æ ¡éªŒ `materialStockId` çš„仓库与 `warehouseId` ä¸€è‡´ï¼ˆå› ä¸ºæ¥æºæ˜¯è™šæ‹Ÿåº“,目标是实际库)
- âœ… æ ¡éªŒä¸Šæž¶ç›®æ ‡åº“位的混放规则
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| é€€æ–™å‡ºåº“ | ä»Žè™šæ‹Ÿçº¿è¾¹åº“扣减(checkFlag=false,允许负库存) |
| é€€æ–™å…¥åº“ | å…¥åˆ°ç”¨æˆ·é€‰æ‹©çš„实际仓库库位 |
| é»˜è®¤ä¸Šæž¶åº“位 | å»ºè®®é»˜è®¤å¡«å…¥é¢†æ–™å‡ºåº“时的原始仓库库位 |
| ä»“库过滤 | ä¸Šæž¶ç›®æ ‡ä»“库下拉应排除虚拟线边仓库 |
## æ³¨æ„äº‹é¡¹
- åŽç«¯å·²åšå…¼å®¹ï¼šå¦‚果前端暂时不改,旧数据(warehouseId=虚拟库)仍能通过事务流水反查原始仓库,不会报错
- å‰ç«¯æ”¹å®ŒåŽï¼Œæ–°çš„退料单 detail åº”传实际仓库的库位,逻辑更清晰
- å·²å­˜åœ¨çš„æ—§é€€æ–™æ•°æ®ä¸å—影响(反查兜底)
docs/route_process_output_item_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,143 @@
# å·¥è‰ºè·¯çº¿å·¥åº - äº§å‡ºäº§å“å­—段 - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- å·¥è‰ºè·¯çº¿ - å·¥åºé…ç½®é¡µï¼ˆæ–°å¢ž/编辑工序时选择产出产品)
- æŽ’产 - ä»»åŠ¡æ–°å¢žé¡µï¼ˆè‡ªåŠ¨å¸¦å…¥äº§å‡ºäº§å“ï¼‰
- BOM é…ç½®é¡µï¼ˆå·¥åºäº§å‡ºäº§å“å˜æ›´åŽï¼ŒBOM çš„产品字段联动)
## ä¸šåŠ¡èƒŒæ™¯
**问题**:当前所有工序的产出都是最终成品,无法区分中间工序的半成品产出。例如:
- å·¥åº1:拉丝 â†’ äº§å‡º"铝杆"(半成品),不是"钢芯铝绞线"
- å·¥åº2:绞线 â†’ äº§å‡º"钢芯铝绞线"(最终成品)
**解决**:工艺路线工序新增"产出产品"字段,排产时自动带入任务的物料。
## ä¸šåŠ¡æµç¨‹ä¸Žæ•°æ®å¸¦å…¥
1. **工艺路线配置** â†’ æ¯é“工序选择产出产品(半成品/成品)
2. **排产** â†’ é€‰æ‹©å·¥åºåŽï¼Œè‡ªåŠ¨ä»Žå·¥åºå¸¦å‡º `outputItemId` ä½œä¸ºä»»åŠ¡çš„ `itemId`
3. **报工产出** â†’ ä»Žä»»åС冗余 `itemId`(可能是半成品),产出单和库存自动正确
4. **倒冲扣料** â†’ BOM æŸ¥è¯¢æŒ‰å·¥åº+产出产品匹配,自动正确
## API
### å·¥åºä¿å­˜æŽ¥å£ï¼ˆå˜æ›´ï¼‰
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /mes/pro/route-process/create | åˆ›å»ºå·¥åº |
| PUT | /mes/pro/route-process/update | æ›´æ–°å·¥åº |
**请求参数新增:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| outputItemId | Long | å¦ | äº§å‡ºäº§å“ç‰©æ–™ç¼–号(为空时默认用工单成品) |
**请求示例(新增字段):**
```json
{
  "routeId": 7,
  "processId": 10,
  "sort": 1,
  "keyFlag": false,
  "checkFlag": false,
  "backflushFlag": true,
  "outputItemId": 31
}
```
### å·¥åºæŸ¥è¯¢æŽ¥å£ï¼ˆå˜æ›´ï¼‰
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /mes/pro/route-process/list-by-route | æŒ‰å·¥è‰ºè·¯çº¿æŸ¥å·¥åºåˆ—表 |
| GET | /mes/pro/route-process/get-by-route-and-process | æŒ‰è·¯çº¿+工序查配置 |
| GET | /mes/pro/route-process/get | èŽ·å–å•ä¸ªå·¥åº |
**响应字段新增:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| outputItemId | Long | äº§å‡ºäº§å“ç‰©æ–™ç¼–号 |
| outputItemCode | String | äº§å‡ºäº§å“ç‰©æ–™ç¼–码 |
| outputItemName | String | äº§å‡ºäº§å“ç‰©æ–™åç§° |
**响应示例(新增字段):**
```json
{
  "code": 0,
  "data": {
    "id": 1,
    "routeId": 7,
    "processId": 10,
    "processCode": "PROC_001",
    "processName": "拉丝",
    "sort": 1,
    "keyFlag": false,
    "checkFlag": false,
    "backflushFlag": true,
    "outputItemId": 31,
    "outputItemCode": "ITEM_20260716004",
    "outputItemName": "铝杆"
  }
}
```
## å­—段展示规则
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| outputItemId | å·¥åºé…ç½®è¡¨å• | ç‰©æ–™é€‰æ‹©å™¨ï¼Œå¯é€‰åŠæˆå“æˆ–成品 |
| outputItemCode | å·¥åºåˆ—表、工序详情 | åªè¯»å±•示 |
| outputItemName | å·¥åºåˆ—表、工序详情 | åªè¯»å±•示 |
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. å·¥è‰ºè·¯çº¿ - å·¥åºé…ç½®
在"是否倒冲"开关之后,新增"产出产品"选择器:
- ä½¿ç”¨ç‰©æ–™é€‰æ‹©ç»„件,筛选范围与工单产品选择器一致
- éžæœ€åŽå·¥åºï¼šé€‰æ‹©è¯¥å·¥åºäº§å‡ºçš„半成品
- æœ€åŽä¸€é“工序:可留空(默认为工单成品),也可显式选择最终成品
- ä¿å­˜æ—¶å°† `outputItemId` æäº¤åˆ°åŽç«¯
### 2. æŽ’产 - ä»»åŠ¡æ–°å¢ž
- å½“用户选择工单 + å·¥è‰ºè·¯çº¿ + å·¥åºåŽï¼Œè°ƒç”¨ `/mes/pro/route-process/get-by-route-and-process` èŽ·å–å·¥åºé…ç½®
- å¦‚果工序配置了 `outputItemId`,自动填入任务的物料字段(展示 `outputItemName`)
- å¦‚果工序未配置 `outputItemId`,自动回退工单的成品
- ç”¨æˆ·å¯ä»¥æ‰‹åŠ¨ä¿®æ”¹ä»»åŠ¡çš„ç‰©æ–™ï¼ˆè¦†ç›–è‡ªåŠ¨å¡«å…¥å€¼ï¼‰
### 3. BOM é…ç½®
当工序配置了 `outputItemId` åŽï¼š
- è¯¥å·¥åºçš„ BOM è®°å½•çš„"产品"字段应自动关联到 `outputItemId`
- å³ BOM è¡¨ `mes_pro_route_product_bom.product_id` åº”与工序的 `outputItemId` ä¸€è‡´
- **注意**:现有 BOM æ•°æ®çš„ `product_id` å…¨éƒ¨æ˜¯æœ€ç»ˆæˆå“ ID,配置工序产出产品后需重新调整 BOM
### 4. å·¥åºåˆ—表展示
工序列表中新增"产出产品"列,展示 `outputItemName`,未配置时显示"默认(工单成品)"。
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| outputItemId ä¸ºç©º | æŽ’产时任务的 itemId é»˜è®¤ç”¨å·¥å•çš„ productId(向后兼容) |
| outputItemId ä¸ä¸ºç©º | æŽ’产时任务的 itemId è‡ªåŠ¨ç”¨å·¥åºçš„ outputItemId |
| æœ€åŽä¸€é“工序 | å»ºè®®ä¸å¡« outputItemId,或填最终成品,效果一样 |
| éžæœ€åŽå·¥åº | å»ºè®®å¡«å†™åŠæˆå“ï¼Œè¿™æ ·æŠ¥å·¥äº§å‡ºæ—¶å…¥çº¿è¾¹åº“的是半成品 |
| å‰ç«¯æ˜¾å¼ä¼  itemId | åŽç«¯ä¼˜å…ˆä½¿ç”¨å‰ç«¯ä¼ å…¥å€¼ï¼ˆå‘后兼容现有排产逻辑) |
## æ³¨æ„äº‹é¡¹
- çŽ°æœ‰å·¥è‰ºæœ¯çº¿æ•°æ®ä¸éœ€è¦è¿ç§»ï¼Œ`outputItemId` ä¸ºç©ºæ—¶è¡Œä¸ºä¸Žä¹‹å‰å®Œå…¨ä¸€è‡´
- é…ç½®äº† `outputItemId` åŽï¼Œéœ€åŒæ­¥è°ƒæ•´è¯¥å·¥åºçš„ BOM é…ç½®ï¼ˆ`product_id` æ”¹ä¸ºåŠæˆå“ ID)
- åŠæˆå“äº§å‡ºåŽä¼šå…¥çº¿è¾¹åº“(WIP),下一道工序消耗时通过 BOM å€’冲或手工投料扣减
- æŠ¥å·¥é¡µé¢çš„"产品物料"字段可能显示半成品,这是正确的,不要误以为是数据错误
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/MesProRouteProcessController.java
@@ -7,8 +7,10 @@
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.process.MesProRouteProcessRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.process.MesProRouteProcessSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessDO;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.pro.process.MesProProcessService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProcessService;
import io.swagger.v3.oas.annotations.Operation;
@@ -40,6 +42,8 @@
    private MesProRouteProcessService routeProcessService;
    @Resource
    private MesProProcessService processService;
    @Resource
    private MesMdItemService mdItemService;
    @PostMapping("/create")
    @Operation(summary = "创建工艺路线工序")
@@ -116,12 +120,20 @@
        list.forEach(item -> { if (item.getNextProcessId() != null) processIds.add(item.getNextProcessId()); });
        Map<Long, MesProProcessDO> processMap = convertMap(
                processService.getProcessList(new ArrayList<>(processIds)), MesProProcessDO::getId);
        // 2. æ‹¼æŽ¥ VO
        // 2. æ‰¹é‡èŽ·å–äº§å‡ºäº§å“ç‰©æ–™ä¿¡æ¯
        Set<Long> outputItemIds = convertSet(list, MesProRouteProcessDO::getOutputItemId);
        outputItemIds.remove(null);
        Map<Long, MesMdItemDO> outputItemMap = outputItemIds.isEmpty()
                ? Collections.emptyMap()
                : mdItemService.getItemMap(outputItemIds);
        // 3. æ‹¼æŽ¥ VO
        return BeanUtils.toBean(list, MesProRouteProcessRespVO.class, vo -> {
            MapUtils.findAndThen(processMap, vo.getProcessId(),
                    process -> vo.setProcessCode(process.getCode()).setProcessName(process.getName()));
            MapUtils.findAndThen(processMap, vo.getNextProcessId(),
                    nextProcess -> vo.setNextProcessName(nextProcess.getName()));
            MapUtils.findAndThen(outputItemMap, vo.getOutputItemId(),
                    item -> vo.setOutputItemCode(item.getCode()).setOutputItemName(item.getName()));
        });
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/process/MesProRouteProcessRespVO.java
@@ -54,6 +54,15 @@
    @Schema(description = "是否倒冲(true=报工时按BOM自动扣料;false=不自动扣料)", example = "true")
    private Boolean backflushFlag;
    @Schema(description = "产出产品物料编号", example = "100")
    private Long outputItemId;
    @Schema(description = "产出产品物料编码")
    private String outputItemCode;
    @Schema(description = "产出产品物料名称")
    private String outputItemName;
    @Schema(description = "备注")
    private String remark;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/process/MesProRouteProcessSaveReqVO.java
@@ -44,6 +44,9 @@
    @Schema(description = "是否倒冲(true=报工时按BOM自动扣料;false=不自动扣料)", example = "true")
    private Boolean backflushFlag;
    @Schema(description = "产出产品物料编号(为空时默认用工单成品)", example = "100")
    private Long outputItemId;
    @Schema(description = "备注")
    private String remark;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/template/MesQcTemplateItemController.java
@@ -6,10 +6,10 @@
import cn.iocoder.yudao.module.mes.controller.admin.qc.template.vo.item.MesQcTemplateItemPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.template.vo.item.MesQcTemplateItemRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.template.vo.item.MesQcTemplateItemSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateItemDO;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateItemService;
import io.swagger.v3.oas.annotations.Operation;
@@ -38,7 +38,7 @@
    @Resource
    private MesQcTemplateItemService templateItemService;
    @Resource
    private MesMdItemService mdItemService;
    private MesMdmItemService mdmItemService;
    @Resource
    private MesMdUnitMeasureService unitMeasureService;
@@ -90,12 +90,12 @@
        if (list.isEmpty()) {
            return Collections.emptyList();
        }
        // æ‰¹é‡æŸ¥è¯¢ç‰©æ–™
        Map<Long, MesMdItemDO> itemMap = mdItemService.getItemMap(
        // æ‰¹é‡æŸ¥è¯¢ç‰©æ–™ï¼ˆitemId å­˜çš„æ˜¯ mdm_item_id,关联 mdm_item è¡¨ï¼‰
        Map<Long, MesMdmItemDO> itemMap = mdmItemService.getItemMap(
                convertSet(list, MesQcTemplateItemDO::getItemId));
        // æ‰¹é‡æŸ¥è¯¢è®¡é‡å•位
        Map<Long, MesMdUnitMeasureDO> unitMeasureMap = unitMeasureService.getUnitMeasureMap(
                convertSet(itemMap.values(), MesMdItemDO::getUnitMeasureId));
                convertSet(itemMap.values(), MesMdmItemDO::getUnitMeasureId));
        // æ‹¼è£… VO
        return BeanUtils.toBean(list, MesQcTemplateItemRespVO.class, vo -> {
            findAndThen(itemMap, vo.getItemId(), item -> {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnissue/MesWmReturnIssueLineController.java
@@ -12,6 +12,7 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnissue.MesWmReturnIssueLineDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnissue.MesWmReturnIssueLinePageVO;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
import cn.iocoder.yudao.module.mes.service.pro.task.MesProTaskService;
@@ -88,8 +89,8 @@
    @PreAuthorize("@ss.hasPermission('mes:wm-return-issue:query')")
    public CommonResult<PageResult<MesWmReturnIssueLineRespVO>> getReturnIssueLinePage(
            @Valid MesWmReturnIssueLinePageReqVO pageReqVO) {
        PageResult<MesWmReturnIssueLineDO> pageResult = issueLineService.getReturnIssueLinePage(pageReqVO);
        return success(new PageResult<>(buildRespVOList(pageResult.getList()), pageResult.getTotal()));
        PageResult<MesWmReturnIssueLinePageVO> pageResult = issueLineService.getReturnIssueLinePage(pageReqVO);
        return success(new PageResult<>(buildRespVOListFromPageVO(pageResult.getList()), pageResult.getTotal()));
    }
    @GetMapping("/list-by-issue")
@@ -104,6 +105,40 @@
    // ==================== æ‹¼æŽ¥ VO ====================
    /**
     * åˆ†é¡µæŸ¥è¯¢ç»“果构建 RespVO(SQL å·²å…³è” mdm_item,code/name/specification ç›´æŽ¥ä»Ž PageVO å–)
     */
    private List<MesWmReturnIssueLineRespVO> buildRespVOListFromPageVO(List<MesWmReturnIssueLinePageVO> list) {
        if (CollUtil.isEmpty(list)) {
            return Collections.emptyList();
        }
        // 1. èŽ·å¾—å…³è”æ•°æ®ï¼ˆè®¡é‡å•ä½ã€ç”Ÿäº§ä»»åŠ¡ï¼‰
        Map<Long, MesMdItemDO> itemMap = mdmItemService.getItemMap(
                convertSet(list, MesWmReturnIssueLinePageVO::getItemId));
        Map<Long, MesMdUnitMeasureDO> unitMeasureMap = unitMeasureService.getUnitMeasureMap(
                convertSet(itemMap.values(), MesMdItemDO::getUnitMeasureId));
        Map<Long, MesProTaskDO> taskMap = taskService.getTaskMap(
                convertSet(list, MesWmReturnIssueLinePageVO::getTaskId));
        // 2. æž„建结果
        return BeanUtils.toBean(list, MesWmReturnIssueLineRespVO.class, vo -> {
            // code/name/specification ä»Ž PageVO ç›´æŽ¥æ˜ å°„(字段名不同需手动赋值)
            list.stream()
                    .filter(v -> java.util.Objects.equals(v.getId(), vo.getId()))
                    .findFirst()
                    .ifPresent(pageVO -> {
                        vo.setItemCode(pageVO.getCode());
                        vo.setItemName(pageVO.getName());
                        vo.setSpecification(pageVO.getSpecification());
                    });
            // è®¡é‡å•位仍需从 itemMap èŽ·å–
            MapUtils.findAndThen(itemMap, vo.getItemId(), item ->
                    MapUtils.findAndThen(unitMeasureMap, item.getUnitMeasureId(),
                            unit -> vo.setUnitMeasureName(unit.getName())));
            MapUtils.findAndThen(taskMap, vo.getTaskId(), task ->
                    vo.setTaskCode(task.getCode()).setTaskName(task.getName()));
        });
    }
    private List<MesWmReturnIssueLineRespVO> buildRespVOList(List<MesWmReturnIssueLineDO> list) {
        if (CollUtil.isEmpty(list)) {
            return Collections.emptyList();
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnissue/vo/MesWmReturnIssueSaveReqVO.java
@@ -22,8 +22,7 @@
    @NotBlank(message = "退料单名称不能为空")
    private String name;
    @Schema(description = "生产任务 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "生产任务 ID ä¸èƒ½ä¸ºç©º")
    @Schema(description = "生产任务 ID(非必填,不关联任务时传空)", example = "1")
    private Long taskId;
    @Schema(description = "生产工单 ID", example = "1")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/route/MesProRouteProcessDO.java
@@ -84,6 +84,13 @@
     */
    private Boolean backflushFlag;
    /**
     * äº§å‡ºäº§å“ç‰©æ–™ç¼–号
     *
     * å…³è” {@link cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO#getId()}
     * ä¸ºç©ºæ—¶é»˜è®¤ç”¨å·¥å•的成品
     */
    private Long outputItemId;
    /**
     * å¤‡æ³¨
     */
    private String remark;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/returnissue/MesWmReturnIssueLinePageVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnissue;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.math.BigDecimal;
/**
 * MES ç”Ÿäº§é€€æ–™å•行 åˆ†é¡µæŸ¥è¯¢ VO(关联 mdm_item çš„ code/name/specification)
 */
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MesWmReturnIssueLinePageVO extends MesWmReturnIssueLineDO {
    /**
     * ç‰©æ–™ç¼–码
     *
     * å…³è” {@link cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO#getCode()}
     */
    private String code;
    /**
     * ç‰©æ–™åç§°
     *
     * å…³è” {@link cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO#getName()}
     */
    private String name;
    /**
     * è§„格型号
     *
     * å…³è” {@link cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO#getSpecification()}
     */
    private String specification;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/returnissue/MesWmReturnIssueLineMapper.java
@@ -2,10 +2,13 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.wm.returnissue.vo.line.MesWmReturnIssueLinePageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnissue.MesWmReturnIssueLineDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnissue.MesWmReturnIssueLinePageVO;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@@ -15,10 +18,13 @@
@Mapper
public interface MesWmReturnIssueLineMapper extends BaseMapperX<MesWmReturnIssueLineDO> {
    default PageResult<MesWmReturnIssueLineDO> selectPage(MesWmReturnIssueLinePageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesWmReturnIssueLineDO>()
                .eqIfPresent(MesWmReturnIssueLineDO::getIssueId, reqVO.getIssueId())
                .orderByDesc(MesWmReturnIssueLineDO::getId));
    IPage<MesWmReturnIssueLinePageVO> selectPageByReqVO(Page<MesWmReturnIssueLinePageVO> page,
                                                         @Param("reqVO") MesWmReturnIssueLinePageReqVO reqVO);
    default PageResult<MesWmReturnIssueLinePageVO> selectPage(MesWmReturnIssueLinePageReqVO reqVO) {
        Page<MesWmReturnIssueLinePageVO> mpPage = new Page<>(reqVO.getPageNo(), reqVO.getPageSize());
        IPage<MesWmReturnIssueLinePageVO> result = selectPageByReqVO(mpPage, reqVO);
        return new PageResult<>(result.getRecords(), result.getTotal());
    }
    default List<MesWmReturnIssueLineDO> selectListByIssueId(Long issueId) {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/transaction/MesWmTransactionMapper.java
@@ -29,4 +29,28 @@
                .orderByDesc(MesWmTransactionDO::getTransactionTime));
    }
    /**
     * æŸ¥æ‰¾è™šæ‹Ÿçº¿è¾¹åº“库存记录对应的原始出库仓库信息
     * <p>
     * é€šè¿‡è™šæ‹Ÿçº¿è¾¹åº“çš„ materialStockId,找到首次入库事务(IN),
     * å†é€šè¿‡ relatedTransactionId æ‰¾åˆ°å…³è”的出库事务(OUT),
     * èŽ·å–ç‰©æ–™ä»Žå“ªä¸ªå®žé™…ä»“åº“å‡ºåº“è¿›å…¥è™šæ‹Ÿçº¿è¾¹åº“çš„ã€‚
     *
     * @param materialStockId è™šæ‹Ÿçº¿è¾¹åº“库存记录 ID
     * @return åŽŸå§‹å‡ºåº“äº‹åŠ¡ï¼ˆåŒ…å« warehouseId/locationId/areaId),不存在返回 null
     */
    default MesWmTransactionDO selectOriginalOutTransaction(Long materialStockId) {
        // 1. æ‰¾åˆ°è¯¥åº“存记录的首次入库事务
        MesWmTransactionDO firstInTransaction = selectOne(new LambdaQueryWrapperX<MesWmTransactionDO>()
                .eq(MesWmTransactionDO::getMaterialStockId, materialStockId)
                .eq(MesWmTransactionDO::getType, 1) // IN
                .orderByAsc(MesWmTransactionDO::getId)
                .last("LIMIT 1"));
        if (firstInTransaction == null || firstInTransaction.getRelatedTransactionId() == null) {
            return null;
        }
        // 2. é€šè¿‡ relatedTransactionId æ‰¾åˆ°åŽŸå§‹å‡ºåº“äº‹åŠ¡
        return selectById(firstInTransaction.getRelatedTransactionId());
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskServiceImpl.java
@@ -18,6 +18,8 @@
import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService;
import cn.iocoder.yudao.module.mes.service.pro.process.MesProProcessService;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessDO;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProcessService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
import jakarta.annotation.Resource;
@@ -66,6 +68,9 @@
    @Resource
    private MesProRouteService routeService;
    @Resource
    @Lazy
    private MesProRouteProcessService routeProcessService;
    @Resource
    private MesMdAutoCodeRecordService autoCodeRecordService;
    @Override
@@ -75,10 +80,23 @@
        workstationService.validateWorkstationExists(createReqVO.getWorkstationId());
        routeService.validateRouteExists(createReqVO.getRouteId());
        processService.validateProcessExistsAndEnable(createReqVO.getProcessId());
        MesMdItemDO item = itemService.validateItemExists(createReqVO.getItemId());
        // 2.1 æž„建任务 DO:自动填充客户信息
        // 2. è‡ªåŠ¨ç¡®å®š itemId:优先前端传入 â†’ å·¥è‰ºè·¯çº¿å·¥åºçš„产出产品 â†’ å·¥å•成品
        Long itemId = createReqVO.getItemId();
        if (itemId == null) {
            MesProRouteProcessDO routeProcess = routeProcessService
                    .getRouteProcessByRouteIdAndProcessId(createReqVO.getRouteId(), createReqVO.getProcessId());
            if (routeProcess != null && routeProcess.getOutputItemId() != null) {
                itemId = routeProcess.getOutputItemId();
            } else {
                itemId = workOrder.getProductId();
            }
        }
        MesMdItemDO item = itemService.validateItemExists(itemId);
        // 3.1 æž„建任务 DO:自动填充客户信息
        MesProTaskDO task = BeanUtils.toBean(createReqVO, MesProTaskDO.class)
                .setItemId(itemId) // ç¡®ä¿ä½¿ç”¨è®¡ç®—后的 itemId
                .setName(buildTaskName(item, createReqVO.getQuantity()))
                .setCode(autoCodeRecordService.generateAutoCode(MesMdAutoCodeRuleCodeEnum.PRO_TASK_CODE.getCode()))
                .setStatus(MesProTaskStatusEnum.PREPARE.getStatus());
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/template/MesQcTemplateItemServiceImpl.java
@@ -7,9 +7,11 @@
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.qc.template.vo.item.MesQcTemplateItemPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.template.vo.item.MesQcTemplateItemSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateItemDO;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.template.MesQcTemplateItemMapper;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@@ -38,6 +40,8 @@
    @Resource
    @Lazy
    private MesQcTemplateService templateService;
    @Resource
    private MesMdItemService mesMdItemService;
    @Override
    public Long createTemplateItem(MesQcTemplateItemSaveReqVO createReqVO) {
@@ -113,7 +117,8 @@
    @Override
    public MesQcTemplateItemDO getRequiredTemplateByItemIdAndType(Long itemId, Integer qcType) {
        // 1. æŸ¥å‡º itemId å…³è”的所有 templateItem
        List<MesQcTemplateItemDO> templateItems = templateItemMapper.selectListByItemId(itemId);
        MesMdItemDO item1 = mesMdItemService.getItem(itemId);
        List<MesQcTemplateItemDO> templateItems = templateItemMapper.selectListByItemId(item1.getMdmItemId());
        if (CollUtil.isEmpty(templateItems)) {
            throw exception(QC_NO_TEMPLATE);
        }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstock/MesWmMaterialStockService.java
@@ -114,6 +114,20 @@
                                                  Long batchId, String batchCode, Long vendorId, LocalDateTime receiptTime);
    /**
     * æŒ‰ç»„合键查找库存记录(只查不创建)
     *
     * @param itemId      ç‰©æ–™ç¼–号
     * @param warehouseId ä»“库编号
     * @param locationId  åº“区编号
     * @param areaId      åº“位编号
     * @param batchId     æ‰¹æ¬¡ç¼–号
     * @param vendorId    ä¾›åº”商编号
     * @return åº“存记录(不存在返回 null)
     */
    MesWmMaterialStockDO getMaterialStockByCompositeKey(Long itemId, Long warehouseId, Long locationId, Long areaId,
                                                         Long batchId, Long vendorId);
    /**
     * æ›´æ–°åº“存数量
     *
     * @param id åº“存记录编号
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstock/MesWmMaterialStockServiceImpl.java
@@ -9,6 +9,7 @@
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockFreezeReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockListReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemTypeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO;
@@ -17,6 +18,7 @@
import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstock.MesWmMaterialStockMapper;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemTypeService;
import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService;
@@ -52,6 +54,8 @@
    @Resource
    private MesMdmItemService mdmItemService;
    @Resource
    private MesMdItemService mdItemService;
    @Resource
    private MesMdItemTypeService itemTypeService;
    @Resource
@@ -94,10 +98,13 @@
            List<MesMdItemTypeDO> children = itemTypeService.getItemTypeChildrenList(pageReqVO.getItemTypeId());
            itemTypeIds.addAll(convertSet(children, MesMdItemTypeDO::getId));
        }
        // 1.2 è§£æž itemId
        // 1.2 è§£æž itemId:前端传的是 mes_md_item.id,库存表存的是 mdm_item_id,需要转换
        Set<Long> itemIds = null;
        if (pageReqVO.getItemId() != null) {
            itemIds = SetUtils.asSet(pageReqVO.getItemId());
            MesMdItemDO item = mdItemService.getItem(pageReqVO.getItemId());
            if (item != null && item.getMdmItemId() != null) {
                itemIds = SetUtils.asSet(item.getMdmItemId());
            }
        }
        // 1.3 è§£æž virtualFilter:转换为虚拟仓 warehouseId
        Long virtualWarehouseId = null;
@@ -181,7 +188,15 @@
                .frozen(false)
                .build();
        materialStockMapper.insert(newStock);
        return newStock;
        // 3. é‡æ–°æŸ¥è¯¢ï¼Œç¡®ä¿æ‹¿åˆ°æ•°æ®åº“生成的完整记录(id、createTime ç­‰ï¼‰
        MesWmMaterialStockDO inserted = materialStockMapper.selectById(newStock.getId());
        return inserted != null ? inserted : newStock;
    }
    @Override
    public MesWmMaterialStockDO getMaterialStockByCompositeKey(Long itemId, Long warehouseId, Long locationId, Long areaId,
                                                                Long batchId, Long vendorId) {
        return materialStockMapper.selectByCompositeKey(itemId, warehouseId, locationId, areaId, batchId, vendorId);
    }
    @Override
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productissue/MesWmProductIssueDetailServiceImpl.java
@@ -4,14 +4,18 @@
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.module.mes.controller.admin.wm.productissue.vo.detail.MesWmProductIssueDetailSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productissue.MesWmProductIssueDetailDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productissue.MesWmProductIssueDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productissue.MesWmProductIssueLineDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.productissue.MesWmProductIssueDetailMapper;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmProductIssueStatusEnum;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
@@ -41,6 +45,8 @@
    private MesWmWarehouseAreaService warehouseAreaService;
    @Resource
    private MesWmMaterialStockService materialStockService;
    @Resource
    private MesMdItemService mesMdItemService;
    @Override
    public Long createProductIssueDetail(MesWmProductIssueDetailSaveReqVO createReqVO) {
@@ -122,6 +128,7 @@
    private void validateProductIssueDetailSaveData(MesWmProductIssueDetailSaveReqVO reqVO) {
        // æ ¡éªŒçˆ¶æ•°æ®å­˜åœ¨
        MesWmProductIssueLineDO line = issueLineService.validateProductIssueLineExists(reqVO.getLineId());
        MesMdItemDO item = mesMdItemService.getItem(line.getItemId());
        if (ObjUtil.notEqual(line.getIssueId(), reqVO.getIssueId())) {
            throw exception(WM_PRODUCT_ISSUE_DETAIL_LINE_NOT_MATCH);
        }
@@ -131,7 +138,7 @@
            throw exception(WM_PRODUCT_ISSUE_STATUS_INVALID);
        }
        // æ ¡éªŒç‰©æ–™åŒ¹é…ï¼ˆæ˜Žç»† itemId å¿…须与行 itemId ä¸€è‡´ï¼‰
        validateDetailItemMatch(reqVO.getItemId(), line.getItemId());
        validateDetailItemMatch(reqVO.getItemId(), item.getMdmItemId());
        // æ ¡éªŒä»“库、库区、库位的关联关系
        warehouseAreaService.validateWarehouseAreaExists(
                reqVO.getWarehouseId(), reqVO.getLocationId(), reqVO.getAreaId());
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productissue/MesWmProductIssueServiceImpl.java
@@ -248,7 +248,7 @@
            Long outTransactionId = wmTransactionService.createTransaction(new MesWmTransactionSaveReqDTO()
                    .setType(MesWmTransactionTypeEnum.OUT.getType()).setItemId(detail.getItemId())
                    .setQuantity(detail.getQuantity().negate()) // åº“存减少
                    .setBatchId(detail.getBatchId()).setBatchCode(detail.getBatchCode())
                    .setBatchId(detail.getBatchId()).setBatchCode(detail.getBatchCode()).setMaterialStockId(detail.getMaterialStockId())
                    .setWarehouseId(detail.getWarehouseId()).setLocationId(detail.getLocationId()).setAreaId(detail.getAreaId())
                    .setBizType(MesBizTypeConstants.WM_ISSUE).setBizId(issue.getId())
                    .setBizCode(issue.getCode()).setBizLineId(detail.getLineId()));
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnissue/MesWmReturnIssueDetailServiceImpl.java
@@ -6,6 +6,7 @@
import cn.iocoder.yudao.module.mes.controller.admin.wm.returnissue.vo.detail.MesWmReturnIssueDetailSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnissue.MesWmReturnIssueDetailDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnissue.MesWmReturnIssueLineDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.returnissue.MesWmReturnIssueDetailMapper;
import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
@@ -111,15 +112,27 @@
        if (ObjUtil.notEqual(line.getItemId(), reqVO.getItemId())) {
            throw exception(WM_RETURN_ISSUE_DETAIL_ITEM_MISMATCH);
        }
        // æ ¡éªŒä»“库、库区、库位的关联关系
        // æ ¡éªŒä¸Šæž¶ç›®æ ‡ä»“库、库区、库位的关联关系
        warehouseAreaService.validateWarehouseAreaExists(
                reqVO.getWarehouseId(), reqVO.getLocationId(), reqVO.getAreaId());
        // æ ¡éªŒåº“位物料/批次混放规则
        // æ ¡éªŒä¸Šæž¶ç›®æ ‡åº“位的物料/批次混放规则
        materialStockService.checkAreaMixingRule(reqVO.getAreaId(), reqVO.getItemId(), reqVO.getBatchId());
        // æ ¡éªŒåº“存记录
        materialStockService.validateSelectedStock(
                reqVO.getMaterialStockId(), reqVO.getItemId(), reqVO.getBatchId(), reqVO.getBatchCode(),
                reqVO.getWarehouseId(), reqVO.getLocationId(), reqVO.getAreaId(), reqVO.getQuantity());
        // æ ¡éªŒæ¥æºåº“存记录(materialStockId æŒ‡å‘虚拟线边库,warehouseId æ˜¯ä¸Šæž¶ç›®æ ‡ä»“库,两者仓库不同是正常的)
        if (reqVO.getMaterialStockId() != null) {
            MesWmMaterialStockDO stock = materialStockService.getMaterialStock(reqVO.getMaterialStockId());
            if (stock == null) {
                throw exception(WM_MATERIAL_STOCK_NOT_EXISTS);
            }
            // æ ¡éªŒç‰©æ–™å’Œæ‰¹æ¬¡ä¸€è‡´
            if (ObjUtil.notEqual(stock.getItemId(), reqVO.getItemId())) {
                throw exception(WM_MATERIAL_STOCK_SELECTION_MISMATCH);
            }
            // æ ¡éªŒåº“存数量充足
            if (reqVO.getQuantity() != null && stock.getQuantity() != null
                    && stock.getQuantity().compareTo(reqVO.getQuantity()) < 0) {
                throw exception(WM_MATERIAL_STOCK_INSUFFICIENT);
            }
        }
        // æ ¡éªŒæ˜Žç»†æ€»æ•°é‡ä¸è¶…过行数量(排除自身)
        validateDetailQuantityNotExceed(reqVO.getLineId(), reqVO.getQuantity(), reqVO.getId(), line);
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnissue/MesWmReturnIssueLineService.java
@@ -4,6 +4,7 @@
import cn.iocoder.yudao.module.mes.controller.admin.wm.returnissue.vo.line.MesWmReturnIssueLinePageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.returnissue.vo.line.MesWmReturnIssueLineSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnissue.MesWmReturnIssueLineDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnissue.MesWmReturnIssueLinePageVO;
import jakarta.validation.Valid;
import java.math.BigDecimal;
@@ -50,7 +51,7 @@
     * @param pageReqVO åˆ†é¡µæŸ¥è¯¢
     * @return ç”Ÿäº§é€€æ–™å•行分页
     */
    PageResult<MesWmReturnIssueLineDO> getReturnIssueLinePage(MesWmReturnIssueLinePageReqVO pageReqVO);
    PageResult<MesWmReturnIssueLinePageVO> getReturnIssueLinePage(MesWmReturnIssueLinePageReqVO pageReqVO);
    /**
     * æ ¹æ®é€€æ–™å• ID èŽ·å–è¡Œåˆ—è¡¨
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnissue/MesWmReturnIssueLineServiceImpl.java
@@ -8,6 +8,7 @@
import cn.iocoder.yudao.module.mes.controller.admin.wm.returnissue.vo.line.MesWmReturnIssueLineSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnissue.MesWmReturnIssueDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnissue.MesWmReturnIssueLineDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnissue.MesWmReturnIssueLinePageVO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.returnissue.MesWmReturnIssueLineMapper;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmReturnIssueStatusEnum;
@@ -87,7 +88,7 @@
    }
    @Override
    public PageResult<MesWmReturnIssueLineDO> getReturnIssueLinePage(MesWmReturnIssueLinePageReqVO pageReqVO) {
    public PageResult<MesWmReturnIssueLinePageVO> getReturnIssueLinePage(MesWmReturnIssueLinePageReqVO pageReqVO) {
        return issueLineMapper.selectPage(pageReqVO);
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnissue/MesWmReturnIssueServiceImpl.java
@@ -33,6 +33,8 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseAreaDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.transaction.MesWmTransactionDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.transaction.MesWmTransactionMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -78,6 +80,8 @@
    private MesWmWarehouseLocationService locationService;
    @Resource
    private MesWmWarehouseAreaService areaService;
    @Resource
    private MesWmTransactionMapper transactionMapper;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -85,13 +89,15 @@
        // 1. æ ¡éªŒå…³è”数据
        validateReturnIssueSaveData(createReqVO);
        // 2. ä»Ž task è‡ªåЍ另入 workOrderId å’Œ workstationId
        MesProTaskDO task = taskService.validateTaskExists(createReqVO.getTaskId());
        if (createReqVO.getWorkOrderId() == null) {
            createReqVO.setWorkOrderId(task.getWorkOrderId());
        }
        if (createReqVO.getWorkstationId() == null) {
            createReqVO.setWorkstationId(task.getWorkstationId());
        // 2. ä»Ž task è‡ªåЍ另入 workOrderId å’Œ workstationId(taskId éžå¿…填)
        if (createReqVO.getTaskId() != null) {
            MesProTaskDO task = taskService.validateTaskExists(createReqVO.getTaskId());
            if (createReqVO.getWorkOrderId() == null) {
                createReqVO.setWorkOrderId(task.getWorkOrderId());
            }
            if (createReqVO.getWorkstationId() == null) {
                createReqVO.setWorkstationId(task.getWorkstationId());
            }
        }
        // 3. æ’入主表
@@ -109,13 +115,15 @@
        // 1.2 æ ¡éªŒå…³è”数据
        validateReturnIssueSaveData(updateReqVO);
        // 2. ä»Ž task è‡ªåЍ另入 workOrderId å’Œ workstationId
        MesProTaskDO task = taskService.validateTaskExists(updateReqVO.getTaskId());
        if (updateReqVO.getWorkOrderId() == null) {
            updateReqVO.setWorkOrderId(task.getWorkOrderId());
        }
        if (updateReqVO.getWorkstationId() == null) {
            updateReqVO.setWorkstationId(task.getWorkstationId());
        // 2. ä»Ž task è‡ªåЍ另入 workOrderId å’Œ workstationId(taskId éžå¿…填)
        if (updateReqVO.getTaskId() != null) {
            MesProTaskDO task = taskService.validateTaskExists(updateReqVO.getTaskId());
            if (updateReqVO.getWorkOrderId() == null) {
                updateReqVO.setWorkOrderId(task.getWorkOrderId());
            }
            if (updateReqVO.getWorkstationId() == null) {
                updateReqVO.setWorkstationId(task.getWorkstationId());
            }
        }
        // 3. æ›´æ–°ä¸»è¡¨
@@ -232,7 +240,7 @@
        MesWmWarehouseLocationDO virtualLocation = locationService.getWarehouseLocationByCode(MesWmWarehouseLocationDO.WIP_VIRTUAL_LOCATION);
        MesWmWarehouseAreaDO virtualArea = areaService.getWarehouseAreaByCode(MesWmWarehouseAreaDO.WIP_VIRTUAL_AREA);
        // 2. éåŽ†æ˜Žç»†ï¼Œæ¯æ¡æ˜Žç»†äº§ç”Ÿ OUT(虚拟线边库扣减)+ IN(实际仓库增加)
        // 2. éåŽ†æ˜Žç»†ï¼Œæ¯æ¡æ˜Žç»†äº§ç”Ÿ OUT(虚拟线边库扣减)+ IN(上架目标仓库增加)
        List<MesWmReturnIssueDetailDO> details = issueDetailService.getReturnIssueDetailListByIssueId(issue.getId());
        for (MesWmReturnIssueDetailDO detail : details) {
            // 2.1 å…ˆä»Žè™šæ‹Ÿçº¿è¾¹åº“出库(库存减少)
@@ -241,15 +249,31 @@
                    .setQuantity(detail.getQuantity().negate()) // åº“存减少
                    .setBatchId(detail.getBatchId()).setBatchCode(detail.getBatchCode())
                    .setWarehouseId(virtualWarehouse.getId()).setLocationId(virtualLocation.getId()).setAreaId(virtualArea.getId())
                    .setMaterialStockId(detail.getMaterialStockId()) // æŒ‡å®šè™šæ‹Ÿçº¿è¾¹åº“库存记录
                    .setCheckFlag(false) // çº¿è¾¹åº“允许负库存
                    .setBizType(MesBizTypeConstants.WM_RETURN_ISSUE).setBizId(issue.getId())
                    .setBizCode(issue.getCode()).setBizLineId(detail.getLineId()));
            // 2.2 å†å…¥å®žé™…仓库(库存增加)
            // 2.2 ç¡®å®šä¸Šæž¶ç›®æ ‡ä»“库:detail çš„ warehouseId/locationId/areaId æ˜¯ä¸Šæž¶ç›®æ ‡ï¼ˆå®žé™…仓库)
            //     å¦‚果前端仍传了虚拟线边库(兼容旧数据),则通过事务流水反查原始仓库
            Long targetWarehouseId = detail.getWarehouseId();
            Long targetLocationId = detail.getLocationId();
            Long targetAreaId = detail.getAreaId();
            if (virtualWarehouse.getId().equals(targetWarehouseId) && detail.getMaterialStockId() != null) {
                MesWmTransactionDO originalOutTx = transactionMapper.selectOriginalOutTransaction(detail.getMaterialStockId());
                if (originalOutTx != null) {
                    targetWarehouseId = originalOutTx.getWarehouseId();
                    targetLocationId = originalOutTx.getLocationId();
                    targetAreaId = originalOutTx.getAreaId();
                }
            }
            // 2.3 å…¥ä¸Šæž¶ç›®æ ‡ä»“库(库存增加)
            wmTransactionService.createTransaction(new MesWmTransactionSaveReqDTO()
                    .setType(MesWmTransactionTypeEnum.IN.getType()).setItemId(detail.getItemId())
                    .setQuantity(detail.getQuantity()) // åº“存增加(退料回仓)
                    .setBatchId(detail.getBatchId()).setBatchCode(detail.getBatchCode())
                    .setWarehouseId(detail.getWarehouseId()).setLocationId(detail.getLocationId()).setAreaId(detail.getAreaId())
                    .setWarehouseId(targetWarehouseId).setLocationId(targetLocationId).setAreaId(targetAreaId)
                    .setBizType(MesBizTypeConstants.WM_RETURN_ISSUE).setBizId(issue.getId())
                    .setBizCode(issue.getCode()).setBizLineId(detail.getLineId())
                    .setRelatedTransactionId(outTransactionId)); // å…³è”出库事务
@@ -348,12 +372,17 @@
     */
    private void validateReturnIssueSaveData(MesWmReturnIssueSaveReqVO reqVO) {
        validateCodeUnique(reqVO.getId(), reqVO.getCode());
        // æ ¡éªŒ task å­˜åœ¨
        MesProTaskDO task = taskService.validateTaskExists(reqVO.getTaskId());
        // æ ¡éªŒ task å…³è”的工单已确认
        Long workOrderId = reqVO.getWorkOrderId() != null ? reqVO.getWorkOrderId() : task.getWorkOrderId();
        if (workOrderId != null) {
            workOrderService.validateWorkOrderConfirmed(workOrderId);
        // æ ¡éªŒ task å­˜åœ¨ï¼ˆtaskId éžå¿…填,传了才校验)
        if (reqVO.getTaskId() != null) {
            MesProTaskDO task = taskService.validateTaskExists(reqVO.getTaskId());
            // æ ¡éªŒ task å…³è”的工单已确认
            Long workOrderId = reqVO.getWorkOrderId() != null ? reqVO.getWorkOrderId() : task.getWorkOrderId();
            if (workOrderId != null) {
                workOrderService.validateWorkOrderConfirmed(workOrderId);
            }
        } else if (reqVO.getWorkOrderId() != null) {
            // æ²¡ä¼  taskId ä½†ä¼ äº† workOrderId,校验工单已确认
            workOrderService.validateWorkOrderConfirmed(reqVO.getWorkOrderId());
        }
        if (reqVO.getWorkstationId() != null) {
            workstationService.validateWorkstationExistsAndEnable(reqVO.getWorkstationId());
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/MesWmTransactionServiceImpl.java
@@ -77,9 +77,12 @@
        // 1.5 å…³è”事务校验
        checkRelatedTransaction(reqDTO);
        // 2.1 èŽ·å–æˆ–åˆ›å»ºåº“å­˜è®°å½•ï¼ˆå¦‚æžœä¼ å…¥äº† materialStockId åˆ™ç›´æŽ¥ä½¿ç”¨ï¼Œé¿å…é‡æ–°åŒ¹é…ï¼‰
        // 2.1 èŽ·å–åº“å­˜è®°å½•
        MesWmMaterialStockDO materialStock;
        MesWmTransactionTypeEnum transactionType = MesWmTransactionTypeEnum.valueOf(reqDTO.getType());
        boolean isInbound = transactionType != null && transactionType.isInbound();
        if (reqDTO.getMaterialStockId() != null) {
            // ä¼ å…¥äº† materialStockId,直接使用
            materialStock = materialStockService.getMaterialStock(reqDTO.getMaterialStockId());
            if (materialStock == null) {
                throw exception(WM_MATERIAL_STOCK_NOT_EXISTS);
@@ -91,10 +94,22 @@
                    || ObjUtil.notEqual(materialStock.getAreaId(), reqDTO.getAreaId())) {
                throw exception(WM_MATERIAL_STOCK_SELECTION_MISMATCH);
            }
        } else {
        } else if (isInbound) {
            // å…¥åº“事务:找不到则自动创建库存记录
            materialStock = materialStockService.getOrCreateMaterialStock(
                    reqDTO.getItemId(), reqDTO.getWarehouseId(), reqDTO.getLocationId(), reqDTO.getAreaId(),
                    reqDTO.getBatchId(), reqDTO.getBatchCode(), reqDTO.getVendorId(), reqDTO.getReceiptTime());
        } else {
            // å‡ºåº“事务:库存记录必须已存在,不允许自动创建
            materialStock = materialStockService.getMaterialStockByCompositeKey(
                    reqDTO.getItemId(), reqDTO.getWarehouseId(), reqDTO.getLocationId(), reqDTO.getAreaId(),
                    reqDTO.getBatchId(), reqDTO.getVendorId());
            if (materialStock == null) {
                throw exception(WM_MATERIAL_STOCK_NOT_EXISTS);
            }
        }
        if (materialStock == null || materialStock.getId() == null) {
            throw exception(WM_MATERIAL_STOCK_NOT_EXISTS);
        }
        // 2.2 å†»ç»“校验(仓库、库区、库位、库存记录)
        checkFrozen(reqDTO, materialStock);
yudao-module-mes/src/main/resources/mapper/qc/pendinginspect/MesQcPendingInspectMapper.xml
@@ -74,9 +74,9 @@
                ppl.item_id AS itemId,
                ppl.quantity AS quantity,
                NULL AS vendorId,
                pp.work_order_id AS workOrderId,
                pp.workstation_id AS workstationId,
                pp.task_id AS taskId,
                pf.work_order_id AS workOrderId,
                pf.workstation_id AS workstationId,
                pf.task_id AS taskId,
                NULL AS clientId
            FROM mes_wm_product_produce_line ppl
                JOIN mes_wm_product_produce pp ON ppl.produce_id = pp.id
@@ -126,7 +126,7 @@
                NULL AS vendorId,
                ri.work_order_id AS workOrderId,
                ri.workstation_id AS workstationId,
                NULL AS taskId,
                ri.task_id AS taskId,
                NULL AS clientId
            FROM mes_wm_return_issue ri
                JOIN mes_wm_return_issue_line ril ON ri.id = ril.issue_id
yudao-module-mes/src/main/resources/mapper/wm/returnissue/MesWmReturnIssueLineMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.iocoder.yudao.module.mes.dal.mysql.wm.returnissue.MesWmReturnIssueLineMapper">
    <select id="selectPageByReqVO" resultType="cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnissue.MesWmReturnIssueLinePageVO">
        SELECT t1.*, t2.code, t2.name, t2.specification
        FROM mes_wm_return_issue_line t1
        LEFT JOIN mdm_item t2 ON t1.item_id = t2.id AND t2.deleted = 0
        WHERE t1.deleted = 0
        <if test="reqVO.issueId != null">
            AND t1.issue_id = #{reqVO.issueId}
        </if>
        ORDER BY t1.id DESC
    </select>
</mapper>