liyong
5 天以前 274dd8b724dd4485658e1d2a3f825a007f625bd7
Merge remote-tracking branch 'origin/dev_business' into dev_business
已添加22个文件
已修改35个文件
3121 ■■■■■ 文件已修改
docs/confirm_receipt_qc_flow.md 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/erp_purchase_qc_check_flag.md 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/purchase_flow_frontend_guide.md 202 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/purchase_order_confirm_receipt.md 160 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/purchase_request_in_status.md 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/purchase/ErpSupplierApi.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/purchase/dto/ErpPurchaseOrderItemRespDTO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/purchase/dto/ErpSupplierRespDTO.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/pom.xml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/api/purchase/ErpSupplierApiImpl.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseOrderController.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseRequestController.java 182 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderRespVO.java 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderSaveReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestPageReqVO.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestRespVO.java 130 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestSaveReqVO.java 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseOrderDO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseOrderItemDO.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseRequestDO.java 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseRequestItemDO.java 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseRequestItemMapper.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseRequestMapper.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/redis/no/ErpNoRedisDAO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpAuditStatus.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpPurchaseRequestStatusEnum.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErrorCodeConstants.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderService.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java 230 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestService.java 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestServiceImpl.java 459 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/listener/ErpPurchaseRequestStatusListener.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/arrivalnotice/MesArrivalNoticeApi.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/arrivalnotice/MesArrivalNoticeApiImpl.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/arrivalnotice/MesWmArrivalNoticeController.java 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/itemreceipt/MesWmItemReceiptController.java 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/arrivalnotice/MesWmArrivalNoticeLineDO.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/arrivalnotice/MesWmArrivalNoticeMapper.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/itemreceipt/MesWmItemReceiptMapper.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/productsales/MesWmProductSalesMapper.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/salesnotice/MesWmSalesNoticeMapper.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/qc/MesQcCheckResultEnum.java 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/iqc/MesQcIqcService.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/iqc/MesQcIqcServiceImpl.java 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/arrivalnotice/MesWmArrivalNoticeLineService.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/arrivalnotice/MesWmArrivalNoticeLineServiceImpl.java 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/arrivalnotice/MesWmArrivalNoticeService.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/arrivalnotice/MesWmArrivalNoticeServiceImpl.java 111 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemreceipt/MesWmItemReceiptDetailServiceImpl.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemreceipt/MesWmItemReceiptServiceImpl.java 82 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesLineServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/salesnotice/MesWmSalesNoticeServiceImpl.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wms/approval/listener/WmsInboundStatusListener.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wms/approval/listener/WmsOutboundStatusListener.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wms/approval/listener/WmsTransferStatusListener.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/confirm_receipt_qc_flow.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,117 @@
# ç¡®è®¤æ”¶è´§ä¸Žè´¨æ£€æµç¨‹ - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- é‡‡è´­è®¢å•列表页
- é‡‡è´­è®¢å•详情页
- åˆ°è´§é€šçŸ¥å•列表页
- åˆ°è´§é€šçŸ¥å•详情页
- IQC æ¥æ–™æ£€éªŒå•页面
## ä¸šåŠ¡æµç¨‹ä¸Žæ•°æ®å¸¦å…¥
```
采购申请审批通过 â†’ ç”Ÿæˆé‡‡è´­è®¢å•(已审核) â†’ ç¡®è®¤æ”¶è´§
→ åˆ¤æ–­æ˜¯å¦å­˜åœ¨éœ€è´¨æ£€ç‰©æ–™
  â”œâ”€â”€ æ˜¯ â†’ ç”Ÿæˆåˆ°è´§é€šçŸ¥å•(待质检)→ è‡ªåŠ¨åˆ›å»º IQC å• â†’ IQC æ¥æ–™è´¨æ£€
  â”‚      â”œâ”€â”€ åˆæ ¼ â†’ ç­‰å¾…所有质检完成
  â”‚      â”œâ”€â”€ ç‰¹é‡‡ â†’ ç­‰å¾…所有质检完成
  â”‚      â”œâ”€â”€ ä¸åˆæ ¼ï¼ˆé€€è´§ï¼‰â†’ çŠ¶æ€ä¿æŒå¾…è´¨æ£€
  â”‚      â””── ä¸åˆæ ¼ï¼ˆæŠ¥åºŸï¼‰â†’ çŠ¶æ€ä¿æŒå¾…è´¨æ£€
  â”‚      â†“
  â”‚  æ‰€æœ‰éœ€æ£€ç‰©æ–™è´¨æ£€å®Œæˆä¸”可入库(合格/特采)→ åˆ°è´§é€šçŸ¥å•(待入库)
  â””── å¦ â†’ ç”Ÿæˆåˆ°è´§é€šçŸ¥å•(待入库)
```
**字段带入说明:**
| æºå•据 | ç›®æ ‡å•据 | å¸¦å…¥å­—段 |
|--------|----------|----------|
| é‡‡è´­è®¢å• | åˆ°è´§é€šçŸ¥å• | ä¾›åº”商、采购订单号 |
| é‡‡è´­è®¢å•明细 | åˆ°è´§é€šçŸ¥å•行 | ç‰©æ–™ã€åˆ°è´§æ•°é‡ã€è´¨æ£€æ ‡è¯†(qcCheckFlag) |
| åˆ°è´§é€šçŸ¥å•行 | IQC æ£€éªŒå• | ç‰©æ–™ã€åˆ°è´§æ•°é‡ã€ä¾›åº”商 |
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /erp/purchase-order/confirm-receipt | ç¡®è®¤æ”¶è´§ |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | æ˜¯ | é‡‡è´­è®¢å•编号 |
**响应:**
```json
{
  "code": 0,
  "data": 123,  // ç”Ÿæˆçš„到货通知单编号
  "msg": "操作成功"
}
```
## å­—段展示规则
### é‡‡è´­è®¢å•明细新增字段
| å­—段 | ç±»åž‹ | å±•示位置 | è¯´æ˜Ž |
|------|------|----------|------|
| qcCheckFlag | Boolean | åˆ—表、详情、表单 | æ˜¯å¦éœ€è¦æ¥æ–™æ£€éªŒï¼Œé»˜è®¤ false |
### åˆ°è´§é€šçŸ¥å•行新增字段
| å­—段 | ç±»åž‹ | å±•示位置 | è¯´æ˜Ž |
|------|------|----------|------|
| iqcCheckFlag | Boolean | åˆ—表、详情 | æ˜¯å¦éœ€è¦æ¥æ–™æ£€éªŒï¼ˆä»Žé‡‡è´­è®¢å•带入) |
| iqcId | Long | è¯¦æƒ… | å…³è”çš„ IQC æ£€éªŒå•编号(自动创建后回写) |
| iqcCheckResult | Integer | åˆ—表、详情 | è´¨æ£€ç»“果:1合格 2特采 3不合格退货 4不合格报废 |
| qualifiedQuantity | BigDecimal | åˆ—表、详情 | åˆæ ¼æ•°é‡ï¼ˆè´¨æ£€å®ŒæˆåŽå›žå†™ï¼‰ |
### åˆ°è´§é€šçŸ¥å•状态流转
| çŠ¶æ€å€¼ | çŠ¶æ€å | è¯´æ˜Ž |
|--------|--------|------|
| 0 | è‰ç¨¿ | æ–°å»ºçŠ¶æ€ |
| 10 | å¾…质检 | å­˜åœ¨éœ€è´¨æ£€ç‰©æ–™ï¼Œç­‰å¾… IQC å®Œæˆ |
| 20 | å¾…入库 | æ— éœ€è´¨æ£€æˆ–所有质检完成且可入库 |
| 30 | å·²å®Œæˆ | å…¥åº“完成 |
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
### ç¡®è®¤æ”¶è´§æŒ‰é’®å±•示
| åœºæ™¯ | è§„则 |
|------|------|
| æ˜¾ç¤ºæ¡ä»¶ | è®¢å•状态=已审核(20) ä¸” å…¥åº“状态≠全部入库(2) |
| ç‚¹å‡»åŽ | è°ƒç”¨ confirmReceipt æŽ¥å£ï¼Œè¿”回到货通知单编号 |
| æˆåŠŸåŽ | æç¤º"确认收货成功,已生成到货通知单",刷新列表 |
### è´¨æ£€ç»“果对入库的影响
| è´¨æ£€ç»“æžœ | å€¼ | æ˜¯å¦å¯å…¥åº“ | è¯´æ˜Ž |
|----------|-----|------------|------|
| åˆæ ¼ | 1 | âœ… æ˜¯ | æ­£å¸¸å…¥åº“ |
| ç‰¹é‡‡ | 2 | âœ… æ˜¯ | ç‰¹æ®Šæ‰¹å‡†å…¥åº“ |
| ä¸åˆæ ¼é€€è´§ | 3 | âŒ å¦ | é€€å›žä¾›åº”商,不可入库 |
| ä¸åˆæ ¼æŠ¥åºŸ | 4 | âŒ å¦ | æŠ¥åºŸå¤„理,不可入库 |
### åˆ°è´§é€šçŸ¥å•状态推进规则
| å½“前状态 | æŽ¨è¿›æ¡ä»¶ | ç›®æ ‡çŠ¶æ€ |
|----------|----------|----------|
| è‰ç¨¿ | æäº¤æ—¶å­˜åœ¨éœ€è´¨æ£€è¡Œ | å¾…质检 |
| è‰ç¨¿ | æäº¤æ—¶æ— éœ€è´¨æ£€è¡Œ | å¾…入库 |
| å¾…质检 | æ‰€æœ‰éœ€æ£€è¡Œå®Œæˆè´¨æ£€ï¼Œä¸”都为合格/特采 | å¾…入库 |
| å¾…质检 | å­˜åœ¨é€€è´§/报废结果的行 | ä¿æŒå¾…质检 |
| å¾…入库 | æ‰‹åŠ¨å®Œæˆæ“ä½œ | å·²å®Œæˆ |
## æ³¨æ„äº‹é¡¹
1. **自动创建 IQC å•**:确认收货时,如果采购订单明细设置了 `qcCheckFlag=true`,系统会自动创建对应的 IQC æ£€éªŒå•,无需手动创建。
2. **质检结果回写**:IQC æ£€éªŒå®ŒæˆåŽï¼Œä¼šè‡ªåŠ¨å›žå†™åˆ°è´§é€šçŸ¥å•è¡Œçš„ `iqcCheckResult` å’Œ `qualifiedQuantity` å­—段。
3. **部分收货**:一个采购订单可以多次确认收货(部分收货场景),每次生成独立的到货通知单。
4. **入库前提条件**:只有到货通知单状态为"待入库"时才能执行采购入库操作。
5. **质检失败处理**:如果质检结果为退货或报废,到货通知单状态保持"待质检",需要人工介入处理(如重新安排收货)。
docs/erp_purchase_qc_check_flag.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,103 @@
# ERP é‡‡è´­è®¢å•质检字段与 MES æ¥æ–™æ£€éªŒè‡ªåŠ¨åˆ›å»º - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- ERP é‡‡è´­è®¢å•列表页
- ERP é‡‡è´­è®¢å•新增/编辑页
- ERP é‡‡è´­è®¢å•详情页
- MES åˆ°è´§é€šçŸ¥å•列表页
- MES åˆ°è´§é€šçŸ¥å•详情页
- MES æ¥æ–™æ£€éªŒï¼ˆIQC)列表页
## ä¸šåŠ¡æµç¨‹
### 1. é‡‡è´­è®¢å•新增质检标识
1. ç”¨æˆ·åœ¨ ERP é‡‡è´­è®¢å•新增/编辑页面,可以为每个产品明细勾选"是否需要来料检验"
2. ä¿å­˜æ—¶ï¼Œ`qcCheckFlag` å­—段随订单明细一起保存
### 2. ä»Žé‡‡è´­è®¢å•生成到货通知单
1. ç”¨æˆ·ç‚¹å‡»"生成到货通知单"按钮
2. ç³»ç»Ÿè‡ªåŠ¨å°†é‡‡è´­è®¢å•æ˜Žç»†çš„ `qcCheckFlag` å¸¦å…¥åˆ°è´§é€šçŸ¥å•行的 `iqcCheckFlag`
3. ç”¨æˆ·å¯åœ¨åˆ°è´§é€šçŸ¥å•详情页修改每行的质检标识
### 3. åˆ°è´§é€šçŸ¥å•提交自动创建 IQC
1. ç”¨æˆ·æäº¤åˆ°è´§é€šçŸ¥å•
2. å¦‚果有行标记为需要质检(`iqcCheckFlag = true`):
   - åˆ°è´§é€šçŸ¥å•状态变为"待质检"
   - ç³»ç»Ÿè‡ªåŠ¨ä¸ºæ¯ä¸ªéœ€è¦è´¨æ£€çš„è¡Œåˆ›å»ºä¸€æ¡ IQC æ¥æ–™æ£€éªŒå•
   - è¡Œä¸Šçš„ `iqcId` å…³è”到创建的检验单
