1.仓储物流-批次号优化覆盖数据
2.仓储物流-新增自定义出入库流水查询
3.采购管理-清理无用代码和表
4.销售管理-清理无用代码和表
5.销售流程状态字段数据回游
已添加18个文件
已重命名1个文件
已修改47个文件
已删除20个文件
5337 ■■■■■ 文件已修改
delete_erp_customer.ps1 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
delete_purchase_request.bat 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/erp_sale_order_status_feedback.md 111 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/item_type_integration.md 145 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/material_stock_adjust.md 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/mdm_item_batch_config.md 215 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/mdm_mes_item_sync.md 343 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/wm_transaction_list.md 135 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/wm_transaction_query.md 167 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/sale/ErpSaleOrderApi.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/sale/enums/ErpSaleOrderOutStatusEnum.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/api/sale/ErpSaleOrderApiImpl.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/ErpFinanceReceiptController.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseRequestController.java 210 ●●●●● 补丁 | 查看 | 原始文档 | 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 156 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestSaveReqVO.java 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpCustomerController.java 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/customer/ErpCustomerPageReqVO.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/customer/ErpCustomerRespVO.java 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/customer/ErpCustomerSaveReqVO.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderPageReqVO.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/finance/ErpFinanceReceiptDO.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseOrderDO.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseRequestDO.java 129 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseRequestItemDO.java 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpCustomerDO.java 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleOrderDO.java 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleOrderItemDO.java 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseRequestItemMapper.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseRequestMapper.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpCustomerMapper.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSaleOrderMapper.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/redis/no/ErpNoRedisDAO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpPurchaseRequestStatusEnum.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestService.java 118 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestServiceImpl.java 401 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/listener/ErpPurchaseRequestStatusListener.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpCustomerService.java 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpCustomerServiceImpl.java 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderService.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java 37 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/category/MdmItemCategoryApi.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/category/MdmItemCategoryApiImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/category/dto/MdmItemCategoryRespDTO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/item/MdmItemBatchConfigApi.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/item/MdmItemBatchConfigApiImpl.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/item/MdmItemSyncApi.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/item/MdmItemSyncApiImpl.java 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/item/dto/MdmItemBatchConfigRespDTO.java 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/item/dto/MdmItemSyncReqDTO.java 109 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/category/vo/MdmItemCategoryRespVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/category/vo/MdmItemCategorySaveReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/MdmItemController.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemRespVO.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemSaveReqVO.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/dataobject/category/MdmItemCategoryDO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/mysql/category/MdmItemCategoryMapper.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/category/MdmItemCategoryService.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/category/MdmItemCategoryServiceImpl.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/item/MdmItemBatchConfigService.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/item/MdmItemBatchConfigServiceImpl.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/item/MdmItemServiceImpl.java 98 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/item/dto/MesMdItemSyncReqDTO.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/item/MesMdItemApiImpl.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/item/vo/MesMdItemSaveReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/batch/vo/MesWmBatchGenerateReqVO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/vo/MesWmMaterialStockAdjustReqVO.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/transaction/MesWmTransactionController.java 161 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/transaction/vo/MesWmTransactionPageReqVO.java 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/transaction/vo/MesWmTransactionRespVO.java 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/materialstock/MesWmMaterialStockMapper.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/transaction/MesWmTransactionMapper.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemBatchConfigServiceImpl.java 86 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemServiceImpl.java 110 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemTypeServiceImpl.java 74 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/batch/MesWmBatchServiceImpl.java 315 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstock/MesWmMaterialStockServiceImpl.java 116 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesServiceImpl.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/salesnotice/MesWmSalesNoticeServiceImpl.java 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/MesWmTransactionService.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/MesWmTransactionServiceImpl.java 30 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/dto/MesWmTransactionSaveReqDTO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/resources/mapper/wm/batch/MesWmBatchMapper.xml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
delete_erp_customer.ps1
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,12 @@
# ERP å®¢æˆ·ç›¸å…³æ–‡ä»¶åˆ é™¤è„šæœ¬
# è¯·åœ¨ PowerShell ä¸­æ‰§è¡Œä»¥ä¸‹å‘½ä»¤ï¼š
Remove-Item -Force "yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpCustomerController.java"
Remove-Item -Recurse -Force "yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/customer"
Remove-Item -Force "yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpCustomerService.java"
Remove-Item -Force "yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpCustomerServiceImpl.java"
Remove-Item -Force "yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpCustomerDO.java"
Remove-Item -Force "yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpCustomerMapper.java"
Remove-Item -Force "yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpSaleOrderOutStatusEnum.java"
Write-Host "ERP Customer files deleted successfully!"
delete_purchase_request.bat
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,30 @@
@echo off
chcp 65001
echo æ­£åœ¨åˆ é™¤ ERP é‡‡è´­ç”³è¯·ç›¸å…³ä»£ç æ–‡ä»¶...
REM åˆ é™¤ Controller
del /f "yudao-module-erp\src\main\java\cn\iocoder\yudao\module\erp\controller\admin\purchase\ErpPurchaseRequestController.java"
REM åˆ é™¤ VO ç›®å½•
rd /s /q "yudao-module-erp\src\main\java\cn\iocoder\yudao\module\erp\controller\admin\purchase\vo\request"
REM åˆ é™¤ Service
del /f "yudao-module-erp\src\main\java\cn\iocoder\yudao\module\erp\service\purchase\ErpPurchaseRequestService.java"
del /f "yudao-module-erp\src\main\java\cn\iocoder\yudao\module\erp\service\purchase\ErpPurchaseRequestServiceImpl.java"
REM åˆ é™¤ Listener
del /f "yudao-module-erp\src\main\java\cn\iocoder\yudao\module\erp\service\purchase\listener\ErpPurchaseRequestStatusListener.java"
REM åˆ é™¤ DO
del /f "yudao-module-erp\src\main\java\cn\iocoder\yudao\module\erp\dal\dataobject\purchase\ErpPurchaseRequestDO.java"
del /f "yudao-module-erp\src\main\java\cn\iocoder\yudao\module\erp\dal\dataobject\purchase\ErpPurchaseRequestItemDO.java"
REM åˆ é™¤ Mapper
del /f "yudao-module-erp\src\main\java\cn\iocoder\yudao\module\erp\dal\mysql\purchase\ErpPurchaseRequestMapper.java"
del /f "yudao-module-erp\src\main\java\cn\iocoder\yudao\module\erp\dal\mysql\purchase\ErpPurchaseRequestItemMapper.java"
REM åˆ é™¤ Enum
del /f "yudao-module-erp\src\main\java\cn\iocoder\yudao\module\erp\enums\ErpPurchaseRequestStatusEnum.java"
echo æ–‡ä»¶åˆ é™¤å®Œæˆï¼
pause
docs/erp_sale_order_status_feedback.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,111 @@
# ERP é”€å”®è®¢å•出库状态实时反馈 - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- ERP é”€å”®è®¢å•列表页
- ERP é”€å”®è®¢å•详情页
- MES å‘货通知单管理页
- MES é”€å”®å‡ºåº“单管理页
## ä¸šåŠ¡æµç¨‹ä¸ŽçŠ¶æ€å˜åŒ–
### çŠ¶æ€æžšä¸¾è¯´æ˜Ž
| çŠ¶æ€å€¼ | çŠ¶æ€åç§° | è¯´æ˜Ž |
|--------|----------|------|
| 0 | æœªå‡ºåº“ | é”€å”®è®¢å•初始状态,未开始出库流程 |
| 1 | å·²é¢„ç•™ | å‘货通知单审核通过,库存已预留 |
| 2 | éƒ¨åˆ†å‡ºåº“ | é”€å”®å‡ºåº“单已拣货/占用库存 |
| 3 | å…¨éƒ¨å‡ºåº“ | é”€å”®å‡ºåº“单已完成出库 |
### çŠ¶æ€å˜åŒ–æ—¶æœº
| æ­¥éª¤ | æ“ä½œ | çŠ¶æ€å˜åŒ– | è§¦å‘位置 |
|------|------|----------|----------|
| 1 | å‘货通知单审核通过 | 0 â†’ 1(未出库 â†’ å·²é¢„留) | MES å‘货通知单提交审核 |
| 2 | é”€å”®å‡ºåº“单拣货 | 1 â†’ 2(已预留 â†’ éƒ¨åˆ†å‡ºåº“) | MES é”€å”®å‡ºåº“单拣货确认 |
| 3 | é”€å”®å‡ºåº“单完成 | 2 â†’ 3(部分出库 â†’ å…¨éƒ¨å‡ºåº“) | MES é”€å”®å‡ºåº“单完成出库 |
## API å˜æ›´
### æ–°å¢ž API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| PUT | /rpc-api/erp/sale-order/updateOrderOutStatusTo | æ›´æ–°é”€å”®è®¢å•出库状态为指定值 |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| orderId | Long | æ˜¯ | é”€å”®è®¢å•编号 |
| outStatus | Integer | æ˜¯ | å‡ºåº“状态(0-3) |
**响应:**
```json
{
  "code": 0,
  "data": true,
  "msg": "操作成功"
}
```
### å“åº”字段变更
**ErpSaleOrderRespDTO æ–°å¢žå­—段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| outStatus | Integer | å‡ºåº“状态(0-3) |
| outCount | BigDecimal | å·²å‡ºåº“数量 |
**ErpSaleOrderItemRespDTO æ–°å¢žå­—段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| outCount | BigDecimal | å·²å‡ºåº“数量 |
## å­—段展示规则
### é”€å”®è®¢å•列表
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| å‡ºåº“状态 | åˆ—表 | ä½¿ç”¨ Tag æ ‡ç­¾å±•示,不同状态不同颜色 |
| å·²å‡ºåº“数量 | åˆ—表 | æ ¼å¼ï¼š`已出库数量/总数量` |
### é”€å”®è®¢å•详情
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| å‡ºåº“状态 | åŸºæœ¬ä¿¡æ¯åŒº | ä½¿ç”¨ Tag æ ‡ç­¾å±•示 |
| å·²å‡ºåº“数量 | åŸºæœ¬ä¿¡æ¯åŒº | æ•°å€¼å±•示 |
| è®¢å•明细-已出库数量 | æ˜Žç»†è¡¨æ ¼ | æ¯è¡Œæ˜¾ç¤ºè¯¥é¡¹çš„已出库数量 |
### å‡ºåº“状态颜色建议
| çŠ¶æ€ | é¢œè‰² | Tag ç±»åž‹ |
|------|------|----------|
| æœªå‡ºåº“ | ç°è‰² | info |
| å·²é¢„ç•™ | è“è‰² | primary |
| éƒ¨åˆ†å‡ºåº“ | æ©™è‰² | warning |
| å…¨éƒ¨å‡ºåº“ | ç»¿è‰² | success |
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| å‘货通知单审核 | å®¡æ ¸é€šè¿‡æ—¶è‡ªåŠ¨æ›´æ–°å…³è”é”€å”®è®¢å•çŠ¶æ€ä¸º"已预留" |
| é”€å”®å‡ºåº“单拣货 | æ‹£è´§ç¡®è®¤æ—¶è‡ªåŠ¨æ›´æ–°å…³è”é”€å”®è®¢å•çŠ¶æ€ä¸º"部分出库" |
| é”€å”®å‡ºåº“单完成 | å‡ºåº“完成时自动更新关联销售订单状态为"全部出库",并累计出库数量 |
| å–消发货通知单 | å¦‚果取消,需要将销售订单状态回退 |
| å–消销售出库单 | å¦‚果取消,需要将销售订单状态回退 |
## æ³¨æ„äº‹é¡¹
1. **状态实时性**:MES æ¨¡å—操作后立即回写 ERP é”€å”®è®¢å•状态,前端刷新即可看到最新状态
2. **多单据关联**:一个销售订单可能对应多个发货通知单和销售出库单,状态会按流程推进
3. **状态回退**:目前仅实现状态推进,暂不支持状态回退(如取消后状态恢复)
4. **出库数量累计**:只有销售出库单完成时才会累计出库数量,拣货阶段仅更新状态
5. **并发控制**:状态更新有事务保证,避免并发更新导致状态不一致
docs/item_type_integration.md
@@ -1,111 +1,120 @@
# ç‰©æ–™åˆ†ç±»ç»Ÿä¸€ä½¿ç”¨ mes_md_item_type - å‰ç«¯è”调方案
# ç‰©æ–™åˆ†ç±»ç»Ÿä¸€ä½¿ç”¨ mdm_item_category - å‰ç«¯è”调方案
## å˜æ›´è¯´æ˜Ž
物料分类已从 MES æ¨¡å—迁移到 MDM æ¨¡å—统一管理:
- åŽŸ MES åˆ†ç±»è¡¨ `mes_md_item_type` ä¿ç•™ä½†ä¸å†ç»´æŠ¤
- ç»Ÿä¸€ä½¿ç”¨ MDM åˆ†ç±»è¡¨ `mdm_item_category`
- MES æ¨¡å—通过兼容层读取 MDM åˆ†ç±»æ•°æ®
## æ¶‰åŠé¡µé¢
- MDM ç‰©æ–™ç®¡ç†é¡µé¢ï¼ˆæ–°å¢ž/编辑物料)
- MES ç‰©æ–™ç®¡ç†é¡µé¢ï¼ˆæ–°å¢ž/编辑物料)
- MDM ç‰©æ–™åˆ†ç±»ç®¡ç†é¡µé¢ï¼ˆä¸»è¦ç»´æŠ¤å…¥å£ï¼‰
- MDM ç‰©æ–™ç®¡ç†é¡µé¢ï¼ˆæ–°å¢ž/编辑物料选择分类)
- MES ç‰©æ–™ç®¡ç†é¡µé¢ï¼ˆæ–°å¢ž/编辑物料选择分类)
## ä¸šåŠ¡æµç¨‹ä¸Žæ•°æ®å¸¦å…¥
1. ç‰©æ–™åˆ†ç±»ç»Ÿä¸€ä½¿ç”¨ `mes_md_item_type` è¡¨ï¼ŒMDM å’Œ MES æ¨¡å—共用同一套分类数据
2. ç‰©æ–™ç±»åž‹å­—段 `itemType` ç”¨äºŽå¿«é€Ÿé€‰æ‹©ï¼š1原料、2半成品、3成品、4辅料
3. å½“选择 `categoryId` æ—¶ï¼Œç›´æŽ¥å…³è”分类 ID;当只选择 `itemType` æ—¶ï¼Œç³»ç»Ÿè‡ªåŠ¨åŒ¹é…å¯¹åº”åç§°çš„åˆ†ç±»
1. ç‰©æ–™åˆ†ç±»ç»Ÿä¸€ä½¿ç”¨ `mdm_item_category` è¡¨ï¼ŒMDM å’Œ MES æ¨¡å—共用同一套分类数据
2. åˆ†ç±»æ–°å¢ž `itemOrProduct` å­—段兼容 MES çš„ ITEM/PRODUCT æ ‡è¯†
3. ç‰©æ–™ç±»åž‹å­—段 `itemType` ç”¨äºŽå¿«é€Ÿé€‰æ‹©ï¼š1原料、2半成品、3成品、4辅料
## å­—段映射
| MDM å­—段 (mdm_item_category) | MES å­—段 (mes_md_item_type) | è¯´æ˜Ž |
|------------------------------|----------------------------|------|
| id | id | åˆ†ç±»ç¼–号 |
| code | code | åˆ†ç±»ç¼–码 |
| name | name | åˆ†ç±»åç§° |
| parentId | parentId | çˆ¶åˆ†ç±»ç¼–号 |
| itemType (Integer) | itemOrProduct (String) | ç±»åž‹æ ‡è¯† |
| sort | sort | æŽ’序 |
| status | status | çŠ¶æ€ |
| remark | remark | å¤‡æ³¨ |
### itemType ä¸Ž itemOrProduct æ˜ å°„关系
| itemType | è¯´æ˜Ž | itemOrProduct |
|----------|------|---------------|
| 1 | ç‰©æ–™ | ITEM |
| 2 | äº§å“ | PRODUCT |
| 3 | åŠæˆå“ | ITEM |
| NULL | å…¨éƒ¨ | - |
## API
### èŽ·å–ç‰©æ–™åˆ†ç±»åˆ—è¡¨
### MDM åˆ†ç±»ç®¡ç†ï¼ˆä¸»è¦æŽ¥å£ï¼‰
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /admin-api/mes/md/item-type/list | èŽ·å–ç‰©æ–™åˆ†ç±»åˆ—è¡¨ |
| POST | /admin-api/mdm/category/create | åˆ›å»ºç‰©æ–™åˆ†ç±» |
| PUT | /admin-api/mdm/category/update | æ›´æ–°ç‰©æ–™åˆ†ç±» |
| DELETE | /admin-api/mdm/category/delete | åˆ é™¤ç‰©æ–™åˆ†ç±» |
| GET | /admin-api/mdm/category/get | èŽ·å–å•ä¸ªåˆ†ç±» |
| GET | /admin-api/mdm/category/list | èŽ·å–åˆ†ç±»åˆ—è¡¨ |
| GET | /admin-api/mdm/category/list-all-simple | èŽ·å–å¯ç”¨çš„åˆ†ç±»åˆ—è¡¨ï¼ˆä¸‹æ‹‰æ¡†ï¼‰ |
**请求参数:**
**请求参数(MdmItemCategorySaveReqVO):**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| status | Integer | å¦ | çŠ¶æ€ç­›é€‰ |
**响应字段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| id | Long | åˆ†ç±»ç¼–号 |
| code | String | åˆ†ç±»ç¼–码 |
| name | String | åˆ†ç±»åç§° |
| parentId | Long | çˆ¶åˆ†ç±»ç¼–号 |
| itemOrProduct | String | ç‰©æ–™/产品标识 |
| sort | Integer | æŽ’序 |
| status | Integer | çŠ¶æ€ |
| id | Long | å¦ | åˆ†ç±»ç¼–号(更新时必填) |
| parentId | Long | å¦ | çˆ¶åˆ†ç±»ç¼–号 |
| code | String | æ˜¯ | åˆ†ç±»ç¼–码 |
| name | String | æ˜¯ | åˆ†ç±»åç§° |
| itemType | Integer | å¦ | é€‚用物料类型:1物料、2产品、3半成品、NULL全部 |
| itemOrProduct | String | å¦ | ç‰©æ–™/产品标识:ITEM、PRODUCT(兼容MES) |
| sort | Integer | å¦ | æŽ’序 |
| status | Integer | æ˜¯ | çŠ¶æ€ï¼š0启用、1禁用 |
| remark | String | å¦ | å¤‡æ³¨ |
**响应示例:**
```json
{
  "code": 0,
  "data": [
    {
      "id": 1,
      "code": "RAW001",
      "name": "原料",
      "parentId": 0,
      "itemOrProduct": "1",
      "sort": 1,
      "status": 0
    }
  ]
  "data": {
    "id": 1,
    "parentId": 0,
    "code": "RAW001",
    "name": "原料",
    "itemType": 1,
    "itemOrProduct": "ITEM",
    "sort": 0,
    "status": 0
  }
}
```
### èŽ·å–ç‰©æ–™åˆ†ç±»è¯¦æƒ…
### MES åˆ†ç±»æŽ¥å£ï¼ˆå…¼å®¹ä¿ç•™ï¼‰
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /admin-api/mes/md/item-type/get | èŽ·å–ç‰©æ–™åˆ†ç±»è¯¦æƒ… |
| GET | /admin-api/mes/md/item-type/list | èŽ·å–ç‰©æ–™åˆ†ç±»åˆ—è¡¨ |
| GET | /admin-api/mes/md/item-type/simple-list | èŽ·å–ç²¾ç®€åˆ—è¡¨ï¼ˆä¸‹æ‹‰æ¡†ï¼‰ |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | æ˜¯ | åˆ†ç±»ç¼–号 |
### ä¿å­˜ç‰©æ–™åˆ†ç±»
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /admin-api/mes/md/item-type/create | åˆ›å»ºç‰©æ–™åˆ†ç±» |
| PUT | /admin-api/mes/md/item-type/update | æ›´æ–°ç‰©æ–™åˆ†ç±» |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | å¦ | åˆ†ç±»ç¼–号(更新时必填) |
| code | String | æ˜¯ | åˆ†ç±»ç¼–码 |
| name | String | æ˜¯ | åˆ†ç±»åç§° |
| parentId | Long | å¦ | çˆ¶åˆ†ç±»ç¼–号 |
| itemOrProduct | String | å¦ | ç‰©æ–™/产品标识 |
| sort | Integer | å¦ | æŽ’序 |
| status | Integer | å¦ | çŠ¶æ€ |
> æ³¨æ„ï¼šMES åˆ†ç±»æŽ¥å£åº•层已改为读取 MDM åˆ†ç±»æ•°æ®ï¼Œå»ºè®®å‰ç«¯ç»Ÿä¸€ä½¿ç”¨ MDM åˆ†ç±»æŽ¥å£ã€‚
## å­—段展示规则
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| categoryId | æ–°å¢ž/编辑表单 | ç‰©æ–™åˆ†ç±»ä¸‹æ‹‰æ¡†ï¼Œæ•°æ®æ¥æºï¼šmes_md_item_type |
| categoryId / itemTypeId | æ–°å¢ž/编辑表单 | ç‰©æ–™åˆ†ç±»ä¸‹æ‹‰æ¡†ï¼Œæ•°æ®æ¥æºï¼šmdm_item_category |
| itemType | æ–°å¢ž/编辑表单 | ç‰©æ–™ç±»åž‹å¿«æ·é€‰æ‹©ï¼š1原料、2半成品、3成品、4辅料 |
| itemOrProduct | åˆ†ç±»è¡¨å• | ç‰©æ–™/产品标识,用于 MES å…¼å®¹ |
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| ç‰©æ–™åˆ†ç±»é€‰æ‹© | ä½¿ç”¨ `mes_md_item_type` è¡¨ï¼ŒMDM å’Œ MES å…±ç”¨åŒä¸€å¥—分类数据 |
| itemType ä¸Ž categoryId å…³ç³» | å½“ categoryId ä¸ºç©ºæ—¶ï¼Œå¯æ ¹æ® itemType è‡ªåŠ¨åŒ¹é…å¯¹åº”åç§°çš„åˆ†ç±» |
| itemType æ˜ å°„ | 1→原料,2→半成品,3→成品,4→辅料 |
| åˆ†ç±»æ ¡éªŒ | ä¿å­˜ç‰©æ–™æ—¶ï¼Œä¼šé€šè¿‡ RPC API æ ¡éªŒåˆ†ç±»æ˜¯å¦å­˜åœ¨ |
| åˆ†ç±»ç®¡ç† | ç»Ÿä¸€åœ¨ MDM æ¨¡å—管理,MES æ¨¡å—只读 |
| åˆ†ç±»åˆ é™¤ | æœ‰å­åˆ†ç±»æˆ–有物料引用时不可删除 |
| ç‰©æ–™åŒæ­¥ | MDM ç‰©æ–™åŒæ­¥åˆ° MES æ—¶ï¼Œåˆ†ç±» ID ä¿æŒä¸€è‡´ |
| å…¼å®¹æ€§ | MES åˆ†ç±»æœåŠ¡ä¼˜å…ˆä»Ž MDM èŽ·å–ï¼Œæœ¬åœ°è¡¨ä½œä¸ºå›žé€€ |
## æ³¨æ„äº‹é¡¹
- ç‰©æ–™åˆ†ç±»æ•°æ®åœ¨ MES æ¨¡å—管理,MDM æ¨¡å—通过 RPC API è°ƒç”¨èŽ·å–
- `itemType` æ˜¯å¿«æ·é€‰æ‹©å­—段,最终会转换为对应的分类编码
- å¦‚果同时设置了 `categoryId` å’Œ `itemType`,优先使用 `categoryId`
- åˆ†ç±»æ”¯æŒæ ‘形结构,可通过 `parentId` æž„建层级关系
1. **菜单变更**:MES çš„"物料产品分类"菜单已禁用,统一使用 MDM çš„"物料分类"菜单
2. **数据一致性**:两表数据已同步,后续维护只需操作 MDM åˆ†ç±»
3. **API å…¼å®¹**:MES åˆ†ç±»æŽ¥å£ä¿ç•™ï¼Œåº•层读取 MDM æ•°æ®ï¼Œå‰ç«¯å¯ç»§ç»­ä½¿ç”¨
4. **字段转换**:前端展示时,`itemOrProduct` å€¼éœ€è½¬æ¢ä¸ºä¸­æ–‡ï¼ˆITEM=物料,PRODUCT=产品)
docs/material_stock_adjust.md
@@ -29,8 +29,10 @@
| warehouseId | Long | æ˜¯ | ä»“库ID |
| locationId | Long | æ˜¯ | åº“区ID |
| areaId | Long | æ˜¯ | åº“位ID |
| batchId | Long | å¦ | æ‰¹æ¬¡ID |
| batchCode | String | å¦ | æ‰¹æ¬¡å· |
| batchId | Long | å¦ | æ‰¹æ¬¡ID(选择已有批次时传入) |
| batchCode | String | å¦ | æ‰¹æ¬¡å·ï¼ˆæ–°å»ºæ‰¹æ¬¡æ—¶ä¼ å…¥ï¼‰ |
| produceDate | DateTime | å¦ | ç”Ÿäº§æ—¥æœŸï¼ˆæ–°å»ºæ‰¹æ¬¡æ—¶ä¼ å…¥ï¼‰ |
| expireDate | DateTime | å¦ | æœ‰æ•ˆæœŸï¼ˆæ–°å»ºæ‰¹æ¬¡æ—¶ä¼ å…¥ï¼‰ |
| vendorId | Long | å¦ | ä¾›åº”商ID |
| quantity | BigDecimal | æ˜¯ | å…¥åº“数量(必须大于0) |
| reason | String | å¦ | å…¥åº“原因 |
@@ -59,7 +61,7 @@
| warehouseId | Long | æ˜¯ | ä»“库ID |
| locationId | Long | æ˜¯ | åº“区ID |
| areaId | Long | æ˜¯ | åº“位ID |
| batchId | Long | å¦ | æ‰¹æ¬¡ID |
| batchId | Long | å¦ | æ‰¹æ¬¡ID(出库时校验批次存在) |
| batchCode | String | å¦ | æ‰¹æ¬¡å· |
| vendorId | Long | å¦ | ä¾›åº”商ID |
| quantity | BigDecimal | æ˜¯ | å‡ºåº“数量(必须大于0) |
@@ -141,6 +143,40 @@
| bizId | åº“存记录ID | åº“存记录ID |
| bizCode | STOCK_ADJUST_{库存ID} | STOCK_ADJUST_{库存ID} |
## æ‰¹æ¬¡å¤„理规则
自定义入库时,系统会根据物料批次配置自动处理批次:
> **批次配置来源**:基础数据(MDM)物料主数据的批次属性配置,在物料详情页维护。
### å…¥åº“批次处理逻辑
| æƒ…况 | å¤„理方式 |
|------|---------|
| ç‰©æ–™æœªå¯ç”¨æ‰¹æ¬¡ç®¡ç† | ä¸å…³è”批次 |
| ä¼ å…¥ batchId | æ ¡éªŒæ‰¹æ¬¡å­˜åœ¨ä¸”归属正确,使用已有批次 |
| æœªä¼  batchId(物料启用批次管理) | æ ¹æ®å‚数自动创建批次记录 |
### æ–°å»ºæ‰¹æ¬¡æ—¶çš„参数
当物料启用批次管理且未传入 batchId æ—¶ï¼Œå¯ä¼ å…¥ä»¥ä¸‹å‚数用于创建批次:
| å‚æ•° | è¯´æ˜Ž |
|------|------|
| batchCode | æ‰¹æ¬¡å·ï¼ˆå¯é€‰ï¼Œä¸ä¼ åˆ™ç³»ç»Ÿè‡ªåŠ¨ç”Ÿæˆï¼‰ |
| produceDate | ç”Ÿäº§æ—¥æœŸ |
| expireDate | æœ‰æ•ˆæœŸ |
| vendorId | ä¾›åº”商ID |
> ç³»ç»Ÿä¼šæ ¹æ®ç‰©æ–™çš„æ‰¹æ¬¡é…ç½®æ ¡éªŒå¿…填参数(如生产日期、有效期等)
### å‡ºåº“批次处理逻辑
| æƒ…况 | å¤„理方式 |
|------|---------|
| ç‰©æ–™æœªå¯ç”¨æ‰¹æ¬¡ç®¡ç† | ä¸æ ¡éªŒæ‰¹æ¬¡ |
| ä¼ å…¥ batchId | æ ¡éªŒæ‰¹æ¬¡å­˜åœ¨ä¸”与物料匹配 |
## æ³¨æ„äº‹é¡¹
- å…¥åº“数量/出库数量必须大于0
docs/mdm_item_batch_config.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,215 @@
# MDM ç‰©æ–™æ‰¹æ¬¡å±žæ€§é…ç½® - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- åŸºç¡€æ•°æ® - ç‰©æ–™äº§å“åˆ—表页
- ç‰©æ–™è¯¦æƒ…/编辑页
- MES ç”Ÿäº§ - ç‰©æ–™äº§å“è¯¦æƒ…页
## ä¸šåŠ¡åœºæ™¯
当物料启用批次管理(`isBatchManaged = true`)时,需要配置批次属性,定义创建批次时哪些字段是必填的。
**批次属性字段(仅保留以下7个):**
| å­—段 | è¯´æ˜Ž |
|------|------|
| ç”Ÿäº§æ—¥æœŸ | æ‰¹æ¬¡çš„生产日期 |
| æœ‰æ•ˆæœŸ | æ‰¹æ¬¡çš„失效日期 |
| å…¥åº“日期 | æ‰¹æ¬¡çš„入库日期 |
| ä¾›åº”商 | æ‰¹æ¬¡çš„供应商 |
| é‡‡è´­è®¢å•编号 | æ‰¹æ¬¡çš„采购订单号 |
| ç”Ÿäº§æ‰¹å· | æ‰¹æ¬¡çš„æ‰¹å· |
| è´¨é‡çŠ¶æ€ | æ‰¹æ¬¡çš„质量状态 |
## åŒå‘同步说明
```
┌─────────────────────────────────────────────────────────────┐
│                    æ‰¹æ¬¡é…ç½®åŒå‘同步                           â”‚
├─────────────────────────────────────────────────────────────┤
│                                                             â”‚
│   MDM ç‰©æ–™é¡µé¢  â†â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â†’  MES ç‰©æ–™é¡µé¢             â”‚
│        â”‚                                  â”‚                 â”‚
│        â–¼                                  â–¼                 â”‚
│   mdm_item_batch_config  â†â”€â”€â”€â”€â”€â”€â”€â”€â”€â†’  mes_md_item_batch_config â”‚
│        â”‚                                  â”‚                 â”‚
│        â””──────────主存储──────────────────┘                 â”‚
│                                                             â”‚
│   è¯´æ˜Žï¼š                                                    â”‚
│   - MDM è¡¨æ˜¯ä¸»å­˜å‚¨ï¼ŒMES è¡¨æ˜¯é•œåƒ                            â”‚
│   - ä»»æ„ä¸€æ–¹ä¿®æ”¹ï¼Œè‡ªåŠ¨åŒæ­¥åˆ°å¦ä¸€æ–¹                           â”‚
│   - æŸ¥è¯¢æ—¶ä¼˜å…ˆä»Ž MDM è¡¨èŽ·å–                                 â”‚
│                                                             â”‚
└─────────────────────────────────────────────────────────────┘
```
## API
### èŽ·å–ç‰©æ–™æ‰¹æ¬¡é…ç½®
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | `/mes/md/item-batch-config/get-by-item-id` | æ ¹æ®ç‰©æ–™ID获取批次配置 |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| itemId | Long | æ˜¯ | ç‰©æ–™ID |
**响应:**
```json
{
  "code": 0,
  "data": {
    "id": 1,
    "itemId": 8,
    "produceDateFlag": true,
    "expireDateFlag": true,
    "receiptDateFlag": false,
    "vendorFlag": true,
    "purchaseOrderCodeFlag": false,
    "lotNumberFlag": true,
    "qualityStatusFlag": false
  },
  "msg": "操作成功"
}
```
**响应字段说明:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| id | Long | é…ç½®ID |
| itemId | Long | ç‰©æ–™ID |
| produceDateFlag | Boolean | ç”Ÿäº§æ—¥æœŸå¿…å¡« |
| expireDateFlag | Boolean | æœ‰æ•ˆæœŸå¿…å¡« |
| receiptDateFlag | Boolean | å…¥åº“日期必填 |
| vendorFlag | Boolean | ä¾›åº”商必填 |
| purchaseOrderCodeFlag | Boolean | é‡‡è´­è®¢å•号必填 |
| lotNumberFlag | Boolean | ç”Ÿäº§æ‰¹å·å¿…å¡« |
| qualityStatusFlag | Boolean | è´¨é‡çŠ¶æ€å¿…å¡« |
### ä¿å­˜ç‰©æ–™æ‰¹æ¬¡é…ç½®
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | `/mes/md/item-batch-config/save` | ä¿å­˜æ‰¹æ¬¡é…ç½®ï¼ˆæ–°å¢ž/更新) |
**请求参数:**
```json
{
  "itemId": 8,
  "produceDateFlag": true,
  "expireDateFlag": true,
  "receiptDateFlag": false,
  "vendorFlag": true,
  "purchaseOrderCodeFlag": false,
  "lotNumberFlag": true,
  "qualityStatusFlag": false
}
```
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| itemId | Long | æ˜¯ | ç‰©æ–™ID |
| produceDateFlag | Boolean | å¦ | ç”Ÿäº§æ—¥æœŸå¿…填,默认 false |
| expireDateFlag | Boolean | å¦ | æœ‰æ•ˆæœŸå¿…填,默认 false |
| receiptDateFlag | Boolean | å¦ | å…¥åº“日期必填,默认 false |
| vendorFlag | Boolean | å¦ | ä¾›åº”商必填,默认 false |
| purchaseOrderCodeFlag | Boolean | å¦ | é‡‡è´­è®¢å•号必填,默认 false |
| lotNumberFlag | Boolean | å¦ | ç”Ÿäº§æ‰¹å·å¿…填,默认 false |
| qualityStatusFlag | Boolean | å¦ | è´¨é‡çŠ¶æ€å¿…å¡«ï¼Œé»˜è®¤ false |
**响应:**
```json
{
  "code": 0,
  "data": 1,
  "msg": "操作成功"
}
```
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. ç‰©æ–™è¯¦æƒ…/编辑页
在物料详情页增加"批次属性配置"区块。
**展示条件:**
- å½“物料的 `isBatchManaged = true` æ—¶æ˜¾ç¤ºè¯¥é…ç½®åŒºå—
- å½“物料的 `isBatchManaged = false` æ—¶éšè—è¯¥é…ç½®åŒºå—
### 2. æ‰¹æ¬¡å±žæ€§é…ç½®è¡¨å•
**表单字段(仅7个):**
| å­—段 | æŽ§ä»¶ç±»åž‹ | è¯´æ˜Ž |
|------|----------|------|
| ç”Ÿäº§æ—¥æœŸ | el-checkbox | å‹¾é€‰è¡¨ç¤ºåˆ›å»ºæ‰¹æ¬¡æ—¶å¿…å¡« |
| æœ‰æ•ˆæœŸ | el-checkbox | å‹¾é€‰è¡¨ç¤ºåˆ›å»ºæ‰¹æ¬¡æ—¶å¿…å¡« |
| å…¥åº“日期 | el-checkbox | å‹¾é€‰è¡¨ç¤ºåˆ›å»ºæ‰¹æ¬¡æ—¶å¿…å¡« |
| ä¾›åº”商 | el-checkbox | å‹¾é€‰è¡¨ç¤ºåˆ›å»ºæ‰¹æ¬¡æ—¶å¿…å¡« |
| é‡‡è´­è®¢å•号 | el-checkbox | å‹¾é€‰è¡¨ç¤ºåˆ›å»ºæ‰¹æ¬¡æ—¶å¿…å¡« |
| ç”Ÿäº§æ‰¹å· | el-checkbox | å‹¾é€‰è¡¨ç¤ºåˆ›å»ºæ‰¹æ¬¡æ—¶å¿…å¡« |
| è´¨é‡çŠ¶æ€ | el-checkbox | å‹¾é€‰è¡¨ç¤ºåˆ›å»ºæ‰¹æ¬¡æ—¶å¿…å¡« |
**建议布局:**
```
┌─────────────────────────────────────────────────────────────┐
│ æ‰¹æ¬¡å±žæ€§é…ç½®                                                  â”‚
├─────────────────────────────────────────────────────────────┤
│  â˜‘ ç”Ÿäº§æ—¥æœŸ    â˜‘ æœ‰æ•ˆæœŸ      â˜ å…¥åº“日期    â˜‘ ä¾›åº”商         â”‚
│  â˜ é‡‡è´­è®¢å•号   â˜‘ ç”Ÿäº§æ‰¹å·    â˜ è´¨é‡çŠ¶æ€                    â”‚
└─────────────────────────────────────────────────────────────┘
```
### 3. æ•°æ®åŠ è½½é€»è¾‘
**进入物料详情页时:**
1. åŠ è½½ç‰©æ–™åŸºæœ¬ä¿¡æ¯
2. åˆ¤æ–­ `isBatchManaged` æ˜¯å¦ä¸º `true`
3. å¦‚果是,调用 `GET /mes/md/item-batch-config/get-by-item-id?itemId={物料ID}` èŽ·å–é…ç½®
4. æ¸²æŸ“配置表单,将返回的配置数据绑定到表单
**保存物料时:**
1. å…ˆä¿å­˜ç‰©æ–™åŸºæœ¬ä¿¡æ¯
2. å¦‚æžœ `isBatchManaged = true`,调用 `POST /mes/md/item-batch-config/save` ä¿å­˜æ‰¹æ¬¡é…ç½®
### 4. æŽ¨èé»˜è®¤é…ç½®
对于常见的采购物料,推荐默认配置:
| å­—段 | æŽ¨è | è¯´æ˜Ž |
|------|------|------|
| ç”Ÿäº§æ—¥æœŸ | â˜‘ | è¿½æº¯ç”Ÿäº§æ—¶é—´ |
| æœ‰æ•ˆæœŸ | â˜‘ | æœ‰ä¿è´¨æœŸçš„物料 |
| å…¥åº“日期 | â˜ | é€šå¸¸è‡ªåŠ¨è®°å½• |
| ä¾›åº”商 | â˜‘ | é‡‡è´­ç‰©æ–™éœ€è¦è¿½æº¯æ¥æº |
| é‡‡è´­è®¢å•号 | â˜ | å¯é€‰ |
| ç”Ÿäº§æ‰¹å· | â˜‘ | æ‰¹æ¬¡è¿½æº¯ |
| è´¨é‡çŠ¶æ€ | â˜ | éœ€è¦è´¨æ£€æ—¶ä½¿ç”¨ |
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| ç‰©æ–™æœªå¯ç”¨æ‰¹æ¬¡ç®¡ç† | ä¸æ˜¾ç¤ºæ‰¹æ¬¡å±žæ€§é…ç½®åŒºå— |
| ç‰©æ–™å¯ç”¨æ‰¹æ¬¡ç®¡ç†ä½†æ— é…ç½® | åˆ›å»ºæ‰¹æ¬¡æ—¶å…è®¸ä»»æ„å­—段(宽松模式) |
| ç‰©æ–™å¯ç”¨æ‰¹æ¬¡ç®¡ç†ä¸”有配置 | åˆ›å»ºæ‰¹æ¬¡æ—¶æ ¡éªŒå¿…填字段 |
| MDM ä¿®æ”¹æ‰¹æ¬¡é…ç½® | è‡ªåŠ¨åŒæ­¥åˆ° MES è¡¨ |
| MES ä¿®æ”¹æ‰¹æ¬¡é…ç½® | è‡ªåŠ¨åŒæ­¥åˆ° MDM è¡¨ |
## æ³¨æ„äº‹é¡¹
- æ‰¹æ¬¡é…ç½®æ˜¯ç‰©æ–™çº§åˆ«çš„,每个物料独立配置
- ä¿®æ”¹æ‰¹æ¬¡é…ç½®ä¸ä¼šå½±å“å·²åˆ›å»ºçš„æ‰¹æ¬¡ï¼Œåªå½±å“åŽç»­æ–°åˆ›å»ºçš„æ‰¹æ¬¡
- MDM å’Œ MES çš„æ‰¹æ¬¡é…ç½®ä¼šè‡ªåŠ¨åŒå‘åŒæ­¥
- åªä¿ç•™å¿…要的7个批次属性字段,其他字段不再使用
docs/mdm_mes_item_sync.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,343 @@
# MDM ä¸Ž MES ç‰©æ–™åŒå‘同步 - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- MDM æ¨¡å—:物料主数据列表页、物料新增/编辑页
- MES æ¨¡å—:物料产品列表页、物料产品新增/编辑页
## ä¸šåŠ¡æµç¨‹è¯´æ˜Ž
### åŒå‘同步关系
```
MDM ç‰©æ–™ â†â€•―――――→ MES ç‰©æ–™
    â†‘                  â†‘
    | syncMes=true     | syncMdm=true
    â””──────────────────┘
```
| æ“ä½œç«¯ | åŒæ­¥è§¦å‘条件 | åŒæ­¥ç›®æ ‡ |
|--------|-------------|---------|
| MDM æ–°å¢ž/编辑物料 | `syncMes = true` | MES `mes_md_item` + `mes_md_item_batch_config` |
| MES æ–°å¢ž/编辑物料 | `syncMdm = true` | MDM `mdm_item` + `mdm_item_batch_config` |
### æ‰¹æ¬¡å±žæ€§å­—段(7个必填项)
| å­—段 | ä¸­æ–‡å | è¯´æ˜Ž |
|------|--------|------|
| produceDateFlag | ç”Ÿäº§æ—¥æœŸå¿…å¡« | å…¥åº“时是否必填生产日期 |
| expireDateFlag | æœ‰æ•ˆæœŸå¿…å¡« | å…¥åº“时是否必填有效期 |
| receiptDateFlag | å…¥åº“日期必填 | å…¥åº“时是否必填入库日期 |
| vendorFlag | ä¾›åº”商必填 | å…¥åº“时是否必填供应商 |
| purchaseOrderCodeFlag | é‡‡è´­è®¢å•号必填 | å…¥åº“时是否必填采购订单号 |
| lotNumberFlag | ç”Ÿäº§æ‰¹å·å¿…å¡« | å…¥åº“时是否必填生产批号 |
| qualityStatusFlag | è´¨é‡çŠ¶æ€å¿…å¡« | å…¥åº“时是否必填质量状态 |
---
## API
### 1. MDM ç‰©æ–™æ–°å¢ž/编辑
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | `/admin-api/mdm/item/create` | æ–°å¢ž MDM ç‰©æ–™ |
| PUT | `/admin-api/mdm/item/update` | ç¼–辑 MDM ç‰©æ–™ |
**请求参数(MdmItemSaveReqVO):**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | å¦ | ç‰©æ–™ç¼–号(编辑时必填) |
| code | String | æ˜¯ | ç‰©æ–™ç¼–码 |
| name | String | æ˜¯ | ç‰©æ–™åç§° |
| specification | String | å¦ | è§„格型号 |
| categoryId | Long | å¦ | åˆ†ç±»ID(关联 MES ç‰©æ–™åˆ†ç±»ï¼‰ |
| unitMeasureId | Long | å¦ | è®¡é‡å•位ID |
| itemType | Integer | æ˜¯ | ç‰©æ–™ç±»åž‹ï¼š1原料, 2半成品, 3成品, 4辅料 |
| status | Integer | æ˜¯ | çŠ¶æ€ï¼š0启用, 1禁用 |
| minStock | BigDecimal | å¦ | æœ€ä½Žåº“å­˜ |
| maxStock | BigDecimal | å¦ | æœ€é«˜åº“å­˜ |
| isBatchManaged | Boolean | å¦ | æ˜¯å¦æ‰¹æ¬¡ç®¡ç† |
| isHighValue | Boolean | å¦ | æ˜¯å¦é«˜å€¼ç‰©æ–™ |
| remark | String | å¦ | å¤‡æ³¨ |
| **syncMes** | **Boolean** | **否** | **是否同步到 MES** |
| **produceDateFlag** | **Boolean** | **否** | **生产日期必填** |
| **expireDateFlag** | **Boolean** | **否** | **有效期必填** |
| **receiptDateFlag** | **Boolean** | **否** | **入库日期必填** |
| **vendorFlag** | **Boolean** | **否** | **供应商必填** |
| **purchaseOrderCodeFlag** | **Boolean** | **否** | **采购订单号必填** |
| **lotNumberFlag** | **Boolean** | **否** | **生产批号必填** |
| **qualityStatusFlag** | **Boolean** | **否** | **质量状态必填** |
**响应:**
```json
{
  "code": 0,
  "msg": "操作成功",
  "data": 123  // ç‰©æ–™ID
}
```
### 2. MES ç‰©æ–™æ–°å¢ž/编辑
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | `/admin-api/mes/md-item/create` | æ–°å¢ž MES ç‰©æ–™ |
| PUT | `/admin-api/mes/md-item/update` | ç¼–辑 MES ç‰©æ–™ |
**请求参数(MesMdItemSaveReqVO):**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | å¦ | ç‰©æ–™ç¼–号(编辑时必填) |
| code | String | æ˜¯ | ç‰©æ–™ç¼–码 |
| name | String | æ˜¯ | ç‰©æ–™åç§° |
| specification | String | å¦ | è§„格型号 |
| unitMeasureId | Long | æ˜¯ | è®¡é‡å•位ID |
| itemTypeId | Long | æ˜¯ | ç‰©æ–™åˆ†ç±»ID |
| safeStockFlag | Boolean | å¦ | æ˜¯å¦å¯ç”¨å®‰å…¨åº“å­˜ |
| minStock | BigDecimal | å¦ | æœ€ä½Žåº“å­˜ |
| maxStock | BigDecimal | å¦ | æœ€é«˜åº“å­˜ |
| highValue | Boolean | å¦ | æ˜¯å¦é«˜å€¼ç‰©æ–™ |
| batchFlag | Boolean | å¦ | æ˜¯å¦æ‰¹æ¬¡ç®¡ç† |
| remark | String | å¦ | å¤‡æ³¨ |
| **syncMdm** | **Boolean** | **否** | **是否同步到 MDM** |
**响应:**
```json
{
  "code": 0,
  "msg": "操作成功",
  "data": 456  // ç‰©æ–™ID
}
```
### 3. MES æ‰¹æ¬¡å±žæ€§é…ç½®ä¿å­˜
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | `/admin-api/mes/md-item-batch-config/create` | æ–°å¢žæ‰¹æ¬¡é…ç½® |
| PUT | `/admin-api/mes/md-item-batch-config/update` | ç¼–辑批次配置 |
**请求参数(MesMdItemBatchConfigSaveReqVO):**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | å¦ | é…ç½®ID(编辑时必填) |
| itemId | Long | æ˜¯ | ç‰©æ–™ID |
| produceDateFlag | Boolean | å¦ | ç”Ÿäº§æ—¥æœŸå¿…å¡« |
| expireDateFlag | Boolean | å¦ | æœ‰æ•ˆæœŸå¿…å¡« |
| receiptDateFlag | Boolean | å¦ | å…¥åº“日期必填 |
| vendorFlag | Boolean | å¦ | ä¾›åº”商必填 |
| purchaseOrderCodeFlag | Boolean | å¦ | é‡‡è´­è®¢å•号必填 |
| lotNumberFlag | Boolean | å¦ | ç”Ÿäº§æ‰¹å·å¿…å¡« |
| qualityStatusFlag | Boolean | å¦ | è´¨é‡çŠ¶æ€å¿…å¡« |
**响应:**
```json
{
  "code": 0,
  "msg": "操作成功",
  "data": 789  // é…ç½®ID
}
```
---
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. MDM ç‰©æ–™æ–°å¢ž/编辑页面
#### 1.1 è¡¨å•字段新增
在物料表单中新增以下字段:
| å­—段 | æŽ§ä»¶ç±»åž‹ | å±•示条件 | è¯´æ˜Ž |
|------|----------|----------|------|
| syncMes | Switch å¼€å…³ | å§‹ç»ˆæ˜¾ç¤º | æ˜¯å¦åŒæ­¥åˆ° MES |
| æ‰¹æ¬¡å±žæ€§é…ç½® | åˆ†ç»„标题 | `isBatchManaged = true` æ—¶æ˜¾ç¤º | æ‰¹æ¬¡å±žæ€§å¿…填配置 |
| produceDateFlag | Switch å¼€å…³ | `isBatchManaged = true` æ—¶æ˜¾ç¤º | ç”Ÿäº§æ—¥æœŸå¿…å¡« |
| expireDateFlag | Switch å¼€å…³ | `isBatchManaged = true` æ—¶æ˜¾ç¤º | æœ‰æ•ˆæœŸå¿…å¡« |
| receiptDateFlag | Switch å¼€å…³ | `isBatchManaged = true` æ—¶æ˜¾ç¤º | å…¥åº“日期必填 |
| vendorFlag | Switch å¼€å…³ | `isBatchManaged = true` æ—¶æ˜¾ç¤º | ä¾›åº”商必填 |
| purchaseOrderCodeFlag | Switch å¼€å…³ | `isBatchManaged = true` æ—¶æ˜¾ç¤º | é‡‡è´­è®¢å•号必填 |
| lotNumberFlag | Switch å¼€å…³ | `isBatchManaged = true` æ—¶æ˜¾ç¤º | ç”Ÿäº§æ‰¹å·å¿…å¡« |
| qualityStatusFlag | Switch å¼€å…³ | `isBatchManaged = true` æ—¶æ˜¾ç¤º | è´¨é‡çŠ¶æ€å¿…å¡« |
#### 1.2 æ•°æ®ç»“æž„
```javascript
data() {
  return {
    form: {
      // åŽŸæœ‰å­—æ®µ...
      syncMes: false,
      // æ‰¹æ¬¡å±žæ€§é…ç½®
      produceDateFlag: false,
      expireDateFlag: false,
      receiptDateFlag: false,
      vendorFlag: false,
      purchaseOrderCodeFlag: false,
      lotNumberFlag: false,
      qualityStatusFlag: false
    }
  }
}
```
#### 1.3 æäº¤é€»è¾‘
提交时将所有字段一起提交到 `/admin-api/mdm/item/create` æˆ– `/admin-api/mdm/item/update`。
**注意事项:**
- å½“ `isBatchManaged = true` æ—¶ï¼Œæ˜¾ç¤ºæ‰¹æ¬¡å±žæ€§é…ç½®åŒºåŸŸ
- åŽç«¯ä¼šè‡ªåŠ¨å°†æ‰¹æ¬¡é…ç½®ä¿å­˜åˆ° `mdm_item_batch_config` è¡¨
- å½“ `syncMes = true` æ—¶ï¼ŒåŽç«¯ä¼šåŒæ­¥ç‰©æ–™å’Œæ‰¹æ¬¡é…ç½®åˆ° MES
---
### 2. MES ç‰©æ–™æ–°å¢ž/编辑页面
#### 2.1 è¡¨å•字段新增
在物料表单中新增以下字段:
| å­—段 | æŽ§ä»¶ç±»åž‹ | å±•示条件 | è¯´æ˜Ž |
|------|----------|----------|------|
| syncMdm | Switch å¼€å…³ | å§‹ç»ˆæ˜¾ç¤º | æ˜¯å¦åŒæ­¥åˆ° MDM |
#### 2.2 æ•°æ®ç»“æž„
```javascript
data() {
  return {
    form: {
      // åŽŸæœ‰å­—æ®µ...
      syncMdm: false
    }
  }
}
```
#### 2.3 æäº¤é€»è¾‘
提交时将 `syncMdm` å­—段一起提交。
**注意事项:**
- å½“ `syncMdm = true` æ—¶ï¼ŒåŽç«¯ä¼šåŒæ­¥ç‰©æ–™å’Œæ‰¹æ¬¡é…ç½®åˆ° MDM
- MES çš„æ‰¹æ¬¡é…ç½®é€šè¿‡å•独的批次配置页面维护(已有功能)
---
### 3. MES æ‰¹æ¬¡å±žæ€§é…ç½®é¡µé¢ï¼ˆå·²æœ‰åŠŸèƒ½ï¼Œæ— éœ€ä¿®æ”¹ï¼‰
该页面已有,用于配置 MES ç‰©æ–™çš„æ‰¹æ¬¡å±žæ€§å¿…填项。当 MES ç‰©æ–™åŒæ­¥åˆ° MDM æ—¶ï¼Œä¼šè‡ªåŠ¨å¸¦ä¸Šè¿™äº›é…ç½®ã€‚
---
## å­—段展示规则
### MDM ç‰©æ–™åˆ—表页
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| code | åˆ—表、详情 | ç‰©æ–™ç¼–码 |
| name | åˆ—表、详情 | ç‰©æ–™åç§° |
| specification | åˆ—表、详情 | è§„格型号 |
| itemType | åˆ—表、详情 | ç‰©æ–™ç±»åž‹ï¼ˆéœ€è½¬æ¢æ˜¾ç¤ºï¼š1原料, 2半成品, 3成品, 4辅料) |
| status | åˆ—表、详情 | çŠ¶æ€ |
| isBatchManaged | åˆ—表、详情 | æ˜¯å¦æ‰¹æ¬¡ç®¡ç† |
| syncMes | è¯¦æƒ… | æ˜¯å¦å·²åŒæ­¥åˆ° MES(根据是否有对应 MES ç‰©æ–™åˆ¤æ–­ï¼‰ |
### MES ç‰©æ–™åˆ—表页
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| code | åˆ—表、详情 | ç‰©æ–™ç¼–码 |
| name | åˆ—表、详情 | ç‰©æ–™åç§° |
| specification | åˆ—表、详情 | è§„格型号 |
| itemTypeName | åˆ—表、详情 | ç‰©æ–™åˆ†ç±»åç§° |
| batchFlag | åˆ—表、详情 | æ˜¯å¦æ‰¹æ¬¡ç®¡ç† |
| syncMdm | è¯¦æƒ… | æ˜¯å¦å·²åŒæ­¥åˆ° MDM(根据是否有对应 MDM ç‰©æ–™åˆ¤æ–­ï¼‰ |
---
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
### åŒæ­¥è§„则
| åœºæ™¯ | è§„则 |
|------|------|
| MDM æ–°å¢žç‰©æ–™ + syncMes=true | åˆ›å»º MDM ç‰©æ–™ â†’ åŒæ­¥åˆ›å»º MES ç‰©æ–™å’Œæ‰¹æ¬¡é…ç½® |
| MDM ç¼–辑物料 + syncMes=true | æ›´æ–° MDM ç‰©æ–™ â†’ åŒæ­¥æ›´æ–° MES ç‰©æ–™å’Œæ‰¹æ¬¡é…ç½®ï¼ˆæŒ‰ code åŒ¹é…ï¼‰ |
| MES æ–°å¢žç‰©æ–™ + syncMdm=true | åˆ›å»º MES ç‰©æ–™ â†’ åŒæ­¥åˆ›å»º MDM ç‰©æ–™å’Œæ‰¹æ¬¡é…ç½® |
| MES ç¼–辑物料 + syncMdm=true | æ›´æ–° MES ç‰©æ–™ â†’ åŒæ­¥æ›´æ–° MDM ç‰©æ–™å’Œæ‰¹æ¬¡é…ç½®ï¼ˆæŒ‰ code åŒ¹é…ï¼‰ |
### æ‰¹æ¬¡é…ç½®åŒæ­¥è§„则
| åœºæ™¯ | è§„则 |
|------|------|
| MDM â†’ MES åŒæ­¥ | å°† MDM çš„ `mdm_item_batch_config` åŒæ­¥åˆ° MES çš„ `mes_md_item_batch_config` |
| MES â†’ MDM åŒæ­¥ | å°† MES çš„ `mes_md_item_batch_config` åŒæ­¥åˆ° MDM çš„ `mdm_item_batch_config` |
### å­—段映射规则
| MDM å­—段 | MES å­—段 | è¯´æ˜Ž |
|----------|----------|------|
| code | code | ç‰©æ–™ç¼–码,唯一标识 |
| name | name | ç‰©æ–™åç§° |
| specification | specification | è§„格型号 |
| unitMeasureId (MDM å•位ID) | unitMeasureId (MES å•位ID) | é€šè¿‡å•位编码转换 |
| categoryId (MDM åˆ†ç±»ID) | itemTypeId (MES åˆ†ç±»ID) | é€šè¿‡åˆ†ç±»ç¼–码转换 |
| itemType | itemOrProduct | 1/2/4→ITEM, 3→PRODUCT |
| isBatchManaged | batchFlag | æ˜¯å¦æ‰¹æ¬¡ç®¡ç† |
| isHighValue | highValue | æ˜¯å¦é«˜å€¼ç‰©æ–™ |
| minStock | minStock | æœ€ä½Žåº“å­˜ |
| maxStock | maxStock | æœ€é«˜åº“å­˜ |
### ç‰©æ–™ç±»åž‹æ˜ å°„
| MDM itemType | MDM è¯´æ˜Ž | MES itemOrProduct | MES è¯´æ˜Ž |
|--------------|----------|-------------------|----------|
| 1 | åŽŸæ–™ | ITEM | ç‰©æ–™ |
| 2 | åŠæˆå“ | ITEM | ç‰©æ–™ |
| 3 | æˆå“ | PRODUCT | äº§å“ |
| 4 | è¾…æ–™ | ITEM | ç‰©æ–™ |
---
## æ³¨æ„äº‹é¡¹
1. **同步失败处理**:同步操作与物料保存是同一事务,如果同步失败,整个保存操作会回滚。
2. **编码唯一性**:物料编码(code)在 MDM å’Œ MES ä¸­éƒ½å¿…须唯一,同步时按 code è¿›è¡ŒåŒ¹é…ï¼ˆå­˜åœ¨åˆ™æ›´æ–°ï¼Œä¸å­˜åœ¨åˆ™æ–°å¢žï¼‰ã€‚
3. **单位/分类映射**:MDM å’Œ MES çš„单位、分类是两套独立的主数据,同步时通过编码(code)进行映射,确保两边有相同的编码数据。
4. **批次配置同步时机**:
   - MDM ä¿å­˜ç‰©æ–™æ—¶ï¼Œæ‰¹æ¬¡é…ç½®ä¿å­˜åˆ°æœ¬åœ° `mdm_item_batch_config` è¡¨
   - å¦‚æžœ `syncMes=true`,同时同步到 MES çš„ `mes_md_item_batch_config` è¡¨
   - MES åŒç†
5. **前端展示建议**:
   - åœ¨åˆ—表页增加"同步状态"列,显示是否已同步到对方系统
   - å¯ä»¥é€šè¿‡ç‰©æ–™ç¼–码查询对方系统是否存在来判断同步状态
6. **数据一致性**:
   - å»ºè®®åœ¨ä¸€ç«¯ç»´æŠ¤ç‰©æ–™æ•°æ®åŽï¼ŒåŒæ­¥åˆ°å¦ä¸€ç«¯
   - é¿å…ä¸¤è¾¹åŒæ—¶ç¼–辑同一物料导致数据冲突
---
## è°ƒè¯•建议
1. é¦–先测试 MDM â†’ MES åŒæ­¥ï¼šåˆ›å»º MDM ç‰©æ–™ï¼Œå‹¾é€‰ syncMes,检查 MES æ˜¯å¦ç”Ÿæˆå¯¹åº”物料
2. æµ‹è¯• MES â†’ MDM åŒæ­¥ï¼šåˆ›å»º MES ç‰©æ–™ï¼Œå‹¾é€‰ syncMdm,检查 MDM æ˜¯å¦ç”Ÿæˆå¯¹åº”物料
3. æµ‹è¯•批次配置同步:在 MDM è®¾ç½®æ‰¹æ¬¡å±žæ€§ï¼ŒåŒæ­¥åŽæ£€æŸ¥ MES çš„æ‰¹æ¬¡é…ç½®æ˜¯å¦ä¸€è‡´
4. æ£€æŸ¥æ•°æ®åº“:
   - MDM ç‰©æ–™ï¼š`mdm_item`、`mdm_item_batch_config`
   - MES ç‰©æ–™ï¼š`mes_md_item`、`mes_md_item_batch_config`
docs/wm_transaction_list.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,135 @@
# åº“存事务流水 - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- åº“存事务流水列表页面(新增)
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /mes/wm/transaction/page | åˆ†é¡µæŸ¥è¯¢åº“存事务流水 |
### è¯·æ±‚参数
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| itemId | Long | å¦ | ç‰©æ–™ID |
| batchCode | String | å¦ | æ‰¹æ¬¡å·ï¼Œæ¨¡ç³ŠåŒ¹é… |
| warehouseId | Long | å¦ | ä»“库ID |
| locationId | Long | å¦ | åº“区ID |
| areaId | Long | å¦ | åº“位ID |
| type | Integer | å¦ | äº‹åŠ¡ç±»åž‹ï¼ˆ1-入库 2-出库) |
| bizType | Integer | å¦ | ä¸šåŠ¡ç±»åž‹ï¼ˆ126-自定义入库 127-自定义出库 ç­‰ï¼‰ |
| bizCode | String | å¦ | ä¸šåŠ¡å•å·ï¼Œæ¨¡ç³ŠåŒ¹é… |
| materialStockId | Long | å¦ | åº“存记录ID |
| startTime | LocalDateTime | å¦ | å¼€å§‹æ—¶é—´ |
| endTime | LocalDateTime | å¦ | ç»“束时间 |
| pageNo | Integer | æ˜¯ | é¡µç ï¼ˆç»§æ‰¿è‡ª PageParam) |
| pageSize | Integer | æ˜¯ | æ¯é¡µæ¡æ•°ï¼ˆç»§æ‰¿è‡ª PageParam) |
### å“åº”字段
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| id | Long | ç¼–号 |
| type | Integer | äº‹åŠ¡ç±»åž‹ï¼ˆ1-入库 2-出库) |
| typeName | String | äº‹åŠ¡ç±»åž‹åç§°ï¼ˆå…¥åº“/出库) |
| bizType | Integer | ä¸šåŠ¡ç±»åž‹ |
| bizTypeName | String | ä¸šåŠ¡ç±»åž‹åç§°ï¼ˆè‡ªå®šä¹‰å…¥åº“/自定义出库/采购入库/销售出库 ç­‰ï¼‰ |
| bizCode | String | ä¸šåŠ¡å•å· |
| itemId | Long | ç‰©æ–™ID |
| itemCode | String | ç‰©æ–™ç¼–码 |
| itemName | String | ç‰©æ–™åç§° |
| quantity | BigDecimal | å˜åŠ¨æ•°é‡ï¼ˆæ­£æ•°=入库,负数=出库) |
| batchId | Long | æ‰¹æ¬¡ID |
| batchCode | String | æ‰¹æ¬¡å· |
| warehouseId | Long | ä»“库ID |
| warehouseName | String | ä»“库名称 |
| locationId | Long | åº“区ID |
| locationName | String | åº“区名称 |
| areaId | Long | åº“位ID |
| areaName | String | åº“位名称 |
| transactionTime | LocalDateTime | äº‹åŠ¡å‘ç”Ÿæ—¶é—´ |
| createTime | LocalDateTime | åˆ›å»ºæ—¶é—´ |
### å“åº”示例
```json
{
  "code": 0,
  "data": {
    "total": 100,
    "list": [
      {
        "id": 1,
        "type": 1,
        "typeName": "入库",
        "bizType": 126,
        "bizTypeName": "自定义入库",
        "bizCode": "STOCK_ADJUST_6",
        "itemId": 2,
        "itemCode": "ITEM001",
        "itemName": "螺丝",
        "quantity": 100,
        "batchId": 7,
        "batchCode": "T8811",
        "warehouseId": 1,
        "warehouseName": "成品仓",
        "locationId": 1,
        "locationName": "A区",
        "areaId": 1,
        "areaName": "A-01-01",
        "transactionTime": "2025-07-15 10:30:00",
        "createTime": "2025-07-15 10:30:00"
      }
    ]
  },
  "msg": "操作成功"
}
```
## å­—段展示规则
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| typeName | åˆ—表、详情 | äº‹åŠ¡ç±»åž‹åç§°ï¼Œä½¿ç”¨æ ‡ç­¾å±•ç¤ºï¼ˆå…¥åº“=绿色,出库=红色) |
| bizTypeName | åˆ—表、详情 | ä¸šåŠ¡ç±»åž‹åç§° |
| bizCode | åˆ—表、详情 | ä¸šåŠ¡å•å·ï¼Œå¯ç‚¹å‡»è·³è½¬åˆ°å¯¹åº”ä¸šåŠ¡å•æ® |
| itemCode | åˆ—表 | ç‰©æ–™ç¼–码 |
| itemName | åˆ—表 | ç‰©æ–™åç§° |
| quantity | åˆ—表 | å˜åŠ¨æ•°é‡ï¼Œæ­£æ•°æ˜¾ç¤ºç»¿è‰²ï¼Œè´Ÿæ•°æ˜¾ç¤ºçº¢è‰² |
| batchCode | åˆ—表 | æ‰¹æ¬¡å· |
| warehouseName | åˆ—表 | ä»“库名称 |
| locationName | åˆ—表 | åº“区名称 |
| areaName | åˆ—表 | åº“位名称 |
| transactionTime | åˆ—表 | äº‹åŠ¡å‘ç”Ÿæ—¶é—´ï¼Œä½œä¸ºé»˜è®¤æŽ’åºå­—æ®µ |
## æœç´¢æ¡ä»¶
| æ¡ä»¶ | ç»„ä»¶ | è¯´æ˜Ž |
|------|------|------|
| ç‰©æ–™ | ç‰©æ–™é€‰æ‹©å™¨ | é€‰æ‹©ç‰©æ–™åŽæŸ¥è¯¢ |
| æ‰¹æ¬¡å· | Input | æ¨¡ç³ŠåŒ¹é… |
| ä»“库 | ä¸‹æ‹‰é€‰æ‹© | é€‰æ‹©ä»“库 |
| åº“区 | ä¸‹æ‹‰é€‰æ‹© | æ ¹æ®ä»“库联动 |
| åº“位 | ä¸‹æ‹‰é€‰æ‹© | æ ¹æ®åº“区联动 |
| äº‹åŠ¡ç±»åž‹ | ä¸‹æ‹‰é€‰æ‹© | å…¥åº“/出库 |
| ä¸šåŠ¡ç±»åž‹ | ä¸‹æ‹‰é€‰æ‹© | è‡ªå®šä¹‰å…¥åº“/自定义出库 ç­‰ |
| ä¸šåŠ¡å•å· | Input | æ¨¡ç³ŠåŒ¹é… |
| æ—¶é—´èŒƒå›´ | DatePicker | äº‹åŠ¡å‘ç”Ÿæ—¶é—´èŒƒå›´ |
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| æ•°æ®æ¥æº | åº“存事务流水由系统自动生成,不可人工创建、修改、删除 |
| æŽ’序 | é»˜è®¤æŒ‰äº‹åŠ¡å‘ç”Ÿæ—¶é—´å€’åºæŽ’åˆ— |
| æƒé™ | éœ€è¦ `mes:wm-transaction:query` æƒé™ |
## æ³¨æ„äº‹é¡¹
- æµæ°´è®°å½•只读,不允许新增、修改、删除
- quantity å­—段正数表示入库,负数表示出库,前端展示时可根据符号显示不同颜色
- bizCode å¯ä½œä¸ºè·³è½¬é“¾æŽ¥ï¼Œç‚¹å‡»åŽè·³è½¬åˆ°å¯¹åº”的业务单据详情页
- ä¸šåŠ¡ç±»åž‹è¾ƒå¤šï¼Œä¸‹æ‹‰é€‰æ‹©æ¡†å»ºè®®æŒ‰ä½¿ç”¨é¢‘çŽ‡æŽ’åºï¼ˆè‡ªå®šä¹‰å…¥åº“ã€è‡ªå®šä¹‰å‡ºåº“æ”¾å‰é¢ï¼‰
docs/wm_transaction_query.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,167 @@
# åº“存事务流水查询 - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- åº“存事务流水列表页(新增页面,菜单路径:仓储物流 > åº“存事务流水)
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /mes/wm/transaction/page | åˆ†é¡µæŸ¥è¯¢åº“存事务流水 |
### è¯·æ±‚参数
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| itemId | Long | å¦ | ç‰©æ–™ID |
| batchCode | String | å¦ | æ‰¹æ¬¡å·ï¼Œæ¨¡ç³ŠåŒ¹é… |
| warehouseId | Long | å¦ | ä»“库ID |
| locationId | Long | å¦ | åº“区ID |
| areaId | Long | å¦ | åº“位ID |
| type | Integer | å¦ | äº‹åŠ¡ç±»åž‹ï¼ˆ1-入库 2-出库) |
| bizType | Integer | å¦ | ä¸šåŠ¡ç±»åž‹ |
| bizCode | String | å¦ | ä¸šåŠ¡å•å·ï¼Œæ¨¡ç³ŠåŒ¹é… |
| materialStockId | Long | å¦ | åº“存记录ID |
| startTime | String | å¦ | å¼€å§‹æ—¶é—´ï¼ˆæ ¼å¼ï¼šyyyy-MM-dd HH:mm:ss) |
| endTime | String | å¦ | ç»“束时间(格式:yyyy-MM-dd HH:mm:ss) |
| pageNo | Integer | æ˜¯ | é¡µç ï¼Œé»˜è®¤1 |
| pageSize | Integer | æ˜¯ | æ¯é¡µæ¡æ•°ï¼Œé»˜è®¤10 |
### å“åº”字段
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| id | Long | ç¼–号 |
| type | Integer | äº‹åŠ¡ç±»åž‹ï¼ˆ1-入库 2-出库) |
| typeName | String | äº‹åŠ¡ç±»åž‹åç§°ï¼ˆå…¥åº“/出库) |
| bizType | Integer | ä¸šåŠ¡ç±»åž‹ç¼–ç  |
| bizTypeName | String | ä¸šåŠ¡ç±»åž‹åç§° |
| bizCode | String | ä¸šåŠ¡å•å· |
| itemId | Long | ç‰©æ–™ID |
| itemCode | String | ç‰©æ–™ç¼–码 |
| itemName | String | ç‰©æ–™åç§° |
| quantity | BigDecimal | å˜åŠ¨æ•°é‡ï¼ˆæ­£æ•°=入库,负数=出库) |
| batchId | Long | æ‰¹æ¬¡ID |
| batchCode | String | æ‰¹æ¬¡å· |
| warehouseId | Long | ä»“库ID |
| warehouseName | String | ä»“库名称 |
| locationId | Long | åº“区ID |
| locationName | String | åº“区名称 |
| areaId | Long | åº“位ID |
| areaName | String | åº“位名称 |
| transactionTime | String | äº‹åŠ¡å‘ç”Ÿæ—¶é—´ |
| createTime | String | åˆ›å»ºæ—¶é—´ |
### å“åº”示例
```json
{
  "code": 0,
  "data": {
    "total": 50,
    "list": [
      {
        "id": 1,
        "type": 1,
        "typeName": "入库",
        "bizType": 126,
        "bizTypeName": "自定义入库",
        "bizCode": "STOCK_ADJUST_6",
        "itemId": 2,
        "itemCode": "M001",
        "itemName": "螺丝",
        "quantity": 100.00,
        "batchId": 7,
        "batchCode": "T8811",
        "warehouseId": 1,
        "warehouseName": "成品仓",
        "locationId": 1,
        "locationName": "A区",
        "areaId": 1,
        "areaName": "A-01-01",
        "transactionTime": "2025-07-15 10:30:00",
        "createTime": "2025-07-15 10:30:00"
      }
    ]
  },
  "msg": "操作成功"
}
```
## ä¸šåŠ¡ç±»åž‹è¯´æ˜Ž
| bizType | bizTypeName | è¯´æ˜Ž |
|---------|-------------|------|
| 126 | è‡ªå®šä¹‰å…¥åº“ | åº“存调整入库 |
| 127 | è‡ªå®šä¹‰å‡ºåº“ | åº“存调整出库 |
| 110 | é‡‡è´­å…¥åº“ | é‡‡è´­å…¥åº“单 |
| 119 | é”€å”®å‡ºåº“ | é”€å”®å‡ºåº“单 |
| 112 | è°ƒæ‹¨å‡ºåº“ | åº“存调拨出库 |
| 113 | è°ƒæ‹¨å…¥åº“ | åº“存调拨入库 |
| 115 | å…¶ä»–入库 | å…¶ä»–入库单 |
| 114 | å…¶ä»–出库 | å…¶ä»–出库单 |
| 116 | ç”Ÿäº§é¢†æ–™ | ç”Ÿäº§é¢†æ–™å• |
| 117 | ç”Ÿäº§é€€æ–™ | ç”Ÿäº§é€€æ–™å• |
| 118 | ç”Ÿäº§å…¥åº“ | ç”Ÿäº§å…¥åº“单 |
| 123 | ç”Ÿäº§æ¶ˆè€— | ç”Ÿäº§æŠ¥å·¥æ¶ˆè€— |
| 124 | äº§å“äº§å‡º | ç”Ÿäº§æŠ¥å·¥äº§å‡º |
| 125 | ä¾›åº”商退货 | ä¾›åº”商退货出库 |
| 120 | é”€å”®é€€è´§ | é”€å”®é€€è´§å…¥åº“ |
| 121 | å¤–协发料 | å¤–协发料 |
| 122 | å¤–协入库 | å¤–协入库 |
## æœç´¢æ¡ä»¶
| æ¡ä»¶å­—段 | ç»„件类型 | è¯´æ˜Ž |
|----------|----------|------|
| ç‰©æ–™ | ç‰©æ–™é€‰æ‹©å¼¹çª— | é€‰æ‹©ç‰©æ–™åŽå¸¦å‡º itemId |
| æ‰¹æ¬¡å· | Input è¾“入框 | æ”¯æŒæ¨¡ç³Šæœç´¢ |
| ä»“库 | ä¸‹æ‹‰é€‰æ‹© | é€‰æ‹©ä»“库 |
| åº“区 | ä¸‹æ‹‰é€‰æ‹© | æ ¹æ®ä»“库联动加载 |
| åº“位 | ä¸‹æ‹‰é€‰æ‹© | æ ¹æ®åº“区联动加载 |
| äº‹åŠ¡ç±»åž‹ | ä¸‹æ‹‰é€‰æ‹© | é€‰é¡¹ï¼šå…¥åº“、出库 |
| ä¸šåŠ¡ç±»åž‹ | ä¸‹æ‹‰é€‰æ‹© | é€‰é¡¹è§ä¸Šè¡¨ä¸šåŠ¡ç±»åž‹è¯´æ˜Ž |
| ä¸šåŠ¡å•å· | Input è¾“入框 | æ”¯æŒæ¨¡ç³Šæœç´¢ |
| æ—¶é—´èŒƒå›´ | DatePicker æ—¥æœŸèŒƒå›´ | é€‰æ‹©äº‹åŠ¡å‘ç”Ÿæ—¶é—´èŒƒå›´ |
## åˆ—表字段展示
| åˆ—名 | å­—段 | å®½åº¦ | è¯´æ˜Ž |
|------|------|------|------|
| äº‹åŠ¡ç±»åž‹ | typeName | 80 | å…¥åº“显示绿色标签,出库显示红色标签 |
| ä¸šåŠ¡ç±»åž‹ | bizTypeName | 100 | ä¸šåŠ¡ç±»åž‹åç§° |
| ä¸šåŠ¡å•å· | bizCode | 150 | å¯ç‚¹å‡»è·³è½¬åˆ°å¯¹åº”业务单据 |
| ç‰©æ–™ç¼–码 | itemCode | 120 | - |
| ç‰©æ–™åç§° | itemName | 150 | - |
| æ‰¹æ¬¡å· | batchCode | 120 | æ— æ‰¹æ¬¡æ—¶æ˜¾ç¤ºç©º |
| æ•°é‡ | quantity | 100 | æ­£æ•°æ˜¾ç¤ºç»¿è‰²ï¼Œè´Ÿæ•°æ˜¾ç¤ºçº¢è‰² |
| ä»“库 | warehouseName | 100 | - |
| åº“区 | locationName | 100 | - |
| åº“位 | areaName | 100 | - |
| äº‹åŠ¡æ—¶é—´ | transactionTime | 160 | æ ¼å¼ï¼šyyyy-MM-dd HH:mm:ss |
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| æ•°æ®åªè¯» | æµæ°´è®°å½•由系统自动生成,不可人工新增、修改、删除 |
| é»˜è®¤æŽ’序 | æŒ‰äº‹åŠ¡æ—¶é—´å€’åºæŽ’åˆ— |
| æ•°é‡æ˜¾ç¤º | quantity > 0 æ˜¾ç¤ºç»¿è‰²ï¼Œquantity < 0 æ˜¾ç¤ºçº¢è‰² |
| ç±»åž‹æ ‡ç­¾ | å…¥åº“显示绿色 success æ ‡ç­¾ï¼Œå‡ºåº“显示红色 danger æ ‡ç­¾ |
| ä¸šåŠ¡å•å·è·³è½¬ | ç‚¹å‡» bizCode å¯è·³è½¬åˆ°å¯¹åº”业务单据详情页 |
## æƒé™é…ç½®
| æƒé™æ ‡è¯† | è¯´æ˜Ž |
|----------|------|
| mes:wm-transaction:query | æŸ¥è¯¢åº“存事务流水 |
## æ³¨æ„äº‹é¡¹
1. æµæ°´è®°å½•为系统自动生成,页面不提供新增、编辑、删除按钮
2. ä¸šåŠ¡å•å·ï¼ˆbizCode)可作为跳转链接,点击跳转到对应业务单据
3. æœç´¢æ¡ä»¶ä¸­çš„仓库、库区、库位存在联动关系:
   - é€‰æ‹©ä»“库后,库区下拉只显示该仓库下的库区
   - é€‰æ‹©åº“区后,库位下拉只显示该库区下的库位