3. å¦‚果没有需要质检的行,状态直接变为"待入库"
## API å˜æ›´
### ERP é‡‡è´­è®¢å•
#### è¯·æ±‚参数变更
**ErpPurchaseOrderSaveReqVO.Item æ–°å¢žå­—段:**
| å­—段 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| qcCheckFlag | Boolean | å¦ | æ˜¯å¦éœ€è¦æ¥æ–™æ£€éªŒ |
#### å“åº”参数变更
**ErpPurchaseOrderRespVO.Item æ–°å¢žå­—段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| qcCheckFlag | Boolean | æ˜¯å¦éœ€è¦æ¥æ–™æ£€éªŒ |
### MES åˆ°è´§é€šçŸ¥å•
#### å“åº”参数变更
**到货通知单行新增字段(已存在,强调带入逻辑):**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| iqcCheckFlag | Boolean | æ˜¯å¦éœ€è¦æ¥æ–™æ£€éªŒï¼ˆä»Žé‡‡è´­è®¢å•带入) |
| iqcId | Long | å…³è”çš„ IQC æ£€éªŒå• ID(提交后自动创建) |
## å­—段展示规则
### ERP é‡‡è´­è®¢å•
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| æ˜¯å¦è´¨æ£€ | è®¢å•明细表格 | ä½¿ç”¨ Checkbox æˆ– Switch å±•示,可编辑 |
### MES åˆ°è´§é€šçŸ¥å•
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| æ˜¯å¦è´¨æ£€ | è¡Œæ˜Žç»†è¡¨æ ¼ | ä½¿ç”¨ Tag æˆ– Checkbox å±•示,提交后不可修改 |
| IQC æ£€éªŒå• | è¡Œæ˜Žç»†è¡¨æ ¼ | å…³è”的检验单编号,点击可跳转(提交后自动关联) |
### MES æ¥æ–™æ£€éªŒ
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| æ¥æºå•据 | è¯¦æƒ…页 | æ˜¾ç¤ºåˆ°è´§é€šçŸ¥å•编号,点击可跳转 |
| æ¥æºå•据行 | è¯¦æƒ…页 | æ˜¾ç¤ºåˆ°è´§é€šçŸ¥å•行编号 |
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| é‡‡è´­è®¢å•保存 | `qcCheckFlag` é»˜è®¤ä¸º false(不质检) |
| ç”Ÿæˆåˆ°è´§é€šçŸ¥å• | ä»Žé‡‡è´­è®¢å•明细带入 `qcCheckFlag` åˆ° `iqcCheckFlag` |
| åˆ°è´§é€šçŸ¥å•提交 | å¦‚有需要质检的行,自动创建 IQC æ£€éªŒå•并关联 |
| IQC åˆ›å»ºæ¡ä»¶ | ç‰©æ–™å¿…须配置了 IQC æ£€éªŒæ¨¡æ¿ï¼Œå¦åˆ™è·³è¿‡å¹¶è®°å½•日志 |
| IQC å•据状态 | è‡ªåŠ¨åˆ›å»ºçš„ IQC ä¸ºè‰ç¨¿çŠ¶æ€ï¼Œéœ€è¦æ£€éªŒå‘˜å¡«å†™æ£€éªŒç»“æžœ |
## æ³¨æ„äº‹é¡¹
1. **检验模板配置**:物料必须先在 MES è´¨é‡æ¨¡å—配置 IQC æ£€éªŒæ¨¡æ¿ï¼Œå¦åˆ™è‡ªåŠ¨åˆ›å»º IQC ä¼šå¤±è´¥
2. **数据带入**:从采购订单生成到货通知单时,质检标识会自动带入,但用户可在到货通知单中修改
3. **状态流转**:到货通知单提交后,需要质检的行会进入"待质检"状态,所有检验完成后才可入库
4. **检验单关联**:提交后行上会关联 IQC æ£€éªŒå• ID,点击可跳转查看检验详情
5. **异常处理**:如果某行创建 IQC å¤±è´¥ï¼ˆå¦‚缺少模板),不会中断整个提交流程,会记录日志继续处理其他行
docs/purchase_flow_frontend_guide.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,202 @@
# é‡‡è´­æµç¨‹å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- é‡‡è´­è®¢å•列表页
- é‡‡è´­è®¢å•详情页
- åˆ°è´§é€šçŸ¥å•列表页
- åˆ°è´§é€šçŸ¥å•详情页
## ä¸šåŠ¡æµç¨‹
```
采购申请审批通过 â†’ è‡ªåŠ¨ç”Ÿæˆé‡‡è´­è®¢å•(已审核)
    â†’ ç‚¹å‡»"确认收货" â†’ ç”Ÿæˆåˆ°è´§é€šçŸ¥å•(待入库/待质检)
    â†’ IQC质检(如需要) â†’ é‡‡è´­å…¥åº“
```
---
## ä¸€ã€é‡‡è´­è®¢å•模块
### 1. æ–°å¢žæŒ‰é’®ï¼šç¡®è®¤æ”¶è´§
**位置:** é‡‡è´­è®¢å•列表页、详情页
**展示条件:**
- è®¢å•状态 = å·²å®¡æ ¸(20)
- å…¥åº“状态 â‰  å…¨éƒ¨å…¥åº“(2)
**API:**
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /erp/purchase-order/confirm-receipt | ç¡®è®¤æ”¶è´§ |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | æ˜¯ | é‡‡è´­è®¢å•编号 |
**响应:**
```json
{
  "code": 0,
  "data": 123,  // ç”Ÿæˆçš„到货通知单编号
  "msg": "操作成功"
}
```
### 2. æ–°å¢žå­—段:采购订单明细
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| qcCheckFlag | Boolean | æ˜¯å¦éœ€è¦æ¥æ–™æ£€éªŒï¼Œé»˜è®¤ false |
**展示位置:** é‡‡è´­è®¢å•明细表格
---
## äºŒã€é‡‡è´­ç”³è¯·æ¨¡å—
### æ–°å¢žå­—段:采购申请明细
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| qcCheckFlag | Boolean | æ˜¯å¦éœ€è¦æ¥æ–™æ£€éªŒï¼Œé»˜è®¤ false |
**展示位置:** é‡‡è´­ç”³è¯·æ˜Žç»†è¡¨æ ¼
### æ–°å¢žå­—段:采购申请主表
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| depositPrice | BigDecimal | å®šé‡‘金额,默认 0 |
**展示位置:** é‡‡è´­ç”³è¯·è¡¨å•
---
## ä¸‰ã€åˆ°è´§é€šçŸ¥å•模块
### 1. çŠ¶æ€æµè½¬è¯´æ˜Ž
| çŠ¶æ€å€¼ | çŠ¶æ€å | è¯´æ˜Ž |
|--------|--------|------|
| 0 | è‰ç¨¿ | æ–°å»ºçŠ¶æ€ |
| 2 | å¾…质检 | å­˜åœ¨éœ€è´¨æ£€ç‰©æ–™ï¼Œç­‰å¾… IQC å®Œæˆ |
| 3 | å¾…入库 | æ— éœ€è´¨æ£€æˆ–所有质检完成且可入库 |
| 4 | å·²å®Œæˆ | å…¥åº“完成 |
### 2. æ–°å¢žå­—段:到货通知单行
| å­—段 | ç±»åž‹ | å±•示位置 | è¯´æ˜Ž |
|------|------|----------|------|
| iqcCheckFlag | Boolean | åˆ—表、详情 | æ˜¯å¦éœ€è¦æ¥æ–™æ£€éªŒ |
| iqcId | Long | è¯¦æƒ… | å…³è”çš„ IQC æ£€éªŒå•编号 |
| iqcCheckResult | Integer | åˆ—表、详情 | è´¨æ£€ç»“æžœ |
| qualifiedQuantity | BigDecimal | åˆ—表、详情 | åˆæ ¼æ•°é‡ |
### 3. è´¨æ£€ç»“果枚举
| å€¼ | åç§° | æ˜¯å¦å¯å…¥åº“ |
|-----|------|------------|
| 1 | åˆæ ¼ | âœ… æ˜¯ |
| 2 | ç‰¹é‡‡ | âœ… æ˜¯ |
| 3 | ä¸åˆæ ¼é€€è´§ | âŒ å¦ |
| 4 | ä¸åˆæ ¼æŠ¥åºŸ | âŒ å¦ |
### 4. æ–°å¢žæŽ¥å£ï¼šæ ¹æ®é‡‡è´­è®¢å•生成到货通知单
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /mes/wm/arrival-notice/generate-from-purchase-order | æ ¹æ®é‡‡è´­è®¢å•生成到货通知单 |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| purchaseOrderId | Long | æ˜¯ | é‡‡è´­è®¢å•编号 |
**响应:**
```json
{
  "code": 0,
  "data": 123,  // ç”Ÿæˆçš„到货通知单编号
  "msg": "操作成功"
}
```
---
## å››ã€å­—段带入说明
### æ•°æ®æµè½¬æ˜ å°„
| æºå•据 | ç›®æ ‡å•据 | å¸¦å…¥å­—段 |
|--------|----------|----------|
| é‡‡è´­ç”³è¯·æ˜Žç»† | é‡‡è´­è®¢å•明细 | qcCheckFlag(质检标识) |
| é‡‡è´­ç”³è¯·ä¸»è¡¨ | é‡‡è´­è®¢å•主表 | depositPrice(定金金额) |
| é‡‡è´­è®¢å• | åˆ°è´§é€šçŸ¥å• | ä¾›åº”商、采购订单号 |
| é‡‡è´­è®¢å•明细 | åˆ°è´§é€šçŸ¥å•行 | ç‰©æ–™ã€åˆ°è´§æ•°é‡ã€qcCheckFlag |
| åˆ°è´§é€šçŸ¥å•行 | IQC æ£€éªŒå• | ç‰©æ–™ã€åˆ°è´§æ•°é‡ã€ä¾›åº”商 |
---
## äº”、按钮展示规则汇总
### é‡‡è´­è®¢å•列表/详情页
| æŒ‰é’® | æ˜¾ç¤ºæ¡ä»¶ |
|------|----------|
| ç¡®è®¤æ”¶è´§ | è®¢å•状态=已审核(20) ä¸” å…¥åº“状态≠全部入库(2) |
### é‡‡è´­ç”³è¯·æ˜Žç»†è¡¨æ ¼
| åˆ— | è¯´æ˜Ž |
|----|------|
| æ˜¯å¦è´¨æ£€ | å¼€å…³ç»„件,默认关闭 |
### é‡‡è´­è®¢å•明细表格
| åˆ— | è¯´æ˜Ž |
|----|------|
| æ˜¯å¦è´¨æ£€ | åªè¯»å±•示,从采购申请带入 |
### åˆ°è´§é€šçŸ¥å•行表格
| åˆ— | è¯´æ˜Ž |
|----|------|
| è´¨æ£€æ ‡è¯† | åªè¯»å±•示 |
| è´¨æ£€ç»“æžœ | åªè¯»å±•示,质检完成后回写 |
| åˆæ ¼æ•°é‡ | åªè¯»å±•示,质检完成后回写 |
---
## å…­ã€æ³¨æ„äº‹é¡¹
1. **确认收货成功后**:返回到货通知单编号,前端可提示并刷新列表,或跳转到货通知单详情
2. **一个采购订单可多次确认收货**:支持部分收货场景,每次生成独立的到货通知单
3. **质检自动创建**:确认收货时,系统自动创建 IQC æ£€éªŒå•,无需手动创建
4. **入库前提条件**:只有到货通知单状态为"待入库"时才能执行采购入库
5. **质检失败处理**:如果质检结果为退货或报废,到货通知单状态保持"待质检",需人工处理
---
## ä¸ƒã€æƒé™é…ç½®
| æƒé™æ ‡è¯† | è¯´æ˜Ž |
|----------|------|
| erp:purchase-order:confirm-receipt | ç¡®è®¤æ”¶è´§æƒé™ |
---
## å…«ã€API æ±‡æ€»
| æ¨¡å— | æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|------|
| é‡‡è´­è®¢å• | POST | /erp/purchase-order/confirm-receipt | ç¡®è®¤æ”¶è´§ |
| åˆ°è´§é€šçŸ¥å• | POST | /mes/wm/arrival-notice/generate-from-purchase-order | æ ¹æ®é‡‡è´­è®¢å•生成 |
docs/purchase_order_confirm_receipt.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,160 @@
# é‡‡è´­è®¢å•确认收货 - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- é‡‡è´­è®¢å•列表页
- é‡‡è´­è®¢å•详情页
## ä¸šåŠ¡æµç¨‹
```
采购申请审批通过 â†’ ç”Ÿæˆé‡‡è´­è®¢å•(已审核) â†’ ç¡®è®¤æ”¶è´§ â†’ ç”Ÿæˆåˆ°è´§é€šçŸ¥å•(待入库/待质检) â†’ æ‰§è¡Œå…¥åº“
```
**确认收货后的自动处理:**
- å¦‚果采购订单明细设置了 `qcCheckFlag=true`(需要质检),到货通知单自动进入"待质检"状态,并创建 IQC æ£€éªŒå•
- å¦‚果采购订单明细设置了 `qcCheckFlag=false`(无需质检),到货通知单直接进入"待入库"状态
## æ–°å¢ž API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /erp/purchase-order/confirm-receipt | ç¡®è®¤æ”¶è´§ |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | æ˜¯ | é‡‡è´­è®¢å•编号 |
**响应:**
```json
{
  "code": 0,
  "data": 123,  // ç”Ÿæˆçš„到货通知单编号
  "msg": "操作成功"
}
```
**业务规则:**
- åªæœ‰å·²å®¡æ ¸çŠ¶æ€çš„é‡‡è´­è®¢å•æ‰èƒ½ç¡®è®¤æ”¶è´§
- å·²å…¨éƒ¨å…¥åº“的采购订单不能确认收货
- ç¡®è®¤æ”¶è´§åŽè¿”回生成的到货通知单编号,前端可跳转到货通知单详情
## å­—段变更
### é‡‡è´­ç”³è¯·æ˜Žç»†æ–°å¢žå­—段
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| qcCheckFlag | Boolean | æ˜¯å¦éœ€è¦æ¥æ–™æ£€éªŒï¼Œé»˜è®¤ false |
### é‡‡è´­è®¢å•明细新增字段
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| qcCheckFlag | Boolean | æ˜¯å¦éœ€è¦æ¥æ–™æ£€éªŒï¼Œä»Žé‡‡è´­ç”³è¯·å¸¦å…¥ |
### é‡‡è´­ç”³è¯·æ–°å¢žå­—段
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| depositPrice | BigDecimal | å®šé‡‘金额,默认 0 |
### é‡‡è´­è®¢å•新增字段
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| depositPrice | BigDecimal | å®šé‡‘金额,从采购申请带入 |
## æŒ‰é’®å±•示规则
### é‡‡è´­è®¢å•列表/详情页
| æŒ‰é’® | æ˜¾ç¤ºæ¡ä»¶ |
|------|----------|
| ç¡®è®¤æ”¶è´§ | è®¢å•状态=已审核(20) ä¸” å…¥åº“状态≠全部入库(2) |
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. é‡‡è´­è®¢å•列表页 - æ·»åŠ ç¡®è®¤æ”¶è´§æŒ‰é’®
```html
<el-button
  type="success"
  v-if="scope.row.status === 20 && scope.row.inStatus !== 2"
  @click="handleConfirmReceipt(scope.row)"
  v-hasPermi="['erp:purchase-order:confirm-receipt']">
  ç¡®è®¤æ”¶è´§
</el-button>
```
### 2. æ–¹æ³•实现
```js
// ç¡®è®¤æ”¶è´§
async handleConfirmReceipt(row) {
  try {
    await this.$modal.confirm(`确认对采购订单【${row.no}】进行收货操作?`);
    const res = await confirmReceipt(row.id);
    this.$modal.msgSuccess('确认收货成功,已生成到货通知单');
    // å¯é€‰ï¼šè·³è½¬åˆ°è´§é€šçŸ¥å•详情
    // this.$router.push({ path: '/mes/wm/arrival-notice/detail', query: { id: res.data } });
    this.getList();
  } catch (e) {
    // å–消或失败
  }
}
```
### 3. API æŽ¥å£
```js
// api/erp/purchase/order.js
export function confirmReceipt(id) {
  return request({
    url: '/erp/purchase-order/confirm-receipt',
    method: 'post',
    params: { id }
  })
}
```
### 4. é‡‡è´­ç”³è¯·è¡¨å• - æ·»åŠ è´¨æ£€æ ‡è¯†å­—æ®µ
在采购申请明细表格中添加"是否质检"列:
```html
<el-table-column label="是否质检" prop="qcCheckFlag" width="100">
  <template #default="scope">
    <el-switch v-model="scope.row.qcCheckFlag" />
  </template>
</el-table-column>
```
### 5. é‡‡è´­ç”³è¯·è¡¨å• - æ·»åŠ å®šé‡‘å­—æ®µ
```html
<el-form-item label="定金金额" prop="depositPrice">
  <el-input-number
    v-model="form.depositPrice"
    :min="0"
    :precision="2"
    :controls="false"
    placeholder="请输入定金金额" />
</el-form-item>
```
## æƒé™é…ç½®
| æƒé™æ ‡è¯† | è¯´æ˜Ž |
|----------|------|
| erp:purchase-order:confirm-receipt | ç¡®è®¤æ”¶è´§æƒé™ |
## æ³¨æ„äº‹é¡¹
1. ç¡®è®¤æ”¶è´§æ“ä½œä¼šè‡ªåŠ¨ç”Ÿæˆåˆ°è´§é€šçŸ¥å•ï¼Œæ— éœ€æ‰‹åŠ¨åˆ›å»º
2. è´¨æ£€æ ‡è¯†ä»Žé‡‡è´­ç”³è¯· â†’ é‡‡è´­è®¢å• â†’ åˆ°è´§é€šçŸ¥å•全程传递
3. å®šé‡‘金额从采购申请自动带入采购订单
4. ä¸€ä¸ªé‡‡è´­è®¢å•可以多次确认收货(部分收货场景)
docs/purchase_request_in_status.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,97 @@
# é‡‡è´­å…¥åº“状态回写 - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- é‡‡è´­ç”³è¯·åˆ—表页
- é‡‡è´­ç”³è¯·è¯¦æƒ…页
- é‡‡è´­è®¢å•列表页
- é‡‡è´­è®¢å•详情页
## ä¸šåŠ¡æµç¨‹ä¸Žæ•°æ®å¸¦å…¥
```
采购申请审批通过 â†’ ç”Ÿæˆé‡‡è´­è®¢å•(已审核) â†’ ç¡®è®¤æ”¶è´§ â†’ ç”Ÿæˆåˆ°è´§é€šçŸ¥å•(待入库) â†’ æ‰§è¡Œå…¥åº“
       â†“                    â†“                                           â†“
   order_id/order_no   request_item_id å…³è”                         in_status å›žå†™
```
**状态回写流程:**
1. å…¥åº“单完成 â†’ å›žå†™é‡‡è´­è®¢å•明细入库数量(in_count)
2. å…¥åº“单完成 â†’ å›žå†™é‡‡è´­è®¢å•入库状态(in_status)
3. å…¥åº“单完成 â†’ å›žå†™é‡‡è´­ç”³è¯·æ˜Žç»†å…¥åº“数量(in_count)
4. å…¥åº“单完成 â†’ å›žå†™é‡‡è´­ç”³è¯·å…¥åº“状态(in_status)
## å­—段变更
### é‡‡è´­ç”³è¯·æ–°å¢žå­—段
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| inStatus | Integer | å…¥åº“状态(0:未入库 1:部分入库 2:全部入库) |
### é‡‡è´­ç”³è¯·æ˜Žç»†æ–°å¢žå­—段
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| inCount | BigDecimal | å…¥åº“数量 |
### é‡‡è´­è®¢å•明细新增字段
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| requestItemId | Long | é‡‡è´­ç”³è¯·æ˜Žç»†ç¼–号(系统自动关联) |
## å“åº”字段变更
### é‡‡è´­ç”³è¯·è¯¦æƒ…响应新增
```json
{
  "inStatus": 0
}
```
### é‡‡è´­ç”³è¯·æ˜Žç»†å“åº”新增
```json
{
  "items": [
    {
      "inCount": 0
    }
  ]
}
```
### é‡‡è´­è®¢å•详情响应新增
无变化(`requestItemId` ä¸ºç³»ç»Ÿå†…部关联,前端无需展示)
## å­—段展示规则
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| å…¥åº“状态 | é‡‡è´­ç”³è¯·åˆ—表、详情 | ä½¿ç”¨æ ‡ç­¾å±•示:未入库/部分入库/全部入库 |
| å…¥åº“数量 | é‡‡è´­ç”³è¯·æ˜Žç»†è¡¨æ ¼ | æ•°å€¼å±•示,保留6位小数 |
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| é‡‡è´­ç”³è¯·å…¥åº“状态 | æ ¹æ®é‡‡è´­è®¢å•入库状态自动更新,无需手动操作 |
| é‡‡è´­ç”³è¯·æ˜Žç»†å…¥åº“数量 | æ ¹æ®é‡‡è´­è®¢å•明细入库数量累加更新 |
| å¤šæ¬¡å…¥åº“ | æ”¯æŒéƒ¨åˆ†å…¥åº“,入库状态自动流转 |
## å…¥åº“状态枚举
| å€¼ | åç§° | é¢œè‰²å»ºè®® |
|----|------|----------|
| 0 | æœªå…¥åº“ | info(灰色) |
| 1 | éƒ¨åˆ†å…¥åº“ | warning(橙色) |
| 2 | å…¨éƒ¨å…¥åº“ | success(绿色) |
## æ³¨æ„äº‹é¡¹
1. å…¥åº“状态由系统自动回写,无需手动维护
2. ä¸€ä¸ªé‡‡è´­ç”³è¯·å¯¹åº”一个采购订单,入库状态同步
3. å¦‚果采购申请未指定供应商,审批通过后需要手动选择供应商生成订单
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java
@@ -62,6 +62,9 @@
    @Override
    public List<ProcessDefinition> getProcessDefinitionList(Set<String> ids) {
        if (CollUtil.isEmpty(ids)) {
            return emptyList();
        }
        return repositoryService.createProcessDefinitionQuery().processDefinitionIds(ids).list();
    }
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/purchase/ErpSupplierApi.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
package cn.iocoder.yudao.module.erp.api.purchase;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.erp.api.purchase.dto.ErpSupplierRespDTO;
import java.util.Collection;
import java.util.List;
/**
 * ERP ä¾›åº”商 API æŽ¥å£
 */
public interface ErpSupplierApi {
    String PREFIX = "/rpc-api/erp/supplier";
    /**
     * èŽ·å–ä¾›åº”å•†
     *
     * @param id ä¾›åº”商编号
     * @return ä¾›åº”商
     */
    CommonResult<ErpSupplierRespDTO> getSupplier(Long id);
    /**
     * æ ¡éªŒä¾›åº”商存在
     *
     * @param id ä¾›åº”商编号
     * @return ä¾›åº”商
     */
    CommonResult<ErpSupplierRespDTO> validateSupplier(Long id);
    /**
     * èŽ·å–ä¾›åº”å•†åˆ—è¡¨
     *
     * @param ids ç¼–号列表
     * @return ä¾›åº”商列表
     */
    CommonResult<List<ErpSupplierRespDTO>> getSupplierList(Collection<Long> ids);
}
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/purchase/dto/ErpPurchaseOrderItemRespDTO.java
@@ -36,4 +36,7 @@
    @Schema(description = "备注", example = "备注信息")
    private String remark;
    @Schema(description = "是否需要来料检验", example = "true")
    private Boolean qcCheckFlag;
}
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/purchase/dto/ErpSupplierRespDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,48 @@
package cn.iocoder.yudao.module.erp.api.purchase.dto;
import lombok.Data;
/**
 * ERP ä¾›åº”商 Response DTO
 */
@Data
public class ErpSupplierRespDTO {
    /**
     * ä¾›åº”商编号
     */
    private Long id;
    /**
     * ä¾›åº”商名称
     */
    private String name;
    /**
     * è”系人
     */
    private String contact;
    /**
     * æ‰‹æœºå·ç 
     */
    private String mobile;
    /**
     * è”系电话
     */
    private String telephone;
    /**
     * ç”µå­é‚®ç®±
     */
    private String email;
    /**
     * å¼€å¯çŠ¶æ€
     */
    private Integer status;
    /**
     * çº³ç¨Žäººè¯†åˆ«å·
     */
    private String taxNo;
    /**
     * ç¨Žçއ
     */
    private java.math.BigDecimal taxPercent;
}
yudao-module-erp/pom.xml
@@ -52,6 +52,13 @@
            <version>${revision}</version>
        </dependency>
        <!-- MES åˆ¶é€ æ‰§è¡Œç³»ç»Ÿ API -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-mes-api</artifactId>
            <version>${revision}</version>
        </dependency>
        <!-- ä¸šåŠ¡ç»„ä»¶ -->
        <!-- Web ç›¸å…³ -->
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/api/purchase/ErpSupplierApiImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.erp.api.purchase;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.erp.api.purchase.dto.ErpSupplierRespDTO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpSupplierDO;
import cn.iocoder.yudao.module.erp.service.purchase.ErpSupplierService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.Collection;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
 * ERP ä¾›åº”商 API å®žçŽ°ç±»
 */