4. æ—¶é—´èŒƒå›´æœç´¢é»˜è®¤å¯æŸ¥è¯¢æœ€è¿‘30天数据
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/sale/ErpSaleOrderApi.java
@@ -68,12 +68,16 @@
    /**
     * æ›´æ–°é”€å”®è®¢å•的出库状态
     *
     * æ ¹æ®æ˜Žç»†å‡ºåº“数量自动计算订单状态:
     * - æ‰€æœ‰æ˜Žç»†éƒ½å®Œæˆå‡ºåº“ â†’ å®ŒæˆçŠ¶æ€
     * - éƒ¨åˆ†æ˜Žç»†å®Œæˆå‡ºåº“ â†’ éƒ¨åˆ†å‡ºåº“状态
     *
     * @param orderId è®¢å•编号
     */
    CommonResult<Boolean> updateOrderOutStatus(Long orderId);
    /**
     * æ›´æ–°é”€å”®è®¢å•的出库状态为指定值
     *
     * @param orderId   è®¢å•编号
     * @param outStatus å‡ºåº“状态
     */
    CommonResult<Boolean> updateOrderOutStatusTo(Long orderId, Integer outStatus);
}
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/sale/enums/ErpSaleOrderOutStatusEnum.java
ÎļþÃû´Ó yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpSaleOrderOutStatusEnum.java ÐÞ¸Ä
@@ -1,4 +1,4 @@
package cn.iocoder.yudao.module.erp.enums;
package cn.iocoder.yudao.module.erp.api.sale.enums;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
@@ -16,8 +16,9 @@
public enum ErpSaleOrderOutStatusEnum implements ArrayValuable<Integer> {
    NOT_OUT(0, "未出库"),
    PART_OUT(1, "部分出库"),
    ALL_OUT(2, "全部出库");
    RESERVED(1, "已预留"),      // å‘货通知单审核通过
    PART_OUT(2, "部分出库"),    // éƒ¨åˆ†æ‹£è´§/出库
    ALL_OUT(3, "全部出库");     // å…¨éƒ¨å‡ºåº“完成
    private final Integer status;
    private final String name;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/api/sale/ErpSaleOrderApiImpl.java
@@ -111,4 +111,14 @@
        return success(true);
    }
    @Override
    public CommonResult<Boolean> updateOrderOutStatusTo(Long orderId, Integer outStatus) {
        if (orderId == null || outStatus == null) {
            return success(false);
        }
        saleOrderMapper.updateById(new ErpSaleOrderDO()
                .setId(orderId).setOutStatus(outStatus));
        return success(true);
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/ErpFinanceReceiptController.java
@@ -15,10 +15,10 @@
import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpAccountDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpFinanceReceiptDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpFinanceReceiptItemDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpCustomerDO;
import cn.iocoder.yudao.module.erp.service.finance.ErpAccountService;
import cn.iocoder.yudao.module.erp.service.finance.ErpFinanceReceiptService;
import cn.iocoder.yudao.module.erp.service.sale.ErpCustomerService;
import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO;
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;
@@ -49,7 +49,7 @@
    @Resource
    private ErpFinanceReceiptService financeReceiptService;
    @Resource
    private ErpCustomerService customerService;
    private CrmCustomerApi customerApi;
    @Resource
    private ErpAccountService accountService;
@@ -132,7 +132,7 @@
                convertSet(pageResult.getList(), ErpFinanceReceiptDO::getId));
        Map<Long, List<ErpFinanceReceiptItemDO>> financeReceiptItemMap = convertMultiMap(receiptItemList, ErpFinanceReceiptItemDO::getReceiptId);
        // 1.2 å®¢æˆ·ä¿¡æ¯
        Map<Long, ErpCustomerDO> customerMap = customerService.getCustomerMap(
        Map<Long, CrmCustomerRespDTO> customerMap = customerApi.getCustomerMap(
                convertSet(pageResult.getList(), ErpFinanceReceiptDO::getCustomerId));
        // 1.3 ç»“算账户信息
        Map<Long, ErpAccountDO> accountMap = accountService.getAccountMap(
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseRequestController.java
ÎļþÒÑɾ³ý
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestPageReqVO.java
ÎļþÒÑɾ³ý
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestRespVO.java
ÎļþÒÑɾ³ý
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestSaveReqVO.java
ÎļþÒÑɾ³ý
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpCustomerController.java
ÎļþÒÑɾ³ý
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/customer/ErpCustomerPageReqVO.java
ÎļþÒÑɾ³ý
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/customer/ErpCustomerRespVO.java
ÎļþÒÑɾ³ý
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/customer/ErpCustomerSaveReqVO.java
ÎļþÒÑɾ³ý
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderPageReqVO.java
@@ -17,32 +17,6 @@
@ToString(callSuper = true)
public class ErpSaleOrderPageReqVO extends PageParam {
    /**
     * å‡ºåº“状态 - æ— 
     */
    public static final Integer OUT_STATUS_NONE = 0;
    /**
     * å‡ºåº“状态 - éƒ¨åˆ†
     */
    public static final Integer OUT_STATUS_PART = 1;
    /**
     * å‡ºåº“状态 - å…¨éƒ¨
     */
    public static final Integer OUT_STATUS_ALL = 2;
    /**
     * é€€è´§çŠ¶æ€ - æ— 
     */
    public static final Integer RETURN_STATUS_NONE = 0;
    /**
     * é€€è´§çŠ¶æ€ - éƒ¨åˆ†
     */
    public static final Integer RETURN_STATUS_PART = 1;
    /**
     * é€€è´§çŠ¶æ€ - å…¨éƒ¨
     */
    public static final Integer RETURN_STATUS_ALL = 2;
    @Schema(description = "销售单编号", example = "XS001")
    private String no;
@@ -64,17 +38,5 @@
    @Schema(description = "产品编号", example = "1")
    private Long productId;
    @Schema(description = "出库状态", example = "2")
    private Integer outStatus;
    @Schema(description = "退货状态", example = "2")
    private Integer returnStatus;
    @Schema(description = "是否可出库", example = "true")
    private Boolean outEnable;
    @Schema(description = "是否可退货", example = "true")
    private Boolean returnEnable;
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java
@@ -98,13 +98,11 @@
    // ========== é”€å”®å‡ºåº“ ==========
    @Schema(description = "销售出库数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
    @Schema(description = "销售出库数量", example = "100.00")
    private BigDecimal outCount;
    // ========== é”€å”®é€€è´§ï¼ˆå‡ºåº“)) ==========
    @Schema(description = "销售退货数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
    private BigDecimal returnCount;
    @Schema(description = "出库状态", example = "0")
    private Integer outStatus;
    @Data
    public static class Item {
@@ -136,13 +134,8 @@
        // ========== é”€å”®å‡ºåº“ ==========
        @Schema(description = "销售出库数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
        @Schema(description = "销售出库数量", example = "100.00")
        private BigDecimal outCount;
        // ========== é”€å”®é€€è´§ï¼ˆå…¥åº“)) ==========
        @Schema(description = "销售退货数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
        private BigDecimal returnCount;
        // ========== å…³è”字段 ==========
@@ -154,8 +147,8 @@
        private String productUnitName;
        @Schema(description = "库存数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
        private BigDecimal stockCount; // è¯¥å­—段仅仅在“详情”和“编辑”时使用
        private BigDecimal stockCount; // è¯¥å­—段仅仅在详情和编辑时使用
    }
}
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/finance/ErpFinanceReceiptDO.java
@@ -1,7 +1,6 @@
package cn.iocoder.yudao.module.erp.dal.dataobject.finance;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpCustomerDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseOrderDO.java
@@ -53,16 +53,6 @@
     */
    private Long accountId;
    /**
     * å…³è”采购申请编号
     *
     * å…³è” {@link ErpPurchaseRequestDO#getId()}
     */
    private Long requestId;
    /**
     * é‡‡è´­ç”³è¯·å•号
     */
    private String requestNo;
    /**
     * ä¸‹å•æ—¶é—´
     */
    private LocalDateTime orderTime;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseRequestDO.java
ÎļþÒÑɾ³ý
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseRequestItemDO.java
ÎļþÒÑɾ³ý
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpCustomerDO.java
ÎļþÒÑɾ³ý
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleOrderDO.java
@@ -115,21 +115,16 @@
    private String remark;
    // ========== é”€å”®å‡ºåº“ ==========
    /**
     * é”€å”®å‡ºåº“数量
     */
    private BigDecimal outCount;
    // ========== é”€å”®é€€è´§ï¼ˆå…¥åº“)) ==========
    /**
     * é”€å”®é€€è´§æ•°é‡
     */
    private BigDecimal returnCount;
    /**
     * å‡ºåº“状态
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.module.erp.enums.ErpSaleOrderOutStatusEnum}
     * æžšä¸¾ {@link cn.iocoder.yudao.module.erp.api.sale.enums.ErpSaleOrderOutStatusEnum}
     */
    private Integer outStatus;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleOrderItemDO.java
@@ -78,15 +78,10 @@
    private String remark;
    // ========== é”€å”®å‡ºåº“ ==========
    /**
     * é”€å”®å‡ºåº“数量
     */
    private BigDecimal outCount;
    // ========== é”€å”®é€€è´§ï¼ˆå…¥åº“)) ==========
    /**
     * é”€å”®é€€è´§æ•°é‡
     */
    private BigDecimal returnCount;
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseRequestItemMapper.java
ÎļþÒÑɾ³ý
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseRequestMapper.java
ÎļþÒÑɾ³ý
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpCustomerMapper.java
ÎļþÒÑɾ³ý
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSaleOrderMapper.java
@@ -7,11 +7,8 @@
import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderPageReqVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOrderDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOrderItemDO;
import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.Objects;
/**
 * ERP é”€å”®è®¢å• Mapper
@@ -30,32 +27,6 @@
                .likeIfPresent(ErpSaleOrderDO::getRemark, reqVO.getRemark())
                .eqIfPresent(ErpSaleOrderDO::getCreator, reqVO.getCreator())
                .orderByDesc(ErpSaleOrderDO::getId);
        // å…¥åº“状态。为什么需要 t. çš„原因,是因为联表查询时,需要指定表名,不然会报 out_count é”™è¯¯
        if (Objects.equals(reqVO.getOutStatus(), ErpSaleOrderPageReqVO.OUT_STATUS_NONE)) {
            query.eq(ErpSaleOrderDO::getOutCount, 0);
        } else if (Objects.equals(reqVO.getOutStatus(), ErpSaleOrderPageReqVO.OUT_STATUS_PART)) {
            query.gt(ErpSaleOrderDO::getOutCount, 0).apply("t.out_count < t.total_count");
        } else if (Objects.equals(reqVO.getOutStatus(), ErpSaleOrderPageReqVO.OUT_STATUS_ALL)) {
            query.apply("t.out_count = t.total_count");
        }
        // é€€è´§çŠ¶æ€
        if (Objects.equals(reqVO.getReturnStatus(), ErpSaleOrderPageReqVO.RETURN_STATUS_NONE)) {
            query.eq(ErpSaleOrderDO::getReturnCount, 0);
        } else if (Objects.equals(reqVO.getReturnStatus(), ErpSaleOrderPageReqVO.RETURN_STATUS_PART)) {
            query.gt(ErpSaleOrderDO::getReturnCount, 0).apply("t.return_count < t.total_count");
        } else if (Objects.equals(reqVO.getReturnStatus(), ErpSaleOrderPageReqVO.RETURN_STATUS_ALL)) {
            query.apply("t.return_count = t.total_count");
        }
        // å¯é”€å”®å‡ºåº“
        if (Boolean.TRUE.equals(reqVO.getOutEnable())) {
            query.eq(ErpSaleOrderDO::getStatus, ErpAuditStatus.APPROVE.getStatus())
                    .apply("t.out_count < t.total_count");
        }
        // å¯é”€å”®é€€è´§
        if (Boolean.TRUE.equals(reqVO.getReturnEnable())) {
            query.eq(ErpSaleOrderDO::getStatus, ErpAuditStatus.APPROVE.getStatus())
                    .apply("t.return_count < t.out_count");
        }
        if (reqVO.getProductId() != null) {
            query.leftJoin(ErpSaleOrderItemDO.class, ErpSaleOrderItemDO::getOrderId, ErpSaleOrderDO::getId)
                    .eq(reqVO.getProductId() != null, ErpSaleOrderItemDO::getProductId, reqVO.getProductId())
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/redis/no/ErpNoRedisDAO.java
@@ -28,10 +28,6 @@
     * é‡‡è´­è®¢å• {@link cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderDO}
     */
    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}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpPurchaseRequestStatusEnum.java
ÎļþÒÑɾ³ý
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java
@@ -16,7 +16,6 @@
import cn.iocoder.yudao.module.erp.enums.ErpPurchaseOrderInStatusEnum;
import cn.iocoder.yudao.module.erp.service.finance.ErpAccountService;
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;
@@ -30,9 +29,6 @@
/**
 * ERP é‡‡è´­è®¢å• Service å®žçŽ°ç±»
 *
 * æ³¨æ„ï¼šé‡‡è´­è®¢å•不需要 BPM å®¡æ‰¹ï¼Œå®¡æ‰¹æµç¨‹åœ¨é‡‡è´­ç”³è¯·é˜¶æ®µå®Œæˆã€‚
 * é‡‡è´­è®¢å•由采购申请生成,或直接创建(状态为已审核)。
 *
 * @author èŠ‹é“æºç 
 */
@@ -52,10 +48,6 @@
    private ErpSupplierService supplierService;
    @Resource
    private ErpAccountService accountService;
    @Resource
    @Lazy
    private ErpPurchaseRequestService purchaseRequestService;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -320,10 +312,6 @@
        // 4. æ›´æ–°é‡‡è´­è®¢å•入库状态
        purchaseOrderMapper.updateById(new ErpPurchaseOrderDO()
                .setId(orderId).setInStatus(inStatus));
        // 5. åŒæ­¥æ›´æ–°é‡‡è´­ç”³è¯·çš„入库状态(如果有关联的采购申请)
        if (order.getRequestId() != null) {
            purchaseRequestService.updatePurchaseRequestInStatus(order.getRequestId(), inStatus);
        }
    }
    /**
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestService.java
ÎļþÒÑɾ³ý
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestServiceImpl.java
ÎļþÒÑɾ³ý
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/listener/ErpPurchaseRequestStatusListener.java
ÎļþÒÑɾ³ý
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpCustomerService.java
ÎļþÒÑɾ³ý
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpCustomerServiceImpl.java
ÎļþÒÑɾ³ý
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderService.java
@@ -65,14 +65,6 @@
    void updateSaleOrderOutCount(Long id, Map<Long, BigDecimal> outCountMap);
    /**
     * æ›´æ–°é”€å”®è®¢å•的退货数量
     *
     * @param orderId ç¼–号
     * @param returnCountMap é€€è´§æ•°é‡ Map:key é”€å”®è®¢å•项编号;value é€€è´§æ•°é‡
     */
    void updateSaleOrderReturnCount(Long orderId, Map<Long, BigDecimal> returnCountMap);
    /**
     * åˆ é™¤é”€å”®è®¢å•
     *
     * @param ids ç¼–号数组
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java
@@ -15,7 +15,7 @@
import cn.iocoder.yudao.module.erp.dal.mysql.sale.ErpSaleOrderMapper;
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.ErpSaleOrderOutStatusEnum;
import cn.iocoder.yudao.module.erp.api.sale.enums.ErpSaleOrderOutStatusEnum;
import cn.iocoder.yudao.module.erp.service.finance.ErpAccountService;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import jakarta.annotation.Resource;
@@ -83,7 +83,8 @@
        // 2.1 æ’入订单
        ErpSaleOrderDO saleOrder = BeanUtils.toBean(createReqVO, ErpSaleOrderDO.class, in -> in
                .setNo(no).setStatus(ErpAuditStatus.PROCESS.getStatus()));
                .setNo(no).setStatus(ErpAuditStatus.PROCESS.getStatus())
                .setOutCount(BigDecimal.ZERO).setOutStatus(ErpSaleOrderOutStatusEnum.NOT_OUT.getStatus()));
        calculateTotalPrice(saleOrder, saleOrderItems);
        saleOrderMapper.insert(saleOrder);
        // 2.2 æ’入订单项
@@ -149,11 +150,6 @@
        if (!approve && outCount.compareTo(BigDecimal.ZERO) > 0) {
            throw exception(SALE_ORDER_PROCESS_FAIL_EXISTS_OUT);
        }
        // 1.4 å­˜åœ¨é”€å”®é€€è´§å•,无法反审核
        BigDecimal returnCount = saleOrder.getReturnCount() != null ? saleOrder.getReturnCount() : BigDecimal.ZERO;
        if (!approve && returnCount.compareTo(BigDecimal.ZERO) > 0) {
            throw exception(SALE_ORDER_PROCESS_FAIL_EXISTS_RETURN);
        }
        // 2. æ›´æ–°çŠ¶æ€
        int updateCount = saleOrderMapper.updateByIdAndStatus(id, saleOrder.getStatus(),
@@ -176,11 +172,6 @@
        BigDecimal outCount = saleOrder.getOutCount() != null ? saleOrder.getOutCount() : BigDecimal.ZERO;
        if (outCount.compareTo(BigDecimal.ZERO) > 0) {
            throw exception(SALE_ORDER_CANCEL_FAIL_EXISTS_OUT);
        }
        // 1.4 å­˜åœ¨é”€å”®é€€è´§å•,无法作废
        BigDecimal returnCount = saleOrder.getReturnCount() != null ? saleOrder.getReturnCount() : BigDecimal.ZERO;
        if (returnCount.compareTo(BigDecimal.ZERO) > 0) {
            throw exception(SALE_ORDER_CANCEL_FAIL_EXISTS_RETURN);
        }
        // 2. æ›´æ–°çŠ¶æ€ä¸ºå·²ä½œåºŸ
@@ -269,26 +260,6 @@
        // 2. æ›´æ–°é”€å”®è®¢å•
        BigDecimal totalOutCount = getSumValue(outCountMap.values(), value -> value, BigDecimal::add, BigDecimal.ZERO);
        saleOrderMapper.updateById(new ErpSaleOrderDO().setId(id).setOutCount(totalOutCount));
    }
    @Override
    public void updateSaleOrderReturnCount(Long orderId, Map<Long, BigDecimal> returnCountMap) {
        List<ErpSaleOrderItemDO> orderItems = saleOrderItemMapper.selectListByOrderId(orderId);
        // 1. æ›´æ–°æ¯ä¸ªé”€å”®è®¢å•项
        orderItems.forEach(item -> {
            BigDecimal returnCount = returnCountMap.getOrDefault(item.getId(), BigDecimal.ZERO);
            if (item.getReturnCount().equals(returnCount)) {
                return;
            }
            if (returnCount.compareTo(item.getOutCount()) > 0) {
                throw exception(SALE_ORDER_ITEM_RETURN_FAIL_OUT_EXCEED,
                        "产品ID:" + item.getProductId(), item.getOutCount());
            }
            saleOrderItemMapper.updateById(new ErpSaleOrderItemDO().setId(item.getId()).setReturnCount(returnCount));
        });
        // 2. æ›´æ–°é”€å”®è®¢å•
        BigDecimal totalReturnCount = getSumValue(returnCountMap.values(), value -> value, BigDecimal::add, BigDecimal.ZERO);
        saleOrderMapper.updateById(new ErpSaleOrderDO().setId(orderId).setReturnCount(totalReturnCount));
    }
    @Override
@@ -403,4 +374,4 @@
        }
    }
}
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/category/MdmItemCategoryApi.java
@@ -39,4 +39,12 @@
     */
    CommonResult<MdmItemCategoryRespDTO> validateItemCategoryExists(Long id);
    /**
     * æ ¹æ®ç‰©æ–™/产品标识获取分类
     *
     * @param itemOrProduct ç‰©æ–™/产品标识
     * @return ç‰©æ–™åˆ†ç±»
     */
    CommonResult<MdmItemCategoryRespDTO> getItemCategoryByItemOrProduct(String itemOrProduct);
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/category/MdmItemCategoryApiImpl.java
@@ -46,4 +46,10 @@
        return success(BeanUtils.toBean(itemCategory, MdmItemCategoryRespDTO.class));
    }
    @Override
    public CommonResult<MdmItemCategoryRespDTO> getItemCategoryByItemOrProduct(String itemOrProduct) {
        MdmItemCategoryDO itemCategory = itemCategoryService.getItemCategoryByItemOrProduct(itemOrProduct);
        return success(BeanUtils.toBean(itemCategory, MdmItemCategoryRespDTO.class));
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/category/dto/MdmItemCategoryRespDTO.java
@@ -22,6 +22,12 @@
    @Schema(description = "物料类型", example = "1")
    private Integer itemType;
    @Schema(description = "物料/产品标识(兼容MES)", example = "ITEM")
    private String itemOrProduct;
    @Schema(description = "排序", example = "1")
    private Integer sort;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Integer status;
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/item/MdmItemBatchConfigApi.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,56 @@
package cn.iocoder.yudao.module.mdm.api.item;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemBatchConfigRespDTO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemBatchConfigDO;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
 * MDM ç‰©æ–™æ‰¹æ¬¡é…ç½® API æŽ¥å£
 *
 * @author èŠ‹é“æºç 
 */
public interface MdmItemBatchConfigApi {
    String PREFIX = "/rpc-api/mdm/item-batch-config";
    /**
     * æ ¹æ®ç‰©æ–™ç¼–号获取批次配置
     *
     * @param itemId ç‰©æ–™ç¼–号
     * @return æ‰¹æ¬¡é…ç½®ï¼ˆä¸å­˜åœ¨è¿”回 null)
     */
    CommonResult<MdmItemBatchConfigRespDTO> getItemBatchConfigByItemId(Long itemId);
    /**
     * æ ¹æ®ç‰©æ–™ç¼–号获取批次配置(便捷方法)
     *
     * @param itemId ç‰©æ–™ç¼–号
     * @return æ‰¹æ¬¡é…ç½®ï¼ˆä¸å­˜åœ¨è¿”回 null)
     */
    default MdmItemBatchConfigRespDTO getItemBatchConfig(Long itemId) {
        CommonResult<MdmItemBatchConfigRespDTO> result = getItemBatchConfigByItemId(itemId);
        return result != null ? result.getData() : null;
    }
    /**
     * ä¿å­˜æ‰¹æ¬¡é…ç½®
     *
     * @param config æ‰¹æ¬¡é…ç½®
     * @return é…ç½®ç¼–号
     */
    CommonResult<Long> saveItemBatchConfig(MdmItemBatchConfigDO config);
    /**
     * ä¿å­˜æ‰¹æ¬¡é…ç½®ï¼ˆä¾¿æ·æ–¹æ³•)
     *
     * @param config æ‰¹æ¬¡é…ç½®
     * @return é…ç½®ç¼–号
     */
    default Long saveItemBatch(MdmItemBatchConfigDO config) {
        CommonResult<Long> result = saveItemBatchConfig(config);
        return result != null ? result.getData() : null;
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/item/MdmItemBatchConfigApiImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.mdm.api.item;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemBatchConfigRespDTO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemBatchConfigDO;
import cn.iocoder.yudao.module.mdm.service.item.MdmItemBatchConfigService;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RestController;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
 * MDM ç‰©æ–™æ‰¹æ¬¡é…ç½® API å®žçŽ°ç±»
 *
 * @author èŠ‹é“æºç 
 */
@RestController
@Validated
public class MdmItemBatchConfigApiImpl implements MdmItemBatchConfigApi {
    @Resource
    private MdmItemBatchConfigService itemBatchConfigService;
    @Override
    public CommonResult<MdmItemBatchConfigRespDTO> getItemBatchConfigByItemId(Long itemId) {
        MdmItemBatchConfigDO config = itemBatchConfigService.getItemBatchConfigByItemId(itemId);
        return success(BeanUtils.toBean(config, MdmItemBatchConfigRespDTO.class));
    }
    @Override
    public CommonResult<Long> saveItemBatchConfig(MdmItemBatchConfigDO config) {
        return success(itemBatchConfigService.saveItemBatchConfig(config));
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/item/MdmItemSyncApi.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
package cn.iocoder.yudao.module.mdm.api.item;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemSyncReqDTO;
/**
 * MDM ç‰©æ–™åŒæ­¥ API æŽ¥å£
 *
 * ä¾› MES æ¨¡å—同步物料主数据到 MDM ä½¿ç”¨
 *
 * @author èŠ‹é“æºç 
 */
public interface MdmItemSyncApi {
    String PREFIX = "/rpc-api/mdm/item-sync";
    /**
     * åŒæ­¥ç‰©æ–™åˆ° MDM(按 code åŒ¹é…ï¼Œå­˜åœ¨åˆ™æ›´æ–°ï¼Œä¸å­˜åœ¨åˆ™åˆ›å»ºï¼‰
     *
     * @param syncReqDTO åŒæ­¥ä¿¡æ¯
     * @return MDM ç‰©æ–™ç¼–号
     */
    CommonResult<Long> syncItemToMdm(MdmItemSyncReqDTO syncReqDTO);
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/item/MdmItemSyncApiImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,102 @@
package cn.iocoder.yudao.module.mdm.api.item;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemSyncReqDTO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemBatchConfigDO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemDO;
import cn.iocoder.yudao.module.mdm.dal.mysql.item.MdmItemBatchConfigMapper;
import cn.iocoder.yudao.module.mdm.dal.mysql.item.MdmItemMapper;
import jakarta.annotation.Resource;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RestController;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
 * MDM ç‰©æ–™åŒæ­¥ API å®žçŽ°ç±»
 *
 * @author èŠ‹é“æºç 
 */
@RestController
@Validated
public class MdmItemSyncApiImpl implements MdmItemSyncApi {
    @Resource
    private MdmItemMapper itemMapper;
    @Resource
    private MdmItemBatchConfigMapper itemBatchConfigMapper;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public CommonResult<Long> syncItemToMdm(MdmItemSyncReqDTO syncReqDTO) {
        // 1. æŒ‰ code æŸ¥è¯¢æ˜¯å¦å·²å­˜åœ¨
        MdmItemDO existingItem = itemMapper.selectByCode(syncReqDTO.getCode());
        // 2. ä¿å­˜æˆ–更新物料主数据
        MdmItemDO item = BeanUtils.toBean(syncReqDTO, MdmItemDO.class);
        Long itemId;
        if (existingItem != null) {
            // æ›´æ–°
            item.setId(existingItem.getId());
            itemMapper.updateById(item);
            itemId = existingItem.getId();
        } else {
            // æ–°å¢ž
            itemMapper.insert(item);
            itemId = item.getId();
        }
        // 3. åŒæ­¥æ‰¹æ¬¡å±žæ€§é…ç½®
        syncBatchConfig(itemId, syncReqDTO);
        return success(itemId);
    }
    /**
     * åŒæ­¥æ‰¹æ¬¡å±žæ€§é…ç½®
     */
    private void syncBatchConfig(Long itemId, MdmItemSyncReqDTO syncReqDTO) {
        // æ£€æŸ¥æ˜¯å¦æœ‰æ‰¹æ¬¡é…ç½®æ•°æ®
        if (syncReqDTO.getProduceDateFlag() == null
                && syncReqDTO.getExpireDateFlag() == null
                && syncReqDTO.getReceiptDateFlag() == null
                && syncReqDTO.getVendorFlag() == null
                && syncReqDTO.getPurchaseOrderCodeFlag() == null
                && syncReqDTO.getLotNumberFlag() == null
                && syncReqDTO.getQualityStatusFlag() == null) {
            return;
        }
        // æŸ¥è¯¢å·²æœ‰é…ç½®
        MdmItemBatchConfigDO existing = itemBatchConfigMapper.selectOne(MdmItemBatchConfigDO::getItemId, itemId);
        MdmItemBatchConfigDO config = new MdmItemBatchConfigDO();
        config.setItemId(itemId);
        config.setProduceDateFlag(Boolean.TRUE.equals(syncReqDTO.getProduceDateFlag()));
        config.setExpireDateFlag(Boolean.TRUE.equals(syncReqDTO.getExpireDateFlag()));
        config.setReceiptDateFlag(Boolean.TRUE.equals(syncReqDTO.getReceiptDateFlag()));
        config.setVendorFlag(Boolean.TRUE.equals(syncReqDTO.getVendorFlag()));
        config.setPurchaseOrderCodeFlag(Boolean.TRUE.equals(syncReqDTO.getPurchaseOrderCodeFlag()));
        config.setLotNumberFlag(Boolean.TRUE.equals(syncReqDTO.getLotNumberFlag()));
        config.setQualityStatusFlag(Boolean.TRUE.equals(syncReqDTO.getQualityStatusFlag()));
        // æ¸…空不需要的字段
        config.setClientFlag(false);
        config.setSalesOrderCodeFlag(false);
        config.setWorkOrderFlag(false);
        config.setTaskFlag(false);
        config.setWorkstationFlag(false);
        config.setToolFlag(false);
        config.setMoldFlag(false);
        if (existing != null) {
            config.setId(existing.getId());
            itemBatchConfigMapper.updateById(config);
        } else {
            itemBatchConfigMapper.insert(config);
        }
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/item/dto/MdmItemBatchConfigRespDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,95 @@
package cn.iocoder.yudao.module.mdm.api.item.dto;
import lombok.Data;
import java.io.Serializable;
/**
 * MDM ç‰©æ–™æ‰¹æ¬¡é…ç½® Response DTO
 *
 * @author èŠ‹é“æºç 
 */
@Data
public class MdmItemBatchConfigRespDTO implements Serializable {
    /**
     * ç¼–号
     */
    private Long id;
    /**
     * ç‰©æ–™ç¼–号
     */
    private Long itemId;
    /**
     * ç”Ÿäº§æ—¥æœŸå¿…å¡«
     */
    private Boolean produceDateFlag;
    /**
     * æœ‰æ•ˆæœŸå¿…å¡«
     */
    private Boolean expireDateFlag;
    /**
     * å…¥åº“日期必填
     */
    private Boolean receiptDateFlag;
    /**
     * ä¾›åº”商必填
     */
    private Boolean vendorFlag;
    /**
     * å®¢æˆ·å¿…å¡«
     */
    private Boolean clientFlag;
    /**
     * é”€å”®è®¢å•编号必填
     */
    private Boolean salesOrderCodeFlag;
    /**
     * é‡‡è´­è®¢å•编号必填
     */
    private Boolean purchaseOrderCodeFlag;
    /**
     * ç”Ÿäº§å·¥å•å¿…å¡«
     */
    private Boolean workOrderFlag;
    /**
     * ç”Ÿäº§ä»»åŠ¡å¿…å¡«
     */
    private Boolean taskFlag;
    /**
     * å·¥ä½œç«™å¿…å¡«
     */
    private Boolean workstationFlag;
    /**
     * å·¥å…·å¿…å¡«
     */
    private Boolean toolFlag;
    /**
     * æ¨¡å…·å¿…å¡«
     */
    private Boolean moldFlag;
    /**
     * ç”Ÿäº§æ‰¹å·å¿…å¡«
     */
    private Boolean lotNumberFlag;
    /**
     * è´¨é‡çŠ¶æ€å¿…å¡«
     */
    private Boolean qualityStatusFlag;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/item/dto/MdmItemSyncReqDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,109 @@
package cn.iocoder.yudao.module.mdm.api.item.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
/**
 * MDM ç‰©æ–™åŒæ­¥ Request DTO
 *
 * @author èŠ‹é“æºç 
 */
@Schema(description = "MDM ç‰©æ–™åŒæ­¥ Request DTO")
@Data
public class MdmItemSyncReqDTO {
    @Schema(description = "物料编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "ITEM001")
    private String code;
    @Schema(description = "物料名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "螺丝")
    private String name;
    @Schema(description = "规格型号", example = "M6*20")
    private String specification;
    @Schema(description = "物料类型:1原料, 2半成品, 3成品, 4辅料", example = "1")
    private Integer itemType;
    @Schema(description = "计量单位ID", example = "1")
    private Long unitMeasureId;
    @Schema(description = "分类ID", example = "1")
    private Long categoryId;
    @Schema(description = "品牌ID", example = "1")
    private Long brandId;
    @Schema(description = "状态:0启用, 1禁用", example = "0")
    private Integer status;
    @Schema(description = "采购价", example = "10.00")
    private BigDecimal purchasePrice;
    @Schema(description = "销售价", example = "20.00")
    private BigDecimal salesPrice;
    @Schema(description = "成本价", example = "15.00")
    private BigDecimal costPrice;
    @Schema(description = "最低价", example = "8.00")
    private BigDecimal minPrice;
    @Schema(description = "安全库存", example = "100")
    private BigDecimal safetyStock;
    @Schema(description = "最低库存", example = "50")
    private BigDecimal minStock;
    @Schema(description = "最高库存", example = "1000")
    private BigDecimal maxStock;
    @Schema(description = "最小订货量", example = "10")
    private BigDecimal minOrderQty;
    @Schema(description = "提前期(天)", example = "7")
    private Integer leadTime;
    @Schema(description = "是否批次管理", example = "true")
    private Boolean isBatchManaged;
    @Schema(description = "是否序列号管理", example = "false")
    private Boolean isSerialManaged;
    @Schema(description = "有效期(天)", example = "365")
    private Integer expiryDay;
    @Schema(description = "是否高值物料", example = "false")
    private Boolean isHighValue;
    @Schema(description = "重量", example = "1.5")
    private BigDecimal weight;
    @Schema(description = "备注", example = "备注")
    private String remark;
    // ========== æ‰¹æ¬¡å±žæ€§é…ç½® ==========
    @Schema(description = "生产日期必填", example = "true")
    private Boolean produceDateFlag;
    @Schema(description = "有效期必填", example = "true")
    private Boolean expireDateFlag;
    @Schema(description = "入库日期必填", example = "false")
    private Boolean receiptDateFlag;
    @Schema(description = "供应商必填", example = "true")
    private Boolean vendorFlag;
    @Schema(description = "采购订单号必填", example = "false")
    private Boolean purchaseOrderCodeFlag;
    @Schema(description = "生产批号必填", example = "true")
    private Boolean lotNumberFlag;
    @Schema(description = "质量状态必填", example = "false")
    private Boolean qualityStatusFlag;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/category/vo/MdmItemCategoryRespVO.java
@@ -24,6 +24,9 @@
    @Schema(description = "物料类型", example = "1")
    private Integer itemType;
    @Schema(description = "物料/产品标识(兼容MES)", example = "ITEM")
    private String itemOrProduct;
    @Schema(description = "排序", example = "1")
    private Integer sort;
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/category/vo/MdmItemCategorySaveReqVO.java
@@ -22,6 +22,9 @@
    @Schema(description = "物料类型", example = "1")
    private Integer itemType;
    @Schema(description = "物料/产品标识(兼容MES)", example = "ITEM")
    private String itemOrProduct;
    @Schema(description = "排序", example = "1")
    private Integer sort;
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/MdmItemController.java
@@ -7,9 +7,14 @@
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemRespVO;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemSaveReqVO;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemStatusUpdateReqVO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.category.MdmItemCategoryDO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemBatchConfigDO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemDO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.unit.MdmUnitMeasureDO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.warehouse.MdmWarehouseDO;
import cn.iocoder.yudao.module.mdm.service.brand.MdmBrandService;
import cn.iocoder.yudao.module.mdm.service.category.MdmItemCategoryService;
import cn.iocoder.yudao.module.mdm.service.item.MdmItemBatchConfigService;
import cn.iocoder.yudao.module.mdm.service.item.MdmItemService;
import cn.iocoder.yudao.module.mdm.service.unit.MdmUnitMeasureService;
import cn.iocoder.yudao.module.mdm.service.warehouse.MdmWarehouseService;
@@ -38,10 +43,15 @@
    private MdmItemService itemService;
    @Resource
    private MdmItemCategoryService itemCategoryService;
    @Resource
    private MdmUnitMeasureService unitMeasureService;
    @Resource
    private MdmWarehouseService warehouseService;
    @Resource
    private MdmBrandService brandService;
    @Resource
    private MdmItemBatchConfigService itemBatchConfigService;
    @PostMapping("/create")
    @Operation(summary = "创建物料")
@@ -84,6 +94,8 @@
        MdmItemRespVO resp = BeanUtils.toBean(item, MdmItemRespVO.class);
        if (resp != null) {
            enrichItemRespVO(resp);
            // å¡«å……批次属性配置
            enrichItemBatchConfig(resp);
        }
        return success(resp);
    }
@@ -93,6 +105,9 @@
    @PreAuthorize("@ss.hasPermission('mdm:item:query')")
    public CommonResult<PageResult<MdmItemRespVO>> getItemPage(@Valid MdmItemPageReqVO pageReqVO) {
        PageResult<MdmItemDO> pageResult = itemService.getItemPage(pageReqVO);
        // å…³è”查询分类名称
        Map<Long, MdmItemCategoryDO> categoryMap = itemCategoryService.getItemCategoryMap(
                convertSet(pageResult.getList(), MdmItemDO::getCategoryId));
        // å…³è”查询计量单位名称
        Map<Long, MdmUnitMeasureDO> unitMap = unitMeasureService.getUnitMeasureMap(
                convertSet(pageResult.getList(), MdmItemDO::getUnitMeasureId));
@@ -116,6 +131,11 @@
                        .collect(Collectors.toMap(MdmWarehouseDO::getId, w -> w));
        // èµ‹å€¼
        respList.forEach(resp -> {
            // åˆ†ç±»åç§°
            MdmItemCategoryDO category = categoryMap.get(resp.getCategoryId());
            if (category != null) {
                resp.setCategoryName(category.getName());
            }
            MdmUnitMeasureDO unit = unitMap.get(resp.getUnitMeasureId());
            if (unit != null) {
                resp.setUnitMeasureName(unit.getName());
@@ -146,9 +166,16 @@
    }
    /**
     * å¡«å……物料响应 VO çš„关联名称(主单位、辅单位、仓库名称)
     * å¡«å……物料响应 VO çš„关联名称(分类、主单位、辅单位、仓库名称)
     */
    private void enrichItemRespVO(MdmItemRespVO resp) {
        // åˆ†ç±»åç§°
        if (resp.getCategoryId() != null) {
            MdmItemCategoryDO category = itemCategoryService.getItemCategory(resp.getCategoryId());
            if (category != null) {
                resp.setCategoryName(category.getName());
            }
        }
        // ä¸»å•位名称
        if (resp.getUnitMeasureId() != null) {
            MdmUnitMeasureDO unit = unitMeasureService.getUnitMeasure(resp.getUnitMeasureId());
@@ -179,4 +206,20 @@
        }
    }
    /**
     * å¡«å……物料批次属性配置
     */
    private void enrichItemBatchConfig(MdmItemRespVO resp) {
        MdmItemBatchConfigDO config = itemBatchConfigService.getItemBatchConfigByItemId(resp.getId());
        if (config != null) {
            resp.setProduceDateFlag(config.getProduceDateFlag());
            resp.setExpireDateFlag(config.getExpireDateFlag());
            resp.setReceiptDateFlag(config.getReceiptDateFlag());
            resp.setVendorFlag(config.getVendorFlag());
            resp.setPurchaseOrderCodeFlag(config.getPurchaseOrderCodeFlag());
            resp.setLotNumberFlag(config.getLotNumberFlag());
            resp.setQualityStatusFlag(config.getQualityStatusFlag());
        }
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemRespVO.java
@@ -100,6 +100,29 @@
    @Schema(description = "是否批次管理", example = "false")
    private Boolean isBatchManaged;
    // ==================== æ‰¹æ¬¡å±žæ€§é…ç½® ====================
    @Schema(description = "生产日期", example = "false")
    private Boolean produceDateFlag;
    @Schema(description = "有效期", example = "false")
    private Boolean expireDateFlag;
    @Schema(description = "入库日期", example = "false")
    private Boolean receiptDateFlag;
    @Schema(description = "供应商", example = "false")
    private Boolean vendorFlag;
    @Schema(description = "采购订单编号", example = "false")
    private Boolean purchaseOrderCodeFlag;
    @Schema(description = "生产批号", example = "false")
    private Boolean lotNumberFlag;
    @Schema(description = "质量状态", example = "false")
    private Boolean qualityStatusFlag;
    @Schema(description = "是否高值物料", example = "false")
    private Boolean isHighValue;
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemSaveReqVO.java
@@ -90,4 +90,27 @@
    @Schema(description = "是否同步到 MES", example = "false")
    private Boolean syncMes;
    // ========== æ‰¹æ¬¡å±žæ€§é…ç½®ï¼ˆåŒæ­¥æ—¶ä¸€èµ·ä¿å­˜ï¼‰ ==========
    @Schema(description = "生产日期必填", example = "true")
    private Boolean produceDateFlag;
    @Schema(description = "有效期必填", example = "true")
    private Boolean expireDateFlag;
    @Schema(description = "入库日期必填", example = "false")
    private Boolean receiptDateFlag;
    @Schema(description = "供应商必填", example = "true")
    private Boolean vendorFlag;
    @Schema(description = "采购订单号必填", example = "false")
    private Boolean purchaseOrderCodeFlag;
    @Schema(description = "生产批号必填", example = "true")
    private Boolean lotNumberFlag;
    @Schema(description = "质量状态必填", example = "false")
    private Boolean qualityStatusFlag;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/dataobject/category/MdmItemCategoryDO.java
@@ -53,6 +53,11 @@
    private Integer itemType;
    /**
     * ç‰©æ–™/产品标识(兼容MES: ITEM/PRODUCT)
     */
    private String itemOrProduct;
    /**
     * æŽ’序
     */
    private Integer sort;
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/mysql/category/MdmItemCategoryMapper.java
@@ -60,4 +60,8 @@
        return selectList(MdmItemCategoryDO::getId, ids);
    }
    default MdmItemCategoryDO selectByItemOrProduct(String itemOrProduct) {
        return selectOne(MdmItemCategoryDO::getItemOrProduct, itemOrProduct);
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/category/MdmItemCategoryService.java
@@ -106,4 +106,12 @@
     */
    List<MdmItemCategoryDO> getSimpleItemList();
    /**
     * æ ¹æ®ç‰©æ–™/产品标识获取分类
     *
     * @param itemOrProduct ç‰©æ–™/产品标识
     * @return ç‰©æ–™åˆ†ç±»
     */
    MdmItemCategoryDO getItemCategoryByItemOrProduct(String itemOrProduct);
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/category/MdmItemCategoryServiceImpl.java
@@ -155,4 +155,9 @@
        return itemCategoryMapper.selectList(new MdmItemCategoryDO().setStatus(0));
    }
    @Override
    public MdmItemCategoryDO getItemCategoryByItemOrProduct(String itemOrProduct) {
        return itemCategoryMapper.selectByItemOrProduct(itemOrProduct);
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/item/MdmItemBatchConfigService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,35 @@
package cn.iocoder.yudao.module.mdm.service.item;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemBatchConfigDO;
/**
 * MDM ç‰©æ–™æ‰¹æ¬¡é…ç½® Service æŽ¥å£
 *
 * @author èŠ‹é“æºç 
 */
public interface MdmItemBatchConfigService {
    /**
     * æ ¹æ®ç‰©æ–™ç¼–号获取批次配置
     *
     * @param itemId ç‰©æ–™ç¼–号
     * @return æ‰¹æ¬¡é…ç½®ï¼Œä¸å­˜åœ¨è¿”回 null
     */
    MdmItemBatchConfigDO getItemBatchConfigByItemId(Long itemId);
    /**
     * ä¿å­˜æ‰¹æ¬¡é…ç½®ï¼ˆæ–°å¢žæˆ–更新)
     *
     * @param config æ‰¹æ¬¡é…ç½®
     * @return é…ç½®ç¼–号
     */
    Long saveItemBatchConfig(MdmItemBatchConfigDO config);
    /**
     * æ ¹æ®ç‰©æ–™ç¼–号删除批次配置
     *
     * @param itemId ç‰©æ–™ç¼–号
     */
    void deleteItemBatchConfigByItemId(Long itemId);
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/item/MdmItemBatchConfigServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,61 @@
package cn.iocoder.yudao.module.mdm.service.item;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemBatchConfigDO;
import cn.iocoder.yudao.module.mdm.dal.mysql.item.MdmItemBatchConfigMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.util.List;
/**
 * MDM ç‰©æ–™æ‰¹æ¬¡é…ç½® Service å®žçŽ°ç±»
 *
 * @author èŠ‹é“æºç 
 */
@Service
@Validated
public class MdmItemBatchConfigServiceImpl implements MdmItemBatchConfigService {
    @Resource
    private MdmItemBatchConfigMapper itemBatchConfigMapper;
    @Override
    public MdmItemBatchConfigDO getItemBatchConfigByItemId(Long itemId) {
        List<MdmItemBatchConfigDO> list = itemBatchConfigMapper.selectList(MdmItemBatchConfigDO::getItemId, itemId);
        return CollUtil.getFirst(list);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long saveItemBatchConfig(MdmItemBatchConfigDO config) {
        // æ¸…空不需要的字段(只保留必要的7个字段)
        config.setClientFlag(false);
        config.setSalesOrderCodeFlag(false);
        config.setWorkOrderFlag(false);
        config.setTaskFlag(false);
        config.setWorkstationFlag(false);
        config.setToolFlag(false);
        config.setMoldFlag(false);
        // æŸ¥è¯¢å·²æœ‰é…ç½®
        MdmItemBatchConfigDO existing = getItemBatchConfigByItemId(config.getItemId());
        if (existing != null) {
            // æ›´æ–°
            config.setId(existing.getId());
            itemBatchConfigMapper.updateById(config);
            return existing.getId();
        }
        // æ–°å¢ž
        itemBatchConfigMapper.insert(config);
        return config.getId();
    }
    @Override
    public void deleteItemBatchConfigByItemId(Long itemId) {
        itemBatchConfigMapper.delete(MdmItemBatchConfigDO::getItemId, itemId);
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/item/MdmItemServiceImpl.java
@@ -2,7 +2,6 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemPageReqVO;
@@ -10,20 +9,21 @@
import cn.iocoder.yudao.module.mdm.dal.dataobject.category.MdmItemCategoryDO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemDO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.unit.MdmUnitMeasureDO;
import cn.iocoder.yudao.module.mdm.dal.mysql.item.MdmItemBatchConfigMapper;
import cn.iocoder.yudao.module.mdm.dal.mysql.item.MdmItemMapper;
import cn.iocoder.yudao.module.mdm.service.brand.MdmBrandService;
import cn.iocoder.yudao.module.mdm.service.category.MdmItemCategoryService;
import cn.iocoder.yudao.module.mdm.service.unit.MdmUnitMeasureService;
import cn.iocoder.yudao.module.mes.api.item.MesMdItemApi;
import cn.iocoder.yudao.module.mes.api.item.MesMdItemTypeApi;
import cn.iocoder.yudao.module.mes.api.item.dto.MesMdItemSyncReqDTO;
import cn.iocoder.yudao.module.mes.api.item.dto.MesMdItemTypeRespDTO;
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.util.*;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
@@ -49,9 +49,10 @@
    private MdmBrandService brandService;
    @Resource
    @Lazy
    private MesMdItemApi mesMdItemApi;
    @Resource
    private MesMdItemTypeApi mesMdItemTypeApi;
    private MdmItemBatchConfigMapper itemBatchConfigMapper;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -62,6 +63,9 @@
        // æ’å…¥ MDM ç‰©æ–™
        MdmItemDO item = BeanUtils.toBean(createReqVO, MdmItemDO.class);
        itemMapper.insert(item);
        // ä¿å­˜æ‰¹æ¬¡é…ç½®ï¼ˆå¦‚果有)
        saveBatchConfig(item.getId(), createReqVO);
        // åŒæ­¥åˆ° MES(勾选 syncMes æ—¶ï¼‰
        if (Boolean.TRUE.equals(createReqVO.getSyncMes())) {
@@ -81,6 +85,9 @@
        // æ›´æ–° MDM ç‰©æ–™
        MdmItemDO updateObj = BeanUtils.toBean(updateReqVO, MdmItemDO.class);
        itemMapper.updateById(updateObj);
        // ä¿å­˜æ‰¹æ¬¡é…ç½®ï¼ˆå¦‚果有)
        saveBatchConfig(updateReqVO.getId(), updateReqVO);
        // åŒæ­¥åˆ° MES(勾选 syncMes æ—¶ï¼‰
        if (Boolean.TRUE.equals(updateReqVO.getSyncMes())) {
@@ -110,25 +117,24 @@
            }
            unitCode = unitMeasure.getCode();
        }
        // è§£æžåˆ†ç±» id -> code(使用 MES ç‰©æ–™åˆ†ç±» API)
        // è§£æžåˆ†ç±» id -> code(使用 MDM æœ¬åœ°åˆ†ç±»æœåŠ¡ï¼‰
        String itemTypeCode = null;
        if (reqVO.getCategoryId() != null) {
            CommonResult<MesMdItemTypeRespDTO> result = mesMdItemTypeApi.getItemType(reqVO.getCategoryId());
            if (result != null && result.isSuccess() && result.getData() != null) {
                itemTypeCode = result.getData().getCode();
            MdmItemCategoryDO category = itemCategoryService.getItemCategory(reqVO.getCategoryId());
            if (category != null) {
                itemTypeCode = category.getCode();
            }
        } else if (reqVO.getItemType() != null) {
            // æ ¹æ® itemType æŸ¥æ‰¾å¯¹åº”分类编码
            // itemType: 1原料, 2半成品, 3成品, 4辅料
            String itemTypeName = convertItemTypeToName(reqVO.getItemType());
            CommonResult<List<MesMdItemTypeRespDTO>> listResult = mesMdItemTypeApi.getItemTypeList(null);
            if (listResult != null && listResult.isSuccess() && listResult.getData() != null) {
                itemTypeCode = listResult.getData().stream()
                        .filter(c -> c.getName() != null && c.getName().equals(itemTypeName))
                        .findFirst()
                        .map(MesMdItemTypeRespDTO::getCode)
                        .orElse(null);
            }
            List<MdmItemCategoryDO> categories = itemCategoryService.getItemCategoryList(
                    itemCategoryService.getItemCategoryChildrenList(null).stream().map(MdmItemCategoryDO::getId).collect(Collectors.toSet()));
            itemTypeCode = categories.stream()
                    .filter(c -> c.getName() != null && c.getName().equals(itemTypeName))
                    .findFirst()
                    .map(MdmItemCategoryDO::getCode)
                    .orElse(null);
        }
        // ç»„装同步 DTO
        MesMdItemSyncReqDTO syncReqDTO = new MesMdItemSyncReqDTO();
@@ -143,6 +149,14 @@
        syncReqDTO.setHighValue(reqVO.getIsHighValue());
        syncReqDTO.setBatchFlag(reqVO.getIsBatchManaged());
        syncReqDTO.setRemark(reqVO.getRemark());
        // æ‰¹æ¬¡é…ç½®å­—段
        syncReqDTO.setProduceDateFlag(reqVO.getProduceDateFlag());
        syncReqDTO.setExpireDateFlag(reqVO.getExpireDateFlag());
        syncReqDTO.setReceiptDateFlag(reqVO.getReceiptDateFlag());
        syncReqDTO.setVendorFlag(reqVO.getVendorFlag());
        syncReqDTO.setPurchaseOrderCodeFlag(reqVO.getPurchaseOrderCodeFlag());
        syncReqDTO.setLotNumberFlag(reqVO.getLotNumberFlag());
        syncReqDTO.setQualityStatusFlag(reqVO.getQualityStatusFlag());
        // create èµ° createItem,update èµ° updateItem(MES ä¾§æŒ‰ code upsert)
        if (reqVO.getId() == null) {
            mesMdItemApi.createItem(syncReqDTO);
@@ -205,8 +219,9 @@
        if (categoryId == null) {
            return;
        }
        CommonResult<MesMdItemTypeRespDTO> result = mesMdItemTypeApi.getItemType(categoryId);
        if (result == null || !result.isSuccess() || result.getData() == null) {
        // ä½¿ç”¨ MDM æœ¬åœ°åˆ†ç±»æœåŠ¡æ ¡éªŒ
        MdmItemCategoryDO category = itemCategoryService.getItemCategory(categoryId);
        if (category == null) {
            throw exception(MDM_ITEM_CATEGORY_NOT_EXISTS);
        }
    }
@@ -295,4 +310,51 @@
        return itemMapper.selectByCode(code);
    }
    /**
     * ä¿å­˜æ‰¹æ¬¡é…ç½®ï¼ˆåˆ° MDM æœ¬åœ°è¡¨ï¼‰
     */
    private void saveBatchConfig(Long itemId, MdmItemSaveReqVO reqVO) {
        // æ£€æŸ¥æ˜¯å¦æœ‰æ‰¹æ¬¡é…ç½®æ•°æ®
        if (reqVO.getProduceDateFlag() == null
                && reqVO.getExpireDateFlag() == null
                && reqVO.getReceiptDateFlag() == null
                && reqVO.getVendorFlag() == null
                && reqVO.getPurchaseOrderCodeFlag() == null
                && reqVO.getLotNumberFlag() == null
                && reqVO.getQualityStatusFlag() == null) {
            return;
        }
        // æŸ¥è¯¢å·²æœ‰é…ç½®
        cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemBatchConfigDO existing =
                itemBatchConfigMapper.selectOne(
                        cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemBatchConfigDO::getItemId, itemId);
        cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemBatchConfigDO config =
                new cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemBatchConfigDO();
        config.setItemId(itemId);
        config.setProduceDateFlag(Boolean.TRUE.equals(reqVO.getProduceDateFlag()));
        config.setExpireDateFlag(Boolean.TRUE.equals(reqVO.getExpireDateFlag()));
        config.setReceiptDateFlag(Boolean.TRUE.equals(reqVO.getReceiptDateFlag()));
        config.setVendorFlag(Boolean.TRUE.equals(reqVO.getVendorFlag()));
        config.setPurchaseOrderCodeFlag(Boolean.TRUE.equals(reqVO.getPurchaseOrderCodeFlag()));
        config.setLotNumberFlag(Boolean.TRUE.equals(reqVO.getLotNumberFlag()));
        config.setQualityStatusFlag(Boolean.TRUE.equals(reqVO.getQualityStatusFlag()));
        // æ¸…空不需要的字段
        config.setClientFlag(false);
        config.setSalesOrderCodeFlag(false);
        config.setWorkOrderFlag(false);
        config.setTaskFlag(false);
        config.setWorkstationFlag(false);
        config.setToolFlag(false);
        config.setMoldFlag(false);
        if (existing != null) {
            config.setId(existing.getId());
            itemBatchConfigMapper.updateById(config);
        } else {
            itemBatchConfigMapper.insert(config);
        }
    }
}
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/item/dto/MesMdItemSyncReqDTO.java
@@ -42,4 +42,27 @@
    @Schema(description = "备注", example = "备注")
    private String remark;
    // ========== æ‰¹æ¬¡å±žæ€§é…ç½® ==========
    @Schema(description = "生产日期必填", example = "true")
    private Boolean produceDateFlag;
    @Schema(description = "有效期必填", example = "true")
    private Boolean expireDateFlag;
    @Schema(description = "入库日期必填", example = "false")
    private Boolean receiptDateFlag;
    @Schema(description = "供应商必填", example = "true")
    private Boolean vendorFlag;
    @Schema(description = "采购订单号必填", example = "false")
    private Boolean purchaseOrderCodeFlag;
    @Schema(description = "生产批号必填", example = "true")
    private Boolean lotNumberFlag;
    @Schema(description = "质量状态必填", example = "false")
    private Boolean qualityStatusFlag;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/item/MesMdItemApiImpl.java
@@ -3,9 +3,11 @@
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.api.item.dto.MesMdItemSyncReqDTO;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.MesMdItemSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.batchconfig.MesMdItemBatchConfigSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemTypeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemBatchConfigService;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemTypeService;
import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
@@ -36,12 +38,17 @@
    private MesMdItemTypeService itemTypeService;
    @Resource
    private MesMdUnitMeasureService unitMeasureService;
    @Resource
    private MesMdItemBatchConfigService itemBatchConfigService;
    @Override
    public Long createItem(MesMdItemSyncReqDTO syncReqDTO) {
        log.info("[createItem] åŒæ­¥åˆ›å»º MES ç‰©æ–™äº§å“ï¼Œcode={}", syncReqDTO.getCode());
        MesMdItemSaveReqVO saveReqVO = buildSaveReqVO(syncReqDTO);
        return itemService.createItem(saveReqVO);
        Long itemId = itemService.createItem(saveReqVO);
        // åŒæ­¥æ‰¹æ¬¡é…ç½®
        saveBatchConfig(itemId, syncReqDTO);
        return itemId;
    }
    @Override
@@ -50,12 +57,16 @@
        MesMdItemSaveReqVO saveReqVO = buildSaveReqVO(syncReqDTO);
        // æŒ‰ code åŒ¹é…ï¼šå­˜åœ¨åˆ™æ›´æ–°ï¼Œä¸å­˜åœ¨åˆ™åˆ›å»ºï¼ˆupsert)
        MesMdItemDO existItem = itemService.getItemByCode(syncReqDTO.getCode());
        Long itemId;
        if (existItem == null) {
            itemService.createItem(saveReqVO);
            itemId = itemService.createItem(saveReqVO);
        } else {
            saveReqVO.setId(existItem.getId());
            itemService.updateItem(saveReqVO);
            itemId = existItem.getId();
        }
        // åŒæ­¥æ‰¹æ¬¡é…ç½®
        saveBatchConfig(itemId, syncReqDTO);
    }
    /**
@@ -79,4 +90,30 @@
        return saveReqVO;
    }
    /**
     * ä¿å­˜æ‰¹æ¬¡é…ç½®
     */
    private void saveBatchConfig(Long itemId, MesMdItemSyncReqDTO syncReqDTO) {
        // æ£€æŸ¥æ˜¯å¦æœ‰æ‰¹æ¬¡é…ç½®æ•°æ®
        if (syncReqDTO.getProduceDateFlag() == null
                && syncReqDTO.getExpireDateFlag() == null
                && syncReqDTO.getReceiptDateFlag() == null
                && syncReqDTO.getVendorFlag() == null
                && syncReqDTO.getPurchaseOrderCodeFlag() == null
                && syncReqDTO.getLotNumberFlag() == null
                && syncReqDTO.getQualityStatusFlag() == null) {
            return;
        }
        MesMdItemBatchConfigSaveReqVO configReqVO = new MesMdItemBatchConfigSaveReqVO();
        configReqVO.setItemId(itemId);
        configReqVO.setProduceDateFlag(syncReqDTO.getProduceDateFlag());
        configReqVO.setExpireDateFlag(syncReqDTO.getExpireDateFlag());
        configReqVO.setReceiptDateFlag(syncReqDTO.getReceiptDateFlag());
        configReqVO.setVendorFlag(syncReqDTO.getVendorFlag());
        configReqVO.setPurchaseOrderCodeFlag(syncReqDTO.getPurchaseOrderCodeFlag());
        configReqVO.setLotNumberFlag(syncReqDTO.getLotNumberFlag());
        configReqVO.setQualityStatusFlag(syncReqDTO.getQualityStatusFlag());
        itemBatchConfigService.saveItemBatchConfig(configReqVO);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/item/vo/MesMdItemSaveReqVO.java
@@ -51,4 +51,7 @@
    @Schema(description = "备注", example = "备注")
    private String remark;
    @Schema(description = "是否同步到 MDM", example = "true")
    private Boolean syncMdm;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/batch/vo/MesWmBatchGenerateReqVO.java
@@ -20,6 +20,11 @@
    private Long itemId;
    /**
     * æ‰¹æ¬¡å·ï¼ˆä¼ å…¥åˆ™ä½¿ç”¨è¯¥æ‰¹æ¬¡å·ï¼Œä¸ä¼ åˆ™è‡ªåŠ¨ç”Ÿæˆï¼‰
     */
    private String batchCode;
    /**
     * ç”Ÿäº§æ—¥æœŸ
     */
    private LocalDateTime produceDate;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/vo/MesWmMaterialStockAdjustReqVO.java
@@ -1,11 +1,16 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
import java.time.LocalDate;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
/**
 * åº“存调整(自定义出入库)Request VO
@@ -30,15 +35,35 @@
    @NotNull(message = "库位ID不能为空")
    private Long areaId;
    @Schema(description = "批次ID", example = "1")
    // ========== æ‰¹æ¬¡ç›¸å…³å­—段 ==========
    @Schema(description = "批次ID(选择已有批次时传入)", example = "1")
    private Long batchId;
    @Schema(description = "批次号", example = "BATCH001")
    @Schema(description = "批次号(新建批次时传入,系统自动创建批次记录)", example = "BATCH001")
    private String batchCode;
    @Schema(description = "生产日期(新建批次时传入)", example = "2025-01-01")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
    @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY)
    private LocalDate produceDate;
    @Schema(description = "有效期(新建批次时传入)", example = "2026-01-01")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
    @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY)
    private LocalDate expireDate;
    @Schema(description = "供应商ID", example = "1")
    private Long vendorId;
    @Schema(description = "生产批号", example = "LOT001")
    private String lotNumber;
    @Schema(description = "质量状态(0-待检验 1-合格 2-不合格,支持数字或字符串)", example = "1")
    private Object qualityStatus;
    // ========== å…¶ä»–字段 ==========
    @Schema(description = "数量(必须大于0)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
    @NotNull(message = "数量不能为空")
    @Positive(message = "数量必须大于0")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/transaction/MesWmTransactionController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,161 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.transaction;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.wm.transaction.vo.MesWmTransactionPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.transaction.vo.MesWmTransactionRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.transaction.MesWmTransactionDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseAreaDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseLocationService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService;
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.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
/**
 * åº“存事务流水 Controller
 */
@Tag(name = "管理后台 - åº“存事务流水")
@RestController
@RequestMapping("/mes/wm/transaction")
@Validated
public class MesWmTransactionController {
    @Resource
    private MesWmTransactionService transactionService;
    @Resource
    private MesMdmItemService mdmItemService;
    @Resource
    private MesWmWarehouseService warehouseService;
    @Resource
    private MesWmWarehouseLocationService locationService;
    @Resource
    private MesWmWarehouseAreaService areaService;
    @GetMapping("/page")
    @Operation(summary = "获取库存事务流水分页")
    @Parameter(name = "id", description = "编号", required = true, example = "1")
    @PreAuthorize("@ss.hasPermission('mes:wm-transaction:query')")
    public CommonResult<PageResult<MesWmTransactionRespVO>> getTransactionPage(@Valid MesWmTransactionPageReqVO pageReqVO) {
        PageResult<MesWmTransactionDO> pageResult = transactionService.getTransactionPage(pageReqVO);
        if (pageResult.getTotal() == 0) {
            return success(PageResult.empty(pageResult.getTotal()));
        }
        // å…³è”查询物料信息
        Map<Long, MesMdmItemDO> itemMap = mdmItemService.getItemMap(
                convertSet(pageResult.getList(), MesWmTransactionDO::getItemId));
        // å…³è”查询仓库信息
        Map<Long, MesWmWarehouseDO> warehouseMap = warehouseService.getWarehouseMap(
                convertSet(pageResult.getList(), MesWmTransactionDO::getWarehouseId));
        // å…³è”查询库区信息
        Map<Long, MesWmWarehouseLocationDO> locationMap = locationService.getWarehouseLocationMap(
                convertSet(pageResult.getList(), MesWmTransactionDO::getLocationId));
        // å…³è”查询库位信息
        Map<Long, MesWmWarehouseAreaDO> areaMap = areaService.getWarehouseAreaMap(
                convertSet(pageResult.getList(), MesWmTransactionDO::getAreaId));
        // è½¬æ¢ä¸º VO
        List<MesWmTransactionRespVO> voList = pageResult.getList().stream().map(transaction -> {
            MesWmTransactionRespVO vo = BeanUtils.toBean(transaction, MesWmTransactionRespVO.class);
            // äº‹åŠ¡ç±»åž‹åç§°
            MesWmTransactionTypeEnum typeEnum = MesWmTransactionTypeEnum.valueOf(transaction.getType());
            vo.setTypeName(typeEnum != null ? typeEnum.getName() : "未知");
            // ä¸šåŠ¡ç±»åž‹åç§°
            vo.setBizTypeName(getBizTypeName(transaction.getBizType()));
            // ç‰©æ–™ä¿¡æ¯
            MesMdmItemDO item = itemMap.get(transaction.getItemId());
            if (item != null) {
                vo.setItemCode(item.getCode());
                vo.setItemName(item.getName());
            }
            // ä»“库信息
            MesWmWarehouseDO warehouse = warehouseMap.get(transaction.getWarehouseId());
            if (warehouse != null) {
                vo.setWarehouseName(warehouse.getName());
            }
            // åº“区信息
            MesWmWarehouseLocationDO location = locationMap.get(transaction.getLocationId());
            if (location != null) {
                vo.setLocationName(location.getName());
            }
            // åº“位信息
            MesWmWarehouseAreaDO area = areaMap.get(transaction.getAreaId());
            if (area != null) {
                vo.setAreaName(area.getName());
            }
            return vo;
        }).collect(Collectors.toList());
        return success(new PageResult<>(voList, pageResult.getTotal()));
    }
    /**
     * èŽ·å–ä¸šåŠ¡ç±»åž‹åç§°
     */
    private String getBizTypeName(Integer bizType) {
        if (bizType == null) {
            return "未知";
        }
        switch (bizType) {
            case MesBizTypeConstants.WM_STOCK_ADJUST_IN:
                return "自定义入库";
            case MesBizTypeConstants.WM_STOCK_ADJUST_OUT:
                return "自定义出库";
            case MesBizTypeConstants.WM_ITEM_RECEIPT_IN:
                return "采购入库";
            case MesBizTypeConstants.WM_PRODUCT_SALES:
                return "销售出库";
            case MesBizTypeConstants.WM_TRANSFER_IN:
                return "调拨入库";
            case MesBizTypeConstants.WM_TRANSFER_OUT:
                return "调拨出库";
            case MesBizTypeConstants.WM_MISC_RECPT:
                return "其他入库";
            case MesBizTypeConstants.WM_MISC_ISSUE:
                return "其他出库";
            case MesBizTypeConstants.WM_ISSUE:
                return "生产领料";
            case MesBizTypeConstants.WM_RETURN_ISSUE:
                return "生产退料";
            case MesBizTypeConstants.WM_PRODUCT_RECPT:
                return "生产入库";
            case MesBizTypeConstants.WM_ITEM_CONSUME:
                return "生产消耗";
            case MesBizTypeConstants.WM_PRODUCT_PRODUCE:
                return "产品产出";
            case MesBizTypeConstants.WM_RETURN_VENDOR:
                return "供应商退货";
            case MesBizTypeConstants.WM_RETURN_SALES:
                return "销售退货";
            case MesBizTypeConstants.WM_OUTSOURCE_ISSUE:
                return "外协发料";
            case MesBizTypeConstants.WM_OUTSOURCE_RECPT:
                return "外协入库";
            default:
                return "未知类型(" + bizType + ")";
        }
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/transaction/vo/MesWmTransactionPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,58 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.transaction.vo;
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_HOUR_MINUTE_SECOND;
/**
 * åº“存事务流水查询 Request VO
 */
@Schema(description = "管理后台 - åº“存事务流水查询 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MesWmTransactionPageReqVO extends PageParam {
    @Schema(description = "物料ID", example = "1")
    private Long itemId;
    @Schema(description = "批次号", example = "BATCH001")
    private String batchCode;
    @Schema(description = "仓库ID", example = "1")
    private Long warehouseId;
    @Schema(description = "库区ID", example = "1")
    private Long locationId;
    @Schema(description = "库位ID", example = "1")
    private Long areaId;
    @Schema(description = "事务类型(1-入库 2-出库)", example = "1")
    private Integer type;
    @Schema(description = "业务类型(126-自定义入库 127-自定义出库 ç­‰ï¼‰", example = "126")
    private Integer bizType;
    @Schema(description = "业务单号", example = "STOCK_ADJUST_1")
    private String bizCode;
    @Schema(description = "库存记录ID", example = "1")
    private Long materialStockId;
    @Schema(description = "开始时间")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime startTime;
    @Schema(description = "结束时间")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime endTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/transaction/vo/MesWmTransactionRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,82 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.transaction.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * åº“存事务流水 Response VO
 */
@Schema(description = "管理后台 - åº“存事务流水 Response VO")
@Data
public class MesWmTransactionRespVO {
    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long id;
    @Schema(description = "事务类型(1-入库 2-出库)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Integer type;
    @Schema(description = "事务类型名称", example = "入库")
    private String typeName;
    @Schema(description = "业务类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "126")
    private Integer bizType;
    @Schema(description = "业务类型名称", example = "自定义入库")
    private String bizTypeName;
    @Schema(description = "业务单号", example = "STOCK_ADJUST_1")
    private String bizCode;
    // ========== ç‰©æ–™ç»´åº¦ ==========
    @Schema(description = "物料ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long itemId;
    @Schema(description = "物料编码", example = "ITEM001")
    private String itemCode;
    @Schema(description = "物料名称", example = "螺丝")
    private String itemName;
    @Schema(description = "变动数量(正数=入库,负数=出库)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
    private BigDecimal quantity;
    @Schema(description = "批次ID", example = "1")
    private Long batchId;
    @Schema(description = "批次号", example = "BATCH001")
    private String batchCode;
    // ========== åº“存位置 ==========
    @Schema(description = "仓库ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long warehouseId;
    @Schema(description = "仓库名称", example = "成品仓")
    private String warehouseName;
    @Schema(description = "库区ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long locationId;
    @Schema(description = "库区名称", example = "A区")
    private String locationName;
    @Schema(description = "库位ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long areaId;
    @Schema(description = "库位名称", example = "A-01-01")
    private String areaName;
    // ========== æ—¶é—´ ==========
    @Schema(description = "事务发生时间", requiredMode = Schema.RequiredMode.REQUIRED)
    private LocalDateTime transactionTime;
    @Schema(description = "创建时间")
    private LocalDateTime createTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/materialstock/MesWmMaterialStockMapper.java
@@ -99,8 +99,7 @@
                .setSql("reserved_quantity = reserved_quantity + " + count);
        if (checkFlag && count.compareTo(BigDecimal.ZERO) > 0) {
            // æ ¡éªŒå¯ç”¨é‡å……足:quantity - frozen_quantity - reserved_quantity >= count
            updateWrapper.setSql("reserved_quantity = reserved_quantity + " + count +
                    " WHERE quantity - IFNULL(frozen_quantity, 0) - IFNULL(reserved_quantity, 0) >= " + count);
            updateWrapper.apply("quantity - IFNULL(frozen_quantity, 0) - IFNULL(reserved_quantity, 0) >= {0}", count);
        }
        return update(null, updateWrapper);
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/transaction/MesWmTransactionMapper.java
@@ -1,6 +1,9 @@
package cn.iocoder.yudao.module.mes.dal.mysql.wm.transaction;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.wm.transaction.vo.MesWmTransactionPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.transaction.MesWmTransactionDO;
import org.apache.ibatis.annotations.Mapper;
@@ -10,4 +13,20 @@
@Mapper
public interface MesWmTransactionMapper extends BaseMapperX<MesWmTransactionDO> {
    default PageResult<MesWmTransactionDO> selectPage(MesWmTransactionPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesWmTransactionDO>()
                .eqIfPresent(MesWmTransactionDO::getItemId, reqVO.getItemId())
                .likeIfPresent(MesWmTransactionDO::getBatchCode, reqVO.getBatchCode())
                .eqIfPresent(MesWmTransactionDO::getWarehouseId, reqVO.getWarehouseId())
                .eqIfPresent(MesWmTransactionDO::getLocationId, reqVO.getLocationId())
                .eqIfPresent(MesWmTransactionDO::getAreaId, reqVO.getAreaId())
                .eqIfPresent(MesWmTransactionDO::getType, reqVO.getType())
                .eqIfPresent(MesWmTransactionDO::getBizType, reqVO.getBizType())
                .likeIfPresent(MesWmTransactionDO::getBizCode, reqVO.getBizCode())
                .eqIfPresent(MesWmTransactionDO::getMaterialStockId, reqVO.getMaterialStockId())
                .geIfPresent(MesWmTransactionDO::getTransactionTime, reqVO.getStartTime())
                .leIfPresent(MesWmTransactionDO::getTransactionTime, reqVO.getEndTime())
                .orderByDesc(MesWmTransactionDO::getTransactionTime));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemBatchConfigServiceImpl.java
@@ -4,9 +4,14 @@
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.batchconfig.MesMdItemBatchConfigSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemBatchConfigDO;
import cn.iocoder.yudao.module.mes.dal.mysql.md.item.MesMdItemBatchConfigMapper;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemBatchConfigApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemBatchConfigRespDTO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemBatchConfigDO;
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 static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -15,6 +20,9 @@
/**
 * MES ç‰©æ–™æ‰¹æ¬¡å±žæ€§é…ç½® Service å®žçŽ°ç±»
 *
 * æ‰¹æ¬¡é…ç½®ç»Ÿä¸€å­˜å‚¨åœ¨ MDM è¡¨ï¼ˆmdm_item_batch_config)
 * MES æœ¬åœ°è¡¨ï¼ˆmes_md_item_batch_config)仅用于兼容历史数据和 MES æ¨¡å—内部查询
 *
 * @author èŠ‹é“æºç 
 */
@@ -26,12 +34,29 @@
    private MesMdItemBatchConfigMapper itemBatchConfigMapper;
    @Resource
    @Lazy // é¿å…å¾ªçŽ¯ä¾èµ–ï¼ˆMesMdItemServiceImpl ä¹Ÿä¼šæ³¨å…¥æœ¬ Service)
    @Lazy
    private MesMdItemService itemService;
    @Resource
    @Lazy
    private MdmItemApi mdmItemApi;
    @Resource
    private MdmItemBatchConfigApi mdmItemBatchConfigApi;
    @Override
    public MesMdItemBatchConfigDO getItemBatchConfigByItemId(Long itemId) {
        return itemBatchConfigMapper.selectByItemId(itemId);
        // ä¼˜å…ˆä»Ž MES æœ¬åœ°è¡¨æŸ¥è¯¢ï¼ˆæ€§èƒ½æ›´å¥½ï¼‰
        MesMdItemBatchConfigDO mesConfig = itemBatchConfigMapper.selectByItemId(itemId);
        if (mesConfig != null) {
            return mesConfig;
        }
        // æœ¬åœ°è¡¨ä¸å­˜åœ¨ï¼Œå°è¯•从 MDM èŽ·å–ï¼ˆä¸å†™å…¥æœ¬åœ°ï¼Œé¿å…å‰¯ä½œç”¨ï¼‰
        MdmItemBatchConfigRespDTO mdmConfig = mdmItemBatchConfigApi.getItemBatchConfig(itemId);
        if (mdmConfig != null) {
            return BeanUtils.toBean(mdmConfig, MesMdItemBatchConfigDO.class);
        }
        return null;
    }
    @Override
@@ -44,26 +69,33 @@
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long saveItemBatchConfig(MesMdItemBatchConfigSaveReqVO saveReqVO) {
        // 1. æ ¡éªŒç‰©æ–™å­˜åœ¨
        if (itemService.getItem(saveReqVO.getItemId()) == null) {
        // 1. æ ¡éªŒç‰©æ–™å­˜åœ¨ï¼ˆä»Ž MDM æ ¡éªŒï¼‰
        if (mdmItemApi.getItem(saveReqVO.getItemId()).getCheckedData() == null) {
            throw exception(MD_ITEM_NOT_EXISTS);
        }
        // 2. æŸ¥è¯¢å·²æœ‰é…ç½®ï¼Œå†³å®šæ–°å¢žè¿˜æ˜¯æ›´æ–°
        MesMdItemBatchConfigDO existing = itemBatchConfigMapper.selectByItemId(saveReqVO.getItemId());
        if (existing != null) {
            // æ›´æ–°
            MesMdItemBatchConfigDO updateObj = BeanUtils.toBean(saveReqVO, MesMdItemBatchConfigDO.class);
            updateObj.setId(existing.getId());
            itemBatchConfigMapper.updateById(updateObj);
            return existing.getId();
        }
        // 2. æ¸…空不需要的字段(只保留必要的7个字段)
        MesMdItemBatchConfigSaveReqVO cleanReqVO = new MesMdItemBatchConfigSaveReqVO();
        cleanReqVO.setId(saveReqVO.getId());
        cleanReqVO.setItemId(saveReqVO.getItemId());
        cleanReqVO.setProduceDateFlag(saveReqVO.getProduceDateFlag());
        cleanReqVO.setExpireDateFlag(saveReqVO.getExpireDateFlag());
        cleanReqVO.setReceiptDateFlag(saveReqVO.getReceiptDateFlag());
        cleanReqVO.setVendorFlag(saveReqVO.getVendorFlag());
        cleanReqVO.setPurchaseOrderCodeFlag(saveReqVO.getPurchaseOrderCodeFlag());
        cleanReqVO.setLotNumberFlag(saveReqVO.getLotNumberFlag());
        cleanReqVO.setQualityStatusFlag(saveReqVO.getQualityStatusFlag());
        // æ–°å¢ž
        MesMdItemBatchConfigDO insertObj = BeanUtils.toBean(saveReqVO, MesMdItemBatchConfigDO.class);
        itemBatchConfigMapper.insert(insertObj);
        return insertObj.getId();
        // 3. ä¿å­˜åˆ° MDM è¡¨
        MdmItemBatchConfigDO mdmConfig = BeanUtils.toBean(cleanReqVO, MdmItemBatchConfigDO.class);
        Long configId = mdmItemBatchConfigApi.saveItemBatch(mdmConfig);
        // 4. åŒæ­¥ä¿å­˜åˆ° MES æœ¬åœ°è¡¨
        saveToMesTable(cleanReqVO);
        return configId;
    }
    @Override
@@ -71,4 +103,22 @@
        itemBatchConfigMapper.deleteByItemId(itemId);
    }
}
    /**
     * ä¿å­˜åˆ° MES æœ¬åœ°è¡¨
     */
    private void saveToMesTable(MesMdItemBatchConfigSaveReqVO saveReqVO) {
        MesMdItemBatchConfigDO existing = itemBatchConfigMapper.selectByItemId(saveReqVO.getItemId());
        MesMdItemBatchConfigDO mesConfig = BeanUtils.toBean(saveReqVO, MesMdItemBatchConfigDO.class);
        if (existing != null) {
            // å­˜åœ¨åˆ™æ›´æ–°ï¼Œä½¿ç”¨å·²æœ‰çš„ ID
            mesConfig.setId(existing.getId());
            itemBatchConfigMapper.updateById(mesConfig);
        } else {
            // ä¸å­˜åœ¨åˆ™æ’入,让数据库自动生成 ID
            mesConfig.setId(null);
            itemBatchConfigMapper.insert(mesConfig);
        }
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemServiceImpl.java
@@ -22,6 +22,8 @@
import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
import cn.iocoder.yudao.module.mes.service.wm.barcode.MesWmBarcodeService;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemSyncApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemSyncReqDTO;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -38,7 +40,7 @@
/**
 * MES ç‰©æ–™äº§å“ Service å®žçŽ°ç±»
 *
 * æ³¨æ„ï¼šå½“前保持使用 MES æœ¬åœ°è¡¨ï¼Œå¾… MDM æ•°æ®è¿ç§»å®ŒæˆåŽå†åˆ‡æ¢åˆ° MDM API
 * æ”¯æŒ MES å’Œ MDM ç‰©æ–™æ•°æ®åŒå‘同步
 *
 * @author èŠ‹é“æºç 
 */
@@ -66,7 +68,11 @@
    @Resource
    private MesMdAutoCodeRecordService autoCodeRecordService;
    @Resource
    private MdmItemSyncApi mdmItemSyncApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createItem(MesMdItemSaveReqVO createReqVO) {
        // æ ¡éªŒæ•°æ®
        validateItemSaveData(createReqVO);
@@ -80,10 +86,17 @@
        // è‡ªåŠ¨ç”Ÿæˆæ¡ç 
        barcodeService.autoGenerateBarcode(BarcodeBizTypeEnum.ITEM.getValue(),
                item.getId(), item.getCode(), item.getName());
        // åŒæ­¥åˆ° MDM(勾选 syncMdm æ—¶ï¼‰
        if (Boolean.TRUE.equals(createReqVO.getSyncMdm())) {
            syncItemToMdm(item, createReqVO);
        }
        return item.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateItem(MesMdItemSaveReqVO updateReqVO) {
        // æ ¡éªŒå­˜åœ¨
        validateItemExists(updateReqVO.getId());
@@ -94,6 +107,12 @@
        MesMdItemDO updateObj = BeanUtils.toBean(updateReqVO, MesMdItemDO.class);
        clearStockIfNotSafe(updateObj); // å¦‚果未启用安全库存,清零库存上下限
        itemMapper.updateById(updateObj);
        // åŒæ­¥åˆ° MDM(勾选 syncMdm æ—¶ï¼‰
        if (Boolean.TRUE.equals(updateReqVO.getSyncMdm())) {
            MesMdItemDO item = itemMapper.selectById(updateReqVO.getId());
            syncItemToMdm(item, updateReqVO);
        }
    }
    private void validateItemSaveData(MesMdItemSaveReqVO reqVO) {
@@ -369,4 +388,93 @@
        return respVO;
    }
    /**
     * åŒæ­¥ç‰©æ–™åˆ° MDM
     *
     * @param item     MES ç‰©æ–™æ•°æ®
     * @param saveReqVO ä¿å­˜è¯·æ±‚(包含批次配置)
     */
    private void syncItemToMdm(MesMdItemDO item, MesMdItemSaveReqVO saveReqVO) {
        // 1. èŽ·å–ç‰©æ–™ç±»åž‹å¯¹åº”çš„ itemType
        Integer itemType = null;
        if (item.getItemTypeId() != null) {
            MesMdItemTypeDO itemTypeDO = itemTypeService.getItemType(item.getItemTypeId());
            if (itemTypeDO != null) {
                // MES: ITEM=物料, PRODUCT=产品 -> MDM: 1=原料, 2=半成品, 3=成品, 4=辅料
                itemType = convertMesItemTypeToMdm(itemTypeDO.getItemOrProduct(), itemTypeDO.getName());
            }
        }
        // 2. èŽ·å–è®¡é‡å•ä½ç¼–ç 
        String unitCode = null;
        Long unitMeasureId = null;
        if (item.getUnitMeasureId() != null) {
            MesMdUnitMeasureDO unitMeasure = unitMeasureService.getUnitMeasure(item.getUnitMeasureId());
            if (unitMeasure != null) {
                unitCode = unitMeasure.getCode();
            }
            unitMeasureId = item.getUnitMeasureId();
        }
        // 3. èŽ·å–æ‰¹æ¬¡é…ç½®
        MesMdItemBatchConfigDO batchConfig = itemBatchConfigService.getItemBatchConfigByItemId(item.getId());
        // 4. ç»„装同步 DTO
        MdmItemSyncReqDTO syncReqDTO = new MdmItemSyncReqDTO();
        syncReqDTO.setCode(item.getCode());
        syncReqDTO.setName(item.getName());
        syncReqDTO.setSpecification(item.getSpecification());
        syncReqDTO.setItemType(itemType);
        syncReqDTO.setUnitMeasureId(unitMeasureId);
        syncReqDTO.setCategoryId(item.getItemTypeId());
        syncReqDTO.setStatus(item.getStatus());
        syncReqDTO.setMinStock(item.getMinStock());
        syncReqDTO.setMaxStock(item.getMaxStock());
        syncReqDTO.setIsBatchManaged(item.getBatchFlag());
        syncReqDTO.setIsHighValue(item.getHighValue());
        syncReqDTO.setRemark(item.getRemark());
        // 5. åŒæ­¥æ‰¹æ¬¡é…ç½®
        if (batchConfig != null) {
            syncReqDTO.setProduceDateFlag(batchConfig.getProduceDateFlag());
            syncReqDTO.setExpireDateFlag(batchConfig.getExpireDateFlag());
            syncReqDTO.setReceiptDateFlag(batchConfig.getReceiptDateFlag());
            syncReqDTO.setVendorFlag(batchConfig.getVendorFlag());
            syncReqDTO.setPurchaseOrderCodeFlag(batchConfig.getPurchaseOrderCodeFlag());
            syncReqDTO.setLotNumberFlag(batchConfig.getLotNumberFlag());
            syncReqDTO.setQualityStatusFlag(batchConfig.getQualityStatusFlag());
        }
        // 6. è°ƒç”¨ MDM åŒæ­¥ API
        mdmItemSyncApi.syncItemToMdm(syncReqDTO);
    }
    /**
     * å°† MES ç‰©æ–™ç±»åž‹è½¬æ¢ä¸º MDM ç‰©æ–™ç±»åž‹
     * MES: ITEM(物料)/PRODUCT(产品)
     * MDM: 1原料, 2半成品, 3成品, 4辅料
     */
    private Integer convertMesItemTypeToMdm(String itemOrProduct, String typeName) {
        if (itemOrProduct == null) {
            return null;
        }
        // æ ¹æ®åç§°åŒ¹é…
        if (typeName != null) {
            if (typeName.contains("原料")) {
                return 1;
            } else if (typeName.contains("半成品")) {
                return 2;
            } else if (typeName.contains("成品")) {
                return 3;
            } else if (typeName.contains("辅料")) {
                return 4;
            }
        }
        // æ ¹æ® ITEM/PRODUCT å…œåº•
        if ("PRODUCT".equals(itemOrProduct)) {
            return 3; // äº§å“ -> æˆå“
        }
        return 1; // é»˜è®¤åŽŸæ–™
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemTypeServiceImpl.java
@@ -2,7 +2,10 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mdm.api.category.MdmItemCategoryApi;
import cn.iocoder.yudao.module.mdm.api.category.dto.MdmItemCategoryRespDTO;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.type.MesMdItemTypeListReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.type.MesMdItemTypeSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemTypeDO;
@@ -20,6 +23,9 @@
/**
 * MES ç‰©æ–™äº§å“åˆ†ç±» Service å®žçŽ°ç±»
 *
 * å…¼å®¹å±‚实现:数据存储在 mes_md_item_type è¡¨ï¼Œä½†ä¼˜å…ˆä»Ž MDM åˆ†ç±» API èŽ·å–
 * å®žé™…分类数据已迁移到 mdm_item_category è¡¨
 *
 * @author èŠ‹é“æºç 
 */
@Service
@@ -27,10 +33,13 @@
public class MesMdItemTypeServiceImpl implements MesMdItemTypeService {
    @Resource
    private MdmItemCategoryApi mdmItemCategoryApi;
    @Resource
    private MesMdItemTypeMapper itemTypeMapper;
    @Resource
    @Lazy // å»¶è¿ŸåŠ è½½ï¼Œé¿å…å¾ªçŽ¯ä¾èµ–
    @Lazy
    private MesMdItemService itemService;
    @Override
@@ -38,7 +47,7 @@
        // æ ¡éªŒæ•°æ®
        validateItemTypeSaveData(createReqVO);
        // æ’å…¥
        // æ’入到本地表(保持兼容)
        MesMdItemTypeDO itemType = BeanUtils.toBean(createReqVO, MesMdItemTypeDO.class);
        itemTypeMapper.insert(itemType);
        return itemType.getId();
@@ -51,7 +60,7 @@
        // æ ¡éªŒæ•°æ®
        validateItemTypeSaveData(updateReqVO);
        // æ›´æ–°
        // æ›´æ–°æœ¬åœ°è¡¨
        MesMdItemTypeDO updateObj = BeanUtils.toBean(updateReqVO, MesMdItemTypeDO.class);
        itemTypeMapper.updateById(updateObj);
    }
@@ -83,6 +92,12 @@
    }
    private void validateItemTypeExists(Long id) {
        // ä¼˜å…ˆä»Ž MDM èŽ·å–
        CommonResult<MdmItemCategoryRespDTO> result = mdmItemCategoryApi.getItemCategory(id);
        if (result.isSuccess() && result.getData() != null) {
            return;
        }
        // å›žé€€åˆ°æœ¬åœ°è¡¨
        if (itemTypeMapper.selectById(id) == null) {
            throw exception(MD_ITEM_TYPE_NOT_EXISTS);
        }
@@ -96,26 +111,36 @@
        if (Objects.equals(id, parentId)) {
            throw exception(MD_ITEM_TYPE_PARENT_ERROR);
        }
        // 2. çˆ¶åˆ†ç±»ä¸å­˜åœ¨
        MesMdItemTypeDO parentItemType = itemTypeMapper.selectById(parentId);
        // 2. çˆ¶åˆ†ç±»ä¸å­˜åœ¨ - ä¼˜å…ˆä»Ž MDM èŽ·å–
        CommonResult<MdmItemCategoryRespDTO> result = mdmItemCategoryApi.getItemCategory(parentId);
        MesMdItemTypeDO parentItemType = null;
        if (result.isSuccess() && result.getData() != null) {
            parentItemType = convertToMesDO(result.getData());
        } else {
            parentItemType = itemTypeMapper.selectById(parentId);
        }
        if (parentItemType == null) {
            throw exception(MD_ITEM_TYPE_PARENT_NOT_EXITS);
        }
        // 3. é€’归校验父分类,如果父分类是自己的子分类,则报错,避免形成环路
        if (id == null) { // id ä¸ºç©ºï¼Œè¯´æ˜Žæ–°å¢žï¼Œä¸éœ€è¦è€ƒè™‘环路
        if (id == null) {
            return;
        }
        for (int i = 0; i < Short.MAX_VALUE; i++) {
            // 3.1 æ ¡éªŒçŽ¯è·¯
            parentId = parentItemType.getParentId();
            if (Objects.equals(id, parentId)) {
                throw exception(MD_ITEM_TYPE_PARENT_IS_CHILD);
            }
            // 3.2 ç»§ç»­é€’归下一级父分类
            if (parentId == null || MesMdItemTypeDO.PARENT_ID_ROOT.equals(parentId)) {
                break;
            }
            parentItemType = itemTypeMapper.selectById(parentId);
            // ç»§ç»­æŸ¥æ‰¾çˆ¶çº§
            CommonResult<MdmItemCategoryRespDTO> parentResult = mdmItemCategoryApi.getItemCategory(parentId);
            if (parentResult.isSuccess() && parentResult.getData() != null) {
                parentItemType = convertToMesDO(parentResult.getData());
            } else {
                parentItemType = itemTypeMapper.selectById(parentId);
            }
            if (parentItemType == null) {
                break;
            }
@@ -144,16 +169,24 @@
    @Override
    public MesMdItemTypeDO getItemType(Long id) {
        // ä¼˜å…ˆä»Ž MDM èŽ·å–
        CommonResult<MdmItemCategoryRespDTO> result = mdmItemCategoryApi.getItemCategory(id);
        if (result.isSuccess() && result.getData() != null) {
            return convertToMesDO(result.getData());
        }
        // å›žé€€åˆ°æœ¬åœ°è¡¨
        return itemTypeMapper.selectById(id);
    }
    @Override
    public MesMdItemTypeDO getItemTypeByCode(String code) {
        // ä»Žæœ¬åœ°è¡¨æŸ¥
        return itemTypeMapper.selectByCode(code);
    }
    @Override
    public List<MesMdItemTypeDO> getItemTypeList(MesMdItemTypeListReqVO listReqVO) {
        // ä»Žæœ¬åœ°è¡¨æŸ¥
        return itemTypeMapper.selectList(listReqVO);
    }
@@ -167,16 +200,12 @@
    @Override
    public List<MesMdItemTypeDO> getItemTypeChildrenList(Long parentId) {
        List<MesMdItemTypeDO> allList = itemTypeMapper.selectList(); // åˆ†ç±»æ€»é‡å°ï¼Œå…¨é‡æŸ¥
        // é€’归收集所有子分类
        List<MesMdItemTypeDO> allList = itemTypeMapper.selectList();
        List<MesMdItemTypeDO> result = new ArrayList<>();
        collectItemTypeChildren(result, parentId, allList);
        return result;
    }
    /**
     * é€’归收集所有子分类
     */
    private void collectItemTypeChildren(List<MesMdItemTypeDO> result, Long parentId, List<MesMdItemTypeDO> allList) {
        for (MesMdItemTypeDO itemType : allList) {
            if (Objects.equals(itemType.getParentId(), parentId)) {
@@ -186,4 +215,19 @@
        }
    }
}
    /**
     * è½¬æ¢ MDM åˆ†ç±» DTO ä¸º MES DO
     */
    private MesMdItemTypeDO convertToMesDO(MdmItemCategoryRespDTO dto) {
        MesMdItemTypeDO mesDO = new MesMdItemTypeDO();
        mesDO.setId(dto.getId());
        mesDO.setCode(dto.getCode());
        mesDO.setName(dto.getName());
        mesDO.setParentId(dto.getParentId());
        mesDO.setItemOrProduct(dto.getItemOrProduct());
        mesDO.setSort(dto.getSort());
        mesDO.setStatus(dto.getStatus());
        return mesDO;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/batch/MesWmBatchServiceImpl.java
@@ -2,19 +2,20 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchGenerateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemBatchConfigDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.batch.MesWmBatchMapper;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeRuleCodeEnum;
import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemBatchConfigService;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
import cn.iocoder.yudao.module.mes.service.wm.barcode.MesWmBarcodeService;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemBatchConfigApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemBatchConfigRespDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -50,7 +51,7 @@
    @Resource
    private MesMdmItemService mdmItemService;
    @Resource
    private MesMdItemBatchConfigService itemBatchConfigService;
    private MdmItemBatchConfigApi itemBatchConfigApi;
    @Resource
    private MesMdAutoCodeRecordService autoCodeRecordService;
    @Resource
@@ -80,119 +81,160 @@
            // æœªå¯ç”¨æ‰¹æ¬¡ç®¡ç†ï¼Œè¿”回 null
            return null;
        }
        // 1.2 æŸ¥è¯¢ç‰©æ–™æ‰¹æ¬¡é…ç½®
        MesMdItemBatchConfigDO config = itemBatchConfigService.validateItemBatchConfigExists(reqVO.getItemId());
        // è°ƒè¯•日志:打印接收到的日期
        log.info("[getOrGenerateBatchCode] æ”¶åˆ°è¯·æ±‚, itemId={}, batchCode={}, produceDate={}",
                reqVO.getItemId(), reqVO.getBatchCode(), reqVO.getProduceDate());
        // 1.2 æŸ¥è¯¢ç‰©æ–™æ‰¹æ¬¡é…ç½®ï¼ˆä»Ž MDM èŽ·å–ï¼Œä¸å­˜åœ¨åˆ™ä½¿ç”¨é»˜è®¤é…ç½®ï¼‰
        MdmItemBatchConfigRespDTO config = itemBatchConfigApi.getItemBatchConfig(reqVO.getItemId());
        // 2. æ ¹æ®é…ç½®æ ¡éªŒå¿…填参数并清空不需要的参数
        MesWmBatchDO batch = MesWmBatchDO.builder().itemId(reqVO.getItemId()).build();
        // ç”Ÿäº§æ—¥æœŸ
        if (Boolean.TRUE.equals(config.getProduceDateFlag())) {
            if (ObjUtil.isNull(reqVO.getProduceDate())) {
                throw exception(WM_BATCH_PRODUCE_DATE_REQUIRED);
        if (config != null) {
            // ç”Ÿäº§æ—¥æœŸ
            if (Boolean.TRUE.equals(config.getProduceDateFlag())) {
                if (ObjUtil.isNull(reqVO.getProduceDate())) {
                    throw exception(WM_BATCH_PRODUCE_DATE_REQUIRED);
                }
                batch.setProduceDate(reqVO.getProduceDate());
            }
            batch.setProduceDate(reqVO.getProduceDate());
            // å…¥åº“日期
            if (Boolean.TRUE.equals(config.getReceiptDateFlag())) {
                if (ObjUtil.isNull(reqVO.getReceiptDate())) {
                    throw exception(WM_BATCH_RECEIPT_DATE_REQUIRED);
                }
                batch.setReceiptDate(reqVO.getReceiptDate());
            }
            // æœ‰æ•ˆæœŸ
            if (Boolean.TRUE.equals(config.getExpireDateFlag())) {
                if (ObjUtil.isNull(reqVO.getExpireDate())) {
                    throw exception(WM_BATCH_EXPIRE_DATE_REQUIRED);
                }
                batch.setExpireDate(reqVO.getExpireDate());
            }
            // ä¾›åº”商
            if (Boolean.TRUE.equals(config.getVendorFlag())) {
                if (ObjUtil.isNull(reqVO.getVendorId())) {
                    throw exception(WM_BATCH_VENDOR_REQUIRED);
                }
                batch.setVendorId(reqVO.getVendorId());
            }
            // å®¢æˆ·
            if (Boolean.TRUE.equals(config.getClientFlag())) {
                if (ObjUtil.isNull(reqVO.getClientId())) {
                    throw exception(WM_BATCH_CLIENT_REQUIRED);
                }
                batch.setClientId(reqVO.getClientId());
            }
            // é‡‡è´­è®¢å•编号
            if (Boolean.TRUE.equals(config.getPurchaseOrderCodeFlag())) {
                if (ObjUtil.isNull(reqVO.getPurchaseOrderCode())) {
                    throw exception(WM_BATCH_PURCHASE_ORDER_CODE_REQUIRED);
                }
                batch.setPurchaseOrderCode(reqVO.getPurchaseOrderCode());
            }
            // é”€å”®è®¢å•编号
            if (Boolean.TRUE.equals(config.getSalesOrderCodeFlag())) {
                if (ObjUtil.isNull(reqVO.getSalesOrderCode())) {
                    throw exception(WM_BATCH_CUSTOMER_ORDER_CODE_REQUIRED);
                }
                batch.setSalesOrderCode(reqVO.getSalesOrderCode());
            }
            // ç”Ÿäº§å·¥å•
            if (Boolean.TRUE.equals(config.getWorkOrderFlag())) {
                if (ObjUtil.isNull(reqVO.getWorkOrderId())) {
                    throw exception(WM_BATCH_WORK_ORDER_REQUIRED);
                }
                batch.setWorkOrderId(reqVO.getWorkOrderId());
            }
            // ç”Ÿäº§ä»»åŠ¡
            if (Boolean.TRUE.equals(config.getTaskFlag())) {
                if (ObjUtil.isNull(reqVO.getTaskId())) {
                    throw exception(WM_BATCH_TASK_REQUIRED);
                }
                batch.setTaskId(reqVO.getTaskId());
            }
            // å·¥ä½œç«™
            if (Boolean.TRUE.equals(config.getWorkstationFlag())) {
                if (ObjUtil.isNull(reqVO.getWorkstationId())) {
                    throw exception(WM_BATCH_WORKSTATION_REQUIRED);
                }
                batch.setWorkstationId(reqVO.getWorkstationId());
            }
            // å·¥å…·
            if (Boolean.TRUE.equals(config.getToolFlag())) {
                if (ObjUtil.isNull(reqVO.getToolId())) {
                    throw exception(WM_BATCH_TOOL_REQUIRED);
                }
                batch.setToolId(reqVO.getToolId());
            }
            // æ¨¡å…·
            if (Boolean.TRUE.equals(config.getMoldFlag())) {
                if (ObjUtil.isNull(reqVO.getMoldId())) {
                    throw exception(WM_BATCH_MOLD_REQUIRED);
                }
                batch.setMoldId(reqVO.getMoldId());
            }
            // ç”Ÿäº§æ‰¹å·
            if (Boolean.TRUE.equals(config.getLotNumberFlag())) {
                if (ObjUtil.isNull(reqVO.getLotNumber())) {
                    throw exception(WM_BATCH_LOT_NUMBER_REQUIRED);
                }
                batch.setLotNumber(reqVO.getLotNumber());
            }
            // è´¨é‡çŠ¶æ€
            if (Boolean.TRUE.equals(config.getQualityStatusFlag())) {
                if (ObjUtil.isNull(reqVO.getQualityStatus())) {
                    throw exception(WM_BATCH_QUALITY_STATUS_REQUIRED);
                }
                batch.setQualityStatus(reqVO.getQualityStatus());
            }
        }
        // å…¥åº“日期
        if (Boolean.TRUE.equals(config.getReceiptDateFlag())) {
            if (ObjUtil.isNull(reqVO.getReceiptDate())) {
                throw exception(WM_BATCH_RECEIPT_DATE_REQUIRED);
        // æ— æ‰¹æ¬¡é…ç½®æ—¶ï¼Œä½¿ç”¨ä¼ å…¥çš„参数(宽松模式,适用于自定义入库等场景)
        else {
            if (reqVO.getProduceDate() != null) {
                batch.setProduceDate(reqVO.getProduceDate());
            }
            batch.setReceiptDate(reqVO.getReceiptDate());
        }
        // æœ‰æ•ˆæœŸ
        if (Boolean.TRUE.equals(config.getExpireDateFlag())) {
            if (ObjUtil.isNull(reqVO.getExpireDate())) {
                throw exception(WM_BATCH_EXPIRE_DATE_REQUIRED);
            if (reqVO.getReceiptDate() != null) {
                batch.setReceiptDate(reqVO.getReceiptDate());
            }
            batch.setExpireDate(reqVO.getExpireDate());
        }
        // ä¾›åº”商
        if (Boolean.TRUE.equals(config.getVendorFlag())) {
            if (ObjUtil.isNull(reqVO.getVendorId())) {
                throw exception(WM_BATCH_VENDOR_REQUIRED);
            if (reqVO.getExpireDate() != null) {
                batch.setExpireDate(reqVO.getExpireDate());
            }
            batch.setVendorId(reqVO.getVendorId());
        }
        // å®¢æˆ·
        if (Boolean.TRUE.equals(config.getClientFlag())) {
            if (ObjUtil.isNull(reqVO.getClientId())) {
                throw exception(WM_BATCH_CLIENT_REQUIRED);
            if (reqVO.getVendorId() != null) {
                batch.setVendorId(reqVO.getVendorId());
            }
            batch.setClientId(reqVO.getClientId());
        }
        // é‡‡è´­è®¢å•编号
        if (Boolean.TRUE.equals(config.getPurchaseOrderCodeFlag())) {
            if (ObjUtil.isNull(reqVO.getPurchaseOrderCode())) {
                throw exception(WM_BATCH_PURCHASE_ORDER_CODE_REQUIRED);
            if (reqVO.getClientId() != null) {
                batch.setClientId(reqVO.getClientId());
            }
            batch.setPurchaseOrderCode(reqVO.getPurchaseOrderCode());
        }
        // é”€å”®è®¢å•编号
        if (Boolean.TRUE.equals(config.getSalesOrderCodeFlag())) {
            if (ObjUtil.isNull(reqVO.getSalesOrderCode())) {
                throw exception(WM_BATCH_CUSTOMER_ORDER_CODE_REQUIRED);
            }
            batch.setSalesOrderCode(reqVO.getSalesOrderCode());
        }
        // ç”Ÿäº§å·¥å•
        if (Boolean.TRUE.equals(config.getWorkOrderFlag())) {
            if (ObjUtil.isNull(reqVO.getWorkOrderId())) {
                throw exception(WM_BATCH_WORK_ORDER_REQUIRED);
            }
            batch.setWorkOrderId(reqVO.getWorkOrderId());
        }
        // ç”Ÿäº§ä»»åŠ¡
        if (Boolean.TRUE.equals(config.getTaskFlag())) {
            if (ObjUtil.isNull(reqVO.getTaskId())) {
                throw exception(WM_BATCH_TASK_REQUIRED);
            }
            batch.setTaskId(reqVO.getTaskId());
        }
        // å·¥ä½œç«™
        if (Boolean.TRUE.equals(config.getWorkstationFlag())) {
            if (ObjUtil.isNull(reqVO.getWorkstationId())) {
                throw exception(WM_BATCH_WORKSTATION_REQUIRED);
            }
            batch.setWorkstationId(reqVO.getWorkstationId());
        }
        // å·¥å…·
        if (Boolean.TRUE.equals(config.getToolFlag())) {
            if (ObjUtil.isNull(reqVO.getToolId())) {
                throw exception(WM_BATCH_TOOL_REQUIRED);
            }
            batch.setToolId(reqVO.getToolId());
        }
        // æ¨¡å…·
        if (Boolean.TRUE.equals(config.getMoldFlag())) {
            if (ObjUtil.isNull(reqVO.getMoldId())) {
                throw exception(WM_BATCH_MOLD_REQUIRED);
            }
            batch.setMoldId(reqVO.getMoldId());
        }
        // ç”Ÿäº§æ‰¹å·
        if (Boolean.TRUE.equals(config.getLotNumberFlag())) {
            if (ObjUtil.isNull(reqVO.getLotNumber())) {
                throw exception(WM_BATCH_LOT_NUMBER_REQUIRED);
            }
            batch.setLotNumber(reqVO.getLotNumber());
        }
        // è´¨é‡çŠ¶æ€
        if (Boolean.TRUE.equals(config.getQualityStatusFlag())) {
            if (ObjUtil.isNull(reqVO.getQualityStatus())) {
                throw exception(WM_BATCH_QUALITY_STATUS_REQUIRED);
            }
            batch.setQualityStatus(reqVO.getQualityStatus());
        }
        // 3.1 æƒ…况一:查询是否存在匹配的批次
        MesWmBatchDO existingBatch = batchMapper.selectFirst(batch);
        if (existingBatch != null) {
            return existingBatch;
        // 3.1 æƒ…况一:传入了 batchCode,查询是否存在该批次号
        if (StrUtil.isNotBlank(reqVO.getBatchCode())) {
            MesWmBatchDO existingBatch = batchMapper.selectByCode(reqVO.getBatchCode());
            if (existingBatch != null) {
                // æ ¡éªŒç‰©æ–™æ˜¯å¦åŒ¹é…
                if (!existingBatch.getItemId().equals(reqVO.getItemId())) {
                    throw exception(WM_BATCH_ITEM_MISMATCH);
                }
                // æ›´æ–°æ‰¹æ¬¡å±žæ€§ï¼ˆä»…更新传入的非空值)
                updateBatchProperties(existingBatch, batch);
                return existingBatch;
            }
            // ä¸å­˜åœ¨åˆ™ä½¿ç”¨ä¼ å…¥çš„ batchCode åˆ›å»ºæ–°æ‰¹æ¬¡
            batch.setCode(reqVO.getBatchCode());
        } else {
            // 3.2 æƒ…况二:查询是否存在匹配的批次(根据属性匹配)
            MesWmBatchDO existingBatch = batchMapper.selectFirst(batch);
            if (existingBatch != null) {
                return existingBatch;
            }
            // 3.3 æƒ…况三:自动生成新批次号
            String batchCode = autoCodeRecordService.generateAutoCode(MesMdAutoCodeRuleCodeEnum.WM_BATCH_CODE.getCode());
            batch.setCode(batchCode);
        }
        // 3.2 æƒ…况二:生成新批次
        String batchCode = autoCodeRecordService.generateAutoCode(MesMdAutoCodeRuleCodeEnum.WM_BATCH_CODE.getCode());
        batch.setCode(batchCode);
        batchMapper.insert(batch);
        // 4. ç”Ÿæˆæ¡ç 
@@ -200,6 +242,75 @@
        return batch;
    }
    /**
     * æ›´æ–°å·²æœ‰æ‰¹æ¬¡çš„属性(仅更新传入的非空值)
     *
     * @param existingBatch å·²æœ‰æ‰¹æ¬¡
     * @param newBatch æ–°æ‰¹æ¬¡å±žæ€§
     */
    private void updateBatchProperties(MesWmBatchDO existingBatch, MesWmBatchDO newBatch) {
        boolean needUpdate = false;
        if (newBatch.getProduceDate() != null && !newBatch.getProduceDate().equals(existingBatch.getProduceDate())) {
            existingBatch.setProduceDate(newBatch.getProduceDate());
            needUpdate = true;
        }
        if (newBatch.getExpireDate() != null && !newBatch.getExpireDate().equals(existingBatch.getExpireDate())) {
            existingBatch.setExpireDate(newBatch.getExpireDate());
            needUpdate = true;
        }
        if (newBatch.getReceiptDate() != null && !newBatch.getReceiptDate().equals(existingBatch.getReceiptDate())) {
            existingBatch.setReceiptDate(newBatch.getReceiptDate());
            needUpdate = true;
        }
        if (newBatch.getVendorId() != null && !newBatch.getVendorId().equals(existingBatch.getVendorId())) {
            existingBatch.setVendorId(newBatch.getVendorId());
            needUpdate = true;
        }
        if (newBatch.getClientId() != null && !newBatch.getClientId().equals(existingBatch.getClientId())) {
            existingBatch.setClientId(newBatch.getClientId());
            needUpdate = true;
        }
        if (newBatch.getSalesOrderCode() != null && !newBatch.getSalesOrderCode().equals(existingBatch.getSalesOrderCode())) {
            existingBatch.setSalesOrderCode(newBatch.getSalesOrderCode());
            needUpdate = true;
        }
        if (newBatch.getPurchaseOrderCode() != null && !newBatch.getPurchaseOrderCode().equals(existingBatch.getPurchaseOrderCode())) {
            existingBatch.setPurchaseOrderCode(newBatch.getPurchaseOrderCode());
            needUpdate = true;
        }
        if (newBatch.getWorkOrderId() != null && !newBatch.getWorkOrderId().equals(existingBatch.getWorkOrderId())) {
            existingBatch.setWorkOrderId(newBatch.getWorkOrderId());
            needUpdate = true;
        }
        if (newBatch.getTaskId() != null && !newBatch.getTaskId().equals(existingBatch.getTaskId())) {
            existingBatch.setTaskId(newBatch.getTaskId());
            needUpdate = true;
        }
        if (newBatch.getWorkstationId() != null && !newBatch.getWorkstationId().equals(existingBatch.getWorkstationId())) {
            existingBatch.setWorkstationId(newBatch.getWorkstationId());
            needUpdate = true;
        }
        if (newBatch.getToolId() != null && !newBatch.getToolId().equals(existingBatch.getToolId())) {
            existingBatch.setToolId(newBatch.getToolId());
            needUpdate = true;
        }
        if (newBatch.getMoldId() != null && !newBatch.getMoldId().equals(existingBatch.getMoldId())) {
            existingBatch.setMoldId(newBatch.getMoldId());
            needUpdate = true;
        }
        if (newBatch.getLotNumber() != null && !newBatch.getLotNumber().equals(existingBatch.getLotNumber())) {
            existingBatch.setLotNumber(newBatch.getLotNumber());
            needUpdate = true;
        }
        if (newBatch.getQualityStatus() != null && !newBatch.getQualityStatus().equals(existingBatch.getQualityStatus())) {
            existingBatch.setQualityStatus(newBatch.getQualityStatus());
            needUpdate = true;
        }
        if (needUpdate) {
            batchMapper.updateById(existingBatch);
        }
    }
    @Override
    public List<MesWmBatchDO> getForwardBatchList(String code) {
        return getForwardBatchList(code, new HashSet<>(), 0);
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstock/MesWmMaterialStockServiceImpl.java
@@ -19,6 +19,9 @@
import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemTypeService;
import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchGenerateReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
@@ -59,6 +62,9 @@
    @Resource
    @Lazy
    private MesWmTransactionService wmTransactionService;
    @Resource
    @Lazy
    private MesWmBatchService batchService;
    @Override
    public MesWmMaterialStockDO getMaterialStock(Long id) {
@@ -264,28 +270,41 @@
        // 1. æ ¡éªŒç‰©æ–™å­˜åœ¨ä¸”启用
        MesMdmItemDO item = mdmItemService.validateItemExistsAndEnable(reqVO.getItemId());
        // 2. æ ¡éªŒåº“位混放规则
        checkAreaMixingRule(reqVO.getAreaId(), reqVO.getItemId(), reqVO.getBatchId());
        // 2. å¤„理批次(仅入库时需要处理批次)
        Long batchId = reqVO.getBatchId();
        String batchCode = reqVO.getBatchCode();
        if (isInbound) {
            MesWmBatchDO batch = resolveBatch(reqVO, item);
            if (batch != null) {
                batchId = batch.getId();
                batchCode = batch.getCode();
            }
        } else {
            // å‡ºåº“时校验批次存在
            if (batchId != null) {
                batchService.validateBatchExists(batchId, reqVO.getItemId());
            }
        }
        // 3. èŽ·å–æˆ–åˆ›å»ºåº“å­˜è®°å½•
        // 3. æ ¡éªŒåº“位混放规则
        checkAreaMixingRule(reqVO.getAreaId(), reqVO.getItemId(), batchId);
        // 4. èŽ·å–æˆ–åˆ›å»ºåº“å­˜è®°å½•
        MesWmMaterialStockDO stock = getOrCreateMaterialStock(
                reqVO.getItemId(),
                reqVO.getWarehouseId(),
                reqVO.getLocationId(),
                reqVO.getAreaId(),
                reqVO.getBatchId(),
                reqVO.getBatchCode(),
                batchId,
                batchCode,
                reqVO.getVendorId(),
                LocalDateTime.now()
        );
        // 4. è®¡ç®—变动数量:入库为正,出库为负
        // 5. è®¡ç®—变动数量:入库为正,出库为负
        BigDecimal quantity = isInbound ? reqVO.getQuantity() : reqVO.getQuantity().negate();
        // 5. æ›´æ–°åº“存数量(出库时校验库存充足)
        updateMaterialStockQuantity(stock.getId(), quantity, !isInbound);
        // 6. åˆ›å»ºåº“存事务流水
        // 6. åˆ›å»ºåº“存事务流水(内部会更新库存数量)
        int bizType = isInbound ? MesBizTypeConstants.WM_STOCK_ADJUST_IN : MesBizTypeConstants.WM_STOCK_ADJUST_OUT;
        MesWmTransactionSaveReqDTO transaction = new MesWmTransactionSaveReqDTO()
                .setType(isInbound ? MesWmTransactionTypeEnum.IN.getType() : MesWmTransactionTypeEnum.OUT.getType())
@@ -295,8 +314,8 @@
                .setBizLineId(stock.getId())
                .setItemId(reqVO.getItemId())
                .setQuantity(quantity)
                .setBatchId(reqVO.getBatchId())
                .setBatchCode(reqVO.getBatchCode())
                .setBatchId(batchId)
                .setBatchCode(batchCode)
                .setWarehouseId(reqVO.getWarehouseId())
                .setLocationId(reqVO.getLocationId())
                .setAreaId(reqVO.getAreaId())
@@ -308,4 +327,77 @@
        return stock.getId();
    }
    /**
     * è§£æžæ‰¹æ¬¡
     * <p>
     * å…¥åº“时的批次处理逻辑:
     * 1. ç‰©æ–™æœªå¯ç”¨æ‰¹æ¬¡ç®¡ç†ï¼šè¿”回 null
     * 2. ä¼ å…¥ batchId:校验并使用已有批次
     * 3. æœªä¼  batchId:根据 batchCode ç­‰å‚数自动创建批次
     *
     * @param reqVO å…¥åº“请求
     * @param item  ç‰©æ–™ä¿¡æ¯
     * @return æ‰¹æ¬¡è®°å½•(物料未启用批次管理时返回 null)
     */
    private MesWmBatchDO resolveBatch(MesWmMaterialStockAdjustReqVO reqVO, MesMdmItemDO item) {
        // 1. ç‰©æ–™æœªå¯ç”¨æ‰¹æ¬¡ç®¡ç†ï¼Œè¿”回 null
        if (Boolean.FALSE.equals(item.getIsBatchManaged())) {
            return null;
        }
        // 2. ä¼ å…¥ batchId,校验并返回已有批次
        if (reqVO.getBatchId() != null) {
            return batchService.validateBatchExists(reqVO.getBatchId(), reqVO.getItemId());
        }
        // 3. æœªä¼  batchId,自动创建批次
        MesWmBatchGenerateReqVO batchReqVO = new MesWmBatchGenerateReqVO()
                .setItemId(reqVO.getItemId())
                .setBatchCode(reqVO.getBatchCode())
                .setVendorId(reqVO.getVendorId())
                .setProduceDate(reqVO.getProduceDate() != null ? reqVO.getProduceDate().atStartOfDay() : null)
                .setExpireDate(reqVO.getExpireDate() != null ? reqVO.getExpireDate().atStartOfDay() : null)
                .setReceiptDate(LocalDateTime.now())
                .setLotNumber(reqVO.getLotNumber())
                .setQualityStatus(parseQualityStatus(reqVO.getQualityStatus()));
        return batchService.getOrGenerateBatchCode(batchReqVO);
    }
    /**
     * è§£æžè´¨é‡çŠ¶æ€ï¼ˆæ”¯æŒæ•°å­—æˆ–å­—ç¬¦ä¸²ï¼‰
     */
    private Integer parseQualityStatus(Object qualityStatus) {
        if (qualityStatus == null) {
            return null;
        }
        if (qualityStatus instanceof Integer) {
            return (Integer) qualityStatus;
        }
        if (qualityStatus instanceof String) {
            String str = (String) qualityStatus;
            // æ”¯æŒæ•°å­—字符串
            if (str.matches("\\d+")) {
                return Integer.parseInt(str);
            }
            // æ”¯æŒå¸¸è§å­—符串映射
            switch (str.toUpperCase()) {
                case "PENDING":
                case "待检验":
                case "待检":
                    return 0;
                case "PASS":
                case "QUALIFIED":
                case "合格":
                    return 1;
                case "FAIL":
                case "UNQUALIFIED":
                case "不合格":
                    return 2;
                default:
                    throw new IllegalArgumentException("无效的质量状态: " + str);
            }
        }
        throw new IllegalArgumentException("无效的质量状态类型: " + qualityStatus.getClass().getName());
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesServiceImpl.java
@@ -27,6 +27,7 @@
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
import cn.iocoder.yudao.module.erp.api.sale.ErpSaleOrderApi;
import cn.iocoder.yudao.module.erp.api.sale.enums.ErpSaleOrderOutStatusEnum;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -213,6 +214,12 @@
        // æ‰§è¡Œæ‹£è´§ï¼ˆå¾…拣货 â†’ å¾…填写运单)
        productSalesMapper.updateById(new MesWmProductSalesDO()
                .setId(id).setStatus(MesWmProductSalesStatusEnum.SHIPPING.getStatus()));
        // å›žå†™ ERP é”€å”®è®¢å•出库状态为"部分出库"(表示已拣货/占用)
        if (sales.getSaleOrderId() != null) {
            saleOrderApi.updateOrderOutStatusTo(sales.getSaleOrderId(),
                    ErpSaleOrderOutStatusEnum.PART_OUT.getStatus());
        }
    }
    @Override
@@ -316,7 +323,8 @@
                .setBatchId(detail.getBatchId()).setBatchCode(detail.getBatchCode())
                .setWarehouseId(detail.getWarehouseId()).setLocationId(detail.getLocationId()).setAreaId(detail.getAreaId())
                .setBizType(MesBizTypeConstants.WM_PRODUCT_SALES).setBizId(sales.getId())
                .setBizCode(sales.getCode()).setBizLineId(detail.getLineId())));
                .setBizCode(sales.getCode()).setBizLineId(detail.getLineId())
                .setMaterialStockId(detail.getMaterialStockId()))); // æŒ‡å®šåº“存记录ID,避免重新匹配
    }
    @Override
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/salesnotice/MesWmSalesNoticeServiceImpl.java
@@ -21,6 +21,7 @@
import cn.iocoder.yudao.module.erp.api.sale.ErpSaleOrderApi;
import cn.iocoder.yudao.module.erp.api.sale.dto.ErpSaleOrderItemRespDTO;
import cn.iocoder.yudao.module.erp.api.sale.dto.ErpSaleOrderRespDTO;
import cn.iocoder.yudao.module.erp.api.sale.enums.ErpSaleOrderOutStatusEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -114,7 +115,7 @@
    @Transactional(rollbackFor = Exception.class)
    public void submitSalesNotice(Long id) {
        // 1.1 æ ¡éªŒå­˜åœ¨ + è‰ç¨¿çŠ¶æ€
        validateSalesNoticeExistsAndDraft(id);
        MesWmSalesNoticeDO notice = validateSalesNoticeExistsAndDraft(id);
        // 1.2 æ£€æŸ¥æ˜¯å¦æœ‰è¡Œé¡¹ç›®
        List<MesWmSalesNoticeLineDO> lines = salesNoticeLineService.getSalesNoticeLineListByNoticeId(id);
        if (CollUtil.isEmpty(lines)) {
@@ -124,6 +125,12 @@
        // 2. æ›´æ–°çŠ¶æ€ï¼šè‰ç¨¿ â†’ å¾…出库
        salesNoticeMapper.updateById(new MesWmSalesNoticeDO()
                .setId(id).setStatus(APPROVED.getStatus()));
        // 3. å›žå†™ ERP é”€å”®è®¢å•出库状态为"已预留"
        if (notice.getSaleOrderId() != null) {
            saleOrderApi.updateOrderOutStatusTo(notice.getSaleOrderId(),
                    ErpSaleOrderOutStatusEnum.RESERVED.getStatus());
        }
    }
    @Override
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/MesWmTransactionService.java
@@ -1,5 +1,8 @@
package cn.iocoder.yudao.module.mes.service.wm.transaction;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.wm.transaction.vo.MesWmTransactionPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.transaction.MesWmTransactionDO;
import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
import jakarta.validation.Valid;
@@ -27,4 +30,12 @@
     */
    void createTransactionList(List<MesWmTransactionSaveReqDTO> reqDTOs);
    /**
     * åˆ†é¡µæŸ¥è¯¢åº“存事务流水
     *
     * @param pageReqVO æŸ¥è¯¢æ¡ä»¶
     * @return äº‹åŠ¡æµæ°´åˆ†é¡µç»“æžœ
     */
    PageResult<MesWmTransactionDO> getTransactionPage(MesWmTransactionPageReqVO pageReqVO);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/MesWmTransactionServiceImpl.java
@@ -4,6 +4,8 @@
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.wm.transaction.vo.MesWmTransactionPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO;
@@ -75,10 +77,25 @@
        // 1.5 å…³è”事务校验
        checkRelatedTransaction(reqDTO);
        // 2.1 èŽ·å–æˆ–åˆ›å»ºåº“å­˜è®°å½•
        MesWmMaterialStockDO materialStock = materialStockService.getOrCreateMaterialStock(
                reqDTO.getItemId(), reqDTO.getWarehouseId(), reqDTO.getLocationId(), reqDTO.getAreaId(),
                reqDTO.getBatchId(), reqDTO.getBatchCode(), reqDTO.getVendorId(), reqDTO.getReceiptTime());
        // 2.1 èŽ·å–æˆ–åˆ›å»ºåº“å­˜è®°å½•ï¼ˆå¦‚æžœä¼ å…¥äº† materialStockId åˆ™ç›´æŽ¥ä½¿ç”¨ï¼Œé¿å…é‡æ–°åŒ¹é…ï¼‰
        MesWmMaterialStockDO materialStock;
        if (reqDTO.getMaterialStockId() != null) {
            materialStock = materialStockService.getMaterialStock(reqDTO.getMaterialStockId());
            if (materialStock == null) {
                throw exception(WM_MATERIAL_STOCK_NOT_EXISTS);
            }
            // æ ¡éªŒç‰©æ–™ã€æ‰¹æ¬¡ã€ä»“库等维度一致(防止传入错误的 materialStockId)
            if (ObjUtil.notEqual(materialStock.getItemId(), reqDTO.getItemId())
                    || ObjUtil.notEqual(materialStock.getWarehouseId(), reqDTO.getWarehouseId())
                    || ObjUtil.notEqual(materialStock.getLocationId(), reqDTO.getLocationId())
                    || ObjUtil.notEqual(materialStock.getAreaId(), reqDTO.getAreaId())) {
                throw exception(WM_MATERIAL_STOCK_SELECTION_MISMATCH);
            }
        } else {
            materialStock = materialStockService.getOrCreateMaterialStock(
                    reqDTO.getItemId(), reqDTO.getWarehouseId(), reqDTO.getLocationId(), reqDTO.getAreaId(),
                    reqDTO.getBatchId(), reqDTO.getBatchCode(), reqDTO.getVendorId(), reqDTO.getReceiptTime());
        }
        // 2.2 å†»ç»“校验(仓库、库区、库位、库存记录)
        checkFrozen(reqDTO, materialStock);
        // 2.3 æ›´æ–°åº“存数量
@@ -106,6 +123,11 @@
        reqDTOs.forEach(this::createTransaction);
    }
    @Override
    public PageResult<MesWmTransactionDO> getTransactionPage(MesWmTransactionPageReqVO pageReqVO) {
        return transactionMapper.selectPage(pageReqVO);
    }
    // ==================== ç§æœ‰æ ¡éªŒæ–¹æ³• ====================
    /**
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/dto/MesWmTransactionSaveReqDTO.java
@@ -102,6 +102,11 @@
    // ===== å…³è” =====
    /**
     * æŒ‡å®šåº“存记录 ID(出库场景优先使用,避免重新匹配库存记录)
     */
    private Long materialStockId;
    /**
     * å…³è”事务 ID(成对事务中,入库事务关联出库事务)
     */
    private Long relatedTransactionId;
yudao-module-mes/src/main/resources/mapper/wm/batch/MesWmBatchMapper.xml
@@ -14,12 +14,11 @@
        SQL è·¯å¾„:库存事务(消耗 OUT) â†’ æ¶ˆè€—单 â†’ æŠ¥å·¥è®°å½• â†’ åº“存事务(产出 IN) â†’ æ‰¹æ¬¡
    -->
    <select id="selectListByForward" parameterType="String" resultMap="BatchTraceResult">
        SELECT DISTINCT pf.work_order_id, pf.item_id, ppl.batch_id, ppl.batch_code
        SELECT DISTINCT pf.work_order_id, pf.item_id, tx_produce.batch_id, tx_produce.batch_code
        FROM mes_wm_transaction tx_consume
        LEFT JOIN mes_wm_item_consume ic ON tx_consume.biz_id = ic.id AND tx_consume.biz_type = 123 AND ic.deleted = 0
        LEFT JOIN mes_pro_feedback pf ON pf.id = ic.feedback_id AND pf.deleted = 0
        LEFT JOIN mes_wm_transaction tx_produce ON tx_produce.biz_id = pf.id AND tx_produce.biz_type = 124 AND tx_produce.deleted = 0
        LEFT JOIN mes_wm_product_produce pp ON tx_produce.biz_id = pp.id AND tx_produce.biz_type = 124 AND pp.deleted = 0
        WHERE tx_consume.batch_code = #{batchCode}
        AND tx_consume.biz_type = 123
        AND tx_consume.deleted = 0
@@ -31,12 +30,12 @@
        SQL è·¯å¾„:库存事务(产出 IN) â†’ äº§å‡ºå• â†’ æŠ¥å·¥è®°å½• â†’ åº“存事务(消耗 OUT) â†’ æ‰¹æ¬¡
    -->
    <select id="selectListByBackward" parameterType="String" resultMap="BatchTraceResult">
        SELECT DISTINCT ic.work_order_id, icd.item_id, icd.batch_id, icd.batch_code
        SELECT DISTINCT ic.work_order_id, tx_consume.item_id, tx_consume.batch_id, tx_consume.batch_code
        FROM mes_wm_transaction tx_produce
        LEFT JOIN mes_wm_product_produce pp ON tx_produce.biz_id = pp.id AND tx_produce.biz_type = 124 AND pp.deleted = 0
        LEFT JOIN mes_pro_feedback pf ON pf.id = pp.feedback_id AND pf.deleted = 0
        LEFT JOIN mes_wm_transaction tx_consume ON tx_consume.biz_id = pf.id AND tx_consume.biz_type = 123 AND tx_consume.deleted = 0
        LEFT JOIN mes_wm_item_consume ic ON tx_consume.biz_id = ic.id AND tx_consume.biz_type = 123 AND ic.deleted = 0
        LEFT JOIN mes_wm_item_consume ic ON ic.id = tx_consume.biz_id AND ic.deleted = 0
        WHERE tx_produce.batch_code = #{batchCode}
        AND tx_produce.biz_type = 124
        AND tx_produce.deleted = 0