@Service
@Validated
public class ErpSupplierApiImpl implements ErpSupplierApi {
    @Resource
    private ErpSupplierService supplierService;
    @Override
    public CommonResult<ErpSupplierRespDTO> getSupplier(Long id) {
        ErpSupplierDO supplier = supplierService.getSupplier(id);
        return success(BeanUtils.toBean(supplier, ErpSupplierRespDTO.class));
    }
    @Override
    public CommonResult<ErpSupplierRespDTO> validateSupplier(Long id) {
        ErpSupplierDO supplier = supplierService.validateSupplier(id);
        return success(BeanUtils.toBean(supplier, ErpSupplierRespDTO.class));
    }
    @Override
    public CommonResult<List<ErpSupplierRespDTO>> getSupplierList(Collection<Long> ids) {
        List<ErpSupplierDO> list = supplierService.getSupplierList(ids);
        return success(BeanUtils.toBean(list, ErpSupplierRespDTO.class));
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseOrderController.java
@@ -39,6 +39,7 @@
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - ERP é‡‡è´­è®¢å•")
@RestController
@@ -78,6 +79,23 @@
                                                      @RequestParam("status") Integer status) {
        purchaseOrderService.updatePurchaseOrderStatus(id, status);
        return success(true);
    }
    @PostMapping("/submit")
    @Operation(summary = "提交采购订单审批")
    @PreAuthorize("@ss.hasPermission('erp:purchase-order:update-status')")
    public CommonResult<Boolean> submitPurchaseOrder(@RequestParam("id") Long id,
                                                     @RequestParam("processDefinitionKey") String processDefinitionKey) {
        Long userId = getLoginUserId();
        purchaseOrderService.submitPurchaseOrder(id, processDefinitionKey, userId);
        return success(true);
    }
    @GetMapping("/approve-process-list")
    @Operation(summary = "获取采购订单审批流程列表")
    @PreAuthorize("@ss.hasPermission('erp:purchase-order:query')")
    public CommonResult<List<Map<String, Object>>> getApproveProcessList() {
        return success(purchaseOrderService.getPurchaseOrderApproveProcessDefinitionList());
    }
    @DeleteMapping("/delete")
@@ -130,6 +148,14 @@
        ExcelUtils.write(response, "采购订单.xls", "数据", ErpPurchaseOrderRespVO.class, list);
    }
    @PostMapping("/confirm-receipt")
    @Operation(summary = "确认收货")
    @Parameter(name = "id", description = "采购订单编号", required = true)
    @PreAuthorize("@ss.hasPermission('erp:purchase-order:confirm-receipt')")
    public CommonResult<Long> confirmReceipt(@RequestParam("id") Long id) {
        return success(purchaseOrderService.confirmReceipt(id));
    }
    private PageResult<ErpPurchaseOrderRespVO> buildPurchaseOrderVOPageResult(PageResult<ErpPurchaseOrderDO> pageResult) {
        if (CollUtil.isEmpty(pageResult.getList())) {
            return PageResult.empty(pageResult.getTotal());
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseRequestController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,182 @@
package cn.iocoder.yudao.module.erp.controller.admin.purchase;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestPageReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestRespVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestSaveReqVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestItemDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpSupplierDO;
import cn.iocoder.yudao.module.erp.service.purchase.ErpPurchaseRequestService;
import cn.iocoder.yudao.module.erp.service.purchase.ErpSupplierService;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - ERP é‡‡è´­ç”³è¯·")
@RestController
@RequestMapping("/erp/purchase-request")
@Validated
public class ErpPurchaseRequestController {
    @Resource
    private ErpPurchaseRequestService purchaseRequestService;
    @Resource
    private ErpSupplierService supplierService;
    @Resource
    private MdmItemApi mdmItemApi;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private DeptApi deptApi;
    @PostMapping("/create")
    @Operation(summary = "创建采购申请")
    @PreAuthorize("@ss.hasPermission('erp:purchase-request:create')")
    public CommonResult<Long> createPurchaseRequest(@Valid @RequestBody ErpPurchaseRequestSaveReqVO createReqVO) {
        return success(purchaseRequestService.createPurchaseRequest(createReqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "更新采购申请")
    @PreAuthorize("@ss.hasPermission('erp:purchase-request:update')")
    public CommonResult<Boolean> updatePurchaseRequest(@Valid @RequestBody ErpPurchaseRequestSaveReqVO updateReqVO) {
        purchaseRequestService.updatePurchaseRequest(updateReqVO);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除采购申请")
    @Parameter(name = "ids", description = "编号数组", required = true)
    @PreAuthorize("@ss.hasPermission('erp:purchase-request:delete')")
    public CommonResult<Boolean> deletePurchaseRequest(@RequestParam("ids") List<Long> ids) {
        purchaseRequestService.deletePurchaseRequest(ids);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获得采购申请")
    @Parameter(name = "id", description = "编号", required = true, example = "1024")
    @PreAuthorize("@ss.hasPermission('erp:purchase-request:query')")
    public CommonResult<ErpPurchaseRequestRespVO> getPurchaseRequest(@RequestParam("id") Long id) {
        ErpPurchaseRequestDO purchaseRequest = purchaseRequestService.getPurchaseRequest(id);
        if (purchaseRequest == null) {
            return success(null);
        }
        List<ErpPurchaseRequestItemDO> purchaseRequestItemList = purchaseRequestService.getPurchaseRequestItemListByRequestId(id);
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(purchaseRequestItemList, ErpPurchaseRequestItemDO::getProductId));
        return success(BeanUtils.toBean(purchaseRequest, ErpPurchaseRequestRespVO.class, purchaseRequestVO ->
                purchaseRequestVO.setItems(BeanUtils.toBean(purchaseRequestItemList, ErpPurchaseRequestRespVO.Item.class, item -> {
                    MapUtils.findAndThen(itemMap, item.getProductId(), mdmItem -> item.setProductName(mdmItem.getName())
                            .setProductBarCode(mdmItem.getBarCode()));
                }))));
    }
    @GetMapping("/page")
    @Operation(summary = "获得采购申请分页")
    @PreAuthorize("@ss.hasPermission('erp:purchase-request:query')")
    public CommonResult<PageResult<ErpPurchaseRequestRespVO>> getPurchaseRequestPage(@Valid ErpPurchaseRequestPageReqVO pageReqVO) {
        PageResult<ErpPurchaseRequestDO> pageResult = purchaseRequestService.getPurchaseRequestPage(pageReqVO);
        return success(buildPurchaseRequestVOPageResult(pageResult));
    }
    @GetMapping("/export-excel")
    @Operation(summary = "导出采购申请 Excel")
    @PreAuthorize("@ss.hasPermission('erp:purchase-request:export')")
    @ApiAccessLog(operateType = EXPORT)
    public void exportPurchaseRequestExcel(@Valid ErpPurchaseRequestPageReqVO pageReqVO,
                                           HttpServletResponse response) throws IOException {
        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
        List<ErpPurchaseRequestRespVO> list = buildPurchaseRequestVOPageResult(purchaseRequestService.getPurchaseRequestPage(pageReqVO)).getList();
        ExcelUtils.write(response, "采购申请.xls", "数据", ErpPurchaseRequestRespVO.class, list);
    }
    @PutMapping("/submit")
    @Operation(summary = "提交采购申请审批")
    @PreAuthorize("@ss.hasPermission('erp:purchase-request:submit')")
    public CommonResult<Boolean> submitPurchaseRequest(@RequestParam("id") Long id,
                                                       @RequestParam("processDefinitionKey") String processDefinitionKey) {
        Long userId = getLoginUserId();
        purchaseRequestService.submitPurchaseRequest(id, processDefinitionKey, userId);
        return success(true);
    }
    @GetMapping("/approve-process-list")
    @Operation(summary = "获取采购申请审批流程列表")
    @PreAuthorize("@ss.hasPermission('erp:purchase-request:query')")
    public CommonResult<List<Map<String, Object>>> getApproveProcessList() {
        return success(purchaseRequestService.getPurchaseRequestApproveProcessDefinitionList());
    }
    @PostMapping("/generate-order")
    @Operation(summary = "生成采购订单")
    @PreAuthorize("@ss.hasPermission('erp:purchase-request:generate-order')")
    public CommonResult<Long> generatePurchaseOrder(@RequestParam("id") Long id,
                                                    @RequestParam("supplierId") Long supplierId) {
        return success(purchaseRequestService.generatePurchaseOrder(id, supplierId));
    }
    private PageResult<ErpPurchaseRequestRespVO> buildPurchaseRequestVOPageResult(PageResult<ErpPurchaseRequestDO> pageResult) {
        if (CollUtil.isEmpty(pageResult.getList())) {
            return PageResult.empty(pageResult.getTotal());
        }
        // 1.1 ç”³è¯·æ˜Žç»†
        List<ErpPurchaseRequestItemDO> purchaseRequestItemList = purchaseRequestService.getPurchaseRequestItemListByRequestIds(
                convertSet(pageResult.getList(), ErpPurchaseRequestDO::getId));
        Map<Long, List<ErpPurchaseRequestItemDO>> purchaseRequestItemMap = convertMultiMap(purchaseRequestItemList, ErpPurchaseRequestItemDO::getRequestId);
        // 1.2 ç‰©æ–™ä¿¡æ¯
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(purchaseRequestItemList, ErpPurchaseRequestItemDO::getProductId));
        // 1.3 ä¾›åº”商信息
        Map<Long, ErpSupplierDO> supplierMap = supplierService.getSupplierMap(
                convertSet(pageResult.getList(), ErpPurchaseRequestDO::getSupplierId));
        // 1.4 ç”¨æˆ·ä¿¡æ¯
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
                convertSet(pageResult.getList(), ErpPurchaseRequestDO::getRequestUserId));
        // 1.5 éƒ¨é—¨ä¿¡æ¯
        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(
                convertSet(pageResult.getList(), ErpPurchaseRequestDO::getRequestDeptId));
        // 2. å¼€å§‹æ‹¼æŽ¥
        return BeanUtils.toBean(pageResult, ErpPurchaseRequestRespVO.class, purchaseRequest -> {
            purchaseRequest.setItems(BeanUtils.toBean(purchaseRequestItemMap.get(purchaseRequest.getId()), ErpPurchaseRequestRespVO.Item.class,
                    item -> MapUtils.findAndThen(itemMap, item.getProductId(), mdmItem -> item.setProductName(mdmItem.getName())
                            .setProductBarCode(mdmItem.getBarCode()))));
            purchaseRequest.setProductNames(CollUtil.join(purchaseRequest.getItems(), ",", ErpPurchaseRequestRespVO.Item::getProductName));
            MapUtils.findAndThen(supplierMap, purchaseRequest.getSupplierId(), supplier -> purchaseRequest.setSupplierName(supplier.getName()));
            MapUtils.findAndThen(userMap, purchaseRequest.getRequestUserId(), user -> purchaseRequest.setRequestUserName(user.getNickname()));
            MapUtils.findAndThen(deptMap, purchaseRequest.getRequestDeptId(), dept -> purchaseRequest.setRequestDeptName(dept.getName()));
        });
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderRespVO.java
@@ -92,6 +92,9 @@
    @Schema(description = "采购入库数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
    private BigDecimal inCount;
    @Schema(description = "入库状态(0:未入库 1:部分入库 2:全部入库)", example = "0")
    private Integer inStatus;
    // ========== é‡‡è´­é€€è´§ï¼ˆå‡ºåº“)) ==========
    @Schema(description = "采购退货数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
@@ -145,8 +148,13 @@
        private String productUnitName;
        @Schema(description = "库存数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
        private BigDecimal stockCount; // è¯¥å­—段仅仅在“详情”和“编辑”时使用
        private BigDecimal stockCount; // è¯¥å­—段仅仅在详情和编辑时使用
        // ========== è´¨æ£€ ==========
        @Schema(description = "是否需要来料检验", example = "true")
        private Boolean qcCheckFlag;
    }
}
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderSaveReqVO.java
@@ -68,6 +68,9 @@
        @Schema(description = "备注", example = "随便")
        private String remark;
        @Schema(description = "是否需要来料检验", example = "true")
        private Boolean qcCheckFlag;
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,42 @@
package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
@Schema(description = "管理后台 - ERP é‡‡è´­ç”³è¯·åˆ†é¡µ Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class ErpPurchaseRequestPageReqVO extends PageParam {
    @Schema(description = "采购申请单号", example = "CGSQ001")
    private String no;
    @Schema(description = "申请人编号", example = "1")
    private Long requestUserId;
    @Schema(description = "申请部门编号", example = "1")
    private Long requestDeptId;
    @Schema(description = "供应商编号", example = "1")
    private Long supplierId;
    @Schema(description = "状态", example = "0")
    private Integer status;
    @Schema(description = "申请时间")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
    private LocalDateTime[] requestTime;
    @Schema(description = "产品编号", example = "1")
    private Long productId;
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,130 @@
package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - ERP é‡‡è´­ç”³è¯· Response VO")
@Data
public class ErpPurchaseRequestRespVO {
    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long id;
    @Schema(description = "采购申请单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "CGSQ001")
    private String no;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Integer status;
    @Schema(description = "申请人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long requestUserId;
    @Schema(description = "申请人姓名", example = "张三")
    private String requestUserName;
    @Schema(description = "申请部门编号", example = "1")
    private Long requestDeptId;
    @Schema(description = "申请部门名称", example = "生产部")
    private String requestDeptName;
    @Schema(description = "申请时间", requiredMode = Schema.RequiredMode.REQUIRED)
    private LocalDateTime requestTime;
    @Schema(description = "申请理由", example = "生产需要")
    private String requestReason;
    @Schema(description = "供应商编号", example = "1")
    private Long supplierId;
    @Schema(description = "供应商名称", example = "供应商A")
    private String supplierName;
    @Schema(description = "定金金额", example = "1000.00")
    private BigDecimal depositPrice;
    @Schema(description = "合计数量", requiredMode = Schema.RequiredMode.REQUIRED)
    private BigDecimal totalCount;
    @Schema(description = "合计价格", requiredMode = Schema.RequiredMode.REQUIRED)
    private BigDecimal totalPrice;
    @Schema(description = "产品名称列表", example = "产品A,产品B")
    private String productNames;
    @Schema(description = "生成的采购订单编号", example = "1")
    private Long orderId;
    @Schema(description = "生成的采购订单号", example = "CGDD001")
    private String orderNo;
    @Schema(description = "入库状态(0:未入库 1:部分入库 2:全部入库)", example = "0")
    private Integer inStatus;
    @Schema(description = "附件地址", example = "https://xxx.com/xxx.pdf")
    private String fileUrl;
    @Schema(description = "备注", example = "备注")
    private String remark;
    @Schema(description = "创建时间")
    private LocalDateTime createTime;
    @Schema(description = "申请明细列表")
    private java.util.List<Item> items;
    @Schema(description = "申请明细项")
    @Data
    public static class Item {
        @Schema(description = "编号", example = "1")
        private Long id;
        @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
        private Long productId;
        @Schema(description = "产品名称", example = "产品A")
        private String productName;
        @Schema(description = "产品条码", example = "12345678")
        private String productBarCode;
        @Schema(description = "产品单位编号", example = "1")
        private Long productUnitId;
        @Schema(description = "产品单位名称", example = "个")
        private String productUnitName;
        @Schema(description = "产品参考单价", example = "100.00")
        private BigDecimal productPrice;
        @Schema(description = "数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
        private BigDecimal count;
        @Schema(description = "总价", example = "1000.00")
        private BigDecimal totalPrice;
        @Schema(description = "税率,百分比", example = "13")
        private BigDecimal taxPercent;
        @Schema(description = "税额", example = "130.00")
        private BigDecimal taxPrice;
        @Schema(description = "需求日期")
        private LocalDateTime demandTime;
        @Schema(description = "备注", example = "备注")
        private String remark;
        @Schema(description = "是否需要来料检验", example = "true")
        private Boolean qcCheckFlag;
        @Schema(description = "入库数量", example = "10.00")
        private BigDecimal inCount;
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,92 @@
package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - ERP é‡‡è´­ç”³è¯·æ–°å¢ž/修改 Request VO")
@Data
public class ErpPurchaseRequestSaveReqVO {
    @Schema(description = "编号(修改时必填)", example = "1")
    private Long id;
    @Schema(description = "申请时间", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "申请时间不能为空")
    private LocalDateTime requestTime;
    @Schema(description = "申请理由", example = "生产需要")
    private String requestReason;
    @Schema(description = "供应商编号", example = "1")
    private Long supplierId;
    @Schema(description = "优惠率,百分比", example = "10")
    private BigDecimal discountPercent;
    @Schema(description = "定金金额", example = "1000.00")
    private BigDecimal depositPrice;
    @Schema(description = "附件地址", example = "https://xxx.com/xxx.pdf")
    private String fileUrl;
    @Schema(description = "备注", example = "备注")
    private String remark;
    @Schema(description = "申请明细列表", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotEmpty(message = "申请明细不能为空")
    @Valid
    private List<Item> items;
    @Schema(description = "申请明细项")
    @Data
    public static class Item {
        @Schema(description = "编号(修改时必填)", example = "1")
        private Long id;
        @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
        @NotNull(message = "产品编号不能为空")
        private Long productId;
        @Schema(description = "产品单位编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
        @NotNull(message = "产品单位不能为空")
        private Long productUnitId;
        @Schema(description = "产品参考单价", example = "100.00")
        private BigDecimal productPrice;
        @Schema(description = "数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
        @NotNull(message = "数量不能为空")
        private BigDecimal count;
        @Schema(description = "税率,百分比", example = "13")
        private BigDecimal taxPercent;
        @Schema(description = "需求日期")
        private LocalDateTime demandTime;
        @Schema(description = "备注", example = "备注")
        private String remark;
        @Schema(description = "是否需要来料检验", example = "true")
        private Boolean qcCheckFlag;
        @Schema(description = "产品名称(前端展示用,后端忽略)", example = "产品A")
        private String productName;
        @Schema(description = "产品条码(前端展示用,后端忽略)", example = "12345678")
        private String productBarCode;
        @Schema(description = "产品单位名称(前端展示用,后端忽略)", example = "个")
        private String productUnitName;
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseOrderDO.java
@@ -119,4 +119,9 @@
     */
    private Integer inStatus;
    /**
     * BPM æµç¨‹å®žä¾‹ç¼–号
     */
    private String processInstanceId;
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseOrderItemDO.java
@@ -35,6 +35,12 @@
     */
    private Long orderId;
    /**
     * é‡‡è´­ç”³è¯·æ˜Žç»†ç¼–号
     *
     * å…³è” {@link ErpPurchaseRequestItemDO#getId()}
     */
    private Long requestItemId;
    /**
     * ç‰©æ–™ç¼–号
     *
     * å…³è” {@link cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO#getId()}
@@ -89,4 +95,10 @@
     */
    private BigDecimal returnCount;
    // ========== è´¨æ£€ ==========
    /**
     * æ˜¯å¦éœ€è¦æ¥æ–™æ£€éªŒ
     */
    private Boolean qcCheckFlag;
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseRequestDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,141 @@
package cn.iocoder.yudao.module.erp.dal.dataobject.purchase;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * ERP é‡‡è´­ç”³è¯· DO
 */
@TableName("erp_purchase_request")
@KeySequence("erp_purchase_request_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ErpPurchaseRequestDO extends BaseDO {
    /**
     * ç¼–号
     */
    @TableId
    private Long id;
    /**
     * é‡‡è´­ç”³è¯·å•号
     */
    private String no;
    /**
     * çŠ¶æ€
     *
     * 0: æœªæäº¤
     * 10: å®¡æ‰¹ä¸­
     * 20: å®¡æ ¸é€šè¿‡
     * 30: å®¡æ ¸ä¸é€šè¿‡
     * 40: å·²å–消
     */
    private Integer status;
    /**
     * ç”³è¯·äººç¼–号
     */
    private Long requestUserId;
    /**
     * ç”³è¯·éƒ¨é—¨ç¼–号
     */
    private Long requestDeptId;
    /**
     * ç”³è¯·æ—¶é—´
     */
    private LocalDateTime requestTime;
    /**
     * ç”³è¯·ç†ç”±
     */
    private String requestReason;
    /**
     * ä¾›åº”商编号(可选)
     */
    private Long supplierId;
    /**
     * åˆè®¡æ•°é‡
     */
    private BigDecimal totalCount;
    /**
     * åˆè®¡ä»·æ ¼
     */
    private BigDecimal totalPrice;
    /**
     * åˆè®¡äº§å“ä»·æ ¼
     */
    private BigDecimal totalProductPrice;
    /**
     * åˆè®¡ç¨Žé¢
     */
    private BigDecimal totalTaxPrice;
    /**
     * ä¼˜æƒ çŽ‡ï¼Œç™¾åˆ†æ¯”
     */
    private BigDecimal discountPercent;
    /**
     * ä¼˜æƒ é‡‘额
     */
    private BigDecimal discountPrice;
    /**
     * å®šé‡‘金额
     */
    private BigDecimal depositPrice;
    /**
     * é™„件地址
     */
    private String fileUrl;
    /**
     * å¤‡æ³¨
     */
    private String remark;
    /**
     * BPM æµç¨‹å®žä¾‹ç¼–号
     */
    private String processInstanceId;
    /**
     * ç”Ÿæˆçš„采购订单编号
     */
    private Long orderId;
    /**
     * ç”Ÿæˆçš„采购订单号
     */
    private String orderNo;
    /**
     * å…¥åº“状态
     *
     * 0: æœªå…¥åº“
     * 1: éƒ¨åˆ†å…¥åº“
     * 2: å…¨éƒ¨å…¥åº“
     */
    private Integer inStatus;
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseRequestItemDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,91 @@
package cn.iocoder.yudao.module.erp.dal.dataobject.purchase;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * ERP é‡‡è´­ç”³è¯·æ˜Žç»† DO
 */
@TableName("erp_purchase_request_items")
@KeySequence("erp_purchase_request_items_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ErpPurchaseRequestItemDO extends BaseDO {
    /**
     * ç¼–号
     */
    @TableId
    private Long id;
    /**
     * é‡‡è´­ç”³è¯·ç¼–号
     */
    private Long requestId;
    /**
     * äº§å“ç¼–号
     */
    private Long productId;
    /**
     * äº§å“å•位编号
     */
    private Long productUnitId;
    /**
     * äº§å“å‚考单价
     */
    private BigDecimal productPrice;
    /**
     * æ•°é‡
     */
    private BigDecimal count;
    /**
     * æ€»ä»·
     */
    private BigDecimal totalPrice;
    /**
     * ç¨ŽçŽ‡ï¼Œç™¾åˆ†æ¯”
     */
    private BigDecimal taxPercent;
    /**
     * ç¨Žé¢
     */
    private BigDecimal taxPrice;
    /**
     * éœ€æ±‚日期
     */
    private LocalDateTime demandTime;
    /**
     * å¤‡æ³¨
     */
    private String remark;
    /**
     * æ˜¯å¦éœ€è¦æ¥æ–™æ£€éªŒ
     */
    private Boolean qcCheckFlag;
    /**
     * å…¥åº“数量
     */
    private BigDecimal inCount;
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseRequestItemMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.erp.dal.mysql.purchase;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestItemDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/**
 * ERP é‡‡è´­ç”³è¯·æ˜Žç»† Mapper
 */
@Mapper
public interface ErpPurchaseRequestItemMapper extends BaseMapperX<ErpPurchaseRequestItemDO> {
    default List<ErpPurchaseRequestItemDO> selectListByRequestId(Long requestId) {
        return selectList(ErpPurchaseRequestItemDO::getRequestId, requestId);
    }
    default List<ErpPurchaseRequestItemDO> selectListByRequestIds(Collection<Long> requestIds) {
        return selectList(ErpPurchaseRequestItemDO::getRequestId, requestIds);
    }
    default void deleteByRequestId(Long requestId) {
        delete(new LambdaQueryWrapperX<ErpPurchaseRequestItemDO>().eq(ErpPurchaseRequestItemDO::getRequestId, requestId));
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseRequestMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,44 @@
package cn.iocoder.yudao.module.erp.dal.mysql.purchase;
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.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestPageReqVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/**
 * ERP é‡‡è´­ç”³è¯· Mapper
 */
@Mapper
public interface ErpPurchaseRequestMapper extends BaseMapperX<ErpPurchaseRequestDO> {
    default ErpPurchaseRequestDO selectByNo(String no) {
        return selectOne(ErpPurchaseRequestDO::getNo, no);
    }
    default List<ErpPurchaseRequestDO> selectRequestListByIds(Collection<Long> ids) {
        return selectList(ErpPurchaseRequestDO::getId, ids);
    }
    default List<ErpPurchaseRequestDO> selectList(Long supplierId, Integer status) {
        return selectList(new LambdaQueryWrapperX<ErpPurchaseRequestDO>()
                .eqIfPresent(ErpPurchaseRequestDO::getSupplierId, supplierId)
                .eqIfPresent(ErpPurchaseRequestDO::getStatus, status));
    }
    default PageResult<ErpPurchaseRequestDO> selectPage(ErpPurchaseRequestPageReqVO pageReqVO) {
        return selectPage(pageReqVO, new LambdaQueryWrapperX<ErpPurchaseRequestDO>()
                .likeIfPresent(ErpPurchaseRequestDO::getNo, pageReqVO.getNo())
                .eqIfPresent(ErpPurchaseRequestDO::getRequestUserId, pageReqVO.getRequestUserId())
                .eqIfPresent(ErpPurchaseRequestDO::getRequestDeptId, pageReqVO.getRequestDeptId())
                .eqIfPresent(ErpPurchaseRequestDO::getSupplierId, pageReqVO.getSupplierId())
                .eqIfPresent(ErpPurchaseRequestDO::getStatus, pageReqVO.getStatus())
                .betweenIfPresent(ErpPurchaseRequestDO::getRequestTime, pageReqVO.getRequestTime())
                .orderByDesc(ErpPurchaseRequestDO::getId));
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/redis/no/ErpNoRedisDAO.java
@@ -30,6 +30,11 @@
    public static final String PURCHASE_ORDER_NO_PREFIX = "CGDD";
    /**
     * é‡‡è´­ç”³è¯· {@link cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestDO}
     */
    public static final String PURCHASE_REQUEST_NO_PREFIX = "CGSQ";
    /**
     * ä»˜æ¬¾å• {@link cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpFinancePaymentDO}
     */
    public static final String FINANCE_PAYMENT_NO_PREFIX = "FKD";
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpAuditStatus.java
@@ -17,7 +17,8 @@
@Getter
public enum ErpAuditStatus implements ArrayValuable<Integer> {
    PROCESS(10, "未审核"), // å®¡æ ¸ä¸­
    PROCESS(10, "未审核"), // æœªå®¡æ ¸/草稿
    APPROVING(15, "审批中"), // å®¡æ‰¹ä¸­
    APPROVE(20, "已审核"), // å®¡æ ¸é€šè¿‡
    CANCEL(30, "已作废"); // å·²ä½œåºŸ/已撤销
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpPurchaseRequestStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.erp.enums;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/**
 * ERP é‡‡è´­ç”³è¯·çŠ¶æ€æžšä¸¾
 */
@RequiredArgsConstructor
@Getter
public enum ErpPurchaseRequestStatusEnum implements ArrayValuable<Integer> {
    DRAFT(0, "未提交"),
    PROCESS(10, "审批中"),
    APPROVE(20, "审核通过"),
    REJECT(30, "审核不通过"),
    CANCEL(40, "已取消");
    public static final Integer[] ARRAYS = Arrays.stream(values()).map(ErpPurchaseRequestStatusEnum::getStatus).toArray(Integer[]::new);
    /**
     * çŠ¶æ€
     */
    private final Integer status;
    /**
     * çŠ¶æ€å
     */
    private final String name;
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErrorCodeConstants.java
@@ -27,6 +27,8 @@
    ErrorCode PURCHASE_ORDER_PROCESS_FAIL_EXISTS_RETURN = new ErrorCode(1_030_101_010, "反审核失败,已存在对应的采购退货单");
    ErrorCode PURCHASE_ORDER_BPM_CATEGORY_NOT_EXISTS = new ErrorCode(1_030_101_011, "采购审核分类不存在,请先在BPM模块创建采购审核流程分类");
    ErrorCode PURCHASE_ORDER_BPM_PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1_030_101_012, "采购审核分类下没有可用的流程模型,请先创建并发布流程");
    ErrorCode PURCHASE_ORDER_SUBMIT_FAIL_STATUS = new ErrorCode(1_030_101_013, "提交审批失败,只有未审核状态的采购订单才能提交");
    ErrorCode PURCHASE_ORDER_ALL_IN = new ErrorCode(1_030_101_014, "采购订单已全部入库,无需确认收货");
    // ========== ERP é‡‡è´­å…¥åº“(1-030-102-000) ==========
    ErrorCode PURCHASE_IN_NOT_EXISTS = new ErrorCode(1_030_102_000, "采购入库单不存在");
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderService.java
@@ -43,6 +43,30 @@
    void updatePurchaseOrderStatus(Long id, Integer status);
    /**
     * æäº¤é‡‡è´­è®¢å•审批
     *
     * @param id ç¼–号
     * @param processDefinitionKey æµç¨‹å®šä¹‰ Key
     * @param userId ç”¨æˆ·ç¼–号
     */
    void submitPurchaseOrder(Long id, String processDefinitionKey, Long userId);
    /**
     * èŽ·å–é‡‡è´­è®¢å•å®¡æ‰¹æµç¨‹åˆ—è¡¨
     *
     * @return æµç¨‹å®šä¹‰åˆ—表
     */
    List<Map<String, Object>> getPurchaseOrderApproveProcessDefinitionList();
    /**
     * æ›´æ–°é‡‡è´­è®¢å•审批结果(BPM å›žè°ƒï¼‰
     *
     * @param id ç¼–号
     * @param bpmResult BPM å®¡æ‰¹ç»“æžœ
     */
    void updatePurchaseOrderAuditStatus(Long id, Integer bpmResult);
    /**
     * æ›´æ–°é‡‡è´­è®¢å•的入库数量
     *
     * @param id ç¼–号
@@ -136,4 +160,14 @@
     */
    void updatePurchaseOrderInStatus(Long orderId);
    /**
     * ç¡®è®¤æ”¶è´§
     *
     * ç”Ÿæˆåˆ°è´§é€šçŸ¥å•,状态为待入库
     *
     * @param id é‡‡è´­è®¢å•编号
     * @return åˆ°è´§é€šçŸ¥å•编号
     */
    Long confirmReceipt(Long id);
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java
@@ -5,17 +5,32 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService;
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderPageReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderSaveReqVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderItemDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestItemDO;
import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseOrderItemMapper;
import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseOrderMapper;
import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseRequestItemMapper;
import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseRequestMapper;
import cn.iocoder.yudao.module.erp.dal.redis.no.ErpNoRedisDAO;
import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
import cn.iocoder.yudao.module.erp.enums.ErpPurchaseOrderInStatusEnum;
import cn.iocoder.yudao.module.erp.service.finance.ErpAccountService;
import cn.iocoder.yudao.module.mes.api.arrivalnotice.MesArrivalNoticeApi;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.repository.ProcessDefinition;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
@@ -34,7 +49,13 @@
 */
@Service
@Validated
@Slf4j
public class ErpPurchaseOrderServiceImpl implements ErpPurchaseOrderService {
    /**
     * BPM é‡‡è´­è®¢å•审批分类编码
     */
    private static final String PURCHASE_ORDER_APPROVE_CATEGORY_CODE = "purchase_request_approve";
    @Resource
    private ErpPurchaseOrderMapper purchaseOrderMapper;
@@ -48,6 +69,22 @@
    private ErpSupplierService supplierService;
    @Resource
    private ErpAccountService accountService;
    @Resource
    private BpmProcessInstanceApi processInstanceApi;
    @Resource
    private BpmCategoryService bpmCategoryService;
    @Resource
    private BpmProcessDefinitionService bpmProcessDefinitionService;
    @Resource
    @Lazy
    private MesArrivalNoticeApi arrivalNoticeApi;
    @Resource
    private ErpPurchaseRequestItemMapper purchaseRequestItemMapper;
    @Resource
    private ErpPurchaseRequestMapper purchaseRequestMapper;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -66,9 +103,10 @@
            throw exception(PURCHASE_ORDER_NO_EXISTS);
        }
        // 2.1 æ’入订单(采购订单不需要审批,直接为已审核状态)
        // 2.1 æ’入订单(采购订单创建后为未审核状态,需要提交审批)
        ErpPurchaseOrderDO purchaseOrder = BeanUtils.toBean(createReqVO, ErpPurchaseOrderDO.class, in -> in
                .setNo(no).setStatus(ErpAuditStatus.APPROVE.getStatus()));
                .setNo(no).setStatus(ErpAuditStatus.PROCESS.getStatus())
                .setInCount(BigDecimal.ZERO).setInStatus(ErpPurchaseOrderInStatusEnum.NOT_IN.getStatus()));
        calculateTotalPrice(purchaseOrder, purchaseOrderItems);
        purchaseOrderMapper.insert(purchaseOrder);
        // 2.2 æ’入订单项
@@ -119,8 +157,6 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updatePurchaseOrderStatus(Long id, Integer status) {
        // é‡‡è´­è®¢å•不再支持状态流转(不需要审批)
        // æ­¤æ–¹æ³•保留用于特殊情况(如反审核后重新审核)
        boolean approve = ErpAuditStatus.APPROVE.getStatus().equals(status);
        // 1.1 æ ¡éªŒå­˜åœ¨
        ErpPurchaseOrderDO purchaseOrder = validatePurchaseOrderExists(id);
@@ -145,6 +181,109 @@
        if (updateCount == 0) {
            throw exception(approve ? PURCHASE_ORDER_APPROVE_FAIL : PURCHASE_ORDER_PROCESS_FAIL);
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void submitPurchaseOrder(Long id, String processDefinitionKey, Long userId) {
        // 1. æ ¡éªŒå­˜åœ¨ + æœªå®¡æ ¸çŠ¶æ€
        ErpPurchaseOrderDO purchaseOrder = validatePurchaseOrderExists(id);
        if (!ErpAuditStatus.PROCESS.getStatus().equals(purchaseOrder.getStatus())) {
            throw exception(PURCHASE_ORDER_SUBMIT_FAIL_STATUS);
        }
        // 2. åˆ›å»º BPM æµç¨‹å®žä¾‹
        String processInstanceId = processInstanceApi.createProcessInstance(userId,
                new BpmProcessInstanceCreateReqDTO()
                        .setProcessDefinitionKey(processDefinitionKey)
                        .setBusinessKey(String.valueOf(id)));
        // 3. æ›´æ–°é‡‡è´­è®¢å•状态为审批中
        purchaseOrderMapper.updateById(new ErpPurchaseOrderDO()
                .setId(id)
                .setStatus(ErpAuditStatus.APPROVING.getStatus())
                .setProcessInstanceId(processInstanceId));
    }
    @Override
    public List<Map<String, Object>> getPurchaseOrderApproveProcessDefinitionList() {
        // 1. æ ¡éªŒåˆ†ç±»å’Œæµç¨‹å®šä¹‰æ˜¯å¦å­˜åœ¨
        validatePurchaseOrderApproveCategoryAndProcessDefinition();
        // 2. èŽ·å–åˆ†ç±»ä¸‹çš„æµç¨‹å®šä¹‰ä¿¡æ¯
        List<BpmProcessDefinitionInfoDO> definitionInfoList = bpmProcessDefinitionService
                .getProcessDefinitionInfoListByCategory(PURCHASE_ORDER_APPROVE_CATEGORY_CODE);
        if (CollUtil.isEmpty(definitionInfoList)) {
            return Collections.emptyList();
        }
        // 3. éåŽ†èŽ·å–æµç¨‹å®šä¹‰è¯¦æƒ…ï¼Œè¿‡æ»¤æ¿€æ´»çŠ¶æ€ï¼Œä¿ç•™æœ€æ–°ç‰ˆæœ¬
        Map<String, ProcessDefinition> latestVersionMap = new HashMap<>();
        for (BpmProcessDefinitionInfoDO info : definitionInfoList) {
            ProcessDefinition pd = bpmProcessDefinitionService.getProcessDefinition(info.getProcessDefinitionId());
            if (pd == null || pd.isSuspended()) {
                continue;
            }
            ProcessDefinition existing = latestVersionMap.get(pd.getKey());
            if (existing == null || pd.getVersion() > existing.getVersion()) {
                latestVersionMap.put(pd.getKey(), pd);
            }
        }
        // 4. è¿”回流程定义列表
        List<Map<String, Object>> result = new ArrayList<>();
        for (ProcessDefinition pd : latestVersionMap.values()) {
            Map<String, Object> item = new HashMap<>();
            item.put("id", pd.getId());
            item.put("key", pd.getKey());
            item.put("name", pd.getName());
            result.add(item);
        }
        return result;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updatePurchaseOrderAuditStatus(Long id, Integer bpmResult) {
        // 1. æ ¡éªŒå­˜åœ¨
        ErpPurchaseOrderDO purchaseOrder = validatePurchaseOrderExists(id);
        if (!ErpAuditStatus.APPROVING.getStatus().equals(purchaseOrder.getStatus())) {
            log.warn("[updatePurchaseOrderAuditStatus] é‡‡è´­è®¢å•({}) ä¸å¤„于审批中状态", id);
            return;
        }
        // 2. æ ¹æ®å®¡æ‰¹ç»“果更新状态
        Integer newStatus = convertBpmResultToAuditStatus(bpmResult);
        if (newStatus != null) {
            purchaseOrderMapper.updateById(new ErpPurchaseOrderDO()
                    .setId(id).setStatus(newStatus));
        }
    }
    /**
     * æ ¡éªŒé‡‡è´­è®¢å•审批分类和流程定义是否存在
     */
    private void validatePurchaseOrderApproveCategoryAndProcessDefinition() {
        // 1. æ ¡éªŒåˆ†ç±»æ˜¯å¦å­˜åœ¨
        List<BpmCategoryDO> categories = bpmCategoryService.getCategoryListByCode(
                Collections.singletonList(PURCHASE_ORDER_APPROVE_CATEGORY_CODE));
        if (CollUtil.isEmpty(categories)) {
            throw exception(PURCHASE_ORDER_BPM_CATEGORY_NOT_EXISTS);
        }
    }
    /**
     * è½¬æ¢ BPM å®¡æ‰¹ç»“果为审核状态
     */
    private Integer convertBpmResultToAuditStatus(Integer bpmResult) {
        if (BpmTaskStatusEnum.APPROVE.getStatus().equals(bpmResult)) {
            return ErpAuditStatus.APPROVE.getStatus();
        } else if (BpmTaskStatusEnum.REJECT.getStatus().equals(bpmResult)) {
            return ErpAuditStatus.PROCESS.getStatus();
        } else if (BpmTaskStatusEnum.CANCEL.getStatus().equals(bpmResult)) {
            return ErpAuditStatus.CANCEL.getStatus();
        }
        return null;
    }
    private List<ErpPurchaseOrderItemDO> validatePurchaseOrderItems(List<ErpPurchaseOrderSaveReqVO.Item> list) {
@@ -307,11 +446,70 @@
        if (CollUtil.isEmpty(items)) {
            return;
        }
        // 3. è®¡ç®—入库状态
        // 3. è®¡ç®—入库状态和总入库数量
        int inStatus = calculateInStatus(items);
        // 4. æ›´æ–°é‡‡è´­è®¢å•入库状态
        BigDecimal totalInCount = calculateTotalInCount(items);
        // 4. æ›´æ–°é‡‡è´­è®¢å•入库状态和入库数量
        purchaseOrderMapper.updateById(new ErpPurchaseOrderDO()
                .setId(orderId).setInStatus(inStatus));
                .setId(orderId).setInStatus(inStatus).setInCount(totalInCount));
        // 5. å›žå†™é‡‡è´­ç”³è¯·æ˜Žç»†çš„入库数量和采购申请的入库状态
        updatePurchaseRequestInStatus(items, inStatus);
    }
    /**
     * è®¡ç®—总入库数量
     */
    private BigDecimal calculateTotalInCount(List<ErpPurchaseOrderItemDO> items) {
        BigDecimal total = BigDecimal.ZERO;
        for (ErpPurchaseOrderItemDO item : items) {
            if (item.getInCount() != null) {
                total = total.add(item.getInCount());
            }
        }
        return total;
    }
    /**
     * å›žå†™é‡‡è´­ç”³è¯·å…¥åº“数量和入库状态
     *
     * @param orderItems é‡‡è´­è®¢å•明细列表
     * @param orderInStatus é‡‡è´­è®¢å•入库状态
     */
    private void updatePurchaseRequestInStatus(List<ErpPurchaseOrderItemDO> orderItems, int orderInStatus) {
        // èŽ·å–æ‰€æœ‰å…³è”çš„é‡‡è´­ç”³è¯·æ˜Žç»† ID
        Set<Long> requestItemIds = new HashSet<>();
        for (ErpPurchaseOrderItemDO item : orderItems) {
            if (item.getRequestItemId() != null) {
                requestItemIds.add(item.getRequestItemId());
            }
        }
        if (requestItemIds.isEmpty()) {
            return;
        }
        // æ›´æ–°é‡‡è´­ç”³è¯·æ˜Žç»†çš„入库数量
        for (ErpPurchaseOrderItemDO orderItem : orderItems) {
            if (orderItem.getRequestItemId() != null && orderItem.getInCount() != null) {
                purchaseRequestItemMapper.updateById(new ErpPurchaseRequestItemDO()
                        .setId(orderItem.getRequestItemId())
                        .setInCount(orderItem.getInCount()));
            }
        }
        // èŽ·å–é‡‡è´­ç”³è¯·æ˜Žç»†ï¼Œæ‰¾åˆ°å¯¹åº”çš„é‡‡è´­ç”³è¯· ID
        List<ErpPurchaseRequestItemDO> requestItems = purchaseRequestItemMapper.selectBatchIds(requestItemIds);
        if (CollUtil.isEmpty(requestItems)) {
            return;
        }
        Set<Long> requestIds = convertSet(requestItems, ErpPurchaseRequestItemDO::getRequestId);
        // æ›´æ–°é‡‡è´­ç”³è¯·çš„入库状态
        for (Long requestId : requestIds) {
            purchaseRequestMapper.updateById(new ErpPurchaseRequestDO()
                    .setId(requestId)
                    .setInStatus(orderInStatus));
        }
    }
    /**
@@ -345,4 +543,22 @@
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long confirmReceipt(Long id) {
        // 1. æ ¡éªŒé‡‡è´­è®¢å•存在且已审核
        ErpPurchaseOrderDO purchaseOrder = validatePurchaseOrder(id);
        // 2. æ ¡éªŒé‡‡è´­è®¢å•未全部入库
        if (ErpPurchaseOrderInStatusEnum.ALL_IN.getStatus().equals(purchaseOrder.getInStatus())) {
            throw exception(PURCHASE_ORDER_ALL_IN);
        }
        // 3. è°ƒç”¨ MES æŽ¥å£ç”Ÿæˆåˆ°è´§é€šçŸ¥å•
        Long arrivalNoticeId = arrivalNoticeApi.generateArrivalNoticeFromPurchaseOrder(id);
        log.info("[confirmReceipt] é‡‡è´­è®¢å•({}) ç¡®è®¤æ”¶è´§ï¼Œç”Ÿæˆåˆ°è´§é€šçŸ¥å•({})", id, arrivalNoticeId);
        return arrivalNoticeId;
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,114 @@
package cn.iocoder.yudao.module.erp.service.purchase;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestPageReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestSaveReqVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestItemDO;
import jakarta.validation.Valid;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
 * ERP é‡‡è´­ç”³è¯· Service æŽ¥å£
 */
public interface ErpPurchaseRequestService {
    /**
     * åˆ›å»ºé‡‡è´­ç”³è¯·
     *
     * @param createReqVO åˆ›å»ºä¿¡æ¯
     * @return ç¼–号
     */
    Long createPurchaseRequest(@Valid ErpPurchaseRequestSaveReqVO createReqVO);
    /**
     * æ›´æ–°é‡‡è´­ç”³è¯·
     *
     * @param updateReqVO æ›´æ–°ä¿¡æ¯
     */
    void updatePurchaseRequest(@Valid ErpPurchaseRequestSaveReqVO updateReqVO);
    /**
     * åˆ é™¤é‡‡è´­ç”³è¯·
     *
     * @param ids ç¼–号数组
     */
    void deletePurchaseRequest(List<Long> ids);
    /**
     * èŽ·å¾—é‡‡è´­ç”³è¯·
     *
     * @param id ç¼–号
     * @return é‡‡è´­ç”³è¯·
     */
    ErpPurchaseRequestDO getPurchaseRequest(Long id);
    /**
     * èŽ·å¾—é‡‡è´­ç”³è¯·åˆ†é¡µ
     *
     * @param pageReqVO åˆ†é¡µæŸ¥è¯¢
     * @return é‡‡è´­ç”³è¯·åˆ†é¡µ
     */
    PageResult<ErpPurchaseRequestDO> getPurchaseRequestPage(ErpPurchaseRequestPageReqVO pageReqVO);
    /**
     * èŽ·å¾—é‡‡è´­ç”³è¯·é¡¹åˆ—è¡¨
     *
     * @param requestId é‡‡è´­ç”³è¯·ç¼–号
     * @return é‡‡è´­ç”³è¯·é¡¹åˆ—表
     */
    List<ErpPurchaseRequestItemDO> getPurchaseRequestItemListByRequestId(Long requestId);
    /**
     * èŽ·å¾—é‡‡è´­ç”³è¯·é¡¹ List
     *
     * @param requestIds é‡‡è´­ç”³è¯·ç¼–号数组
     * @return é‡‡è´­ç”³è¯·é¡¹ List
     */
    List<ErpPurchaseRequestItemDO> getPurchaseRequestItemListByRequestIds(Collection<Long> requestIds);
    /**
     * æäº¤é‡‡è´­ç”³è¯·å®¡æ‰¹
     *
     * @param id ç¼–号
     * @param processDefinitionKey æµç¨‹å®šä¹‰ Key
     * @param userId ç”¨æˆ·ç¼–号
     */
    void submitPurchaseRequest(Long id, String processDefinitionKey, Long userId);
    /**
     * èŽ·å–é‡‡è´­ç”³è¯·å®¡æ‰¹æµç¨‹åˆ—è¡¨
     *
     * @return æµç¨‹å®šä¹‰åˆ—表
     */
    List<Map<String, Object>> getPurchaseRequestApproveProcessDefinitionList();
    /**
     * æ›´æ–°é‡‡è´­ç”³è¯·å®¡æ‰¹ç»“果(BPM å›žè°ƒï¼‰
     *
     * @param id ç¼–号
     * @param bpmResult BPM å®¡æ‰¹ç»“æžœ
     */
    void updatePurchaseRequestAuditStatus(Long id, Integer bpmResult);
    /**
     * ç”Ÿæˆé‡‡è´­è®¢å•
     *
     * @param id é‡‡è´­ç”³è¯·ç¼–号
     * @param supplierId ä¾›åº”商编号
     * @return é‡‡è´­è®¢å•编号
     */
    Long generatePurchaseOrder(Long id, Long supplierId);
    /**
     * æ ¹æ®ç¼–号列表获取采购申请列表
     *
     * @param ids ç¼–号列表
     * @return é‡‡è´­ç”³è¯·åˆ—表
     */
    List<ErpPurchaseRequestDO> getPurchaseRequestList(List<Long> ids);
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,459 @@
package cn.iocoder.yudao.module.erp.service.purchase;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService;
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestPageReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestSaveReqVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderItemDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestItemDO;
import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseOrderItemMapper;
import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseOrderMapper;
import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseRequestItemMapper;
import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseRequestMapper;
import cn.iocoder.yudao.module.erp.dal.redis.no.ErpNoRedisDAO;
import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
import cn.iocoder.yudao.module.erp.enums.ErpPurchaseRequestStatusEnum;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.repository.ProcessDefinition;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.*;
/**
 * ERP é‡‡è´­ç”³è¯· Service å®žçŽ°ç±»
 */
@Service
@Validated
@Slf4j
public class ErpPurchaseRequestServiceImpl implements ErpPurchaseRequestService {
    /**
     * BPM é‡‡è´­ç”³è¯·å®¡æ‰¹åˆ†ç±»ç¼–码
     */
    private static final String PURCHASE_REQUEST_APPROVE_CATEGORY_CODE = "purchase_request_approve";
    @Resource
    private ErpPurchaseRequestMapper purchaseRequestMapper;
    @Resource
    private ErpPurchaseRequestItemMapper purchaseRequestItemMapper;
    @Resource
    private ErpPurchaseOrderMapper purchaseOrderMapper;
    @Resource
    private ErpPurchaseOrderItemMapper purchaseOrderItemMapper;
    @Resource
    private ErpNoRedisDAO noRedisDAO;
    @Resource
    private ErpSupplierService supplierService;
    @Resource
    private BpmProcessInstanceApi processInstanceApi;
    @Resource
    private BpmCategoryService bpmCategoryService;
    @Resource
    private BpmProcessDefinitionService bpmProcessDefinitionService;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private DeptApi deptApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createPurchaseRequest(ErpPurchaseRequestSaveReqVO createReqVO) {
        // 1. æ ¡éªŒè®¢å•项的有效性
        List<ErpPurchaseRequestItemDO> requestItems = validatePurchaseRequestItems(createReqVO.getItems());
        // 2. æ ¡éªŒä¾›åº”商(如果指定)
        if (createReqVO.getSupplierId() != null) {
            supplierService.validateSupplier(createReqVO.getSupplierId());
        }
        // 3. ç”Ÿæˆç”³è¯·å•号
        String no = noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_REQUEST_NO_PREFIX);
        if (purchaseRequestMapper.selectByNo(no) != null) {
            throw exception(PURCHASE_REQUEST_NO_EXISTS);
        }
        // 4. èŽ·å–å½“å‰ç”¨æˆ·ä¿¡æ¯
        Long userId = Long.parseLong(getCurrentUserId());
        AdminUserRespDTO user = adminUserApi.getUser(userId);
        Long deptId = user != null ? user.getDeptId() : null;
        // 5. æ’入申请单
        ErpPurchaseRequestDO purchaseRequest = BeanUtils.toBean(createReqVO, ErpPurchaseRequestDO.class, req -> req
                .setNo(no)
                .setStatus(ErpPurchaseRequestStatusEnum.DRAFT.getStatus())
                .setRequestUserId(userId)
                .setRequestDeptId(deptId));
        calculateTotalPrice(purchaseRequest, requestItems);
        purchaseRequestMapper.insert(purchaseRequest);
        // 6. æ’入申请明细
        requestItems.forEach(o -> o.setRequestId(purchaseRequest.getId()));
        purchaseRequestItemMapper.insertBatch(requestItems);
        return purchaseRequest.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updatePurchaseRequest(ErpPurchaseRequestSaveReqVO updateReqVO) {
        // 1. æ ¡éªŒå­˜åœ¨ + è‰ç¨¿çŠ¶æ€
        ErpPurchaseRequestDO purchaseRequest = validatePurchaseRequestExistsAndDraft(updateReqVO.getId());
        // 2. æ ¡éªŒä¾›åº”商(如果指定)
        if (updateReqVO.getSupplierId() != null) {
            supplierService.validateSupplier(updateReqVO.getSupplierId());
        }
        // 3. æ ¡éªŒè®¢å•项的有效性
        List<ErpPurchaseRequestItemDO> requestItems = validatePurchaseRequestItems(updateReqVO.getItems());
        // 4. æ›´æ–°ç”³è¯·å•
        ErpPurchaseRequestDO updateObj = BeanUtils.toBean(updateReqVO, ErpPurchaseRequestDO.class);
        calculateTotalPrice(updateObj, requestItems);
        purchaseRequestMapper.updateById(updateObj);
        // 5. æ›´æ–°ç”³è¯·æ˜Žç»†
        updatePurchaseRequestItemList(updateReqVO.getId(), requestItems);
    }
    private void calculateTotalPrice(ErpPurchaseRequestDO purchaseRequest, List<ErpPurchaseRequestItemDO> requestItems) {
        purchaseRequest.setTotalCount(getSumValue(requestItems, ErpPurchaseRequestItemDO::getCount, BigDecimal::add));
        purchaseRequest.setTotalProductPrice(getSumValue(requestItems, ErpPurchaseRequestItemDO::getTotalPrice, BigDecimal::add, BigDecimal.ZERO));
        purchaseRequest.setTotalTaxPrice(getSumValue(requestItems, ErpPurchaseRequestItemDO::getTaxPrice, BigDecimal::add, BigDecimal.ZERO));
        purchaseRequest.setTotalPrice(purchaseRequest.getTotalProductPrice().add(purchaseRequest.getTotalTaxPrice()));
        // è®¡ç®—优惠价格
        if (purchaseRequest.getDiscountPercent() == null) {
            purchaseRequest.setDiscountPercent(BigDecimal.ZERO);
        }
        purchaseRequest.setDiscountPrice(MoneyUtils.priceMultiplyPercent(purchaseRequest.getTotalPrice(), purchaseRequest.getDiscountPercent()));
        purchaseRequest.setTotalPrice(purchaseRequest.getTotalPrice().subtract(purchaseRequest.getDiscountPrice()));
    }
    private List<ErpPurchaseRequestItemDO> validatePurchaseRequestItems(List<ErpPurchaseRequestSaveReqVO.Item> list) {
        return convertList(list, o -> BeanUtils.toBean(o, ErpPurchaseRequestItemDO.class, item -> {
            if (item.getProductPrice() != null && item.getCount() != null) {
                item.setTotalPrice(MoneyUtils.priceMultiply(item.getProductPrice(), item.getCount()));
                if (item.getTaxPercent() != null) {
                    item.setTaxPrice(MoneyUtils.priceMultiplyPercent(item.getTotalPrice(), item.getTaxPercent()));
                }
            }
        }));
    }
    private void updatePurchaseRequestItemList(Long id, List<ErpPurchaseRequestItemDO> newList) {
        List<ErpPurchaseRequestItemDO> oldList = purchaseRequestItemMapper.selectListByRequestId(id);
        List<List<ErpPurchaseRequestItemDO>> diffList = diffList(oldList, newList,
                (oldVal, newVal) -> oldVal.getId().equals(newVal.getId()));
        if (CollUtil.isNotEmpty(diffList.get(0))) {
            diffList.get(0).forEach(o -> o.setRequestId(id));
            purchaseRequestItemMapper.insertBatch(diffList.get(0));
        }
        if (CollUtil.isNotEmpty(diffList.get(1))) {
            purchaseRequestItemMapper.updateBatch(diffList.get(1));
        }
        if (CollUtil.isNotEmpty(diffList.get(2))) {
            purchaseRequestItemMapper.deleteByIds(convertList(diffList.get(2), ErpPurchaseRequestItemDO::getId));
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deletePurchaseRequest(List<Long> ids) {
        List<ErpPurchaseRequestDO> purchaseRequests = purchaseRequestMapper.selectRequestListByIds(ids);
        if (CollUtil.isEmpty(purchaseRequests)) {
            return;
        }
        purchaseRequests.forEach(purchaseRequest -> {
            // åªæœ‰è‰ç¨¿çŠ¶æ€æ‰èƒ½åˆ é™¤
            if (!ErpPurchaseRequestStatusEnum.DRAFT.getStatus().equals(purchaseRequest.getStatus())) {
                throw exception(PURCHASE_REQUEST_DELETE_FAIL_NOT_DRAFT, purchaseRequest.getNo());
            }
        });
        purchaseRequests.forEach(purchaseRequest -> {
            purchaseRequestMapper.deleteById(purchaseRequest.getId());
            purchaseRequestItemMapper.deleteByRequestId(purchaseRequest.getId());
        });
    }
    @Override
    public ErpPurchaseRequestDO getPurchaseRequest(Long id) {
        return purchaseRequestMapper.selectById(id);
    }
    @Override
    public PageResult<ErpPurchaseRequestDO> getPurchaseRequestPage(ErpPurchaseRequestPageReqVO pageReqVO) {
        return purchaseRequestMapper.selectPage(pageReqVO);
    }
    @Override
    public List<ErpPurchaseRequestItemDO> getPurchaseRequestItemListByRequestId(Long requestId) {
        return purchaseRequestItemMapper.selectListByRequestId(requestId);
    }
    @Override
    public List<ErpPurchaseRequestItemDO> getPurchaseRequestItemListByRequestIds(Collection<Long> requestIds) {
        if (CollUtil.isEmpty(requestIds)) {
            return Collections.emptyList();
        }
        return purchaseRequestItemMapper.selectListByRequestIds(requestIds);
    }
    @Override
    public List<ErpPurchaseRequestDO> getPurchaseRequestList(List<Long> ids) {
        if (CollUtil.isEmpty(ids)) {
            return Collections.emptyList();
        }
        return purchaseRequestMapper.selectRequestListByIds(ids);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void submitPurchaseRequest(Long id, String processDefinitionKey, Long userId) {
        // 1. æ ¡éªŒå­˜åœ¨ + è‰ç¨¿çŠ¶æ€
        ErpPurchaseRequestDO purchaseRequest = validatePurchaseRequestExistsAndDraft(id);
        // 2. æ ¡éªŒæ˜¯å¦æœ‰æ˜Žç»†
        List<ErpPurchaseRequestItemDO> items = purchaseRequestItemMapper.selectListByRequestId(id);
        if (CollUtil.isEmpty(items)) {
            throw exception(PURCHASE_REQUEST_NOT_EXISTS); // TODO: æ·»åŠ æ˜Žç»†ä¸ºç©ºçš„é”™è¯¯ç 
        }
        // 3. åˆ›å»º BPM æµç¨‹å®žä¾‹
        String processInstanceId = processInstanceApi.createProcessInstance(userId,
                new BpmProcessInstanceCreateReqDTO()
                        .setProcessDefinitionKey(processDefinitionKey)
                        .setBusinessKey(String.valueOf(id)));
        // 4. æ›´æ–°é‡‡è´­ç”³è¯·çŠ¶æ€ä¸ºå®¡æ‰¹ä¸­
        purchaseRequestMapper.updateById(new ErpPurchaseRequestDO()
                .setId(id)
                .setStatus(ErpPurchaseRequestStatusEnum.PROCESS.getStatus())
                .setProcessInstanceId(processInstanceId));
    }
    @Override
    public List<Map<String, Object>> getPurchaseRequestApproveProcessDefinitionList() {
        // 1. æ ¡éªŒåˆ†ç±»å’Œæµç¨‹å®šä¹‰æ˜¯å¦å­˜åœ¨
        validatePurchaseRequestApproveCategoryAndProcessDefinition();
        // 2. èŽ·å–åˆ†ç±»ä¸‹çš„æµç¨‹å®šä¹‰ä¿¡æ¯
        List<BpmProcessDefinitionInfoDO> definitionInfoList = bpmProcessDefinitionService
                .getProcessDefinitionInfoListByCategory(PURCHASE_REQUEST_APPROVE_CATEGORY_CODE);
        if (CollUtil.isEmpty(definitionInfoList)) {
            return Collections.emptyList();
        }
        // 3. éåŽ†èŽ·å–æµç¨‹å®šä¹‰è¯¦æƒ…ï¼Œè¿‡æ»¤æ¿€æ´»çŠ¶æ€ï¼Œä¿ç•™æœ€æ–°ç‰ˆæœ¬
        Map<String, ProcessDefinition> latestVersionMap = new HashMap<>();
        for (BpmProcessDefinitionInfoDO info : definitionInfoList) {
            ProcessDefinition pd = bpmProcessDefinitionService.getProcessDefinition(info.getProcessDefinitionId());
            if (pd == null || pd.isSuspended()) {
                continue;
            }
            ProcessDefinition existing = latestVersionMap.get(pd.getKey());
            if (existing == null || pd.getVersion() > existing.getVersion()) {
                latestVersionMap.put(pd.getKey(), pd);
            }
        }
        // 4. è¿”回流程定义列表
        List<Map<String, Object>> result = new ArrayList<>();
        for (ProcessDefinition pd : latestVersionMap.values()) {
            Map<String, Object> item = new HashMap<>();
            item.put("id", pd.getId());
            item.put("key", pd.getKey());
            item.put("name", pd.getName());
            result.add(item);
        }
        return result;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updatePurchaseRequestAuditStatus(Long id, Integer bpmResult) {
        // 1. æ ¡éªŒå­˜åœ¨
        ErpPurchaseRequestDO purchaseRequest = validatePurchaseRequestExists(id);
        if (!ErpPurchaseRequestStatusEnum.PROCESS.getStatus().equals(purchaseRequest.getStatus())) {
            log.warn("[updatePurchaseRequestAuditStatus] é‡‡è´­ç”³è¯·({}) ä¸å¤„于审批中状态", id);
            return;
        }
        // 2. æ ¹æ®å®¡æ‰¹ç»“果更新状态
        Integer newStatus = convertBpmResultToStatus(bpmResult);
        if (newStatus != null) {
            purchaseRequestMapper.updateById(new ErpPurchaseRequestDO()
                    .setId(id).setStatus(newStatus));
        }
        // 3. å®¡æ‰¹é€šè¿‡ä¸”有供应商,自动生成采购订单
        if (ErpPurchaseRequestStatusEnum.APPROVE.getStatus().equals(newStatus)
                && purchaseRequest.getSupplierId() != null
                && purchaseRequest.getOrderId() == null) {
            try {
                Long orderId = doGeneratePurchaseOrder(purchaseRequest);
                log.info("[updatePurchaseRequestAuditStatus] é‡‡è´­ç”³è¯·({}) è‡ªåŠ¨ç”Ÿæˆé‡‡è´­è®¢å•({})", id, orderId);
            } catch (Exception e) {
                log.error("[updatePurchaseRequestAuditStatus] é‡‡è´­ç”³è¯·({}) è‡ªåŠ¨ç”Ÿæˆé‡‡è´­è®¢å•å¤±è´¥", id, e);
                // ä¸æŠ›å‡ºå¼‚常,避免影响状态更新
            }
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long generatePurchaseOrder(Long id, Long supplierId) {
        // 1. æ ¡éªŒå­˜åœ¨ + å®¡æ ¸é€šè¿‡çŠ¶æ€
        ErpPurchaseRequestDO purchaseRequest = validatePurchaseRequestExists(id);
        if (!ErpPurchaseRequestStatusEnum.APPROVE.getStatus().equals(purchaseRequest.getStatus())) {
            throw exception(PURCHASE_REQUEST_NOT_APPROVE);
        }
        // 2. æ ¡éªŒæ˜¯å¦å·²ç”Ÿæˆè®¢å•
        if (purchaseRequest.getOrderId() != null) {
            throw exception(PURCHASE_REQUEST_ORDER_EXISTS);
        }
        // 3. æ ¡éªŒä¾›åº”商
        supplierService.validateSupplier(supplierId);
        // 4. è®¾ç½®ä¾›åº”商并生成订单
        purchaseRequest.setSupplierId(supplierId);
        return doGeneratePurchaseOrder(purchaseRequest);
    }
    /**
     * æ‰§è¡Œç”Ÿæˆé‡‡è´­è®¢å•
     *
     * @param purchaseRequest é‡‡è´­ç”³è¯·ï¼ˆéœ€åŒ…含 supplierId)
     * @return é‡‡è´­è®¢å• ID
     */
    private Long doGeneratePurchaseOrder(ErpPurchaseRequestDO purchaseRequest) {
        Long id = purchaseRequest.getId();
        // 1. èŽ·å–ç”³è¯·æ˜Žç»†
        List<ErpPurchaseRequestItemDO> requestItems = purchaseRequestItemMapper.selectListByRequestId(id);
        if (CollUtil.isEmpty(requestItems)) {
            throw exception(PURCHASE_REQUEST_NOT_EXISTS);
        }
        // 2. ç”Ÿæˆé‡‡è´­è®¢å•号
        String orderNo = noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_ORDER_NO_PREFIX);
        if (purchaseOrderMapper.selectByNo(orderNo) != null) {
            throw exception(PURCHASE_ORDER_NO_EXISTS);
        }
        // 3. åˆ›å»ºé‡‡è´­è®¢å•
        ErpPurchaseOrderDO purchaseOrder = new ErpPurchaseOrderDO();
        purchaseOrder.setNo(orderNo);
        purchaseOrder.setStatus(ErpAuditStatus.APPROVE.getStatus()); // é‡‡è´­è®¢å•直接为已审核状态
        purchaseOrder.setSupplierId(purchaseRequest.getSupplierId());
        purchaseOrder.setOrderTime(purchaseRequest.getRequestTime());
        purchaseOrder.setTotalCount(purchaseRequest.getTotalCount());
        purchaseOrder.setTotalPrice(purchaseRequest.getTotalPrice());
        purchaseOrder.setTotalProductPrice(purchaseRequest.getTotalProductPrice());
        purchaseOrder.setTotalTaxPrice(purchaseRequest.getTotalTaxPrice());
        purchaseOrder.setDiscountPercent(purchaseRequest.getDiscountPercent());
        purchaseOrder.setDiscountPrice(purchaseRequest.getDiscountPrice());
        purchaseOrder.setDepositPrice(purchaseRequest.getDepositPrice() != null ? purchaseRequest.getDepositPrice() : BigDecimal.ZERO);
        purchaseOrder.setRemark(purchaseRequest.getRemark());
        purchaseOrder.setInCount(BigDecimal.ZERO);
        purchaseOrder.setInStatus(0);
        purchaseOrderMapper.insert(purchaseOrder);
        // 4. åˆ›å»ºé‡‡è´­è®¢å•明细
        List<ErpPurchaseOrderItemDO> orderItems = convertList(requestItems, item -> {
            ErpPurchaseOrderItemDO orderItem = new ErpPurchaseOrderItemDO();
            orderItem.setOrderId(purchaseOrder.getId());
            orderItem.setRequestItemId(item.getId()); // å…³è”采购申请明细
            orderItem.setProductId(item.getProductId());
            orderItem.setProductUnitId(item.getProductUnitId());
            orderItem.setProductPrice(item.getProductPrice());
            orderItem.setCount(item.getCount());
            orderItem.setTotalPrice(item.getTotalPrice());
            orderItem.setTaxPercent(item.getTaxPercent());
            orderItem.setTaxPrice(item.getTaxPrice());
            orderItem.setRemark(item.getRemark());
            orderItem.setInCount(BigDecimal.ZERO);
            orderItem.setQcCheckFlag(item.getQcCheckFlag() != null ? item.getQcCheckFlag() : false);
            return orderItem;
        });
        purchaseOrderItemMapper.insertBatch(orderItems);
        // 5. æ›´æ–°é‡‡è´­ç”³è¯·çš„订单信息
        purchaseRequestMapper.updateById(new ErpPurchaseRequestDO()
                .setId(id)
                .setOrderId(purchaseOrder.getId())
                .setOrderNo(orderNo));
        return purchaseOrder.getId();
    }
    /**
     * æ ¡éªŒé‡‡è´­ç”³è¯·å®¡æ‰¹åˆ†ç±»å’Œæµç¨‹å®šä¹‰æ˜¯å¦å­˜åœ¨
     */
    private void validatePurchaseRequestApproveCategoryAndProcessDefinition() {
        List<BpmCategoryDO> categories = bpmCategoryService.getCategoryListByCode(
                Collections.singletonList(PURCHASE_REQUEST_APPROVE_CATEGORY_CODE));
        if (CollUtil.isEmpty(categories)) {
            throw exception(PURCHASE_REQUEST_BPM_CATEGORY_NOT_EXISTS);
        }
    }
    /**
     * è½¬æ¢ BPM å®¡æ‰¹ç»“果为状态
     */
    private Integer convertBpmResultToStatus(Integer bpmResult) {
        if (BpmTaskStatusEnum.APPROVE.getStatus().equals(bpmResult)) {
            return ErpPurchaseRequestStatusEnum.APPROVE.getStatus();
        } else if (BpmTaskStatusEnum.REJECT.getStatus().equals(bpmResult)) {
            return ErpPurchaseRequestStatusEnum.REJECT.getStatus();
        } else if (BpmTaskStatusEnum.CANCEL.getStatus().equals(bpmResult)) {
            return ErpPurchaseRequestStatusEnum.CANCEL.getStatus();
        }
        return null;
    }
    private ErpPurchaseRequestDO validatePurchaseRequestExists(Long id) {
        ErpPurchaseRequestDO purchaseRequest = purchaseRequestMapper.selectById(id);
        if (purchaseRequest == null) {
            throw exception(PURCHASE_REQUEST_NOT_EXISTS);
        }
        return purchaseRequest;
    }
    private ErpPurchaseRequestDO validatePurchaseRequestExistsAndDraft(Long id) {
        ErpPurchaseRequestDO purchaseRequest = validatePurchaseRequestExists(id);
        if (!ErpPurchaseRequestStatusEnum.DRAFT.getStatus().equals(purchaseRequest.getStatus())) {
            throw exception(PURCHASE_REQUEST_UPDATE_FAIL_NOT_DRAFT, purchaseRequest.getNo());
        }
        return purchaseRequest;
    }
    private String getCurrentUserId() {
        return cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId().toString();
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/listener/ErpPurchaseRequestStatusListener.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
package cn.iocoder.yudao.module.erp.service.purchase.listener;
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
import cn.iocoder.yudao.module.erp.service.purchase.ErpPurchaseRequestService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
 * é‡‡è´­ç”³è¯·å®¡æ‰¹çŠ¶æ€ç›‘å¬å™¨
 *
 * ç›‘听 BPM æµç¨‹å®žä¾‹çŠ¶æ€å˜æ›´äº‹ä»¶ï¼Œæ›´æ–°é‡‡è´­ç”³è¯·çš„å®¡æ‰¹çŠ¶æ€
 */
@Component
@Slf4j
public class ErpPurchaseRequestStatusListener extends cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEventListener {
    /**
     * é‡‡è´­ç”³è¯·å®¡æ‰¹æµç¨‹å®šä¹‰ Key
     */
    private static final String PROCESS_DEFINITION_KEY = "purchase_request_approve";
    @Resource
    private ErpPurchaseRequestService purchaseRequestService;
    @Override
    protected String getProcessDefinitionKey() {
        return PROCESS_DEFINITION_KEY;
    }
    @Override
    protected void onEvent(BpmProcessInstanceStatusEvent event) {
        Long requestId;
        try {
            requestId = Long.parseLong(event.getBusinessKey());
        } catch (NumberFormatException e) {
            log.warn("[onEvent] businessKey({}) ä¸æ˜¯æœ‰æ•ˆçš„采购申请编号", event.getBusinessKey());
            return;
        }
        log.info("[onEvent] æ”¶åˆ°BPM审批事件,requestId={}, status={}", requestId, event.getStatus());
        try {
            purchaseRequestService.updatePurchaseRequestAuditStatus(requestId, event.getStatus());
            log.info("[onEvent] é‡‡è´­ç”³è¯·({}) å®¡æ‰¹çŠ¶æ€æ›´æ–°å®Œæˆ", requestId);
        } catch (Exception e) {
            log.error("[onEvent] æ›´æ–°é‡‡è´­ç”³è¯·å®¡æ‰¹çŠ¶æ€å¤±è´¥ï¼ŒrequestId={}", requestId, e);
        }
    }
}
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/arrivalnotice/MesArrivalNoticeApi.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package cn.iocoder.yudao.module.mes.api.arrivalnotice;
/**
 * MES åˆ°è´§é€šçŸ¥å• API æŽ¥å£
 */
public interface MesArrivalNoticeApi {
    /**
     * æ ¹æ®é‡‡è´­è®¢å•生成到货通知单(待入库状态)
     *
     * @param purchaseOrderId é‡‡è´­è®¢å•编号
     * @return ç”Ÿæˆçš„到货通知单编号
     */
    Long generateArrivalNoticeFromPurchaseOrder(Long purchaseOrderId);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/arrivalnotice/MesArrivalNoticeApiImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
package cn.iocoder.yudao.module.mes.api.arrivalnotice;
import cn.iocoder.yudao.module.mes.service.wm.arrivalnotice.MesWmArrivalNoticeService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
/**
 * MES åˆ°è´§é€šçŸ¥å• API å®žçŽ°ç±»
 */
@Service
public class MesArrivalNoticeApiImpl implements MesArrivalNoticeApi {
    @Resource
    private MesWmArrivalNoticeService arrivalNoticeService;
    @Override
    public Long generateArrivalNoticeFromPurchaseOrder(Long purchaseOrderId) {
        return arrivalNoticeService.generateArrivalNoticeFromPurchaseOrder(purchaseOrderId);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/arrivalnotice/MesWmArrivalNoticeController.java
@@ -11,12 +11,12 @@
import cn.iocoder.yudao.module.mes.controller.admin.wm.arrivalnotice.vo.MesWmArrivalNoticePageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.arrivalnotice.vo.MesWmArrivalNoticeRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.arrivalnotice.vo.MesWmArrivalNoticeSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.vendor.MesMdVendorDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.arrivalnotice.MesWmArrivalNoticeDO;
import cn.iocoder.yudao.module.mes.service.md.vendor.MesMdVendorService;
import cn.iocoder.yudao.module.mes.service.wm.arrivalnotice.MesWmArrivalNoticeService;
import cn.iocoder.yudao.module.erp.api.purchase.ErpPurchaseOrderApi;
import cn.iocoder.yudao.module.erp.api.purchase.ErpSupplierApi;
import cn.iocoder.yudao.module.erp.api.purchase.dto.ErpPurchaseOrderRespDTO;
import cn.iocoder.yudao.module.erp.api.purchase.dto.ErpSupplierRespDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -45,7 +45,7 @@
    @Resource
    private MesWmArrivalNoticeService arrivalNoticeService;
    @Resource
    private MesMdVendorService vendorService;
    private ErpSupplierApi supplierApi;
    @Resource
    private ErpPurchaseOrderApi purchaseOrderApi;
@@ -129,9 +129,10 @@
        if (CollUtil.isEmpty(list)) {
            return Collections.emptyList();
        }
        // 1. èŽ·å¾—å…³è”æ•°æ®
        Map<Long, MesMdVendorDO> vendorMap = vendorService.getVendorMap(
                convertSet(list, MesWmArrivalNoticeDO::getVendorId));
        // 1. èŽ·å¾—ä¾›åº”å•†ä¿¡æ¯ï¼ˆERP é‡‡è´­ä¾›åº”商)
        Map<Long, ErpSupplierRespDTO> supplierMap = supplierApi.getSupplierList(
                convertSet(list, MesWmArrivalNoticeDO::getVendorId)).getCheckedData().stream()
                .collect(java.util.stream.Collectors.toMap(ErpSupplierRespDTO::getId, v -> v, (a, b) -> a));
        // 2. èŽ·å¾—é‡‡è´­è®¢å•ä¿¡æ¯
        List<Long> purchaseOrderIds = list.stream()
                .map(MesWmArrivalNoticeDO::getPurchaseOrderId)
@@ -143,8 +144,8 @@
                .collect(java.util.stream.Collectors.toMap(ErpPurchaseOrderRespDTO::getId, v -> v, (a, b) -> a));
        // 3. æž„建结果
        return BeanUtils.toBean(list, MesWmArrivalNoticeRespVO.class, vo -> {
            MapUtils.findAndThen(vendorMap, vo.getVendorId(), vendor ->
                    vo.setVendorCode(vendor.getCode()).setVendorName(vendor.getName()));
            MapUtils.findAndThen(supplierMap, vo.getVendorId(), supplier ->
                    vo.setVendorName(supplier.getName()));
            MapUtils.findAndThen(purchaseOrderMap, vo.getPurchaseOrderId(), purchaseOrder ->
                    vo.setPurchaseOrderNo(purchaseOrder.getNo()));
        });
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/itemreceipt/MesWmItemReceiptController.java
@@ -11,14 +11,14 @@
import cn.iocoder.yudao.module.mes.controller.admin.wm.itemreceipt.vo.MesWmItemReceiptPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.itemreceipt.vo.MesWmItemReceiptRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.itemreceipt.vo.MesWmItemReceiptSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.vendor.MesMdVendorDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.iqc.MesQcIqcDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.arrivalnotice.MesWmArrivalNoticeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemreceipt.MesWmItemReceiptDO;
import cn.iocoder.yudao.module.mes.service.md.vendor.MesMdVendorService;
import cn.iocoder.yudao.module.mes.service.qc.iqc.MesQcIqcService;
import cn.iocoder.yudao.module.mes.service.wm.arrivalnotice.MesWmArrivalNoticeService;
import cn.iocoder.yudao.module.mes.service.wm.itemreceipt.MesWmItemReceiptService;
import cn.iocoder.yudao.module.erp.api.purchase.ErpSupplierApi;
import cn.iocoder.yudao.module.erp.api.purchase.dto.ErpSupplierRespDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -47,7 +47,7 @@
    @Resource
    private MesWmItemReceiptService itemReceiptService;
    @Resource
    private MesMdVendorService vendorService;
    private ErpSupplierApi supplierApi;
    @Resource
    private MesQcIqcService iqcService;
    @Resource
@@ -171,17 +171,18 @@
        if (CollUtil.isEmpty(list)) {
            return Collections.emptyList();
        }
        // 1. èŽ·å¾—å…³è”æ•°æ®
        Map<Long, MesMdVendorDO> vendorMap = vendorService.getVendorMap(
                convertSet(list, MesWmItemReceiptDO::getVendorId));
        // 1. èŽ·å¾—å…³è”æ•°æ®ï¼ˆé‡‡è´­å…¥åº“ä½¿ç”¨ ERP ä¾›åº”商)
        Map<Long, ErpSupplierRespDTO> supplierMap = supplierApi.getSupplierList(
                convertSet(list, MesWmItemReceiptDO::getVendorId)).getCheckedData().stream()
                .collect(java.util.stream.Collectors.toMap(ErpSupplierRespDTO::getId, v -> v, (a, b) -> a));
        Map<Long, MesQcIqcDO> iqcMap = iqcService.getIqcMap(
                convertSet(list, MesWmItemReceiptDO::getIqcId));
        Map<Long, MesWmArrivalNoticeDO> noticeMap = arrivalNoticeService.getArrivalNoticeMap(
                convertSet(list, MesWmItemReceiptDO::getNoticeId));
        // 2. æž„建结果
        return BeanUtils.toBean(list, MesWmItemReceiptRespVO.class, vo -> {
            MapUtils.findAndThen(vendorMap, vo.getVendorId(),
                    vendor -> vo.setVendorName(vendor.getName()));
            MapUtils.findAndThen(supplierMap, vo.getVendorId(),
                    supplier -> vo.setVendorName(supplier.getName()));
            MapUtils.findAndThen(iqcMap, vo.getIqcId(),
                    iqc -> vo.setIqcCode(iqc.getCode()));
            MapUtils.findAndThen(noticeMap, vo.getNoticeId(), notice ->
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/arrivalnotice/MesWmArrivalNoticeLineDO.java
@@ -64,6 +64,15 @@
     * å…³è” {@link MesQcIqcDO#getId()}
     */
    private Long iqcId;
    /**
     * è´¨æ£€ç»“æžœ
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.module.mes.enums.qc.MesQcCheckResultEnum}
     * 1-合格 2-特采 3-不合格退货 4-不合格报废
     */
    private Integer iqcCheckResult;
    /**
     * å¤‡æ³¨
     */
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/arrivalnotice/MesWmArrivalNoticeMapper.java
@@ -32,4 +32,8 @@
        return selectCount(MesWmArrivalNoticeDO::getVendorId, vendorId);
    }
    default MesWmArrivalNoticeDO selectByPurchaseOrderId(Long purchaseOrderId) {
        return selectOne(MesWmArrivalNoticeDO::getPurchaseOrderId, purchaseOrderId);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/itemreceipt/MesWmItemReceiptMapper.java
@@ -36,4 +36,8 @@
        return selectList(MesWmItemReceiptDO::getVendorId, vendorId);
    }
    default MesWmItemReceiptDO selectByNoticeId(Long noticeId) {
        return selectOne(MesWmItemReceiptDO::getNoticeId, noticeId);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/productsales/MesWmProductSalesMapper.java
@@ -31,6 +31,10 @@
        return selectOne(MesWmProductSalesDO::getCode, code);
    }
    default MesWmProductSalesDO selectByNoticeId(Long noticeId) {
        return selectOne(MesWmProductSalesDO::getNoticeId, noticeId);
    }
    default List<MesWmProductSalesDO> selectListByClientId(Long clientId) {
        return selectList(MesWmProductSalesDO::getClientId, clientId);
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/salesnotice/MesWmSalesNoticeMapper.java
@@ -27,4 +27,8 @@
        return selectOne(MesWmSalesNoticeDO::getCode, code);
    }
    default MesWmSalesNoticeDO selectBySaleOrderId(Long saleOrderId) {
        return selectOne(MesWmSalesNoticeDO::getSaleOrderId, saleOrderId);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -348,6 +348,7 @@
    ErrorCode QC_IQC_NOT_PREPARE = new ErrorCode(1_040_603_002, "只有草稿状态的检验单才可操作");
    ErrorCode QC_IQC_QUANTITY_MISMATCH = new ErrorCode(1_040_603_004, "合格品与不合格品数量之和须等于检测数量");
    ErrorCode QC_NO_TEMPLATE = new ErrorCode(1_040_603_005, "当前产品未配置检测模板");
    ErrorCode QC_NO_TEMPLATE_FOR_ITEM = new ErrorCode(1_040_603_008, "物料【{}】未配置 IQC æ£€æµ‹æ¨¡æ¿ï¼Œè¯·å…ˆåœ¨è´¨é‡ç®¡ç†ä¸­é…ç½®");
    ErrorCode QC_IQC_SOURCE_DOC_PARAMS_MISSING = new ErrorCode(1_040_603_006, "来源单据类型非空时,来源单据 ID å’Œæ¥æºå•据行 ID ä¸èƒ½ä¸ºç©º");
    ErrorCode QC_IQC_CHECK_RESULT_EMPTY = new ErrorCode(1_040_603_007, "完成检验单前,检测结果必须填写");
    // ========== MES è´¨é‡ç®¡ç†-来料检验行(1-040-603-100) ==========
@@ -458,6 +459,7 @@
    ErrorCode WM_ARRIVAL_NOTICE_IQC_PENDING = new ErrorCode(1_040_704_005, "存在待检验行,无法审批通过");
    ErrorCode WM_ARRIVAL_NOTICE_NO_LINE = new ErrorCode(1_040_704_006, "至少需要一条行项目");
    ErrorCode WM_ARRIVAL_NOTICE_VENDOR_MISMATCH = new ErrorCode(1_040_704_007, "到货通知单的供应商与当前单据不一致");
    ErrorCode WM_ARRIVAL_NOTICE_ALREADY_EXISTS = new ErrorCode(1_040_704_008, "该采购订单已生成到货通知单,不能重复生成");
    ErrorCode WM_ARRIVAL_NOTICE_LINE_NOT_EXISTS = new ErrorCode(1_040_704_100, "到货通知单行不存在");
    ErrorCode WM_ARRIVAL_NOTICE_LINE_NOT_MATCH = new ErrorCode(1_040_704_101, "到货通知单行不属于指定的到货通知单");
@@ -469,7 +471,8 @@
    ErrorCode WM_ITEM_RECEIPT_DETAIL_QUANTITY_MISMATCH = new ErrorCode(1_040_705_004, "明细上架总数与行入库数量不匹配");
    ErrorCode WM_ITEM_RECEIPT_STATUS_ERROR = new ErrorCode(1_040_705_005, "入库单状态不正确");
    ErrorCode WM_ITEM_RECEIPT_CANCEL_NOT_ALLOWED = new ErrorCode(1_040_705_006, "已完成或已取消的入库单不允许取消");
    ErrorCode WM_PURCHASE_ORDER_NOT_EXISTS = new ErrorCode(1_040_705_007, "采购订单不存在");
    ErrorCode WM_ITEM_RECEIPT_ALREADY_EXISTS = new ErrorCode(1_040_705_007, "该到货通知单已生成入库单,不能重复生成");
    ErrorCode WM_PURCHASE_ORDER_NOT_EXISTS = new ErrorCode(1_040_705_008, "采购订单不存在");
    ErrorCode WM_PURCHASE_ORDER_NO_ITEM = new ErrorCode(1_040_705_008, "采购订单无明细数据");
    ErrorCode WM_ITEM_RECEIPT_LINE_NOT_EXISTS = new ErrorCode(1_040_705_100, "采购入库单行不存在");
    ErrorCode WM_ITEM_RECEIPT_LINE_ARRIVAL_NOTICE_LINE_REQUIRED = new ErrorCode(1_040_705_101,
@@ -478,6 +481,7 @@
            "入库单未关联到货通知单,不能选择到货通知单行");
    ErrorCode WM_ITEM_RECEIPT_DETAIL_NOT_EXISTS = new ErrorCode(1_040_705_200, "采购入库明细不存在");
    ErrorCode WM_ITEM_RECEIPT_DETAIL_QUANTITY_EXCEED = new ErrorCode(1_040_705_202, "上架明细总数量不能超过行入库数量");
    ErrorCode WM_ITEM_RECEIPT_BATCH_REQUIRED = new ErrorCode(1_040_705_203, "物料【{}-{}】已开启批次管理,上架明细的批次号不能为空");
    // ========== MES ä»“库管理-领料申请单(1-040-706-000) ==========
    ErrorCode WM_MATERIAL_REQUEST_NOT_EXISTS = new ErrorCode(1_040_706_000, "领料申请单不存在");
@@ -643,8 +647,9 @@
    ErrorCode WM_PRODUCT_SALES_CANNOT_FINISH = new ErrorCode(1_040_714_007, "当前状态不允许执行出库");
    ErrorCode WM_PRODUCT_SALES_CANNOT_CANCEL = new ErrorCode(1_040_714_008, "当前状态不允许取消");
    ErrorCode WM_PRODUCT_SALES_CANNOT_CONFIRM = new ErrorCode(1_040_714_009, "当前状态不允许确认检验通过");
    ErrorCode WM_PRODUCT_SALES_ALREADY_EXISTS = new ErrorCode(1_040_714_010, "该发货通知单已生成销售出库单,不能重复生成");
    ErrorCode WM_PRODUCT_SALES_DETAILS_EMPTY = new ErrorCode(1_040_714_020, "拣货明细不能为空");
    ErrorCode WM_PRODUCT_SALES_DETAIL_QUANTITY_MISMATCH = new ErrorCode(1_040_714_010, "拣货数量与出库数量不匹配");
    ErrorCode WM_PRODUCT_SALES_DETAIL_QUANTITY_MISMATCH = new ErrorCode(1_040_714_011, "拣货数量与出库数量不匹配");
    ErrorCode WM_PRODUCT_SALES_LINE_NOT_EXISTS = new ErrorCode(1_040_714_011, "销售出库单行不存在");
    ErrorCode WM_PRODUCT_SALES_DETAIL_NOT_EXISTS = new ErrorCode(1_040_714_012, "销售出库明细不存在");
    ErrorCode WM_PRODUCT_SALES_STOCK_INSUFFICIENT = new ErrorCode(1_040_714_013, "库存不足,无法拣货");
@@ -698,6 +703,7 @@
    ErrorCode WM_SALES_NOTICE_LINE_NOT_MATCH = new ErrorCode(1_040_720_012, "发货通知单行不属于指定的发货通知单");
    ErrorCode WM_SALES_NOTICE_SALE_ORDER_NOT_EXISTS = new ErrorCode(1_040_720_013, "销售订单不存在");
    ErrorCode WM_SALES_NOTICE_SALE_ORDER_ITEM_EMPTY = new ErrorCode(1_040_720_014, "销售订单明细为空");
    ErrorCode WM_SALES_NOTICE_ALREADY_EXISTS = new ErrorCode(1_040_720_015, "该销售订单已生成发货通知单,不能重复生成");
    // ========== MES ä»“库管理-条码配置(1-040-730-000) ==========
    ErrorCode WM_BARCODE_CONFIG_NOT_EXISTS = new ErrorCode(1_040_730_000, "条码配置不存在");
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/qc/MesQcCheckResultEnum.java
@@ -8,15 +8,15 @@
/**
 * MES æ£€æµ‹ç»“果枚举
 *
 * @author èŠ‹é“æºç 
 */
@Getter
@AllArgsConstructor
public enum MesQcCheckResultEnum implements ArrayValuable<Integer> {
    PASS(1, "检验通过"),
    FAIL(2, "检验不通过");
    QUALIFIED(1, "合格"),
    SPECIAL_APPROVAL(2, "特采"),
    RETURN(3, "不合格退货"),
    SCRAP(4, "不合格报废");
    public static final Integer[] ARRAYS = Arrays.stream(values()).map(MesQcCheckResultEnum::getType).toArray(Integer[]::new);
@@ -34,4 +34,14 @@
        return ARRAYS;
    }
    /**
     * åˆ¤æ–­æ˜¯å¦å¯ä»¥å…¥åº“
     *
     * @param type æ£€æµ‹ç»“æžœ
     * @return true å¯ä»¥å…¥åº“
     */
    public static boolean canInbound(Integer type) {
        return QUALIFIED.getType().equals(type) || SPECIAL_APPROVAL.getType().equals(type);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/iqc/MesQcIqcService.java
@@ -5,6 +5,7 @@
import cn.iocoder.yudao.module.mes.controller.admin.qc.iqc.vo.MesQcIqcSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.defectrecord.MesQcDefectRecordDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.iqc.MesQcIqcDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.arrivalnotice.MesWmArrivalNoticeLineDO;
import jakarta.validation.Valid;
import java.util.Collection;
@@ -95,4 +96,15 @@
     */
    Long getIqcCountByVendorId(Long vendorId);
    /**
     * æ ¹æ®åˆ°è´§é€šçŸ¥å•行自动创建来料检验单
     *
     * @param noticeId        åˆ°è´§é€šçŸ¥å• ID
     * @param line            åˆ°è´§é€šçŸ¥å•行
     * @param noticeCode      åˆ°è´§é€šçŸ¥å•编号
     * @param vendorId        ä¾›åº”商 ID
     * @return IQC æ£€éªŒå• ID
     */
    Long createIqcFromArrivalNotice(Long noticeId, MesWmArrivalNoticeLineDO line, String noticeCode, Long vendorId);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/iqc/MesQcIqcServiceImpl.java
@@ -10,6 +10,7 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.defectrecord.MesQcDefectRecordDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.iqc.MesQcIqcDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.arrivalnotice.MesWmArrivalNoticeLineDO;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.iqc.MesQcIqcMapper;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcDefectLevelEnum;
@@ -22,6 +23,8 @@
import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateItemService;
import cn.iocoder.yudao.module.mes.service.wm.arrivalnotice.MesWmArrivalNoticeService;
import cn.iocoder.yudao.module.mes.service.wm.outsourcereceipt.MesWmOutsourceReceiptService;
import cn.iocoder.yudao.module.erp.api.purchase.ErpSupplierApi;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
@@ -71,6 +74,13 @@
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private MdmItemApi mdmItemApi;
    @Resource
    @Lazy
    private ErpSupplierApi supplierApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createIqc(MesQcIqcSaveReqVO createReqVO) {
@@ -116,9 +126,20 @@
    private void validateIqcSaveData(MesQcIqcSaveReqVO reqVO) {
        // æ ¡éªŒç¼–号唯一
        validateIqcCodeUnique(reqVO.getId(), reqVO.getCode());
        // æ ¡éªŒä¾›åº”商、物料、检测人员存在
        vendorService.validateVendorExists(reqVO.getVendorId());
        itemService.validateItemExists(reqVO.getItemId());
        // æ ¡éªŒä¾›åº”商存在(优先校验 ERP ä¾›åº”商,失败则校验 MDM ä¾›åº”商)
        if (reqVO.getVendorId() != null) {
            try {
                supplierApi.validateSupplier(reqVO.getVendorId());
            } catch (Exception e) {
                // ERP ä¾›åº”商校验失败,尝试 MDM ä¾›åº”商
                vendorService.validateVendorExists(reqVO.getVendorId());
            }
        }
        // æ ¡éªŒç‰©æ–™å­˜åœ¨ï¼ˆä½¿ç”¨ MDM ç‰©æ–™ï¼‰
        if (reqVO.getItemId() != null) {
            mdmItemApi.getItem(reqVO.getItemId()).getCheckedData();
        }
        // æ ¡éªŒæ£€æµ‹äººå‘˜å­˜åœ¨
        adminUserApi.validateUser(reqVO.getInspectorUserId());
    }
@@ -160,7 +181,7 @@
        // æ ¹æ®ä¸åŒçš„æ¥æºå•据类型,回写不同的单据
        if (Objects.equals(iqc.getSourceDocType(), MesBizTypeConstants.WM_ARRIVAL_NOTICE)) {
            arrivalNoticeService.updateArrivalNoticeWhenIqcFinish(iqc.getSourceDocId(), iqc.getSourceLineId(),
                    iqc.getId(), iqc.getQualifiedQuantity());
                    iqc.getId(), iqc.getCheckResult(), iqc.getQualifiedQuantity());
        } else if (Objects.equals(iqc.getSourceDocType(), MesBizTypeConstants.WM_OUTSOURCE_RECPT)) {
            outsourceReceiptService.updateOutsourceReceiptWhenIqcFinish(iqc.getSourceDocId(), iqc.getSourceLineId(),
                    iqc.getId(), iqc.getQualifiedQuantity(), iqc.getUnqualifiedQuantity());
@@ -308,4 +329,65 @@
        return iqcMapper.selectCountByVendorId(vendorId);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createIqcFromArrivalNotice(Long noticeId, MesWmArrivalNoticeLineDO line, String noticeCode, Long vendorId) {
        // 1. æ ¡éªŒç‰©æ–™æ˜¯å¦é…ç½®äº† IQC æ£€éªŒæ¨¡æ¿
        MesQcTemplateItemDO templateItem;
        try {
            templateItem = templateItemService.getRequiredTemplateByItemIdAndType(
                    line.getItemId(), MesQcTypeEnum.IQC.getType());
        } catch (Exception e) {
            // èŽ·å–ç‰©æ–™ä¿¡æ¯ï¼Œæä¾›æ›´æ˜Žç¡®çš„é”™è¯¯æç¤º
            String itemInfo = getItemInfo(line.getItemId());
            throw exception(QC_NO_TEMPLATE_FOR_ITEM, itemInfo);
        }
        Long templateId = templateItem.getTemplateId();
        // 2. ç”Ÿæˆæ£€éªŒå•编码
        String code = "IQC" + System.currentTimeMillis();
        // 3. åˆ›å»º IQC ä¸»è¡¨ï¼ˆå¾…检验状态,初始合格数量为 0)
        MesQcIqcDO iqc = new MesQcIqcDO();
        iqc.setCode(code);
        iqc.setName(noticeCode + "-物料检验");
        iqc.setTemplateId(templateId);
        iqc.setSourceDocType(MesBizTypeConstants.WM_ARRIVAL_NOTICE);
        iqc.setSourceDocId(noticeId);
        iqc.setSourceLineId(line.getId());
        iqc.setSourceDocCode(noticeCode);
        iqc.setVendorId(vendorId);
        iqc.setItemId(line.getItemId());
        iqc.setReceivedQuantity(line.getArrivalQuantity());
        iqc.setCheckQuantity(BigDecimal.ZERO); // å¾…填写
        iqc.setQualifiedQuantity(BigDecimal.ZERO); // å¾…填写
        iqc.setUnqualifiedQuantity(BigDecimal.ZERO); // å¾…填写
        iqc.setReceiveDate(java.time.LocalDateTime.now());
        iqc.setStatus(MesQcStatusEnum.DRAFT.getStatus());
        iqcMapper.insert(iqc);
        // 4. ä»Žæ¨¡æ¿æŒ‡æ ‡è‡ªåŠ¨ç”Ÿæˆæ£€éªŒè¡Œ
        iqcLineService.createLinesFromTemplate(iqc.getId(), templateId);
        return iqc.getId();
    }
    /**
     * èŽ·å–ç‰©æ–™ä¿¡æ¯ï¼ˆç¼–ç +名称)
     */
    private String getItemInfo(Long itemId) {
        if (itemId == null) {
            return "未知物料";
        }
        try {
            cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO item = mdmItemApi.getItem(itemId).getCheckedData();
            if (item != null) {
                return item.getCode() + "-" + item.getName();
            }
        } catch (Exception e) {
            // å¿½ç•¥å¼‚常
        }
        return "物料ID:" + itemId;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/arrivalnotice/MesWmArrivalNoticeLineService.java
@@ -72,9 +72,10 @@
     *
     * @param lineId è¡Œç¼–号
     * @param iqcId IQC æ£€éªŒå•编号
     * @param checkResult è´¨æ£€ç»“果(1合格 2特采 3不合格退货 4不合格报废)
     * @param qualifiedQuantity åˆæ ¼æ•°é‡
     */
    void updateArrivalNoticeLineWhenIqcFinish(Long lineId, Long iqcId, BigDecimal qualifiedQuantity);
    void updateArrivalNoticeLineWhenIqcFinish(Long lineId, Long iqcId, Integer checkResult, BigDecimal qualifiedQuantity);
    /**
     * æ ¡éªŒåˆ°è´§é€šçŸ¥å•行存在
@@ -100,4 +101,12 @@
     */
    void createArrivalNoticeLineFromPurchaseOrderItem(MesWmArrivalNoticeLineDO line);
    /**
     * æ›´æ–°åˆ°è´§é€šçŸ¥å•行的 IQC æ£€éªŒå• ID
     *
     * @param lineId è¡Œç¼–号
     * @param iqcId  IQC æ£€éªŒå•编号
     */
    void updateArrivalNoticeLineIqcId(Long lineId, Long iqcId);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/arrivalnotice/MesWmArrivalNoticeLineServiceImpl.java
@@ -112,12 +112,12 @@
    }
    @Override
    public void updateArrivalNoticeLineWhenIqcFinish(Long lineId, Long iqcId, BigDecimal qualifiedQuantity) {
    public void updateArrivalNoticeLineWhenIqcFinish(Long lineId, Long iqcId, Integer checkResult, BigDecimal qualifiedQuantity) {
        // æ ¡éªŒè¡Œå­˜åœ¨
        validateArrivalNoticeLineExists(lineId);
        // æ›´æ–°
        // æ›´æ–°ï¼šç»‘定 iqcId + è´¨æ£€ç»“æžœ + åˆæ ¼æ•°é‡
        arrivalNoticeLineMapper.updateById(new MesWmArrivalNoticeLineDO()
                .setId(lineId).setIqcId(iqcId).setQualifiedQuantity(qualifiedQuantity));
                .setId(lineId).setIqcId(iqcId).setIqcCheckResult(checkResult).setQualifiedQuantity(qualifiedQuantity));
    }
    @Override
@@ -146,4 +146,10 @@
        arrivalNoticeLineMapper.insert(line);
    }
    @Override
    public void updateArrivalNoticeLineIqcId(Long lineId, Long iqcId) {
        arrivalNoticeLineMapper.updateById(new MesWmArrivalNoticeLineDO()
                .setId(lineId).setIqcId(iqcId));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/arrivalnotice/MesWmArrivalNoticeService.java
@@ -69,9 +69,10 @@
     * @param id åˆ°è´§é€šçŸ¥å•编号
     * @param lineId åˆ°è´§é€šçŸ¥å•行编号
     * @param iqcId IQC æ£€éªŒå•编号
     * @param checkResult è´¨æ£€ç»“果(1合格 2特采 3不合格退货 4不合格报废)
     * @param qualifiedQuantity åˆæ ¼æ•°é‡
     */
    void updateArrivalNoticeWhenIqcFinish(Long id, Long lineId, Long iqcId, BigDecimal qualifiedQuantity);
    void updateArrivalNoticeWhenIqcFinish(Long id, Long lineId, Long iqcId, Integer checkResult, BigDecimal qualifiedQuantity);
    /**
     * å®Œæˆåˆ°è´§é€šçŸ¥å•(待入库 â†’ å·²å®Œæˆï¼‰ï¼Œå†…部调用
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/arrivalnotice/MesWmArrivalNoticeServiceImpl.java
@@ -12,11 +12,15 @@
import cn.iocoder.yudao.module.mes.dal.mysql.wm.arrivalnotice.MesWmArrivalNoticeMapper;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmArrivalNoticeStatusEnum;
import cn.iocoder.yudao.module.erp.api.purchase.ErpPurchaseOrderApi;
import cn.iocoder.yudao.module.erp.api.purchase.ErpSupplierApi;
import cn.iocoder.yudao.module.erp.api.purchase.dto.ErpPurchaseOrderRespDTO;
import cn.iocoder.yudao.module.erp.api.purchase.dto.ErpPurchaseOrderItemRespDTO;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
import cn.iocoder.yudao.module.mes.service.qc.iqc.MesQcIqcService;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcCheckResultEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
@@ -42,14 +46,19 @@
    @Resource
    private MesWmArrivalNoticeLineService arrivalNoticeLineService;
    @Resource
    private cn.iocoder.yudao.module.mes.service.md.vendor.MesMdVendorService vendorService;
    private ErpSupplierApi supplierApi;
    @Resource
    private ErpPurchaseOrderApi purchaseOrderApi;
    @Resource
    private MesMdmItemService mdmItemService;
    @Resource
    @Lazy
    private MesQcIqcService iqcService;
    @Override
    public Long createArrivalNotice(MesWmArrivalNoticeSaveReqVO createReqVO) {
@@ -78,8 +87,8 @@
    private void validateArrivalNoticeSaveData(MesWmArrivalNoticeSaveReqVO reqVO) {
        // æ ¡éªŒç¼–码唯一
        validateCodeUnique(reqVO.getId(), reqVO.getCode());
        // æ ¡éªŒä¾›åº”商存在且启用
        vendorService.validateVendorExistsAndEnable(reqVO.getVendorId());
        // æ ¡éªŒä¾›åº”商存在(ERP é‡‡è´­ä¾›åº”商)
        supplierApi.validateSupplier(reqVO.getVendorId());
        // æ ¡éªŒé‡‡è´­è®¢å•存在
        if (reqVO.getPurchaseOrderId() != null) {
            ErpPurchaseOrderRespDTO purchaseOrder = purchaseOrderApi.getPurchaseOrder(reqVO.getPurchaseOrderId()).getCheckedData();
@@ -115,7 +124,7 @@
    @Transactional(rollbackFor = Exception.class)
    public void submitArrivalNotice(Long id) {
        // 1.1 æ ¡éªŒå­˜åœ¨ + è‰ç¨¿çŠ¶æ€
        validateArrivalNoticeExistsAndDraft(id);
        MesWmArrivalNoticeDO notice = validateArrivalNoticeExistsAndDraft(id);
        // 1.2 æ£€æŸ¥æ˜¯å¦æœ‰è¡Œé¡¹ç›®
        List<MesWmArrivalNoticeLineDO> lines = arrivalNoticeLineService.getArrivalNoticeLineListByNoticeId(id);
        if (CollUtil.isEmpty(lines)) {
@@ -126,9 +135,26 @@
        boolean needCheck = CollectionUtils.anyMatch(lines,
                line -> Boolean.TRUE.equals(line.getIqcCheckFlag()));
        if (needCheck) {
            // éœ€è¦æ£€éªŒï¼Œè¿›å…¥å¾…质检
            // 2.1 éœ€è¦æ£€éªŒï¼Œè¿›å…¥å¾…质检
            arrivalNoticeMapper.updateById(new MesWmArrivalNoticeDO()
                    .setId(id).setStatus(MesWmArrivalNoticeStatusEnum.PENDING_QC.getStatus()));
            // 2.2 ä¸ºéœ€è¦è´¨æ£€çš„行自动创建 IQC æ£€éªŒå•
            for (MesWmArrivalNoticeLineDO line : lines) {
                if (Boolean.TRUE.equals(line.getIqcCheckFlag()) && line.getIqcId() == null) {
                    try {
                        Long iqcId = iqcService.createIqcFromArrivalNotice(
                                id, line, notice.getCode(), notice.getVendorId());
                        // æ›´æ–°è¡Œä¸Šçš„ iqcId
                        arrivalNoticeLineService.updateArrivalNoticeLineIqcId(line.getId(), iqcId);
                        log.info("[submitArrivalNotice][到货通知单({}) è¡Œ({}) è‡ªåŠ¨åˆ›å»º IQC æ£€éªŒå•({})]",
                                id, line.getId(), iqcId);
                    } catch (Exception e) {
                        log.warn("[submitArrivalNotice][到货通知单({}) è¡Œ({}) åˆ›å»º IQC å¤±è´¥: {}]",
                                id, line.getId(), e.getMessage());
                        // ç»§ç»­å¤„理其他行,不中断流程
                    }
                }
            }
            return;
        }
        // ä¸éœ€è¦æ£€éªŒï¼Œç›´æŽ¥è¿›å…¥å¾…入库
@@ -138,7 +164,7 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateArrivalNoticeWhenIqcFinish(Long id, Long lineId, Long iqcId, BigDecimal qualifiedQuantity) {
    public void updateArrivalNoticeWhenIqcFinish(Long id, Long lineId, Long iqcId, Integer checkResult, BigDecimal qualifiedQuantity) {
        // 1.1 æ ¡éªŒåˆ°è´§é€šçŸ¥å•存在
        MesWmArrivalNoticeDO notice = validateArrivalNoticeExists(id);
        // 1.2 æ ¡éªŒçŠ¶æ€ä¸ºå¾…è´¨æ£€
@@ -146,18 +172,28 @@
            throw exception(WM_ARRIVAL_NOTICE_STATUS_NOT_PENDING_QC);
        }
        // 2. æ›´æ–°åˆ°è´§é€šçŸ¥å•行:绑定 iqcId + åˆæ ¼æ•°é‡
        arrivalNoticeLineService.updateArrivalNoticeLineWhenIqcFinish(lineId, iqcId, qualifiedQuantity);
        // 2. æ›´æ–°åˆ°è´§é€šçŸ¥å•行:绑定 iqcId + è´¨æ£€ç»“æžœ + åˆæ ¼æ•°é‡
        arrivalNoticeLineService.updateArrivalNoticeLineWhenIqcFinish(lineId, iqcId, checkResult, qualifiedQuantity);
        // 3.1 åˆ¤æ–­æ˜¯å¦æ‰€æœ‰éœ€æ£€è¡Œéƒ½å·²å®Œæˆ
        // 3.1 åˆ¤æ–­æ˜¯å¦æ‰€æœ‰éœ€æ£€è¡Œéƒ½å·²å®Œæˆè´¨æ£€
        List<MesWmArrivalNoticeLineDO> lines = arrivalNoticeLineService.getArrivalNoticeLineListByNoticeId(id);
        boolean hasUnchecked = CollectionUtils.anyMatch(lines,
                line -> Boolean.TRUE.equals(line.getIqcCheckFlag()) && line.getIqcId() == null);
                line -> Boolean.TRUE.equals(line.getIqcCheckFlag()) && line.getIqcCheckResult() == null);
        if (hasUnchecked) {
            log.info("[updateArrivalNoticeWhenIqcFinish][到货通知单({}) è¿˜æœ‰æœªå®Œæˆæ£€éªŒçš„行,暂不更新状态]", id);
            return;
        }
        // 3.2 æ‰€æœ‰è¡Œæ£€éªŒå®Œæˆï¼Œæ›´æ–°ä¸»è¡¨çŠ¶æ€ PENDING_QC â†’ PENDING_RECEIPT
        // 3.2 åˆ¤æ–­æ˜¯å¦æ‰€æœ‰éœ€æ£€è¡Œéƒ½å¯ä»¥å…¥åº“(合格或特采)
        boolean hasNonInbound = CollectionUtils.anyMatch(lines,
                line -> Boolean.TRUE.equals(line.getIqcCheckFlag())
                        && !MesQcCheckResultEnum.canInbound(line.getIqcCheckResult()));
        if (hasNonInbound) {
            log.info("[updateArrivalNoticeWhenIqcFinish][到货通知单({}) å­˜åœ¨ä¸å¯å…¥åº“的行(退货/报废),状态保持待质检]", id);
            return;
        }
        // 3.3 æ‰€æœ‰è¡Œæ£€éªŒå®Œæˆä¸”可入库,更新主表状态 PENDING_QC â†’ PENDING_RECEIPT
        arrivalNoticeMapper.updateById(new MesWmArrivalNoticeDO()
                .setId(id).setStatus(MesWmArrivalNoticeStatusEnum.PENDING_RECEIPT.getStatus()));
    }
@@ -262,43 +298,76 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long generateArrivalNoticeFromPurchaseOrder(Long purchaseOrderId) {
        // 1. èŽ·å–é‡‡è´­è®¢å•
        // 1. æ ¡éªŒæ˜¯å¦å·²ç”Ÿæˆè¿‡åˆ°è´§é€šçŸ¥å•(防止重复生成)
        MesWmArrivalNoticeDO existNotice = arrivalNoticeMapper.selectByPurchaseOrderId(purchaseOrderId);
        if (existNotice != null) {
            throw exception(WM_ARRIVAL_NOTICE_ALREADY_EXISTS);
        }
        // 2. èŽ·å–é‡‡è´­è®¢å•
        ErpPurchaseOrderRespDTO purchaseOrder = purchaseOrderApi.getPurchaseOrder(purchaseOrderId).getCheckedData();
        if (purchaseOrder == null) {
            throw exception(WM_PURCHASE_ORDER_NOT_EXISTS);
        }
        // 2. èŽ·å–é‡‡è´­è®¢å•æ˜Žç»†
        // 3. èŽ·å–é‡‡è´­è®¢å•æ˜Žç»†
        List<ErpPurchaseOrderItemRespDTO> items = purchaseOrderApi.getPurchaseOrderItemList(purchaseOrderId).getCheckedData();
        if (CollUtil.isEmpty(items)) {
            throw exception(WM_PURCHASE_ORDER_NO_ITEM);
        }
        // 3. ç”Ÿæˆåˆ°è´§é€šçŸ¥å•编码(使用编码规则服务)
        // 4. ç”Ÿæˆåˆ°è´§é€šçŸ¥å•编码
        String code = generateArrivalNoticeCode();
        // 4. åˆ›å»ºåˆ°è´§é€šçŸ¥å•主表
        // 5. åˆ›å»ºåˆ°è´§é€šçŸ¥å•主表,直接设为待入库状态
        MesWmArrivalNoticeDO notice = new MesWmArrivalNoticeDO();
        notice.setCode(code);
        notice.setName(purchaseOrder.getNo() + "到货通知");
        notice.setPurchaseOrderId(purchaseOrderId);
        notice.setPurchaseOrderCode(purchaseOrder.getNo());
        notice.setVendorId(purchaseOrder.getSupplierId());
        notice.setStatus(MesWmArrivalNoticeStatusEnum.PREPARE.getStatus());
        notice.setArrivalDate(java.time.LocalDateTime.now()); // åˆ°è´§æ—¶é—´ä¸ºå½“前时间
        notice.setStatus(MesWmArrivalNoticeStatusEnum.PENDING_RECEIPT.getStatus());
        arrivalNoticeMapper.insert(notice);
        // 5. éåŽ†æ˜Žç»†åˆ›å»ºåˆ°è´§é€šçŸ¥å•è¡Œ
        // 6. éåŽ†æ˜Žç»†åˆ›å»ºåˆ°è´§é€šçŸ¥å•è¡Œ
        boolean hasNeedQc = false;
        for (ErpPurchaseOrderItemRespDTO item : items) {
            MesWmArrivalNoticeLineDO line = new MesWmArrivalNoticeLineDO();
            line.setNoticeId(notice.getId());
            line.setPurchaseOrderItemId(item.getId());
            // æ³¨æ„ï¼šè¿™é‡Œéœ€è¦æ ¹æ® ERP äº§å“ ID æ˜ å°„到 MES ç‰©æ–™ ID
            // å®žé™…项目中需要通过映射服务处理,这里暂时留空让用户手动选择
            line.setArrivalQuantity(item.getCount() != null ? item.getCount().subtract(item.getInCount() != null ? item.getInCount() : BigDecimal.ZERO) : BigDecimal.ZERO);
            line.setIqcCheckFlag(false); // é»˜è®¤ä¸éœ€è¦æ£€éªŒï¼Œç”¨æˆ·å¯ä¿®æ”¹
            line.setItemId(item.getProductId()); // ä»Žé‡‡è´­è®¢å•明细带入物料编号
            BigDecimal arrivalQuantity = item.getCount() != null ? item.getCount().subtract(item.getInCount() != null ? item.getInCount() : BigDecimal.ZERO) : BigDecimal.ZERO;
            line.setArrivalQuantity(arrivalQuantity);
            // ä»Žé‡‡è´­è®¢å•明细带入质检标识
            boolean needQc = Boolean.TRUE.equals(item.getQcCheckFlag());
            line.setIqcCheckFlag(needQc);
            // å¦‚果不需要质检,合格数量等于到货数量
            if (!needQc) {
                line.setQualifiedQuantity(arrivalQuantity);
            } else {
                hasNeedQc = true;
            }
            arrivalNoticeLineService.createArrivalNoticeLineFromPurchaseOrderItem(line);
        }
        // 7. å¦‚果有需要质检的行,更新状态为待质检并自动创建 IQC å•
        if (hasNeedQc) {
            arrivalNoticeMapper.updateById(new MesWmArrivalNoticeDO()
                    .setId(notice.getId()).setStatus(MesWmArrivalNoticeStatusEnum.PENDING_QC.getStatus()));
            // ä¸ºéœ€è¦è´¨æ£€çš„行自动创建 IQC æ£€éªŒå•
            List<MesWmArrivalNoticeLineDO> lines = arrivalNoticeLineService.getArrivalNoticeLineListByNoticeId(notice.getId());
            for (MesWmArrivalNoticeLineDO line : lines) {
                if (Boolean.TRUE.equals(line.getIqcCheckFlag())) {
                    Long iqcId = iqcService.createIqcFromArrivalNotice(
                            notice.getId(), line, notice.getCode(), notice.getVendorId());
                    arrivalNoticeLineService.updateArrivalNoticeLineIqcId(line.getId(), iqcId);
                    log.info("[generateArrivalNoticeFromPurchaseOrder][到货通知单({}) è¡Œ({}) è‡ªåŠ¨åˆ›å»º IQC æ£€éªŒå•({})]",
                            notice.getId(), line.getId(), iqcId);
                }
            }
        }
        return notice.getId();
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemreceipt/MesWmItemReceiptDetailServiceImpl.java
@@ -11,6 +11,7 @@
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
@@ -42,6 +43,7 @@
    private MesWmMaterialStockService materialStockService;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createItemReceiptDetail(MesWmItemReceiptDetailSaveReqVO createReqVO) {
        // 1. æ ¡éªŒæ•°æ®
        itemReceiptService.validateItemReceiptEditable(createReqVO.getReceiptId());
@@ -57,6 +59,7 @@
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateItemReceiptDetail(MesWmItemReceiptDetailSaveReqVO updateReqVO) {
        // 1.1 æ ¡éªŒå­˜åœ¨
        MesWmItemReceiptDetailDO detail = validateItemReceiptDetailExists(updateReqVO.getId());
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemreceipt/MesWmItemReceiptServiceImpl.java
@@ -17,7 +17,6 @@
import cn.iocoder.yudao.module.mes.enums.wm.MesWmItemReceiptStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
import cn.iocoder.yudao.module.mes.service.md.vendor.MesMdVendorService;
import cn.iocoder.yudao.module.mes.service.qc.iqc.MesQcIqcService;
import cn.iocoder.yudao.module.mes.service.wm.arrivalnotice.MesWmArrivalNoticeService;
import cn.iocoder.yudao.module.mes.service.wm.arrivalnotice.MesWmArrivalNoticeLineService;
@@ -27,6 +26,9 @@
import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
import cn.iocoder.yudao.module.erp.api.purchase.ErpPurchaseOrderApi;
import cn.iocoder.yudao.module.erp.api.purchase.ErpSupplierApi;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import jakarta.annotation.Resource;
@@ -39,6 +41,7 @@
import java.math.BigDecimal;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
@@ -63,7 +66,8 @@
    @Lazy
    private MesWmArrivalNoticeService arrivalNoticeService;
    @Resource
    private MesMdVendorService vendorService;
    @Lazy
    private ErpSupplierApi supplierApi;
    @Resource
    @Lazy
    private MesQcIqcService iqcService;
@@ -78,6 +82,9 @@
    @Resource
    private ErpPurchaseOrderApi purchaseOrderApi;
    @Resource
    private MdmItemApi mdmItemApi;
    @Override
    public Long createItemReceipt(MesWmItemReceiptSaveReqVO createReqVO) {
@@ -106,8 +113,8 @@
    private void validateItemReceiptSaveData(MesWmItemReceiptSaveReqVO reqVO) {
        // æ ¡éªŒç¼–码唯一
        validateCodeUnique(reqVO.getId(), reqVO.getCode());
        // æ ¡éªŒä¾›åº”商存在
        vendorService.validateVendorExistsAndEnable(reqVO.getVendorId());
        // æ ¡éªŒä¾›åº”商存在(采购入库使用 ERP ä¾›åº”商)
        supplierApi.validateSupplier(reqVO.getVendorId());
        // æ ¡éªŒåˆ°è´§é€šçŸ¥å•存在
        if (reqVO.getNoticeId() != null) {
            MesWmArrivalNoticeDO notice = arrivalNoticeService.validateArrivalNoticeReadyForReceipt(reqVO.getNoticeId());
@@ -155,9 +162,33 @@
            throw exception(WM_ITEM_RECEIPT_NO_LINE);
        }
        // æ ¡éªŒç‰©æ–™æ‰¹æ¬¡ç®¡ç†ï¼šå¦‚果物料开启批次管理,则明细的批次号不能为空
        validateBatchManagement(lines);
        // æäº¤ï¼ˆè‰ç¨¿ â†’ å¾…上架)
        itemReceiptMapper.updateById(new MesWmItemReceiptDO()
                .setId(id).setStatus(MesWmItemReceiptStatusEnum.APPROVING.getStatus()));
    }
    /**
     * æ ¡éªŒç‰©æ–™æ‰¹æ¬¡ç®¡ç†ï¼šå¦‚果物料开启批次管理,则行的批次号不能为空
     */
    private void validateBatchManagement(List<MesWmItemReceiptLineDO> lines) {
        // èŽ·å–æ‰€æœ‰ç‰©æ–™ID
        List<Long> itemIds = CollectionUtils.convertList(lines, MesWmItemReceiptLineDO::getItemId);
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(itemIds);
        // éåŽ†æ¯ä¸€è¡Œï¼Œæ£€æŸ¥ç‰©æ–™æ˜¯å¦å¼€å¯æ‰¹æ¬¡ç®¡ç†
        for (MesWmItemReceiptLineDO line : lines) {
            MdmItemRespDTO item = itemMap.get(line.getItemId());
            if (item == null) {
                continue;
            }
            // å¦‚果物料开启批次管理,检查行上的批次号是否为空
            if (Boolean.TRUE.equals(item.getIsBatchManaged()) && line.getBatchId() == null) {
                throw exception(WM_ITEM_RECEIPT_BATCH_REQUIRED, item.getCode(), item.getName());
            }
        }
    }
    @Override
@@ -245,14 +276,26 @@
    private void createTransactionList(MesWmItemReceiptDO receipt) {
        List<MesWmItemReceiptDetailDO> details = itemReceiptDetailService.getItemReceiptDetailListByReceiptId(receipt.getId());
        wmTransactionService.createTransactionList(convertList(details, detail -> new MesWmTransactionSaveReqDTO()
                .setType(MesWmTransactionTypeEnum.IN.getType()).setItemId(detail.getItemId())
                .setQuantity(detail.getQuantity()) // å…¥åº“数量为正数
                .setBatchId(detail.getBatchId())
                .setWarehouseId(detail.getWarehouseId()).setLocationId(detail.getLocationId()).setAreaId(detail.getAreaId())
                .setVendorId(receipt.getVendorId()).setReceiptTime(receipt.getReceiptDate())
                .setBizType(MesBizTypeConstants.WM_ITEM_RECEIPT_IN).setBizId(receipt.getId())
                .setBizCode(receipt.getCode()).setBizLineId(detail.getLineId())));
        // èŽ·å–æ‰€æœ‰è¡Œä¿¡æ¯ï¼Œç”¨äºŽèŽ·å–æ‰¹æ¬¡å·
        List<MesWmItemReceiptLineDO> lines = itemReceiptLineService.getItemReceiptLineListByReceiptId(receipt.getId());
        Map<Long, MesWmItemReceiptLineDO> lineMap = CollectionUtils.convertMap(lines, MesWmItemReceiptLineDO::getId);
        wmTransactionService.createTransactionList(convertList(details, detail -> {
            // ä»Žè¡Œä¸ŠèŽ·å–æ‰¹æ¬¡ä¿¡æ¯ï¼ˆå¦‚æžœæ˜Žç»†æ²¡æœ‰æ‰¹æ¬¡å·ï¼‰
            MesWmItemReceiptLineDO line = lineMap.get(detail.getLineId());
            Long batchId = detail.getBatchId() != null ? detail.getBatchId() :
                    (line != null ? line.getBatchId() : null);
            String batchCode = line != null ? line.getBatchCode() : null;
            return new MesWmTransactionSaveReqDTO()
                    .setType(MesWmTransactionTypeEnum.IN.getType()).setItemId(detail.getItemId())
                    .setQuantity(detail.getQuantity())
                    .setBatchId(batchId).setBatchCode(batchCode)
                    .setWarehouseId(detail.getWarehouseId()).setLocationId(detail.getLocationId()).setAreaId(detail.getAreaId())
                    .setVendorId(receipt.getVendorId()).setReceiptTime(receipt.getReceiptDate())
                    .setBizType(MesBizTypeConstants.WM_ITEM_RECEIPT_IN).setBizId(receipt.getId())
                    .setBizCode(receipt.getCode()).setBizLineId(detail.getLineId());
        }));
    }
    @Override
@@ -331,26 +374,33 @@
        // 1. æ ¡éªŒåˆ°è´§é€šçŸ¥å•存在且状态为待入库
        MesWmArrivalNoticeDO notice = arrivalNoticeService.validateArrivalNoticeReadyForReceipt(arrivalNoticeId);
        // 2. èŽ·å–åˆ°è´§é€šçŸ¥å•è¡Œ
        // 2. æ ¡éªŒæ˜¯å¦å·²ç”Ÿæˆè¿‡å…¥åº“单(防止重复生成)
        MesWmItemReceiptDO existReceipt = itemReceiptMapper.selectByNoticeId(arrivalNoticeId);
        if (existReceipt != null) {
            throw exception(WM_ITEM_RECEIPT_ALREADY_EXISTS);
        }
        // 3. èŽ·å–åˆ°è´§é€šçŸ¥å•è¡Œ
        List<MesWmArrivalNoticeLineDO> lines = arrivalNoticeLineService.getArrivalNoticeLineListByNoticeId(arrivalNoticeId);
        if (CollUtil.isEmpty(lines)) {
            throw exception(WM_ARRIVAL_NOTICE_NO_LINE);
        }
        // 3. ç”Ÿæˆå…¥åº“单编码
        // 4. ç”Ÿæˆå…¥åº“单编码
        String code = generateItemReceiptCode();
        // 4. åˆ›å»ºå…¥åº“单主表
        // 5. åˆ›å»ºå…¥åº“单主表
        MesWmItemReceiptDO receipt = new MesWmItemReceiptDO();
        receipt.setCode(code);
        receipt.setName(notice.getName() + "入库单");
        receipt.setNoticeId(arrivalNoticeId);
        receipt.setPurchaseOrderCode(notice.getPurchaseOrderCode());
        receipt.setVendorId(notice.getVendorId());
        receipt.setReceiptDate(java.time.LocalDateTime.now()); // å…¥åº“时间为当前时间
        receipt.setStatus(MesWmItemReceiptStatusEnum.PREPARE.getStatus());
        itemReceiptMapper.insert(receipt);
        // 5. éåŽ†è¡Œåˆ›å»ºå…¥åº“å•è¡Œ
        // 6. éåŽ†è¡Œåˆ›å»ºå…¥åº“å•è¡Œ
        for (MesWmArrivalNoticeLineDO noticeLine : lines) {
            MesWmItemReceiptLineDO receiptLine = new MesWmItemReceiptLineDO();
            receiptLine.setReceiptId(receipt.getId());
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesLineServiceImpl.java
@@ -204,7 +204,7 @@
    @Transactional(rollbackFor = Exception.class)
    public void updateProductSalesLineWhenOqcFinish(Long id, Long oqcId, Integer checkResult) {
        // 1. æ›´æ–°è¡Œçš„ oqcId + qualityStatus
        Integer qualityStatus = Objects.equals(checkResult, MesQcCheckResultEnum.PASS.getType())
        Integer qualityStatus = Objects.equals(checkResult, MesQcCheckResultEnum.QUALIFIED.getType())
                ? MesWmQualityStatusEnum.PASS.getStatus() : MesWmQualityStatusEnum.FAIL.getStatus();
        productSalesLineMapper.updateById(new MesWmProductSalesLineDO()
                .setId(id).setOqcId(oqcId).setQualityStatus(qualityStatus));
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/salesnotice/MesWmSalesNoticeServiceImpl.java
@@ -183,22 +183,28 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long generateSalesNoticeFromSaleOrder(Long saleOrderId) {
        // 1. èŽ·å–é”€å”®è®¢å•ä¿¡æ¯
        // 1. æ ¡éªŒæ˜¯å¦å·²ç”Ÿæˆè¿‡å‘货通知单(防止重复生成)
        MesWmSalesNoticeDO existNotice = salesNoticeMapper.selectBySaleOrderId(saleOrderId);
        if (existNotice != null) {
            throw exception(WM_SALES_NOTICE_ALREADY_EXISTS);
        }
        // 2. èŽ·å–é”€å”®è®¢å•ä¿¡æ¯
        ErpSaleOrderRespDTO saleOrder = saleOrderApi.getSaleOrder(saleOrderId).getCheckedData();
        if (saleOrder == null) {
            throw exception(WM_SALES_NOTICE_SALE_ORDER_NOT_EXISTS);
        }
        // 2. èŽ·å–é”€å”®è®¢å•æ˜Žç»†
        // 3. èŽ·å–é”€å”®è®¢å•æ˜Žç»†
        List<ErpSaleOrderItemRespDTO> saleOrderItems = saleOrderApi.getSaleOrderItemList(saleOrderId).getCheckedData();
        if (CollUtil.isEmpty(saleOrderItems)) {
            throw exception(WM_SALES_NOTICE_SALE_ORDER_ITEM_EMPTY);
        }
        // 3. ç”Ÿæˆå‘货通知单编码
        // 4. ç”Ÿæˆå‘货通知单编码
        String code = autoCodeRecordService.generateAutoCode("SALESNOTICE_CODE", null);
        // 4. åˆ›å»ºå‘货通知单主表
        // 5. åˆ›å»ºå‘货通知单主表
        MesWmSalesNoticeDO notice = new MesWmSalesNoticeDO();
        notice.setCode(code);
        notice.setName("销售订单[" + saleOrder.getNo() + "]发货通知");
@@ -209,7 +215,7 @@
        notice.setStatus(PREPARE.getStatus());
        salesNoticeMapper.insert(notice);
        // 5. éåŽ†æ˜Žç»†åˆ›å»ºå‘è´§é€šçŸ¥å•è¡Œ
        // 6. éåŽ†æ˜Žç»†åˆ›å»ºå‘è´§é€šçŸ¥å•è¡Œ
        for (ErpSaleOrderItemRespDTO saleOrderItem : saleOrderItems) {
            MesWmSalesNoticeLineDO line = new MesWmSalesNoticeLineDO();
            line.setNoticeId(notice.getId());
@@ -315,16 +321,21 @@
            throw exception(WM_SALES_NOTICE_STATUS_NOT_APPROVED);
        }
        // 2. èŽ·å–å‘è´§é€šçŸ¥å•è¡Œ
        // 2. æ ¡éªŒæ˜¯å¦å·²ç”Ÿæˆè¿‡é”€å”®å‡ºåº“单(防止重复生成)
        if (productSalesMapper.selectByNoticeId(noticeId) != null) {
            throw exception(WM_PRODUCT_SALES_ALREADY_EXISTS);
        }
        // 3. èŽ·å–å‘è´§é€šçŸ¥å•è¡Œ
        List<MesWmSalesNoticeLineDO> noticeLines = salesNoticeLineService.getSalesNoticeLineListByNoticeId(noticeId);
        if (CollUtil.isEmpty(noticeLines)) {
            throw exception(WM_SALES_NOTICE_LINE_EMPTY);
        }
        // 3. ç”Ÿæˆé”€å”®å‡ºåº“单编码
        // 4. ç”Ÿæˆé”€å”®å‡ºåº“单编码
        String code = autoCodeRecordService.generateAutoCode("WM_PRODUCT_SALES_CODE", null);
        // 4. åˆ›å»ºé”€å”®å‡ºåº“单主表
        // 5. åˆ›å»ºé”€å”®å‡ºåº“单主表
        MesWmProductSalesDO productSales = new MesWmProductSalesDO();
        productSales.setCode(code);
        productSales.setName("发货通知单[" + notice.getCode() + "]出库");
@@ -340,7 +351,7 @@
        productSales.setStatus(MesWmProductSalesStatusEnum.PREPARE.getStatus());
        productSalesMapper.insert(productSales);
        // 5. éåŽ†å‘è´§é€šçŸ¥å•è¡Œåˆ›å»ºé”€å”®å‡ºåº“å•è¡Œ
        // 6. éåŽ†å‘è´§é€šçŸ¥å•è¡Œåˆ›å»ºé”€å”®å‡ºåº“å•è¡Œ
        for (MesWmSalesNoticeLineDO noticeLine : noticeLines) {
            MesWmProductSalesLineDO salesLine = new MesWmProductSalesLineDO();
            salesLine.setSalesId(productSales.getId());
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wms/approval/listener/WmsInboundStatusListener.java
@@ -4,7 +4,6 @@
import cn.iocoder.yudao.module.mes.service.wms.approval.WmsApprovalService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
/**
@@ -14,29 +13,38 @@
 */
@Component
@Slf4j
public class WmsInboundStatusListener implements ApplicationListener<BpmProcessInstanceStatusEvent> {
public class WmsInboundStatusListener extends cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEventListener {
    /**
     * é‡‡è´­å…¥åº“审批流程定义 Key
     */
    private static final String PROCESS_DEFINITION_KEY = "wms_inbound_approve";
    @Resource
    private WmsApprovalService approvalService;
    @Override
    public void onApplicationEvent(BpmProcessInstanceStatusEvent event) {
        // é€šè¿‡ businessKey æŸ¥è¯¢å…¥åº“单
    protected String getProcessDefinitionKey() {
        return PROCESS_DEFINITION_KEY;
    }
    @Override
    protected void onEvent(BpmProcessInstanceStatusEvent event) {
        Long orderId;
        try {
            orderId = Long.parseLong(event.getBusinessKey());
        } catch (NumberFormatException e) {
            return; // businessKey ä¸æ˜¯æ•°å­—,忽略
            log.warn("[onEvent] businessKey({}) ä¸æ˜¯æœ‰æ•ˆçš„æ•°å­—", event.getBusinessKey());
            return;
        }
        log.info("[onApplicationEvent] æ”¶åˆ°BPM审批事件,businessKey={}, status={}",
        log.info("[onEvent] æ”¶åˆ°BPM审批事件,businessKey={}, status={}",
                event.getBusinessKey(), event.getStatus());
        // è°ƒç”¨å®¡æ‰¹æœåŠ¡æ›´æ–°çŠ¶æ€
        try {
            approvalService.updateAuditStatus(orderId, "INBOUND", event.getStatus());
        } catch (Exception e) {
            log.error("[onApplicationEvent] æ›´æ–°å…¥åº“单审批状态失败,orderId={}", orderId, e);
            log.error("[onEvent] æ›´æ–°å…¥åº“单审批状态失败,orderId={}", orderId, e);
        }
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wms/approval/listener/WmsOutboundStatusListener.java
@@ -4,7 +4,6 @@
import cn.iocoder.yudao.module.mes.service.wms.approval.WmsApprovalService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
/**
@@ -14,29 +13,38 @@
 */
@Component
@Slf4j
public class WmsOutboundStatusListener implements ApplicationListener<BpmProcessInstanceStatusEvent> {
public class WmsOutboundStatusListener extends cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEventListener {
    /**
     * é”€å”®å‡ºåº“审批流程定义 Key
     */
    private static final String PROCESS_DEFINITION_KEY = "wms_outbound_approve";
    @Resource
    private WmsApprovalService approvalService;
    @Override
    public void onApplicationEvent(BpmProcessInstanceStatusEvent event) {
        // é€šè¿‡ businessKey æŸ¥è¯¢å‡ºåº“单
    protected String getProcessDefinitionKey() {
        return PROCESS_DEFINITION_KEY;
    }
    @Override
    protected void onEvent(BpmProcessInstanceStatusEvent event) {
        Long orderId;
        try {
            orderId = Long.parseLong(event.getBusinessKey());
        } catch (NumberFormatException e) {
            return; // businessKey ä¸æ˜¯æ•°å­—,忽略
            log.warn("[onEvent] businessKey({}) ä¸æ˜¯æœ‰æ•ˆçš„æ•°å­—", event.getBusinessKey());
            return;
        }
        log.info("[onApplicationEvent] æ”¶åˆ°BPM审批事件,businessKey={}, status={}",
        log.info("[onEvent] æ”¶åˆ°BPM审批事件,businessKey={}, status={}",
                event.getBusinessKey(), event.getStatus());
        // è°ƒç”¨å®¡æ‰¹æœåŠ¡æ›´æ–°çŠ¶æ€
        try {
            approvalService.updateAuditStatus(orderId, "OUTBOUND", event.getStatus());
        } catch (Exception e) {
            log.error("[onApplicationEvent] æ›´æ–°å‡ºåº“单审批状态失败,orderId={}", orderId, e);
            log.error("[onEvent] æ›´æ–°å‡ºåº“单审批状态失败,orderId={}", orderId, e);
        }
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wms/approval/listener/WmsTransferStatusListener.java
@@ -4,7 +4,6 @@
import cn.iocoder.yudao.module.mes.service.wms.approval.WmsApprovalService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
/**
@@ -14,29 +13,38 @@
 */
@Component
@Slf4j
public class WmsTransferStatusListener implements ApplicationListener<BpmProcessInstanceStatusEvent> {
public class WmsTransferStatusListener extends cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEventListener {
    /**
     * è°ƒæ‹¨å®¡æ‰¹æµç¨‹å®šä¹‰ Key
     */
    private static final String PROCESS_DEFINITION_KEY = "wms_transfer_approve";
    @Resource
    private WmsApprovalService approvalService;
    @Override
    public void onApplicationEvent(BpmProcessInstanceStatusEvent event) {
        // é€šè¿‡ businessKey æŸ¥è¯¢è°ƒæ‹¨å•
    protected String getProcessDefinitionKey() {
        return PROCESS_DEFINITION_KEY;
    }
    @Override
    protected void onEvent(BpmProcessInstanceStatusEvent event) {
        Long orderId;
        try {
            orderId = Long.parseLong(event.getBusinessKey());
        } catch (NumberFormatException e) {
            return; // businessKey ä¸æ˜¯æ•°å­—,忽略
            log.warn("[onEvent] businessKey({}) ä¸æ˜¯æœ‰æ•ˆçš„æ•°å­—", event.getBusinessKey());
            return;
        }
        log.info("[onApplicationEvent] æ”¶åˆ°BPM审批事件,businessKey={}, status={}",
        log.info("[onEvent] æ”¶åˆ°BPM审批事件,businessKey={}, status={}",
                event.getBusinessKey(), event.getStatus());
        // è°ƒç”¨å®¡æ‰¹æœåŠ¡æ›´æ–°çŠ¶æ€
        try {
            approvalService.updateAuditStatus(orderId, "TRANSFER", event.getStatus());
        } catch (Exception e) {
            log.error("[onApplicationEvent] æ›´æ–°è°ƒæ‹¨å•审批状态失败,orderId={}", orderId, e);
            log.error("[onEvent] æ›´æ–°è°ƒæ‹¨å•审批状态失败,orderId={}", orderId, e);
        }
    }