gaoluyang
4 天以前 70ea74cfb981f812bfa4e3ef91843c5ad93f7b27
feat(erp): 采购台账改造与采购退货增强

采购台账(原采购订单):
- 新增采购类型、合同号(手工输入,不自动生成)、项目名称、付款日期、付款方式字段
- 明细行快照产品编号(MDM 物料编码),列表/详情/导出展示
- 支持从销售订单、生产工单带入来源单号与明细
- 付款日期必填;付款日期与订单日期账期超 30 天前端标红提醒
- 销售订单新增收款日期,同口径 30 天提醒
- 菜单名称统一改为「采购台账」,权限串保持不变

采购退货:
- 新增 BPM 审批流程(采购 → 质量 → 仓库),状态新增「审批中」
- 附件统一走 system_storage_attachment
- 处理方式支持退款 / 换货:退款生成负数冲红付款单;换货自动生成到货通知单并逐行 IQC,重新入库质检
- 创建时从采购订单快照合同号,完成时回写采购订单退货数量

配套:
- 新增 sql/mysql/20261104_purchase_ledger.sql、20261104_return_vendor_approval.sql
- 按规则全量重新生成 docs/sql/config_export_all_20261104.sql
- 更新前端联调文档与业务可视化图

Co-Authored-By: Claude Code <noreply@anthropic.com>
已添加12个文件
已修改40个文件
11898 ■■■■■ 文件已修改
docs/project-business/data/objects.json 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/project-business/mermaid/03-purchase-flow.mmd 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sql/config_export_all_20261104.sql 10182 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/20261104_purchase_ledger.sql 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/20261104_return_vendor_approval.sql 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/finance/ErpFinancePaymentApi.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/finance/dto/ErpFinancePaymentRedFlushCreateReqDTO.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/finance/dto/ErpFinancePaymentRespDTO.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/purchase/ErpPurchaseOrderApi.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/purchase/dto/ErpPurchaseOrderRespDTO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/api/finance/ErpFinancePaymentApiImpl.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/api/purchase/ErpPurchaseOrderApiImpl.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/ErpFinancePaymentController.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/payment/ErpFinancePaymentPageReqVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/payment/ErpFinancePaymentRespVO.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseOrderController.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderImportExcelVO.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderPageReqVO.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderRespVO.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderSaveReqVO.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderPageReqVO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderSaveReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/finance/ErpFinancePaymentDO.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseOrderDO.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseOrderItemDO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleOrderDO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/finance/ErpFinancePaymentMapper.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseOrderMapper.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSaleOrderMapper.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/DictTypeConstants.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpPaymentMethodEnum.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpPurchaseOrderSourceTypeEnum.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpPurchaseTypeEnum.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErrorCodeConstants.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/finance/ErpFinancePaymentSourceTypeEnum.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinancePaymentService.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinancePaymentServiceImpl.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnvendor/MesWmReturnVendorController.java 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnvendor/vo/MesWmReturnVendorRespVO.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnvendor/vo/MesWmReturnVendorSaveReqVO.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/returnvendor/MesWmReturnVendorDO.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/DictTypeConstants.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/wm/MesWmReturnVendorHandleTypeEnum.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/wm/MesWmReturnVendorStatusEnum.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/arrivalnotice/MesWmArrivalNoticeService.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/arrivalnotice/MesWmArrivalNoticeServiceImpl.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnvendor/MesWmReturnVendorService.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnvendor/MesWmReturnVendorServiceImpl.java 308 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnvendor/listener/MesWmReturnVendorStatusListener.java 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/project-business/data/objects.json
@@ -15,7 +15,7 @@
    {"name": "询价比价", "module": "供应商管理", "table": "srm_inquiry", "doc": "XJ编号,草稿0→已转订单20→已取消30(10询价中为历史状态);新增/编辑时一并录入产品行×参与供应商报价(srm_inquiry_quote)并选定每个产品的中标供应商,无需单独的报价/比价步骤,确认后按中标供应商拆分生成采购订单草稿并记录 srm_inquiry_order", "upstream": ["供应商", "主数据(物料)"], "downstream": ["采购订单(草稿)"], "keywords": "询价 æ¯”ä»· æŠ¥ä»· ä¸­æ ‡ inquiry quote"},
    {"name": "投标保证金", "module": "供应商管理", "table": "srm_bid_bond", "doc": "BZ编号台账,关联供应商/询价单/采购订单,记录保证金金额、缴纳日期、是否中标、退回状态(0未退1部分2已退3无需退)/退回金额/退回日期", "upstream": ["供应商", "询价比价", "采购订单"], "downstream": ["供应商往来(参考)"], "keywords": "投标保证金 ä¸­æ ‡ é€€å›ž bid bond"},
    {"name": "采购申请", "module": "进销存", "table": "erp_purchase_request", "doc": "CGSQ编号,BPM å®¡æ‰¹ï¼Œé€šè¿‡åŽæœ‰ä¾›åº”商自动生成订单", "upstream": ["生产缺口(人工)"], "downstream": ["采购订单"], "keywords": "采购申请 è¯·è´­ request"},
    {"name": "采购订单", "module": "进销存", "table": "erp_purchase_order", "doc": "CGDD编号,BPM å®¡æ‰¹(草稿0→审批中15→已审核20),入库状态:未0部分1全部2;来源可为采购申请或询价比价确认生成的草稿(来源单号回溯);单价为含税单价,价税合计=单价×数量,税额从含税金额中反算(不再额外加税)", "upstream": ["采购申请", "询价比价(确认中标)"], "downstream": ["到货通知", "采购来票", "供应商退货(约束物料)", "供应商往来"], "keywords": "采购订单 purchase"},
    {"name": "采购订单", "module": "进销存", "table": "erp_purchase_order", "doc": "菜单名「采购台账」,CGDD编号,BPM å®¡æ‰¹(草稿0→审批中15→已审核20),入库状态:未0部分1全部2;表头含采购类型(生产/办公用品/其他)、合同号(手工输入不自动生成)、项目名称、付款日期(必填,超30天账期标红)、付款方式(电汇/汇票/现金/手机支付);明细产品编号快照 MDM ç‰©æ–™ç¼–码;来源可为采购申请、询价比价确认生成的草稿或销售订单/生产工单(来源单号回溯);单价为含税单价,价税合计=单价×数量,税额从含税金额中反算(不再额外加税)", "upstream": ["采购申请", "询价比价(确认中标)"], "downstream": ["到货通知", "采购来票", "供应商退货(约束物料)", "供应商往来"], "keywords": "采购订单 purchase"},
    {"name": "采购来票", "module": "进销存", "table": "erp_purchase_invoice", "doc": "FPD编号,AI OCR é¢„填,累计≤订单总价,付款单的前置;供应商往来右侧展示开票金额/已付/剩余应付", "upstream": ["采购订单"], "downstream": ["付款单", "供应商往来"], "keywords": "来票 å‘票 é‡‡è´­å‘票"},
    {"name": "付款单", "module": "财务", "table": "erp_finance_payment", "doc": "FKD编号,必须关联来票+附件,累计≤来票金额,BPM å®¡æ‰¹", "upstream": ["采购来票"], "downstream": ["结算账户", "供应商往来"], "keywords": "付款 åº”付 payment"},
    {"name": "供应商往来", "module": "进销存", "table": "无独立表(聚合视图)", "doc": "按供应商+时间范围聚合采购订单/来票/付款:汇总累计采购额、累计开票额、累计付款额、未付余额;左侧订单行含已开票/未开票金额,右侧来票行含开票金额/已付/剩余应付", "upstream": ["采购订单", "采购来票", "付款单"], "downstream": [], "keywords": "供应商往来 å¯¹è´¦ å¼€ç¥¨é‡‘额 åº”付 statement"},
@@ -36,7 +36,7 @@
    {"name": "生产领料单", "module": "仓储", "table": "mes_wm_product_issue", "doc": "草稿0→待拣货2→待执行3→完成4,库存-", "upstream": ["生产工单"], "downstream": ["库存台账"], "keywords": "领料 issue"},
    {"name": "销售出库单", "module": "仓储", "table": "mes_wm_product_sales", "doc": "发货通知审核后生成,提交→OQC检测→拣货→发运10→待执行3→完成4,回写销售订单出库数与状态", "upstream": ["发货通知", "销售订单"], "downstream": ["库存台账", "OQC"], "keywords": "销售出库 å‘è´§ å‡ºåº“单 sales"},
    {"name": "销售退货单", "module": "仓储", "table": "mes_wm_return_sales", "doc": "草稿→待检验(RQC)→待上架→待入库→完成,售后与手工两来源", "upstream": ["售后退货申请"], "downstream": ["库存台账", "RQC"], "keywords": "销售退货 é€€è´§å…¥åº“"},
    {"name": "供应商退货单", "module": "仓储", "table": "mes_wm_return_vendor", "doc": "IQC ä¸åˆæ ¼é€€è´§å‘起,待拣货→待执行→完成,库存-", "upstream": ["IQC"], "downstream": ["库存台账", "采购订单(returnCount)"], "keywords": "供应商退货 é€€æ–™ é€€è´§ vendor"},
    {"name": "供应商退货单", "module": "仓储", "table": "mes_wm_return_vendor", "doc": "质量部发起退货,草稿0→审批中1(BPM:采购→质量→仓库)→待拣货2→待执行退货3→完成4,可取消5,库存-;上传附件走 storage-attachment;处理方式 1退款(完成后自动生成负数冲红付款单) / 2换货(完成后自动生成待质检到货通知单并创建 IQC,重新入库质检);合同号从采购订单快照", "upstream": ["IQC", "采购订单(合同号)"], "downstream": ["库存台账", "采购订单(returnCount)", "付款单(冲红)", "到货通知(换货)"], "keywords": "供应商退货 é€€æ–™ é€€è´§ vendor"},
    {"name": "调拨单", "module": "仓储", "table": "mes_wm_transfer", "doc": "内部/外部调拨,成对 MOVE_OUT+MOVE_IN äº‹åŠ¡ï¼Œåœ¨é€”æ•°é‡", "upstream": [], "downstream": ["库存台账"], "keywords": "调拨 ç§»åº“ transfer"},
    {"name": "盘点任务", "module": "仓储", "table": "mes_wm_stock_taking_task", "doc": "静态/动态盘点,行结果:正常/盘盈(ADJUST_IN)/盘亏(ADJUST_OUT),可冻结库存、盲盘", "upstream": ["库存台账"], "downstream": ["库存台账(调整)"], "keywords": "盘点 ç›˜ç›ˆ ç›˜äº stock taking"},
    {"name": "库存台账", "module": "仓储", "table": "mes_wm_material_stock", "doc": "物料×仓库×库区×库位×批次×供应商六元组,数量/冻结/占用/在途四量", "upstream": ["全部入库单"], "downstream": ["全部出库单", "事务流水"], "keywords": "库存 å°è´¦ stock å¯ç”¨é‡"},
docs/project-business/mermaid/03-purchase-flow.mmd
@@ -6,13 +6,13 @@
  S2 -.-> S5
  C1[采购申请 CGSQ·草稿0→审批中10] -->|提交BPM| C2{审批}
  C2 -->|驳回30→可重置回草稿| C1
  C2 -->|通过20且有供应商·自动生成| C3[采购订单 CGDD·审批中15→已审核20]
  C2 -->|通过20且有供应商·自动生成| C3[采购台账 CGDD·采购类型/合同号/项目/付款日期/付款方式·审批中15→已审核20]
  C2 -->|通过20无供应商| C4[手动指定供应商生成订单]
  C4 --> C3
  C3 -->|确认收货| C5[到货通知单·草稿0→待质检2→待入库3→完成4]
  C5 -->|物料需检验·自动创建| C6[IQC æ¥æ–™æ£€éªŒ]
  C6 -->|合格1或特采2 å¯å…¥åº“| C7[采购入库单·草稿0→待上架2→待执行3→完成4]
  C6 -->|不合格退货3| C8[供应商退货·待拣货→待执行→完成·库存扣减]
  C6 -->|不合格退货3| C8[供应商退货·草稿→审批中(BPM é‡‡è´­â†’质量→仓库)→待拣货→待执行→完成·库存扣减<br/>退款→负数冲红付款单;换货→到货通知+IQC é‡æ–°å…¥åº“]
  C6 -->|不合格报废4| C9[NCR ä¸åˆæ ¼å“å¤„ç½®]
  C7 -->|回写 inCount ä¸Ž inStatus æœªå…¥åº“0部分1全部2| C3
  C7 -->|回写申请明细 inCount| C1
docs/sql/config_export_all_20261104.sql
¶Ô±ÈÐÂÎļþ
ÎļþÌ«´ó
sql/mysql/20261104_purchase_ledger.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,117 @@
-- =============================================
-- é‡‡è´­å°è´¦æ”¹é€  + é”€å”®è®¢å•收款日期
-- ç›®æ ‡åº“:mom-ntsh(MySQL 8.4)
-- è¯´æ˜Žï¼š
--   1. è„šæœ¬å¯é‡å¤æ‰§è¡Œï¼šåˆ—ç”¨ information_schema é¢„检查 + PREPARE,字典/菜单用先查后插
--   2. ä¸åŒ…含 tenant_id(本项目不使用多租户)
--   3. åŽ†å²é‡‡è´­å• payment_date å…è®¸ä¸ºç©ºï¼Œç¼–辑时由前端要求补填
-- =============================================
-- ---------------------------------------------
-- ä¸€ã€erp_purchase_order é‡‡è´­å°è´¦è¡¨å¤´å­—段
-- ---------------------------------------------
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'erp_purchase_order' AND column_name = 'purchase_type');
SET @sql = IF(@exists = 0,
    'ALTER TABLE erp_purchase_order ADD COLUMN purchase_type TINYINT NULL COMMENT ''采购类型:1生产 2办公用品 3其他'' AFTER remark',
    'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'erp_purchase_order' AND column_name = 'contract_no');
SET @sql = IF(@exists = 0,
    'ALTER TABLE erp_purchase_order ADD COLUMN contract_no VARCHAR(64) NULL COMMENT ''合同号(客户手工输入,不自动生成)'' AFTER purchase_type',
    'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'erp_purchase_order' AND column_name = 'project_name');
SET @sql = IF(@exists = 0,
    'ALTER TABLE erp_purchase_order ADD COLUMN project_name VARCHAR(255) NULL COMMENT ''项目名称(非必填)'' AFTER contract_no',
    'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'erp_purchase_order' AND column_name = 'payment_date');
SET @sql = IF(@exists = 0,
    'ALTER TABLE erp_purchase_order ADD COLUMN payment_date DATETIME NULL COMMENT ''付款日期'' AFTER project_name',
    'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'erp_purchase_order' AND column_name = 'payment_method');
SET @sql = IF(@exists = 0,
    'ALTER TABLE erp_purchase_order ADD COLUMN payment_method TINYINT NULL COMMENT ''付款方式:1电汇 2汇票 3现金 4手机支付'' AFTER payment_date',
    'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ---------------------------------------------
-- äºŒã€erp_purchase_order_items äº§å“ç¼–号(MDM ç‰©æ–™ç¼–码快照)
-- ---------------------------------------------
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'erp_purchase_order_items' AND column_name = 'product_code');
SET @sql = IF(@exists = 0,
    'ALTER TABLE erp_purchase_order_items ADD COLUMN product_code VARCHAR(64) NULL COMMENT ''产品编号(MDM ç‰©æ–™ç¼–码快照)'' AFTER product_id',
    'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ---------------------------------------------
-- ä¸‰ã€erp_sale_order æ”¶æ¬¾æ—¥æœŸ
-- ---------------------------------------------
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'erp_sale_order' AND column_name = 'receipt_date');
SET @sql = IF(@exists = 0,
    'ALTER TABLE erp_sale_order ADD COLUMN receipt_date DATETIME NULL COMMENT ''收款日期'' AFTER order_time',
    'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ---------------------------------------------
-- å››ã€å­—典:采购类型、付款方式
-- ---------------------------------------------
INSERT INTO system_dict_type (name, type, status, remark)
SELECT 'ERP é‡‡è´­ç±»åž‹', 'erp_purchase_type', 0, '采购台账-采购类型:1生产 2办公用品 3其他'
FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM system_dict_type WHERE type = 'erp_purchase_type' AND deleted = 0);
INSERT INTO system_dict_type (name, type, status, remark)
SELECT 'ERP ä»˜æ¬¾æ–¹å¼', 'erp_payment_method', 0, '采购台账-付款方式:1电汇 2汇票 3现金 4手机支付'
FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM system_dict_type WHERE type = 'erp_payment_method' AND deleted = 0);
INSERT INTO system_dict_data (sort, label, value, dict_type, status, color_type, css_class)
SELECT 0, '生产', '1', 'erp_purchase_type', 0, '', ''
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM system_dict_data WHERE dict_type = 'erp_purchase_type' AND value = '1' AND deleted = 0);
INSERT INTO system_dict_data (sort, label, value, dict_type, status, color_type, css_class)
SELECT 1, '办公用品', '2', 'erp_purchase_type', 0, '', ''
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM system_dict_data WHERE dict_type = 'erp_purchase_type' AND value = '2' AND deleted = 0);
INSERT INTO system_dict_data (sort, label, value, dict_type, status, color_type, css_class)
SELECT 2, '其他', '3', 'erp_purchase_type', 0, '', ''
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM system_dict_data WHERE dict_type = 'erp_purchase_type' AND value = '3' AND deleted = 0);
INSERT INTO system_dict_data (sort, label, value, dict_type, status, color_type, css_class)
SELECT 0, '电汇', '1', 'erp_payment_method', 0, '', ''
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM system_dict_data WHERE dict_type = 'erp_payment_method' AND value = '1' AND deleted = 0);
INSERT INTO system_dict_data (sort, label, value, dict_type, status, color_type, css_class)
SELECT 1, '汇票', '2', 'erp_payment_method', 0, '', ''
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM system_dict_data WHERE dict_type = 'erp_payment_method' AND value = '2' AND deleted = 0);
INSERT INTO system_dict_data (sort, label, value, dict_type, status, color_type, css_class)
SELECT 2, '现金', '3', 'erp_payment_method', 0, '', ''
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM system_dict_data WHERE dict_type = 'erp_payment_method' AND value = '3' AND deleted = 0);
INSERT INTO system_dict_data (sort, label, value, dict_type, status, color_type, css_class)
SELECT 3, '手机支付', '4', 'erp_payment_method', 0, '', ''
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM system_dict_data WHERE dict_type = 'erp_payment_method' AND value = '4' AND deleted = 0);
-- ---------------------------------------------
-- äº”、菜单名称:采购订单 -> é‡‡è´­å°è´¦ï¼ˆæƒé™ä¸²ä¿æŒä¸å˜ï¼Œé¿å…è§’色权限失效)
-- ---------------------------------------------
UPDATE system_menu SET name = '采购台账' WHERE id = 2666;
UPDATE system_menu SET name = '采购台账创建' WHERE id = 2668;
UPDATE system_menu SET name = '采购台账更新' WHERE id = 2669;
UPDATE system_menu SET name = '采购台账删除' WHERE id = 2670;
UPDATE system_menu SET name = '采购台账导出' WHERE id = 2671;
UPDATE system_menu SET name = '采购台账审批' WHERE id = 2672;
sql/mysql/20261104_return_vendor_approval.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,125 @@
-- =============================================
-- é‡‡è´­é€€è´§å¢žå¼ºï¼šBPM å®¡æ‰¹ + é™„ä»¶ + é€€æ¬¾/换货 + è´¢åŠ¡å†²çº¢
-- ç›®æ ‡åº“:mom-ntsh(MySQL 8.4)
-- è¯´æ˜Žï¼š
--   1. è„šæœ¬å¯é‡å¤æ‰§è¡Œï¼šåˆ—ç”¨ information_schema é¢„检查 + PREPARE,字典/菜单用先查后插
--   2. ä¸åŒ…含 tenant_id(本项目不使用多租户)
--   3. äººå·¥å‰ç½®ï¼šéœ€åœ¨ BPM åŽå°åˆ›å»ºå¹¶å‘布流程模型 purchase_return_vendor_approve
--      ï¼ˆèŠ‚ç‚¹ï¼šè´¨é‡éƒ¨å‘èµ· -> é‡‡è´­éƒ¨å®¡æ‰¹ -> è´¨é‡éƒ¨å®¡æ‰¹ -> ä»“库审批),否则提交审批时会提示未发布
-- =============================================
-- ---------------------------------------------
-- ä¸€ã€mes_wm_return_vendor å®¡æ‰¹ä¸Žè´¹ç”¨å­—段
-- ---------------------------------------------
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'mes_wm_return_vendor' AND column_name = 'process_instance_id');
SET @sql = IF(@exists = 0,
    'ALTER TABLE mes_wm_return_vendor ADD COLUMN process_instance_id VARCHAR(64) NULL COMMENT ''BPM流程实例ID'' AFTER status',
    'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'mes_wm_return_vendor' AND column_name = 'process_definition_key');
SET @sql = IF(@exists = 0,
    'ALTER TABLE mes_wm_return_vendor ADD COLUMN process_definition_key VARCHAR(64) NULL COMMENT ''BPM流程定义Key'' AFTER process_instance_id',
    'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'mes_wm_return_vendor' AND column_name = 'contract_no');
SET @sql = IF(@exists = 0,
    'ALTER TABLE mes_wm_return_vendor ADD COLUMN contract_no VARCHAR(64) NULL COMMENT ''采购合同号(创建时从采购订单快照)'' AFTER purchase_order_id',
    'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'mes_wm_return_vendor' AND column_name = 'handle_type');
SET @sql = IF(@exists = 0,
    'ALTER TABLE mes_wm_return_vendor ADD COLUMN handle_type TINYINT NULL COMMENT ''处理方式:1退款 2换货'' AFTER remark',
    'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'mes_wm_return_vendor' AND column_name = 'refund_amount');
SET @sql = IF(@exists = 0,
    'ALTER TABLE mes_wm_return_vendor ADD COLUMN refund_amount DECIMAL(24,6) NULL COMMENT ''退款金额(默认按采购单价×退货数量计算,可修改)'' AFTER handle_type',
    'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'mes_wm_return_vendor' AND column_name = 'red_flush_payment_id');
SET @sql = IF(@exists = 0,
    'ALTER TABLE mes_wm_return_vendor ADD COLUMN red_flush_payment_id BIGINT NULL COMMENT ''负数冲红付款单ID'' AFTER refund_amount',
    'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'mes_wm_return_vendor' AND column_name = 'replacement_notice_id');
SET @sql = IF(@exists = 0,
    'ALTER TABLE mes_wm_return_vendor ADD COLUMN replacement_notice_id BIGINT NULL COMMENT ''换货到货通知单ID'' AFTER red_flush_payment_id',
    'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ---------------------------------------------
-- äºŒã€erp_finance_payment å†²çº¢æ¥æºå­—段
-- ---------------------------------------------
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'erp_finance_payment' AND column_name = 'source_type');
SET @sql = IF(@exists = 0,
    'ALTER TABLE erp_finance_payment ADD COLUMN source_type TINYINT NOT NULL DEFAULT 1 COMMENT ''来源类型:1普通付款 2采购退货冲红'' AFTER remark',
    'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'erp_finance_payment' AND column_name = 'source_id');
SET @sql = IF(@exists = 0,
    'ALTER TABLE erp_finance_payment ADD COLUMN source_id BIGINT NULL COMMENT ''来源单据ID(采购退货单)'' AFTER source_type',
    'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'erp_finance_payment' AND column_name = 'source_no');
SET @sql = IF(@exists = 0,
    'ALTER TABLE erp_finance_payment ADD COLUMN source_no VARCHAR(64) NULL COMMENT ''来源单号'' AFTER source_id',
    'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- invoice_id éœ€å…è®¸ä¸ºç©ºï¼ˆå†²çº¢å•不来票);仅当当前为 NOT NULL æ—¶æ‰ MODIFY
SET @nullable = (SELECT is_nullable FROM information_schema.columns
                 WHERE table_schema = DATABASE() AND table_name = 'erp_finance_payment' AND column_name = 'invoice_id');
SET @sql = IF(@nullable = 'NO',
    'ALTER TABLE erp_finance_payment MODIFY COLUMN invoice_id BIGINT NULL COMMENT ''关联的来票编号''',
    'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ---------------------------------------------
-- ä¸‰ã€å­—典:退货处理方式;退货状态补「审批中」
-- ---------------------------------------------
INSERT INTO system_dict_type (name, type, status, remark)
SELECT 'MES ä¾›åº”商退货处理方式', 'mes_wm_return_vendor_handle_type', 0, '采购退货-处理方式:1退款 2换货'
FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM system_dict_type WHERE type = 'mes_wm_return_vendor_handle_type' AND deleted = 0);
INSERT INTO system_dict_data (sort, label, value, dict_type, status, color_type, css_class)
SELECT 0, '退款', '1', 'mes_wm_return_vendor_handle_type', 0, '', ''
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM system_dict_data WHERE dict_type = 'mes_wm_return_vendor_handle_type' AND value = '1' AND deleted = 0);
INSERT INTO system_dict_data (sort, label, value, dict_type, status, color_type, css_class)
SELECT 1, '换货', '2', 'mes_wm_return_vendor_handle_type', 0, '', ''
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM system_dict_data WHERE dict_type = 'mes_wm_return_vendor_handle_type' AND value = '2' AND deleted = 0);
-- é€€è´§çŠ¶æ€æ–°å¢žã€Œå®¡æ‰¹ä¸­ã€ï¼ˆçŠ¶æ€å€¼ 1,复用 CONFIRMED;历史 0/2/3/4/5 å®Œå…¨ä¸åŠ¨ï¼‰
INSERT INTO system_dict_data (sort, label, value, dict_type, status, color_type, css_class)
SELECT 1, '审批中', '1', 'mes_wm_return_vendor_status', 0, '', ''
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM system_dict_data WHERE dict_type = 'mes_wm_return_vendor_status' AND value = '1' AND deleted = 0);
-- ---------------------------------------------
-- å››ã€èœå•:采购退货新增「提交审批」按钮
-- ---------------------------------------------
INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component, status, visible, keep_alive, always_show)
SELECT 5837, '供应商退货提交审批', 'mes:wm-return-vendor:submit-approval', 3, 7, 5830, '', '', '', 0, 1, 1, 1
FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM system_menu WHERE id = 5837);
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/finance/ErpFinancePaymentApi.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
package cn.iocoder.yudao.module.erp.api.finance;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.erp.api.finance.dto.ErpFinancePaymentRedFlushCreateReqDTO;
import cn.iocoder.yudao.module.erp.api.finance.dto.ErpFinancePaymentRespDTO;
/**
 * ERP ä»˜æ¬¾å• API æŽ¥å£
 *
 * @author è¶…级管理员
 */
public interface ErpFinancePaymentApi {
    String PREFIX = "/rpc-api/erp/finance-payment";
    /**
     * åˆ›å»ºé‡‡è´­é€€è´§è´Ÿæ•°å†²çº¢ä»˜æ¬¾å•
     *
     * è¯¥å•据不校验来票、直接置为已审核;金额按负数入账。
     *
     * @param reqDTO åˆ›å»ºä¿¡æ¯
     * @return ä»˜æ¬¾å•编号
     */
    CommonResult<Long> createRedFlushPayment(ErpFinancePaymentRedFlushCreateReqDTO reqDTO);
    /**
     * èŽ·å¾—ä»˜æ¬¾å•
     *
     * @param id ä»˜æ¬¾å•编号
     * @return ä»˜æ¬¾å•
     */
    CommonResult<ErpFinancePaymentRespDTO> getFinancePayment(Long id);
}
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/finance/dto/ErpFinancePaymentRedFlushCreateReqDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.erp.api.finance.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "RPC æœåŠ¡ - ERP é‡‡è´­é€€è´§è´Ÿæ•°å†²çº¢ä»˜æ¬¾å•创建 Request DTO")
@Data
public class ErpFinancePaymentRedFlushCreateReqDTO {
    @Schema(description = "供应商编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long supplierId;
    @Schema(description = "付款账户名称", example = "基本户")
    private String accountName;
    @Schema(description = "退款金额(正数,服务端按负数入账)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000.00")
    private BigDecimal refundAmount;
    @Schema(description = "来源单据编号(供应商退货单编号)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long sourceId;
    @Schema(description = "来源单号(供应商退货单号)", requiredMode = Schema.RequiredMode.REQUIRED, example = "RV2026110001")
    private String sourceNo;
    @Schema(description = "财务人员编号", example = "1")
    private Long financeUserId;
    @Schema(description = "备注", example = "供应商退货冲红")
    private String remark;
}
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/finance/dto/ErpFinancePaymentRespDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.erp.api.finance.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "RPC æœåŠ¡ - ERP ä»˜æ¬¾å• Response DTO")
@Data
public class ErpFinancePaymentRespDTO {
    @Schema(description = "付款单编号", example = "1")
    private Long id;
    @Schema(description = "付款单号", example = "FKD2026110001")
    private String no;
    @Schema(description = "付款状态", example = "20")
    private Integer status;
    @Schema(description = "来源类型:1普通付款 2采购退货冲红", example = "2")
    private Integer sourceType;
    @Schema(description = "来源单据编号", example = "1")
    private Long sourceId;
    @Schema(description = "来源单号", example = "RV2026110001")
    private String sourceNo;
    @Schema(description = "实付金额,单位:元(冲红单为负数)", example = "-1000.00")
    private BigDecimal paymentPrice;
}
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/purchase/ErpPurchaseOrderApi.java
@@ -66,6 +66,16 @@
    CommonResult<Boolean> updateOrderItemInCount(Long orderItemId, BigDecimal inCount);
    /**
     * ç´¯åŠ é‡‡è´­è®¢å•æ˜Žç»†çš„é€€è´§æ•°é‡
     *
     * ç”¨äºŽä¾›åº”商退货完成后回写采购订单的已退货数量,幂等由调用方状态机保证。
     *
     * @param orderItemId æ˜Žç»†ç¼–号
     * @param returnCount é€€è´§æ•°é‡å¢žé‡ï¼ˆæ­£æ•°ï¼‰
     */
    CommonResult<Boolean> updateOrderItemReturnCount(Long orderItemId, BigDecimal returnCount);
    /**
     * æ›´æ–°é‡‡è´­è®¢å•的入库状态
     *
     * æ ¹æ®æ˜Žç»†å…¥åº“数量自动计算订单状态:
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/purchase/dto/ErpPurchaseOrderRespDTO.java
@@ -34,6 +34,12 @@
    @Schema(description = "已入库数量")
    private BigDecimal inCount;
    @Schema(description = "合同号(手工输入,不自动生成)", example = "HT2026110001")
    private String contractNo;
    @Schema(description = "采购类型:1生产 2办公用品 3其他", example = "1")
    private Integer purchaseType;
    @Schema(description = "备注")
    private String remark;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/api/finance/ErpFinancePaymentApiImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
package cn.iocoder.yudao.module.erp.api.finance;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.erp.api.finance.dto.ErpFinancePaymentRedFlushCreateReqDTO;
import cn.iocoder.yudao.module.erp.api.finance.dto.ErpFinancePaymentRespDTO;
import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpFinancePaymentDO;
import cn.iocoder.yudao.module.erp.service.finance.ErpFinancePaymentService;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
 * ERP ä»˜æ¬¾å• API å®žçŽ°ç±»
 *
 * @author è¶…级管理员
 */
@RestController
@RequestMapping(ErpFinancePaymentApi.PREFIX)
@Validated
public class ErpFinancePaymentApiImpl implements ErpFinancePaymentApi {
    @Resource
    private ErpFinancePaymentService financePaymentService;
    @Override
    public CommonResult<Long> createRedFlushPayment(ErpFinancePaymentRedFlushCreateReqDTO reqDTO) {
        return success(financePaymentService.createRedFlushPayment(reqDTO));
    }
    @Override
    public CommonResult<ErpFinancePaymentRespDTO> getFinancePayment(Long id) {
        ErpFinancePaymentDO payment = financePaymentService.getFinancePayment(id);
        return success(BeanUtils.toBean(payment, ErpFinancePaymentRespDTO.class));
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/api/purchase/ErpPurchaseOrderApiImpl.java
@@ -38,6 +38,9 @@
    @Resource
    private ErpPurchaseOrderItemMapper purchaseOrderItemMapper;
    @Resource
    private cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseOrderMapper purchaseOrderMapper;
    @Override
    public CommonResult<ErpPurchaseOrderRespDTO> getPurchaseOrder(Long id) {
        ErpPurchaseOrderDO order = purchaseOrderService.getPurchaseOrder(id);
@@ -101,6 +104,31 @@
    }
    @Override
    public CommonResult<Boolean> updateOrderItemReturnCount(Long orderItemId, BigDecimal returnCount) {
        if (orderItemId == null || returnCount == null) {
            return success(false);
        }
        ErpPurchaseOrderItemDO item = purchaseOrderItemMapper.selectById(orderItemId);
        if (item == null) {
            return success(false);
        }
        // ç´¯åŠ é€€è´§æ•°é‡
        BigDecimal oldReturnCount = item.getReturnCount() != null ? item.getReturnCount() : BigDecimal.ZERO;
        BigDecimal newReturnCount = oldReturnCount.add(returnCount);
        purchaseOrderItemMapper.updateById(new ErpPurchaseOrderItemDO()
                .setId(orderItemId)
                .setReturnCount(newReturnCount));
        // åŒæ­¥æ›´æ–°è®¢å•级退货数量:按订单下所有明细的退货数量求和,避免只传单条把其它行清零
        BigDecimal totalReturnCount = purchaseOrderItemMapper.selectListByOrderId(item.getOrderId()).stream()
                .map(orderItem -> orderItem.getReturnCount() != null ? orderItem.getReturnCount() : BigDecimal.ZERO)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        purchaseOrderMapper.updateById(new ErpPurchaseOrderDO()
                .setId(item.getOrderId())
                .setReturnCount(totalReturnCount));
        return success(true);
    }
    @Override
    public CommonResult<Boolean> updateOrderInStatus(Long orderId) {
        if (orderId == null) {
            return success(false);
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/ErpFinancePaymentController.java
@@ -16,6 +16,7 @@
import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpFinancePaymentDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpFinancePaymentItemDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInvoiceDO;
import cn.iocoder.yudao.module.erp.enums.finance.ErpFinancePaymentSourceTypeEnum;
import cn.iocoder.yudao.module.srm.api.supplier.SrmSupplierApi;
import cn.iocoder.yudao.module.srm.api.supplier.dto.SrmSupplierRespDTO;
import cn.iocoder.yudao.module.erp.service.finance.ErpFinancePaymentService;
@@ -125,6 +126,7 @@
        ErpPurchaseInvoiceRespVO invoiceFinal = invoiceVO;
        return BeanUtils.toBean(payment, ErpFinancePaymentRespVO.class, financePaymentVO ->
                financePaymentVO.setInvoice(invoiceFinal)
                .setSourceTypeName(resolveSourceTypeName(payment.getSourceType()))
                .setItems(BeanUtils.toBean(paymentItemList, ErpFinancePaymentRespVO.Item.class))
                .setAttachmentList(storageAttachmentApi.listAttachments("erp_finance_payment", payment.getId())));
    }
@@ -148,6 +150,11 @@
        ExcelUtils.write(response, "付款单.xls", "数据", ErpFinancePaymentRespVO.class, list);
    }
    private String resolveSourceTypeName(Integer sourceType) {
        ErpFinancePaymentSourceTypeEnum sourceTypeEnum = ErpFinancePaymentSourceTypeEnum.valueOf(sourceType);
        return sourceTypeEnum != null ? sourceTypeEnum.getName() : null;
    }
    private PageResult<ErpFinancePaymentRespVO> buildFinancePaymentVOPageResult(PageResult<ErpFinancePaymentDO> pageResult) {
        if (CollUtil.isEmpty(pageResult.getList())) {
            return PageResult.empty(pageResult.getTotal());
@@ -169,6 +176,7 @@
        // 2. å¼€å§‹æ‹¼æŽ¥
        return BeanUtils.toBean(pageResult, ErpFinancePaymentRespVO.class, payment -> {
            payment.setItems(BeanUtils.toBean(financePaymentItemMap.get(payment.getId()), ErpFinancePaymentRespVO.Item.class));
            payment.setSourceTypeName(resolveSourceTypeName(payment.getSourceType()));
            MapUtils.findAndThen(supplierMap, payment.getSupplierId(), supplier -> payment.setSupplierName(supplier.getName()));
            MapUtils.findAndThen(userMap, Long.parseLong(payment.getCreator()), user -> payment.setCreatorName(user.getNickname()));
            MapUtils.findAndThen(userMap, payment.getFinanceUserId(), user -> payment.setFinanceUserName(user.getNickname()));
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/payment/ErpFinancePaymentPageReqVO.java
@@ -45,4 +45,10 @@
    @Schema(description = "业务编号", example = "123")
    private String bizNo;
    @Schema(description = "来源类型:1普通付款 2采购退货冲红", example = "2")
    private Integer sourceType;
    @Schema(description = "来源单号", example = "RV2026110001")
    private String sourceNo;
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/payment/ErpFinancePaymentRespVO.java
@@ -62,6 +62,15 @@
    @Schema(description = "备注", example = "你猜")
    private String remark;
    @Schema(description = "来源类型:1普通付款 2采购退货冲红", example = "1")
    private Integer sourceType;
    @Schema(description = "来源类型名称", example = "普通付款")
    private String sourceTypeName;
    @Schema(description = "来源单据编号", example = "1024")
    private Long sourceId;
    @Schema(description = "来源单号", example = "RV2026110001")
    private String sourceNo;
    @Schema(description = "附件列表")
    private List<StorageBlobRespDTO> attachmentList;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseOrderController.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.erp.controller.admin.purchase;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
@@ -137,17 +138,24 @@
                convertSet(purchaseOrderItemList, ErpPurchaseOrderItemDO::getProductId));
        Map<Long, MdmUnitMeasureRespDTO> unitMeasureMap = mdmUnitMeasureApi.getUnitMeasureMap(
                convertSet(purchaseOrderItemList, ErpPurchaseOrderItemDO::getProductUnitId));
        return success(BeanUtils.toBean(purchaseOrder, ErpPurchaseOrderRespVO.class, purchaseOrderVO ->
                purchaseOrderVO.setItems(BeanUtils.toBean(purchaseOrderItemList, ErpPurchaseOrderRespVO.Item.class, item -> {
                    // åº“存信息暂不查询(仓储物流模块)
                    item.setStockCount(BigDecimal.ZERO);
                    MapUtils.findAndThen(itemMap, item.getProductId(), mdmItem -> item.setProductName(mdmItem.getName())
                            .setProductBarCode(mdmItem.getBarCode()));
                    MapUtils.findAndThen(unitMeasureMap, item.getProductUnitId(), unit ->
                            item.setProductUnitName(unit.getName()));
                })).setAttachmentList(storageAttachmentApi.listAttachments("erp_purchase_order", id))
                        .setSourceTypeName(ErpPurchaseOrderSourceTypeEnum.WORK_ORDER.getType()
                                .equals(purchaseOrder.getSourceType()) ? "生产工单" : null)));
        return success(BeanUtils.toBean(purchaseOrder, ErpPurchaseOrderRespVO.class, purchaseOrderVO -> {
            purchaseOrderVO.setItems(BeanUtils.toBean(purchaseOrderItemList, ErpPurchaseOrderRespVO.Item.class, item -> {
                // åº“存信息暂不查询(仓储物流模块)
                item.setStockCount(BigDecimal.ZERO);
                MapUtils.findAndThen(itemMap, item.getProductId(), mdmItem -> item.setProductName(mdmItem.getName())
                        .setProductBarCode(mdmItem.getBarCode()));
                MapUtils.findAndThen(unitMeasureMap, item.getProductUnitId(), unit ->
                        item.setProductUnitName(unit.getName()));
                // äº§å“ç¼–号:优先取明细行快照,历史数据回退 MDM ç‰©æ–™ç¼–码
                if (StrUtil.isBlank(item.getProductCode())) {
                    MapUtils.findAndThen(itemMap, item.getProductId(), mdmItem -> item.setProductCode(mdmItem.getCode()));
                }
            }));
            purchaseOrderVO.setAttachmentList(storageAttachmentApi.listAttachments("erp_purchase_order", id));
            purchaseOrderVO.setSourceTypeName(resolveSourceTypeName(purchaseOrder.getSourceType()));
            purchaseOrderVO.setProductCodes(CollUtil.join(purchaseOrderVO.getItems(), ",",
                    ErpPurchaseOrderRespVO.Item::getProductCode));
        }));
    }
    @GetMapping("/page")
@@ -217,6 +225,18 @@
        return success(true);
    }
    /**
     * è§£æžæ¥æºç±»åž‹åç§°
     */
    private String resolveSourceTypeName(Integer sourceType) {
        if (sourceType == null) {
            return null;
        }
        return Arrays.stream(ErpPurchaseOrderSourceTypeEnum.values())
                .filter(item -> item.getType().equals(sourceType))
                .map(ErpPurchaseOrderSourceTypeEnum::getName).findFirst().orElse(null);
    }
    private PageResult<ErpPurchaseOrderRespVO> buildPurchaseOrderVOPageResult(PageResult<ErpPurchaseOrderDO> pageResult) {
        if (CollUtil.isEmpty(pageResult.getList())) {
            return PageResult.empty(pageResult.getTotal());
@@ -251,12 +271,15 @@
                                .setProductBarCode(mdmItem.getBarCode()));
                        MapUtils.findAndThen(unitMeasureMap, item.getProductUnitId(), unit ->
                                item.setProductUnitName(unit.getName()));
                        if (StrUtil.isBlank(item.getProductCode())) {
                            MapUtils.findAndThen(itemMap, item.getProductId(), mdmItem -> item.setProductCode(mdmItem.getCode()));
                        }
                    }));
            purchaseOrder.setProductNames(CollUtil.join(purchaseOrder.getItems(), ",", ErpPurchaseOrderRespVO.Item::getProductName));
            purchaseOrder.setProductCodes(CollUtil.join(purchaseOrder.getItems(), ",", ErpPurchaseOrderRespVO.Item::getProductCode));
            MapUtils.findAndThen(supplierMap, purchaseOrder.getSupplierId(), supplier -> purchaseOrder.setSupplierName(supplier.getName()));
            MapUtils.findAndThen(userMap, Long.parseLong(purchaseOrder.getCreator()), user -> purchaseOrder.setCreatorName(user.getNickname()));
            purchaseOrder.setSourceTypeName(ErpPurchaseOrderSourceTypeEnum.WORK_ORDER.getType()
                    .equals(purchaseOrder.getSourceType()) ? "生产工单" : null);
            purchaseOrder.setSourceTypeName(resolveSourceTypeName(purchaseOrder.getSourceType()));
            // å‰©ä½™å¯å¼€ç¥¨é‡‘额 = è®¢å•总金额 - å·²æ¥ç¥¨é‡‘额
            purchaseOrder.setRemainInvoicePrice(purchaseOrder.getTotalPrice().subtract(
                    invoicePriceSumMap.getOrDefault(purchaseOrder.getId(), BigDecimal.ZERO)));
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderImportExcelVO.java
@@ -36,6 +36,26 @@
    @Schema(description = "采购时间,格式如 2026-01-01 10:00:00;留空取当前时间")
    private String orderTime;
    @ExcelProperty("采购类型")
    @Schema(description = "采购类型:生产 / åŠžå…¬ç”¨å“ / å…¶ä»–;留空不填")
    private String purchaseType;
    @ExcelProperty("合同号")
    @Schema(description = "合同号(手工填写,系统不自动生成)")
    private String contractNo;
    @ExcelProperty("项目名称")
    @Schema(description = "项目名称,非必填")
    private String projectName;
    @ExcelProperty("付款日期")
    @Schema(description = "付款日期,格式如 2026-01-01 10:00:00;导入允许留空,后续编辑时补填")
    private String paymentDate;
    @ExcelProperty("付款方式")
    @Schema(description = "付款方式:电汇 / æ±‡ç¥¨ / çް金 / æ‰‹æœºæ”¯ä»˜ï¼›ç•™ç©ºä¸å¡«")
    private String paymentMethod;
    @ExcelProperty("产品编码")
    @Schema(description = "产品(物料)编码", requiredMode = Schema.RequiredMode.REQUIRED)
    private String productCode;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderPageReqVO.java
@@ -53,6 +53,19 @@
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime[] orderTime;
    @Schema(description = "采购类型:1生产 2办公用品 3其他", example = "1")
    private Integer purchaseType;
    @Schema(description = "合同号", example = "HT2026110001")
    private String contractNo;
    @Schema(description = "付款日期")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime[] paymentDate;
    @Schema(description = "付款方式:1电汇 2汇票 3现金 4手机支付", example = "1")
    private Integer paymentMethod;
    @Schema(description = "备注", example = "你猜")
    private String remark;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderRespVO.java
@@ -46,6 +46,30 @@
    @ExcelProperty("采购时间")
    private LocalDateTime orderTime;
    // ========== é‡‡è´­å°è´¦ ==========
    @Schema(description = "采购类型:1生产 2办公用品 3其他", example = "1")
    @ExcelProperty(value = "采购类型", converter = DictConvert.class)
    @DictFormat(DictTypeConstants.PURCHASE_TYPE)
    private Integer purchaseType;
    @Schema(description = "合同号(客户手工输入,不自动生成)", example = "HT2026110001")
    @ExcelProperty("合同号")
    private String contractNo;
    @Schema(description = "项目名称(非必填)", example = "1号产线改造")
    @ExcelProperty("项目名称")
    private String projectName;
    @Schema(description = "付款日期")
    @ExcelProperty("付款日期")
    private LocalDateTime paymentDate;
    @Schema(description = "付款方式:1电汇 2汇票 3现金 4手机支付", example = "1")
    @ExcelProperty(value = "付款方式", converter = DictConvert.class)
    @DictFormat(DictTypeConstants.PAYMENT_METHOD)
    private Integer paymentMethod;
    @Schema(description = "合计数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "15663")
    @ExcelProperty("合计数量")
    private BigDecimal totalCount;
@@ -108,6 +132,10 @@
    @ExcelProperty("产品信息")
    private String productNames;
    @Schema(description = "产品编号信息", example = "MDM001,MDM002")
    @ExcelProperty("产品编号")
    private String productCodes;
    // ========== é‡‡è´­å…¥åº“ ==========
    @Schema(description = "采购入库数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
@@ -130,6 +158,9 @@
        @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
        private Long productId;
        @Schema(description = "产品编号(MDM ç‰©æ–™ç¼–码)", example = "MDM001")
        private String productCode;
        @Schema(description = "产品单位单位", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
        private Long productUnitId;
@@ -146,6 +177,12 @@
        @Schema(description = "税额,单位:元", example = "100.00")
        private BigDecimal taxPrice;
        @Schema(description = "合计产品金额,单位:元", example = "100.00")
        private BigDecimal totalProductPrice;
        @Schema(description = "价税合计,单位:元", example = "100.00")
        private BigDecimal totalPrice;
        @Schema(description = "备注", example = "随便")
        private String remark;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderSaveReqVO.java
@@ -26,6 +26,22 @@
    @NotNull(message = "采购时间不能为空")
    private LocalDateTime orderTime;
    @Schema(description = "采购类型:1生产 2办公用品 3其他", example = "1")
    private Integer purchaseType;
    @Schema(description = "合同号(客户手工输入,不自动生成)", example = "HT2026110001")
    private String contractNo;
    @Schema(description = "项目名称(非必填)", example = "1号产线改造")
    private String projectName;
    @Schema(description = "付款日期", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "付款日期不能为空,请选择付款日期后再保存")
    private LocalDateTime paymentDate;
    @Schema(description = "付款方式:1电汇 2汇票 3现金 4手机支付", example = "1")
    private Integer paymentMethod;
    @Schema(description = "优惠率,百分比", requiredMode = Schema.RequiredMode.REQUIRED, example = "99.88")
    private BigDecimal discountPercent;
@@ -60,6 +76,9 @@
        @NotNull(message = "产品编号不能为空")
        private Long productId;
        @Schema(description = "产品编号(MDM ç‰©æ–™ç¼–码快照)", example = "MDM001")
        private String productCode;
        @Schema(description = "产品单位单位", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
        @NotNull(message = "产品单位单位不能为空")
        private Long productUnitId;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderPageReqVO.java
@@ -27,6 +27,10 @@
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime[] orderTime;
    @Schema(description = "收款日期")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime[] receiptDate;
    @Schema(description = "备注", example = "你猜")
    private String remark;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java
@@ -51,6 +51,10 @@
    @ExcelProperty("下单时间")
    private LocalDateTime orderTime;
    @Schema(description = "收款日期(用于 30 å¤©è´¦æœŸæé†’)")
    @ExcelProperty("收款日期")
    private LocalDateTime receiptDate;
    @Schema(description = "合计数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "15663")
    @ExcelProperty("合计数量")
    private BigDecimal totalCount;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderSaveReqVO.java
@@ -30,6 +30,9 @@
    @NotNull(message = "下单时间不能为空")
    private LocalDateTime orderTime;
    @Schema(description = "收款日期(用于 30 å¤©è´¦æœŸæé†’)")
    private LocalDateTime receiptDate;
    @Schema(description = "销售员编号", example = "1888")
    private Long saleUserId;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/finance/ErpFinancePaymentDO.java
@@ -94,4 +94,21 @@
     */
    private String remark;
    /**
     * æ¥æºç±»åž‹
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.module.erp.enums.finance.ErpFinancePaymentSourceTypeEnum}
     */
    private Integer sourceType;
    /**
     * æ¥æºå•据编号
     *
     * é‡‡è´­é€€è´§å†²çº¢æ—¶ï¼Œä¸ºä¾›åº”商退货单编号
     */
    private Long sourceId;
    /**
     * æ¥æºå•号
     */
    private String sourceNo;
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseOrderDO.java
@@ -57,6 +57,32 @@
     */
    private LocalDateTime orderTime;
    // ========== é‡‡è´­å°è´¦ ==========
    /**
     * é‡‡è´­ç±»åž‹
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.module.erp.enums.ErpPurchaseTypeEnum}
     */
    private Integer purchaseType;
    /**
     * åˆåŒå·ï¼ˆå®¢æˆ·æ‰‹å·¥è¾“入,不自动生成)
     */
    private String contractNo;
    /**
     * é¡¹ç›®åç§°ï¼ˆéžå¿…填)
     */
    private String projectName;
    /**
     * ä»˜æ¬¾æ—¥æœŸ
     */
    private LocalDateTime paymentDate;
    /**
     * ä»˜æ¬¾æ–¹å¼
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.module.erp.enums.ErpPaymentMethodEnum}
     */
    private Integer paymentMethod;
    /**
     * åˆè®¡æ•°é‡
     */
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseOrderItemDO.java
@@ -47,6 +47,12 @@
     */
    private Long productId;
    /**
     * äº§å“ç¼–号(MDM ç‰©æ–™ç¼–码快照)
     *
     * å…³è” {@link cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO#getCode()}
     */
    private String productCode;
    /**
     * è®¡é‡å•位编号
     *
     * å…³è” {@link cn.iocoder.yudao.module.mdm.api.unitmeasure.dto.MdmUnitMeasureRespDTO#getId()}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleOrderDO.java
@@ -72,6 +72,11 @@
    private LocalDateTime orderTime;
    /**
     * æ”¶æ¬¾æ—¥æœŸï¼ˆç”¨äºŽ 30 å¤©è´¦æœŸæé†’)
     */
    private LocalDateTime receiptDate;
    /**
     * åˆè®¡æ•°é‡
     */
    private BigDecimal totalCount;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/finance/ErpFinancePaymentMapper.java
@@ -40,6 +40,8 @@
                .likeIfPresent(ErpFinancePaymentDO::getAccountName, reqVO.getAccountName())
                .eqIfPresent(ErpFinancePaymentDO::getStatus, reqVO.getStatus())
                .likeIfPresent(ErpFinancePaymentDO::getRemark, reqVO.getRemark())
                .eqIfPresent(ErpFinancePaymentDO::getSourceType, reqVO.getSourceType())
                .likeIfPresent(ErpFinancePaymentDO::getSourceNo, reqVO.getSourceNo())
                .orderByDesc(ErpFinancePaymentDO::getId);
        if (reqVO.getBizNo() != null) {
            query.leftJoin(ErpFinancePaymentItemDO.class, ErpFinancePaymentItemDO::getPaymentId, ErpFinancePaymentDO::getId)
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseOrderMapper.java
@@ -29,6 +29,10 @@
                .likeIfPresent(ErpPurchaseOrderDO::getNo, reqVO.getNo())
                .eqIfPresent(ErpPurchaseOrderDO::getSupplierId, reqVO.getSupplierId())
                .betweenIfPresent(ErpPurchaseOrderDO::getOrderTime, reqVO.getOrderTime())
                .eqIfPresent(ErpPurchaseOrderDO::getPurchaseType, reqVO.getPurchaseType())
                .likeIfPresent(ErpPurchaseOrderDO::getContractNo, reqVO.getContractNo())
                .betweenIfPresent(ErpPurchaseOrderDO::getPaymentDate, reqVO.getPaymentDate())
                .eqIfPresent(ErpPurchaseOrderDO::getPaymentMethod, reqVO.getPaymentMethod())
                .eqIfPresent(ErpPurchaseOrderDO::getStatus, reqVO.getStatus())
                .likeIfPresent(ErpPurchaseOrderDO::getRemark, reqVO.getRemark())
                .eqIfPresent(ErpPurchaseOrderDO::getCreator, reqVO.getCreator())
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSaleOrderMapper.java
@@ -26,6 +26,7 @@
                .likeIfPresent(ErpSaleOrderDO::getNo, reqVO.getNo())
                .eqIfPresent(ErpSaleOrderDO::getCustomerId, reqVO.getCustomerId())
                .betweenIfPresent(ErpSaleOrderDO::getOrderTime, reqVO.getOrderTime())
                .betweenIfPresent(ErpSaleOrderDO::getReceiptDate, reqVO.getReceiptDate())
                .eqIfPresent(ErpSaleOrderDO::getStatus, reqVO.getStatus())
                .eqIfPresent(ErpSaleOrderDO::getOutStatus, reqVO.getOutStatus())
                .eqIfPresent(ErpSaleOrderDO::getProductionStatus, reqVO.getProductionStatus())
@@ -49,6 +50,7 @@
                .likeIfPresent(ErpSaleOrderDO::getNo, reqVO.getNo())
                .eqIfPresent(ErpSaleOrderDO::getCustomerId, reqVO.getCustomerId())
                .betweenIfPresent(ErpSaleOrderDO::getOrderTime, reqVO.getOrderTime())
                .betweenIfPresent(ErpSaleOrderDO::getReceiptDate, reqVO.getReceiptDate())
                .eqIfPresent(ErpSaleOrderDO::getStatus, reqVO.getStatus())
                .eqIfPresent(ErpSaleOrderDO::getOutStatus, reqVO.getOutStatus())
                .eqIfPresent(ErpSaleOrderDO::getProductionStatus, reqVO.getProductionStatus())
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/DictTypeConstants.java
@@ -9,5 +9,7 @@
    String AUDIT_STATUS = "erp_audit_status"; // å®¡æ ¸çŠ¶æ€
    String STOCK_RECORD_BIZ_TYPE = "erp_stock_record_biz_type"; // åº“存明细的业务类型
    String PURCHASE_TYPE = "erp_purchase_type"; // é‡‡è´­å°è´¦çš„采购类型
    String PAYMENT_METHOD = "erp_payment_method"; // é‡‡è´­å°è´¦çš„付款方式
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpPaymentMethodEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
package cn.iocoder.yudao.module.erp.enums;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
import java.util.Objects;
/**
 * ERP ä»˜æ¬¾æ–¹å¼æžšä¸¾ï¼ˆé‡‡è´­å°è´¦ï¼‰
 *
 * å­—å…¸ {@link DictTypeConstants#PAYMENT_METHOD}
 */
@RequiredArgsConstructor
@Getter
public enum ErpPaymentMethodEnum implements ArrayValuable<Integer> {
    WIRE_TRANSFER(1, "电汇"),
    DRAFT(2, "汇票"),
    CASH(3, "现金"),
    MOBILE_PAY(4, "手机支付");
    public static final Integer[] ARRAYS = Arrays.stream(values())
            .map(ErpPaymentMethodEnum::getType).toArray(Integer[]::new);
    /**
     * ç±»åž‹å€¼
     */
    private final Integer type;
    /**
     * ç±»åž‹å
     */
    private final String name;
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
    /**
     * åˆ¤æ–­ç±»åž‹å€¼æ˜¯å¦åˆæ³•(为空视为未选择,合法)
     */
    public static boolean isValid(Integer type) {
        if (type == null) {
            return true;
        }
        return Arrays.stream(values()).anyMatch(item -> Objects.equals(item.getType(), type));
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpPurchaseOrderSourceTypeEnum.java
@@ -16,7 +16,8 @@
public enum ErpPurchaseOrderSourceTypeEnum implements ArrayValuable<Integer> {
    WORK_ORDER(1, "生产工单"),
    SRM_INQUIRY(2, "SRM询价");
    SRM_INQUIRY(2, "SRM询价"),
    SALE_ORDER(3, "销售订单");
    public static final Integer[] ARRAYS = Arrays.stream(values())
            .map(ErpPurchaseOrderSourceTypeEnum::getType).toArray(Integer[]::new);
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpPurchaseTypeEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,50 @@
package cn.iocoder.yudao.module.erp.enums;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
import java.util.Objects;
/**
 * ERP é‡‡è´­ç±»åž‹æžšä¸¾ï¼ˆé‡‡è´­å°è´¦ï¼‰
 *
 * å­—å…¸ {@link DictTypeConstants#PURCHASE_TYPE}
 */
@RequiredArgsConstructor
@Getter
public enum ErpPurchaseTypeEnum implements ArrayValuable<Integer> {
    PRODUCTION(1, "生产"),
    OFFICE(2, "办公用品"),
    OTHER(3, "其他");
    public static final Integer[] ARRAYS = Arrays.stream(values())
            .map(ErpPurchaseTypeEnum::getType).toArray(Integer[]::new);
    /**
     * ç±»åž‹å€¼
     */
    private final Integer type;
    /**
     * ç±»åž‹å
     */
    private final String name;
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
    /**
     * åˆ¤æ–­ç±»åž‹å€¼æ˜¯å¦åˆæ³•(为空视为未选择,合法)
     */
    public static boolean isValid(Integer type) {
        if (type == null) {
            return true;
        }
        return Arrays.stream(values()).anyMatch(item -> Objects.equals(item.getType(), type));
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErrorCodeConstants.java
@@ -30,6 +30,10 @@
    ErrorCode PURCHASE_ORDER_BPM_PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1_030_101_012, "采购审核分类下没有可用的流程模型,请先创建并发布流程");
    ErrorCode PURCHASE_ORDER_SUBMIT_FAIL_STATUS = new ErrorCode(1_030_101_013, "提交审批失败,只有未审核状态的采购订单才能提交");
    ErrorCode PURCHASE_ORDER_ALL_IN = new ErrorCode(1_030_101_014, "采购订单已全部入库,无需确认收货");
    ErrorCode PURCHASE_ORDER_PAYMENT_DATE_REQUIRED = new ErrorCode(1_030_101_015, "付款日期不能为空,请选择付款日期后再保存");
    ErrorCode PURCHASE_ORDER_PURCHASE_TYPE_INVALID = new ErrorCode(1_030_101_016, "采购类型({})不合法,只能选择:1生产、2办公用品、3其他");
    ErrorCode PURCHASE_ORDER_PAYMENT_METHOD_INVALID = new ErrorCode(1_030_101_017, "付款方式({})不合法,只能选择:1电汇、2汇票、3现金、4手机支付");
    ErrorCode PURCHASE_ORDER_ITEM_PRODUCT_CODE_MISSING = new ErrorCode(1_030_101_018, "物料[{}]未维护 MDM ç‰©æ–™ç¼–码,请先在 MDM ç‰©æ–™æ¡£æ¡ˆè¡¥å……编码后再保存");
    // ========== ERP é‡‡è´­å…¥åº“(1-030-102-000) ==========
    ErrorCode PURCHASE_IN_NOT_EXISTS = new ErrorCode(1_030_102_000, "采购入库单不存在");
@@ -202,6 +206,9 @@
    ErrorCode FINANCE_PAYMENT_BPM_PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1_030_601_013, "付款单审批分类下没有可用的流程模型,请先创建并发布流程");
    ErrorCode FINANCE_PAYMENT_UPDATE_FAIL_APPROVING = new ErrorCode(1_030_601_014, "付款单({})审批中,无法修改");
    ErrorCode FINANCE_PAYMENT_DELETE_FAIL_APPROVING = new ErrorCode(1_030_601_015, "付款单({})审批中,无法删除");
    ErrorCode FINANCE_PAYMENT_RED_FLUSH_AMOUNT_INVALID = new ErrorCode(1_030_601_016, "生成采购退货冲红单失败:退款金额必须大于 0,当前为[{}],请核对退货数量与采购单价后再生成");
    ErrorCode FINANCE_PAYMENT_RED_FLUSH_SOURCE_REQUIRED = new ErrorCode(1_030_601_017, "生成采购退货冲红单失败:未提供来源退货单(单据编号或单号为空),请从供应商退货单页面发起冲红");
    ErrorCode FINANCE_PAYMENT_RED_FLUSH_SUPPLIER_REQUIRED = new ErrorCode(1_030_601_018, "生成采购退货冲红单失败:未提供供应商编号,请确认退货单已选择供应商");
    // ========== ERP æ”¶æ¬¾å• 1-030-602-000 ==========
    ErrorCode FINANCE_RECEIPT_NOT_EXISTS = new ErrorCode(1_030_602_000, "收款单不存在");
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/finance/ErpFinancePaymentSourceTypeEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,46 @@
package cn.iocoder.yudao.module.erp.enums.finance;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
 * ERP ä»˜æ¬¾å•来源类型枚举
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum ErpFinancePaymentSourceTypeEnum implements ArrayValuable<Integer> {
    NORMAL(1, "普通付款"),
    PURCHASE_RETURN_RED_FLUSH(2, "采购退货冲红");
    public static final Integer[] ARRAYS = Arrays.stream(values())
            .map(ErpFinancePaymentSourceTypeEnum::getType).toArray(Integer[]::new);
    /**
     * ç±»åž‹
     */
    private final Integer type;
    /**
     * åç§°
     */
    private final String name;
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
    public static ErpFinancePaymentSourceTypeEnum valueOf(Integer type) {
        if (type == null) {
            return null;
        }
        return Arrays.stream(values()).filter(item -> item.getType().equals(type))
                .findFirst().orElse(null);
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinancePaymentService.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.erp.service.finance;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.erp.api.finance.dto.ErpFinancePaymentRedFlushCreateReqDTO;
import cn.iocoder.yudao.module.erp.controller.admin.finance.vo.payment.ErpFinancePaymentPageReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.finance.vo.payment.ErpFinancePaymentSaveReqVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpFinancePaymentDO;
@@ -66,6 +67,17 @@
    void deleteFinancePayment(List<Long> ids);
    /**
     * åˆ›å»ºé‡‡è´­é€€è´§è´Ÿæ•°å†²çº¢ä»˜æ¬¾å•
     *
     * è¯¥å•据不校验来票、不占用来票可付金额,直接置为已审核;金额按负数入账。
     * åŒä¸€æ¥æºé€€è´§å•重复调用时,返回已生成的冲红单编号(幂等)。
     *
     * @param reqDTO åˆ›å»ºä¿¡æ¯
     * @return ä»˜æ¬¾å•编号
     */
    Long createRedFlushPayment(ErpFinancePaymentRedFlushCreateReqDTO reqDTO);
    /**
     * èŽ·å¾—ä»˜æ¬¾å•
     *
     * @param id ç¼–号
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinancePaymentServiceImpl.java
@@ -2,8 +2,11 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.erp.api.finance.dto.ErpFinancePaymentRedFlushCreateReqDTO;
import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
@@ -20,6 +23,8 @@
import cn.iocoder.yudao.module.erp.dal.mysql.finance.ErpFinancePaymentMapper;
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.common.ErpBizTypeEnum;
import cn.iocoder.yudao.module.erp.enums.finance.ErpFinancePaymentSourceTypeEnum;
import cn.iocoder.yudao.module.erp.service.purchase.ErpPurchaseInvoiceService;
import cn.iocoder.yudao.module.erp.service.purchase.ErpPurchaseInvoiceServiceImpl;
import cn.iocoder.yudao.module.erp.service.purchase.ErpSupplierService;
@@ -33,6 +38,7 @@
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -396,6 +402,73 @@
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createRedFlushPayment(ErpFinancePaymentRedFlushCreateReqDTO reqDTO) {
        // 1.1 æ ¡éªŒæ¥æºå•据:冲红单必须能追溯到退货单
        if (reqDTO.getSourceId() == null || StrUtil.isBlank(reqDTO.getSourceNo())) {
            throw exception(FINANCE_PAYMENT_RED_FLUSH_SOURCE_REQUIRED);
        }
        // 1.2 æ ¡éªŒä¾›åº”商
        if (reqDTO.getSupplierId() == null) {
            throw exception(FINANCE_PAYMENT_RED_FLUSH_SUPPLIER_REQUIRED);
        }
        supplierService.validateSupplier(reqDTO.getSupplierId());
        // 1.3 æ ¡éªŒé€€æ¬¾é‡‘额
        if (reqDTO.getRefundAmount() == null || reqDTO.getRefundAmount().compareTo(BigDecimal.ZERO) <= 0) {
            throw exception(FINANCE_PAYMENT_RED_FLUSH_AMOUNT_INVALID, reqDTO.getRefundAmount());
        }
        // 1.4 æ ¡éªŒè´¢åŠ¡äººå‘˜
        if (reqDTO.getFinanceUserId() != null) {
            adminUserApi.validateUser(reqDTO.getFinanceUserId());
        }
        // 1.5 å¹‚等:同一来源退货单只允许生成一张冲红单
        ErpFinancePaymentDO existing = financePaymentMapper.selectOne(new LambdaQueryWrapperX<ErpFinancePaymentDO>()
                .eq(ErpFinancePaymentDO::getSourceType,
                        ErpFinancePaymentSourceTypeEnum.PURCHASE_RETURN_RED_FLUSH.getType())
                .eq(ErpFinancePaymentDO::getSourceId, reqDTO.getSourceId()));
        if (existing != null) {
            log.info("[createRedFlushPayment][退货单({}) å·²å­˜åœ¨å†²çº¢å•({}),直接复用]",
                    reqDTO.getSourceNo(), existing.getNo());
            return existing.getId();
        }
        // 2.1 ç”Ÿæˆä»˜æ¬¾å•号
        String no = noRedisDAO.generate(ErpNoRedisDAO.FINANCE_PAYMENT_NO_PREFIX);
        if (financePaymentMapper.selectByNo(no) != null) {
            throw exception(FINANCE_PAYMENT_NO_EXISTS);
        }
        // 2.2 é‡‘额按负数入账
        BigDecimal negativeAmount = reqDTO.getRefundAmount().negate();
        ErpFinancePaymentDO payment = new ErpFinancePaymentDO()
                .setNo(no)
                .setStatus(ErpAuditStatus.APPROVE.getStatus())
                .setPaymentTime(LocalDateTime.now())
                .setFinanceUserId(reqDTO.getFinanceUserId())
                .setSupplierId(reqDTO.getSupplierId())
                .setAccountName(reqDTO.getAccountName())
                .setTotalPrice(negativeAmount)
                .setDiscountPrice(BigDecimal.ZERO)
                .setPaymentPrice(negativeAmount)
                .setRemark(StrUtil.blankToDefault(reqDTO.getRemark(), "采购退货冲红"))
                .setSourceType(ErpFinancePaymentSourceTypeEnum.PURCHASE_RETURN_RED_FLUSH.getType())
                .setSourceId(reqDTO.getSourceId())
                .setSourceNo(reqDTO.getSourceNo());
        financePaymentMapper.insert(payment);
        // 3. æ’入付款项(业务类型:采购退货)
        financePaymentItemMapper.insert(new ErpFinancePaymentItemDO()
                .setPaymentId(payment.getId())
                .setBizType(ErpBizTypeEnum.PURCHASE_RETURN.getType())
                .setBizId(reqDTO.getSourceId())
                .setBizNo(reqDTO.getSourceNo())
                .setTotalPrice(negativeAmount)
                .setPaidPrice(BigDecimal.ZERO)
                .setPaymentPrice(negativeAmount)
                .setRemark(StrUtil.blankToDefault(reqDTO.getRemark(), "采购退货冲红")));
        return payment.getId();
    }
    @Override
    public ErpFinancePaymentDO getFinancePayment(Long id) {
        return financePaymentMapper.selectById(id);
    }
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java
@@ -29,7 +29,9 @@
import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseRequestMapper;
import cn.iocoder.yudao.module.erp.dal.redis.no.ErpNoRedisDAO;
import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
import cn.iocoder.yudao.module.erp.enums.ErpPaymentMethodEnum;
import cn.iocoder.yudao.module.erp.enums.ErpPurchaseOrderInStatusEnum;
import cn.iocoder.yudao.module.erp.enums.ErpPurchaseTypeEnum;
import cn.iocoder.yudao.module.erp.service.finance.ErpAccountService;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
@@ -125,6 +127,8 @@
    public Long createPurchaseOrder(ErpPurchaseOrderSaveReqVO createReqVO, boolean submit) {
        // 1.1 æ ¡éªŒè®¢å•项的有效性
        List<ErpPurchaseOrderItemDO> purchaseOrderItems = validatePurchaseOrderItems(createReqVO.getItems());
        // 1.1.1 æ ¡éªŒé‡‡è´­å°è´¦å­—段
        validateLedgerFields(createReqVO);
        // 1.2 æ ¡éªŒä¾›åº”商
        supplierService.validateSupplier(createReqVO.getSupplierId());
        // 1.3 æ ¡éªŒç»“算账户
@@ -175,6 +179,8 @@
        }
        // 1.4 æ ¡éªŒè®¢å•项的有效性
        List<ErpPurchaseOrderItemDO> purchaseOrderItems = validatePurchaseOrderItems(updateReqVO.getItems());
        // 1.5 æ ¡éªŒé‡‡è´­å°è´¦å­—段
        validateLedgerFields(updateReqVO);
        // 2.1 æ›´æ–°è®¢å•
        ErpPurchaseOrderDO updateObj = BeanUtils.toBean(updateReqVO, ErpPurchaseOrderDO.class);
@@ -284,6 +290,7 @@
            }
            ErpPurchaseOrderSaveReqVO.Item itemVO = new ErpPurchaseOrderSaveReqVO.Item();
            itemVO.setProductId(item.getId());
            itemVO.setProductCode(item.getCode());
            itemVO.setProductUnitId(item.getUnitMeasureId());
            itemVO.setProductPrice(row.getProductPrice() == null ? BigDecimal.ZERO : row.getProductPrice());
            itemVO.setCount(row.getCount());
@@ -305,6 +312,11 @@
        purchaseOrder.setNo(no);
        purchaseOrder.setSupplierId(supplierId);
        purchaseOrder.setOrderTime(parseImportOrderTime(head.getOrderTime()));
        purchaseOrder.setPurchaseType(parsePurchaseType(head.getPurchaseType()));
        purchaseOrder.setContractNo(StrUtil.trimToNull(head.getContractNo()));
        purchaseOrder.setProjectName(StrUtil.trimToNull(head.getProjectName()));
        purchaseOrder.setPaymentDate(parseImportDateTime(head.getPaymentDate(), "付款日期"));
        purchaseOrder.setPaymentMethod(parsePaymentMethod(head.getPaymentMethod()));
        purchaseOrder.setRemark(head.getRemark());
        purchaseOrder.setStatus(Boolean.TRUE.equals(directApprove)
                ? ErpAuditStatus.APPROVE.getStatus() : ErpAuditStatus.PROCESS.getStatus());
@@ -324,8 +336,16 @@
     * è§£æžå¯¼å…¥çš„采购时间,兼容多种常见格式;为空取当前时间
     */
    private LocalDateTime parseImportOrderTime(String text) {
        LocalDateTime value = parseImportDateTime(text, "采购时间");
        return value != null ? value : LocalDateTime.now();
    }
    /**
     * è§£æžå¯¼å…¥çš„æ—¥æœŸæ—¶é—´ï¼Œå…¼å®¹å¤šç§å¸¸è§æ ¼å¼ï¼›ä¸ºç©ºè¿”回 null
     */
    private LocalDateTime parseImportDateTime(String text, String fieldName) {
        if (StrUtil.isBlank(text)) {
            return LocalDateTime.now();
            return null;
        }
        String value = text.trim();
        for (String pattern : new String[]{"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy/MM/dd HH:mm:ss"}) {
@@ -342,7 +362,33 @@
                // ç»§ç»­å°è¯•下一个格式
            }
        }
        throw new IllegalArgumentException("采购时间格式不正确:" + text + ",示例 2026-01-01 10:00:00");
        throw new IllegalArgumentException(fieldName + "格式不正确:" + text + ",示例 2026-01-01 10:00:00");
    }
    /**
     * è§£æžå¯¼å…¥çš„采购类型名称;为空或无法识别返回 null
     */
    private Integer parsePurchaseType(String text) {
        if (StrUtil.isBlank(text)) {
            return null;
        }
        String value = text.trim();
        return Arrays.stream(ErpPurchaseTypeEnum.values())
                .filter(item -> item.getName().equals(value))
                .map(ErpPurchaseTypeEnum::getType).findFirst().orElse(null);
    }
    /**
     * è§£æžå¯¼å…¥çš„付款方式名称;为空或无法识别返回 null
     */
    private Integer parsePaymentMethod(String text) {
        if (StrUtil.isBlank(text)) {
            return null;
        }
        String value = text.trim();
        return Arrays.stream(ErpPaymentMethodEnum.values())
                .filter(item -> item.getName().equals(value))
                .map(ErpPaymentMethodEnum::getType).findFirst().orElse(null);
    }
    /**
@@ -515,7 +561,7 @@
    private List<ErpPurchaseOrderItemDO> validatePurchaseOrderItems(List<ErpPurchaseOrderSaveReqVO.Item> list) {
        // è½¬åŒ–为 ErpPurchaseOrderItemDO åˆ—表(产品验证已移除)
        return convertList(list, o -> BeanUtils.toBean(o, ErpPurchaseOrderItemDO.class, item -> {
        List<ErpPurchaseOrderItemDO> items = convertList(list, o -> BeanUtils.toBean(o, ErpPurchaseOrderItemDO.class, item -> {
            item.setTotalPrice(MoneyUtils.priceMultiply(item.getProductPrice(), item.getCount()));
            if (item.getTotalPrice() == null) {
                return;
@@ -525,6 +571,43 @@
                item.setTaxPrice(MoneyUtils.taxPriceIncluded(item.getTotalPrice(), item.getTaxPercent()));
            }
        }));
        // é‡‡è´­å°è´¦ï¼šäº§å“ç¼–号取 MDM ç‰©æ–™ç¼–码快照,保证列表/导出可直接展示
        fillProductCode(items);
        return items;
    }
    /**
     * å›žå¡«æ˜Žç»†è¡Œçš„产品编号(MDM ç‰©æ–™ç¼–码)
     */
    private void fillProductCode(List<ErpPurchaseOrderItemDO> items) {
        if (CollUtil.isEmpty(items)) {
            return;
        }
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(items, ErpPurchaseOrderItemDO::getProductId));
        items.forEach(item -> {
            MdmItemRespDTO mdmItem = itemMap.get(item.getProductId());
            if (mdmItem == null) {
                return;
            }
            if (StrUtil.isBlank(mdmItem.getCode())) {
                throw exception(PURCHASE_ORDER_ITEM_PRODUCT_CODE_MISSING,
                        StrUtil.blankToDefault(mdmItem.getName(), "ID=" + mdmItem.getId()));
            }
            item.setProductCode(mdmItem.getCode());
        });
    }
    /**
     * æ ¡éªŒé‡‡è´­å°è´¦å­—段(采购类型、付款方式)
     */
    private void validateLedgerFields(ErpPurchaseOrderSaveReqVO reqVO) {
        if (!ErpPurchaseTypeEnum.isValid(reqVO.getPurchaseType())) {
            throw exception(PURCHASE_ORDER_PURCHASE_TYPE_INVALID, reqVO.getPurchaseType());
        }
        if (!ErpPaymentMethodEnum.isValid(reqVO.getPaymentMethod())) {
            throw exception(PURCHASE_ORDER_PAYMENT_METHOD_INVALID, reqVO.getPaymentMethod());
        }
    }
    private void updatePurchaseOrderItemList(Long id, List<ErpPurchaseOrderItemDO> newList) {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnvendor/MesWmReturnVendorController.java
@@ -13,8 +13,16 @@
import cn.iocoder.yudao.module.mes.controller.admin.wm.returnvendor.vo.MesWmReturnVendorSaveReqVO;
import cn.iocoder.yudao.module.srm.api.supplier.SrmSupplierApi;
import cn.iocoder.yudao.module.srm.api.supplier.dto.SrmSupplierRespDTO;
import cn.iocoder.yudao.module.erp.api.finance.ErpFinancePaymentApi;
import cn.iocoder.yudao.module.erp.api.finance.dto.ErpFinancePaymentRespDTO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.arrivalnotice.MesWmArrivalNoticeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnvendor.MesWmReturnVendorDO;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmReturnVendorStatusEnum;
import cn.iocoder.yudao.module.mes.service.wm.arrivalnotice.MesWmArrivalNoticeService;
import cn.iocoder.yudao.module.mes.service.wm.returnvendor.MesWmReturnVendorService;
import cn.iocoder.yudao.module.mes.service.wm.returnvendor.MesWmReturnVendorServiceImpl;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -26,6 +34,7 @@
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -44,6 +53,12 @@
    private MesWmReturnVendorService returnVendorService;
    @Resource
    private SrmSupplierApi srmSupplierApi;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @Resource
    private ErpFinancePaymentApi financePaymentApi;
    @Resource
    private MesWmArrivalNoticeService arrivalNoticeService;
    @PostMapping("/create")
    @Operation(summary = "创建供应商退货单")
@@ -109,6 +124,41 @@
        return success(true);
    }
    @PutMapping("/submit-approval")
    @Operation(summary = "提交供应商退货单审批", description = "èµ° BPM æµç¨‹ï¼šé‡‡è´­ -> è´¨é‡ -> ä»“库")
    @Parameter(name = "id", description = "编号", required = true)
    @Parameter(name = "processDefinitionKey", description = "流程定义 Key", required = true, example = "purchase_return_vendor_approve")
    @PreAuthorize("@ss.hasPermission('mes:wm-return-vendor:submit-approval')")
    public CommonResult<Boolean> submitReturnVendorApproval(@RequestParam("id") Long id,
                                                            @RequestParam("processDefinitionKey") String processDefinitionKey) {
        returnVendorService.submitReturnVendorApproval(id, processDefinitionKey);
        return success(true);
    }
    @GetMapping("/attachments")
    @Operation(summary = "获得供应商退货单附件列表")
    @Parameter(name = "id", description = "编号", required = true)
    public CommonResult<List<StorageBlobRespDTO>> getReturnVendorAttachments(@RequestParam("id") Long id) {
        return success(storageAttachmentApi.listAttachments(
                MesWmReturnVendorServiceImpl.RETURN_VENDOR_RECORD_TYPE, id));
    }
    @PostMapping("/red-flush")
    @Operation(summary = "生成采购退货负数冲红付款单", description = "幂等:已生成则直接返回已有冲红单编号")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('mes:wm-return-vendor:finish')")
    public CommonResult<Long> createRedFlushPayment(@RequestParam("id") Long id) {
        return success(returnVendorService.createRedFlushPayment(id));
    }
    @PostMapping("/replacement-notice")
    @Operation(summary = "生成换货到货通知单", description = "幂等:已生成则直接返回已有到货通知单编号")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('mes:wm-return-vendor:finish')")
    public CommonResult<Long> createReplacementNotice(@RequestParam("id") Long id) {
        return success(returnVendorService.createReplacementNotice(id));
    }
    @PutMapping("/stock")
    @Operation(summary = "执行拣货")
    @Parameter(name = "id", description = "编号", required = true)
@@ -162,11 +212,35 @@
                convertSet(list, MesWmReturnVendorDO::getVendorId)).getCheckedData().stream()
                .collect(java.util.stream.Collectors.toMap(SrmSupplierRespDTO::getId, s -> s));
        // 2. æž„建结果
        return BeanUtils.toBean(list, MesWmReturnVendorRespVO.class, vo ->
                MapUtils.findAndThen(vendorMap, vo.getVendorId(), vendor -> {
                    vo.setVendorName(vendor.getName());
                    vo.setVendorCode(vendor.getCode());
                }));
        return BeanUtils.toBean(list, MesWmReturnVendorRespVO.class, vo -> {
            MapUtils.findAndThen(vendorMap, vo.getVendorId(), vendor -> {
                vo.setVendorName(vendor.getName());
                vo.setVendorCode(vendor.getCode());
            });
            vo.setStatusName(resolveStatusName(vo.getStatus()));
            vo.setAttachmentList(storageAttachmentApi.listAttachments(
                    MesWmReturnVendorServiceImpl.RETURN_VENDOR_RECORD_TYPE, vo.getId()));
            // å†²çº¢å•号 / æ¢è´§åˆ°è´§å•号(仅在已生成时查询)
            if (vo.getRedFlushPaymentId() != null) {
                ErpFinancePaymentRespDTO payment = financePaymentApi
                        .getFinancePayment(vo.getRedFlushPaymentId()).getCheckedData();
                if (payment != null) {
                    vo.setRedFlushPaymentNo(payment.getNo());
                }
            }
            if (vo.getReplacementNoticeId() != null) {
                MesWmArrivalNoticeDO notice = arrivalNoticeService.getArrivalNotice(vo.getReplacementNoticeId());
                if (notice != null) {
                    vo.setReplacementNoticeCode(notice.getCode());
                }
            }
        });
    }
    private String resolveStatusName(Integer status) {
        MesWmReturnVendorStatusEnum statusEnum = Arrays.stream(MesWmReturnVendorStatusEnum.values())
                .filter(item -> item.getStatus().equals(status)).findFirst().orElse(null);
        return statusEnum != null ? statusEnum.getName() : null;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnvendor/vo/MesWmReturnVendorRespVO.java
@@ -1,9 +1,12 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.returnvendor.vo;
import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - MES ä¾›åº”商退货单 Response VO")
@Data
@@ -42,6 +45,33 @@
    @Schema(description = "退货原因", example = "质量不合格")
    private String returnReason;
    @Schema(description = "采购合同号", example = "HT2026110001")
    private String contractNo;
    @Schema(description = "处理方式:1退款 2换货", example = "1")
    private Integer handleType;
    @Schema(description = "退款金额", example = "1000.00")
    private BigDecimal refundAmount;
    @Schema(description = "负数冲红付款单编号", example = "1")
    private Long redFlushPaymentId;
    @Schema(description = "负数冲红付款单号", example = "FKD2026110001")
    private String redFlushPaymentNo;
    @Schema(description = "换货到货通知单编号", example = "1")
    private Long replacementNoticeId;
    @Schema(description = "换货到货通知单号", example = "AN2026110001")
    private String replacementNoticeCode;
    @Schema(description = "BPM æµç¨‹å®žä¾‹ç¼–号")
    private String processInstanceId;
    @Schema(description = "附件列表")
    private List<StorageBlobRespDTO> attachmentList;
    @Schema(description = "物流单号", example = "SF1234567890")
    private String transportCode;
@@ -51,6 +81,9 @@
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
    private Integer status;
    @Schema(description = "状态名称", example = "审批中")
    private String statusName;
    @Schema(description = "备注", example = "备注")
    private String remark;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnvendor/vo/MesWmReturnVendorSaveReqVO.java
@@ -5,7 +5,9 @@
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - MES ä¾›åº”商退货单新增/修改 Request VO")
@Data
@@ -39,6 +41,19 @@
    @Schema(description = "退货原因", example = "质量不合格")
    private String returnReason;
    @Schema(description = "处理方式:1退款 2换货", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "处理方式不能为空:1退款 2换货")
    private Integer handleType;
    @Schema(description = "退款金额(处理方式=退款时填写;为空时按采购单价×退货数量计算)", example = "1000.00")
    private BigDecimal refundAmount;
    @Schema(description = "采购合同号(只读,创建时从采购订单快照)", example = "HT2026110001")
    private String contractNo;
    @Schema(description = "附件 blobId åˆ—表", example = "[1, 2, 3]")
    private List<Long> blobIds;
    @Schema(description = "物流单号", example = "SF1234567890")
    private String transportCode;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/returnvendor/MesWmReturnVendorDO.java
@@ -7,6 +7,7 @@
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
@@ -47,6 +48,10 @@
     */
    private String purchaseOrderCode;
    /**
     * é‡‡è´­åˆåŒå·ï¼ˆåˆ›å»ºæ—¶ä»Žé‡‡è´­è®¢å•快照,换货入库沿用)
     */
    private String contractNo;
    /**
     * ä¾›åº”商 ID
     *
     * å…³è” ERP ä¾›åº”商 {@link cn.iocoder.yudao.module.erp.api.purchase.dto.ErpSupplierRespDTO#getId()}
@@ -80,4 +85,34 @@
     */
    private String remark;
    // ========== å®¡æ‰¹ ==========
    /**
     * BPM æµç¨‹å®žä¾‹ç¼–号
     */
    private String processInstanceId;
    /**
     * BPM æµç¨‹å®šä¹‰ Key
     */
    private String processDefinitionKey;
    // ========== è´¹ç”¨ä¿¡æ¯ ==========
    /**
     * å¤„理方式
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.module.mes.enums.wm.MesWmReturnVendorHandleTypeEnum}
     */
    private Integer handleType;
    /**
     * é€€æ¬¾é‡‘额(默认按采购单价 Ã— é€€è´§æ•°é‡è®¡ç®—,可修改)
     */
    private BigDecimal refundAmount;
    /**
     * è´Ÿæ•°å†²çº¢ä»˜æ¬¾å•编号(退款时生成,关联 ERP ä»˜æ¬¾å•)
     */
    private Long redFlushPaymentId;
    /**
     * æ¢è´§åˆ°è´§é€šçŸ¥å•编号(换货时生成,关联 MES åˆ°è´§é€šçŸ¥å•)
     */
    private Long replacementNoticeId;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/DictTypeConstants.java
@@ -57,6 +57,7 @@
    String MES_WM_OUTSOURCE_ISSUE_STATUS = "mes_wm_outsource_issue_status"; // MES å§”外出库状态
    String MES_WM_PRODUCT_ISSUE_STATUS = "mes_wm_product_issue_status"; // MES ç”Ÿäº§é¢†æ–™çŠ¶æ€
    String MES_WM_RETURN_VENDOR_STATUS = "mes_wm_return_vendor_status"; // MES ä¾›åº”商退货单状态
    String MES_WM_RETURN_VENDOR_HANDLE_TYPE = "mes_wm_return_vendor_handle_type"; // MES ä¾›åº”商退货处理方式
    // ========== ä»“储移库与盘点 (WM Transfer) ==========
    String MES_WM_TRANSFER_STATUS = "mes_wm_transfer_status"; // MES è½¬ç§»å•状态
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -660,6 +660,10 @@
    ErrorCode WM_RETURN_VENDOR_PURCHASE_ORDER_REQUIRED = new ErrorCode(1_040_711_008, "请先选择采购订单");
    ErrorCode WM_RETURN_VENDOR_PURCHASE_ORDER_NO_ITEM = new ErrorCode(1_040_711_009, "采购订单无明细数据,无法带入物料");
    ErrorCode WM_RETURN_VENDOR_ITEM_NOT_IN_PURCHASE_ORDER = new ErrorCode(1_040_711_010, "退货物料不在所关联采购订单内,无法添加");
    ErrorCode WM_RETURN_VENDOR_APPROVE_PROCESS_NOT_PUBLISHED = new ErrorCode(1_040_711_011, "采购退货审批流程[{}]未发布,请联系管理员在 BPM åŽå°å‘布流程模型后再提交");
    ErrorCode WM_RETURN_VENDOR_HANDLE_TYPE_INVALID = new ErrorCode(1_040_711_012, "供应商退货单处理方式不正确:当前为[{}],仅支持 1退款 / 2换货,请修改处理方式后再操作");
    ErrorCode WM_RETURN_VENDOR_REFUND_AMOUNT_REQUIRED = new ErrorCode(1_040_711_013, "无法自动计算退款金额:退货单[{}]未关联采购订单,或退货物料[{}]未匹配到采购订单明细。请在退货单填写退款金额后再完成退货");
    ErrorCode WM_RETURN_VENDOR_FINISHED_REQUIRED = new ErrorCode(1_040_711_014, "供应商退货单[{}]当前状态为[{}],只有已完成的退货单才能执行该操作");
    // ========== MES ä»“库管理-产品收货单(1-040-712-000) ==========
    ErrorCode WM_PRODUCT_RECPT_NOT_EXISTS = new ErrorCode(1_040_712_000, "产品收货单不存在");
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/wm/MesWmReturnVendorHandleTypeEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
package cn.iocoder.yudao.module.mes.enums.wm;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
 * MES ä¾›åº”商退货处理方式枚举
 *
 * å­—å…¸ {@link cn.iocoder.yudao.module.mes.enums.DictTypeConstants#MES_WM_RETURN_VENDOR_HANDLE_TYPE}
 */
@Getter
@AllArgsConstructor
public enum MesWmReturnVendorHandleTypeEnum implements ArrayValuable<Integer> {
    REFUND(1, "退款"),
    REPLACEMENT(2, "换货");
    public static final Integer[] ARRAYS = Arrays.stream(values())
            .map(MesWmReturnVendorHandleTypeEnum::getType).toArray(Integer[]::new);
    /**
     * ç±»åž‹å€¼
     */
    private final Integer type;
    /**
     * ç±»åž‹å
     */
    private final String name;
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
    public static MesWmReturnVendorHandleTypeEnum valueOf(Integer type) {
        return Arrays.stream(values()).filter(item -> item.getType().equals(type))
                .findFirst().orElse(null);
    }
    public static boolean isValid(Integer type) {
        return valueOf(type) != null;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/wm/MesWmReturnVendorStatusEnum.java
@@ -23,6 +23,13 @@
     */
    PREPARE(MesOrderStatusConstants.PREPARE, "草稿"),
    /**
     * å®¡æ‰¹ä¸­ï¼ˆBPM)
     *
     * å¯¹åº” MesWmReturnVendorService#submitReturnVendorApproval æ–¹æ³•
     * å¤ç”¨ CONFIRMED(1) çš„状态值,历史数据不迁移
     */
    BPM_APPROVING(MesOrderStatusConstants.CONFIRMED, "审批中"),
    /**
     * å¾…拣货
     *
     * å¯¹åº” MesWmReturnVendorService#submitReturnVendor æ–¹æ³•
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/arrivalnotice/MesWmArrivalNoticeService.java
@@ -142,4 +142,15 @@
     */
    Long generateArrivalNoticeFromPurchaseOrder(Long purchaseOrderId);
    /**
     * æ ¹æ®ä¾›åº”商退货单生成「换货到货通知单」(重新入库 + é‡æ–°è´¨æ£€ï¼Œå…³è”原采购合同)
     *
     * è¯¥å•据不走采购订单唯一性校验,允许同一采购订单存在多张到货通知单。
     * ç”ŸæˆåŽçŠ¶æ€ä¸ºå¾…è´¨æ£€ï¼Œå¹¶é€è¡Œè‡ªåŠ¨åˆ›å»º IQC æ£€éªŒå•。
     *
     * @param returnVendorId ä¾›åº”商退货单编号
     * @return ç”Ÿæˆçš„到货通知单编号
     */
    Long createReplacementArrivalNotice(Long returnVendorId);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/arrivalnotice/MesWmArrivalNoticeServiceImpl.java
@@ -9,7 +9,11 @@
import cn.iocoder.yudao.module.mes.controller.admin.wm.arrivalnotice.vo.MesWmArrivalNoticeSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.arrivalnotice.MesWmArrivalNoticeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.arrivalnotice.MesWmArrivalNoticeLineDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnvendor.MesWmReturnVendorDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnvendor.MesWmReturnVendorLineDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.arrivalnotice.MesWmArrivalNoticeMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.returnvendor.MesWmReturnVendorLineMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.returnvendor.MesWmReturnVendorMapper;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmArrivalNoticeStatusEnum;
import cn.iocoder.yudao.module.erp.api.purchase.ErpPurchaseOrderApi;
import cn.iocoder.yudao.module.srm.api.supplier.SrmSupplierApi;
@@ -28,7 +32,9 @@
import java.math.BigDecimal;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
@@ -46,6 +52,11 @@
    @Resource
    private MesWmArrivalNoticeLineService arrivalNoticeLineService;
    @Resource
    private MesWmReturnVendorMapper returnVendorMapper;
    @Resource
    private MesWmReturnVendorLineMapper returnVendorLineMapper;
    @Resource
    private SrmSupplierApi srmSupplierApi;
@@ -371,6 +382,68 @@
        return notice.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createReplacementArrivalNotice(Long returnVendorId) {
        // 1. æ ¡éªŒé€€è´§å•
        MesWmReturnVendorDO returnVendor = returnVendorMapper.selectById(returnVendorId);
        if (returnVendor == null) {
            throw exception(WM_RETURN_VENDOR_NOT_EXISTS);
        }
        if (returnVendor.getPurchaseOrderId() == null) {
            throw exception(WM_RETURN_VENDOR_PURCHASE_ORDER_REQUIRED);
        }
        List<MesWmReturnVendorLineDO> returnLines = returnVendorLineMapper.selectListByReturnId(returnVendorId);
        if (CollUtil.isEmpty(returnLines)) {
            throw exception(WM_RETURN_VENDOR_NO_LINE);
        }
        // 2. èŽ·å–é‡‡è´­è®¢å•æ˜Žç»†ï¼Œç”¨äºŽæŒ‰ç‰©æ–™åŒ¹é…é‡‡è´­è®¢å•æ˜Žç»†ç¼–å·ï¼ˆä¿è¯å…¥åº“åŽèƒ½å›žå†™å…¥åº“/退货数量)
        Map<Long, Long> purchaseOrderItemIdMap = new HashMap<>();
        List<ErpPurchaseOrderItemRespDTO> orderItems = purchaseOrderApi
                .getPurchaseOrderItemList(returnVendor.getPurchaseOrderId()).getCheckedData();
        if (CollUtil.isNotEmpty(orderItems)) {
            orderItems.stream()
                    .filter(item -> item.getProductId() != null)
                    .forEach(item -> purchaseOrderItemIdMap.putIfAbsent(item.getProductId(), item.getId()));
        }
        // 3. åˆ›å»ºåˆ°è´§é€šçŸ¥å•主表,状态直接置为待质检
        MesWmArrivalNoticeDO notice = new MesWmArrivalNoticeDO();
        notice.setCode(generateArrivalNoticeCode());
        notice.setName(returnVendor.getCode() + "换货到货");
        notice.setPurchaseOrderId(returnVendor.getPurchaseOrderId());
        notice.setPurchaseOrderCode(returnVendor.getPurchaseOrderCode());
        notice.setVendorId(returnVendor.getVendorId());
        notice.setArrivalDate(java.time.LocalDateTime.now());
        notice.setStatus(MesWmArrivalNoticeStatusEnum.PENDING_QC.getStatus());
        notice.setRemark("供应商退货单[" + returnVendor.getCode() + "]换货重新入库,合同号["
                + returnVendor.getContractNo() + "]");
        arrivalNoticeMapper.insert(notice);
        // 4. éåŽ†é€€è´§è¡Œåˆ›å»ºåˆ°è´§é€šçŸ¥å•è¡Œï¼Œå…¨éƒ¨æ ‡è®°éœ€è¦è´¨æ£€
        for (MesWmReturnVendorLineDO returnLine : returnLines) {
            MesWmArrivalNoticeLineDO line = new MesWmArrivalNoticeLineDO();
            line.setNoticeId(notice.getId());
            line.setPurchaseOrderItemId(purchaseOrderItemIdMap.get(returnLine.getItemId()));
            line.setItemId(returnLine.getItemId());
            line.setArrivalQuantity(returnLine.getQuantity());
            line.setIqcCheckFlag(true);
            arrivalNoticeLineService.createArrivalNoticeLineFromPurchaseOrderItem(line);
        }
        // 5. é€è¡Œè‡ªåŠ¨åˆ›å»º IQC æ£€éªŒå•
        List<MesWmArrivalNoticeLineDO> lines = arrivalNoticeLineService.getArrivalNoticeLineListByNoticeId(notice.getId());
        for (MesWmArrivalNoticeLineDO line : lines) {
            Long iqcId = iqcService.createIqcFromArrivalNotice(
                    notice.getId(), line, notice.getCode(), notice.getVendorId());
            arrivalNoticeLineService.updateArrivalNoticeLineIqcId(line.getId(), iqcId);
            log.info("[createReplacementArrivalNotice][换货到货通知单({}) è¡Œ({}) è‡ªåŠ¨åˆ›å»º IQC æ£€éªŒå•({})]",
                    notice.getId(), line.getId(), iqcId);
        }
        return notice.getId();
    }
    /**
     * ç”Ÿæˆåˆ°è´§é€šçŸ¥å•编码
     */
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnvendor/MesWmReturnVendorService.java
@@ -65,6 +65,22 @@
    void submitReturnVendor(Long id);
    /**
     * æäº¤ä¾›åº”商退货单审批(草稿 -> å®¡æ‰¹ä¸­ï¼‰ï¼Œèµ° BPM æµç¨‹ï¼šé‡‡è´­ -> è´¨é‡ -> ä»“库
     *
     * @param id ç¼–号
     * @param processDefinitionKey æµç¨‹å®šä¹‰çš„ Key
     */
    void submitReturnVendorApproval(Long id, String processDefinitionKey);
    /**
     * æ›´æ–°ä¾›åº”商退货单的审批状态(由 BPM æµç¨‹å®žä¾‹çš„回调触发)
     *
     * @param id ç¼–号
     * @param bpmResult BPM å®¡æ‰¹ç»“æžœ
     */
    void updateReturnVendorAuditStatus(Long id, Integer bpmResult);
    /**
     * æ‰§è¡Œæ‹£è´§ï¼ˆå¾…拣货 -> å¾…执行退货)
     *
     * @param id ç¼–号
@@ -79,6 +95,22 @@
    void finishReturnVendor(Long id);
    /**
     * ç”Ÿæˆé‡‡è´­é€€è´§è´Ÿæ•°å†²çº¢ä»˜æ¬¾å•(幂等,已生成则直接返回)
     *
     * @param id é€€è´§å•编号
     * @return ä»˜æ¬¾å•编号
     */
    Long createRedFlushPayment(Long id);
    /**
     * ç”Ÿæˆæ¢è´§åˆ°è´§é€šçŸ¥å•(幂等,已生成则直接返回),走 åˆ°è´§é€šçŸ¥ -> IQC -> é‡‡è´­å…¥åº“ é“¾è·¯
     *
     * @param id é€€è´§å•编号
     * @return åˆ°è´§é€šçŸ¥å•编号
     */
    Long createReplacementNotice(Long id);
    /**
     * å–消供应商退货单(任意非已完成/已取消状态 -> å·²å–消)
     *
     * @param id ç¼–号
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnvendor/MesWmReturnVendorServiceImpl.java
@@ -2,11 +2,14 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.returnvendor.vo.MesWmReturnVendorPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.returnvendor.vo.MesWmReturnVendorSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.returnvendor.vo.line.MesWmReturnVendorLineSaveReqVO;
@@ -15,22 +18,32 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnvendor.MesWmReturnVendorLineDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.returnvendor.MesWmReturnVendorMapper;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmReturnVendorHandleTypeEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmReturnVendorStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
import cn.iocoder.yudao.module.erp.api.finance.ErpFinancePaymentApi;
import cn.iocoder.yudao.module.erp.api.finance.dto.ErpFinancePaymentRedFlushCreateReqDTO;
import cn.iocoder.yudao.module.erp.api.purchase.ErpPurchaseOrderApi;
import cn.iocoder.yudao.module.erp.api.purchase.dto.ErpPurchaseOrderItemRespDTO;
import cn.iocoder.yudao.module.erp.api.purchase.dto.ErpPurchaseOrderRespDTO;
import cn.iocoder.yudao.module.srm.api.supplier.SrmSupplierApi;
import cn.iocoder.yudao.module.mes.service.wm.arrivalnotice.MesWmArrivalNoticeService;
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.system.api.storage.StorageAttachmentApi;
import jakarta.annotation.Resource;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -43,6 +56,11 @@
@Service
@Validated
public class MesWmReturnVendorServiceImpl implements MesWmReturnVendorService {
    /**
     * é™„件业务类型:MES ä¾›åº”商退货单
     */
    public static final String RETURN_VENDOR_RECORD_TYPE = "mes_wm_return_vendor";
    @Resource
    private MesWmReturnVendorMapper returnVendorMapper;
@@ -57,14 +75,22 @@
    private MesWmTransactionService wmTransactionService;
    @Resource
    private ErpPurchaseOrderApi purchaseOrderApi;
    @Resource
    private ErpFinancePaymentApi financePaymentApi;
    @Resource
    private MesWmArrivalNoticeService arrivalNoticeService;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @Resource
    private BpmProcessInstanceApi processInstanceApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createReturnVendor(MesWmReturnVendorSaveReqVO createReqVO) {
        // 1. æ ¡éªŒæ•°æ®
        validateReturnVendorSaveData(createReqVO);
        // 1.1. è‹¥é‡‡è´­è®¢å•号为空但关联了采购订单,则从 ERP é‡‡è´­è®¢å•补齐订单号
        fillPurchaseOrderCode(createReqVO);
        // 1.1. è‹¥é‡‡è´­è®¢å•号为空但关联了采购订单,则从 ERP é‡‡è´­è®¢å•补齐订单号、合同号
        fillFromPurchaseOrder(createReqVO);
        // 2. æ’入主表
        MesWmReturnVendorDO returnVendor = BeanUtils.toBean(createReqVO, MesWmReturnVendorDO.class);
@@ -75,6 +101,9 @@
        if (returnVendor.getPurchaseOrderId() != null) {
            autoImportLinesFromLinkedOrder(returnVendor.getId(), false);
        }
        // 2.2. ç»‘定附件(质量部发起退货时上传的检验报告、照片等)
        storageAttachmentApi.bindAttachments("file", RETURN_VENDOR_RECORD_TYPE,
                returnVendor.getId(), createReqVO.getBlobIds());
        return returnVendor.getId();
    }
@@ -83,8 +112,8 @@
    public void updateReturnVendor(MesWmReturnVendorSaveReqVO updateReqVO) {
        // 1. æ ¡éªŒæ•°æ®ï¼ˆå«å­˜åœ¨ + è‰ç¨¿çŠ¶æ€ï¼‰
        MesWmReturnVendorDO oldReturnVendor = validateReturnVendorSaveData(updateReqVO);
        // 1.1. è‹¥é‡‡è´­è®¢å•号为空但关联了采购订单,则从 ERP é‡‡è´­è®¢å•补齐订单号
        fillPurchaseOrderCode(updateReqVO);
        // 1.1. è‹¥é‡‡è´­è®¢å•号为空但关联了采购订单,则从 ERP é‡‡è´­è®¢å•补齐订单号、合同号
        fillFromPurchaseOrder(updateReqVO);
        Long oldPurchaseOrderId = oldReturnVendor != null ? oldReturnVendor.getPurchaseOrderId() : null;
        Long newPurchaseOrderId = updateReqVO.getPurchaseOrderId();
@@ -100,22 +129,29 @@
        if (newPurchaseOrderId != null) {
            autoImportLinesFromLinkedOrder(updateReqVO.getId(), purchaseOrderChanged);
        }
        // 4. å¢žé‡æ›´æ–°é™„件(只处理新增/删除,未传 blobIds æ—¶ä¸åŠ¨çŽ°æœ‰é™„ä»¶ï¼‰
        storageAttachmentApi.updateAttachments("file", RETURN_VENDOR_RECORD_TYPE,
                updateReqVO.getId(), updateReqVO.getBlobIds());
    }
    /**
     * å½“采购订单号为空、但关联了采购订单时,从 ERP é‡‡è´­è®¢å•补齐采购订单号
     * å…³è”了采购订单时,从 ERP é‡‡è´­è®¢å•补齐采购订单号、合同号(合同号只做快照,不允许手工生成)
     */
    private void fillPurchaseOrderCode(MesWmReturnVendorSaveReqVO reqVO) {
    private void fillFromPurchaseOrder(MesWmReturnVendorSaveReqVO reqVO) {
        if (reqVO == null || reqVO.getPurchaseOrderId() == null) {
            return;
        }
        if (cn.hutool.core.util.StrUtil.isNotBlank(reqVO.getPurchaseOrderCode())) {
            return;
        }
        ErpPurchaseOrderRespDTO purchaseOrder =
                purchaseOrderApi.getPurchaseOrder(reqVO.getPurchaseOrderId()).getCheckedData();
        if (purchaseOrder != null) {
        if (purchaseOrder == null) {
            return;
        }
        if (StrUtil.isBlank(reqVO.getPurchaseOrderCode())) {
            reqVO.setPurchaseOrderCode(purchaseOrder.getNo());
        }
        if (StrUtil.isBlank(reqVO.getContractNo())) {
            reqVO.setContractNo(purchaseOrder.getContractNo());
        }
    }
@@ -129,7 +165,9 @@
        returnVendorDetailService.deleteReturnVendorDetailByReturnId(id);
        // 2.2 çº§è”删除行
        returnVendorLineService.deleteReturnVendorLineByReturnId(id);
        // 2.3 åˆ é™¤ä¸»è¡¨
        // 2.3 åˆ é™¤é™„ä»¶
        storageAttachmentApi.deleteAttachmentsByRecord(RETURN_VENDOR_RECORD_TYPE, id);
        // 2.4 åˆ é™¤ä¸»è¡¨
        returnVendorMapper.deleteById(id);
    }
@@ -161,6 +199,80 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void submitReturnVendorApproval(Long id, String processDefinitionKey) {
        // 1. æ ¡éªŒå­˜åœ¨ + è‰ç¨¿çŠ¶æ€ + è‡³å°‘一条行
        validateReturnVendorExistsAndPrepare(id);
        if (CollUtil.isEmpty(returnVendorLineService.getReturnVendorLineListByReturnId(id))) {
            throw exception(WM_RETURN_VENDOR_NO_LINE);
        }
        // 2. å…¨å±€å…å®¡æ‰¹ï¼šç›´æŽ¥ç½®ä¸ºã€Œå¾…拣货」,无需流程模型
        if (processInstanceApi.isGlobalAutoApprove()) {
            returnVendorMapper.updateById(new MesWmReturnVendorDO()
                    .setId(id)
                    .setStatus(MesWmReturnVendorStatusEnum.APPROVING.getStatus())
                    .setProcessDefinitionKey(processDefinitionKey)
                    .setProcessInstanceId(""));
            return;
        }
        // 3. åˆ›å»º BPM æµç¨‹å®žä¾‹ï¼›æµç¨‹æœªå‘布时给出明确提示
        String processInstanceId;
        try {
            processInstanceId = processInstanceApi.createProcessInstance(getCurrentUserId(),
                    new BpmProcessInstanceCreateReqDTO()
                            .setProcessDefinitionKey(processDefinitionKey)
                            .setBusinessKey(String.valueOf(id)));
        } catch (ServiceException ex) {
            if (cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_DEFINITION_NOT_EXISTS
                    .getCode().equals(ex.getCode())) {
                throw exception(WM_RETURN_VENDOR_APPROVE_PROCESS_NOT_PUBLISHED, processDefinitionKey);
            }
            throw ex;
        }
        // 4. æ›´æ–°é€€è´§å•状态为审批中
        returnVendorMapper.updateById(new MesWmReturnVendorDO()
                .setId(id)
                .setStatus(MesWmReturnVendorStatusEnum.BPM_APPROVING.getStatus())
                .setProcessDefinitionKey(processDefinitionKey)
                .setProcessInstanceId(processInstanceId));
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateReturnVendorAuditStatus(Long id, Integer bpmResult) {
        // 1. æ ¡éªŒå­˜åœ¨ + å®¡æ‰¹ä¸­
        MesWmReturnVendorDO returnVendor = validateReturnVendorExists(id);
        if (!MesWmReturnVendorStatusEnum.BPM_APPROVING.getStatus().equals(returnVendor.getStatus())) {
            return;
        }
        // 2. æ ¹æ®å®¡æ‰¹ç»“果更新状态:通过 -> å¾…拣货;驳回 -> è‰ç¨¿ï¼›å–消 -> å·²å–消
        Integer newStatus = convertBpmResultToReturnVendorStatus(bpmResult);
        if (newStatus != null) {
            returnVendorMapper.updateById(new MesWmReturnVendorDO().setId(id).setStatus(newStatus));
        }
    }
    /**
     * è½¬æ¢ BPM å®¡æ‰¹ç»“果为供应商退货单状态
     */
    private Integer convertBpmResultToReturnVendorStatus(Integer bpmResult) {
        if (cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum.APPROVE.getStatus().equals(bpmResult)) {
            return MesWmReturnVendorStatusEnum.APPROVING.getStatus();
        }
        if (cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum.REJECT.getStatus().equals(bpmResult)) {
            return MesWmReturnVendorStatusEnum.PREPARE.getStatus();
        }
        if (cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum.CANCEL.getStatus().equals(bpmResult)) {
            return MesWmReturnVendorStatusEnum.CANCELED.getStatus();
        }
        return null;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void stockReturnVendor(Long id) {
        // æ ¡éªŒå­˜åœ¨
        MesWmReturnVendorDO returnVendor = validateReturnVendorExists(id);
@@ -186,12 +298,184 @@
            throw exception(WM_RETURN_VENDOR_NO_DETAIL);
        }
        // 1.3 æ ¡éªŒå¤„理方式(退款 / æ¢è´§ï¼‰
        if (!MesWmReturnVendorHandleTypeEnum.isValid(returnVendor.getHandleType())) {
            throw exception(WM_RETURN_VENDOR_HANDLE_TYPE_INVALID, returnVendor.getHandleType());
        }
        // 2. éåŽ†æ‰€æœ‰æ˜Žç»†ï¼Œåˆ›å»ºåº“å­˜äº‹åŠ¡ï¼ˆæ‰£å‡åº“å­˜ + è®°å½•流水)
        createTransactionList(returnVendor, details);
        // 3. æ›´æ–°é€€è´§å•状态
        // 3. å›žå†™é‡‡è´­è®¢å•行的已退货数量(按物料匹配采购订单明细)
        writeBackPurchaseOrderReturnCount(returnVendor);
        // 4. å…ˆç½®ä¸ºå·²å®Œæˆï¼Œä¿è¯é‡å¤è°ƒç”¨ä¼šè¢«çŠ¶æ€æ ¡éªŒæ‹¦æˆªï¼ˆå¹‚ç­‰ï¼‰
        returnVendorMapper.updateById(new MesWmReturnVendorDO()
                .setId(id).setStatus(MesWmReturnVendorStatusEnum.FINISHED.getStatus()));
        // 5. è´¹ç”¨å¤„理:退款 -> è´Ÿæ•°å†²çº¢ä»˜æ¬¾å•;换货 -> æ¢è´§åˆ°è´§é€šçŸ¥å•(重新入库 + é‡æ–°è´¨æ£€ï¼‰
        if (MesWmReturnVendorHandleTypeEnum.REFUND.getType().equals(returnVendor.getHandleType())) {
            createRedFlushPayment(id);
        } else if (MesWmReturnVendorHandleTypeEnum.REPLACEMENT.getType().equals(returnVendor.getHandleType())) {
            createReplacementNotice(id);
        }
    }
    /**
     * æŠŠé€€è´§æ•°é‡å¢žé‡å›žå†™åˆ°å…³è”采购订单的明细行,并同步订单级退货数量
     */
    private void writeBackPurchaseOrderReturnCount(MesWmReturnVendorDO returnVendor) {
        if (returnVendor.getPurchaseOrderId() == null) {
            return;
        }
        List<MesWmReturnVendorLineDO> lines = returnVendorLineService.getReturnVendorLineListByReturnId(returnVendor.getId());
        if (CollUtil.isEmpty(lines)) {
            return;
        }
        List<ErpPurchaseOrderItemRespDTO> orderItems = purchaseOrderApi
                .getPurchaseOrderItemList(returnVendor.getPurchaseOrderId()).getCheckedData();
        if (CollUtil.isEmpty(orderItems)) {
            return;
        }
        Map<Long, Long> purchaseOrderItemIdMap = new HashMap<>();
        orderItems.stream()
                .filter(item -> item.getProductId() != null)
                .forEach(item -> purchaseOrderItemIdMap.putIfAbsent(item.getProductId(), item.getId()));
        for (MesWmReturnVendorLineDO line : lines) {
            Long purchaseOrderItemId = purchaseOrderItemIdMap.get(line.getItemId());
            if (purchaseOrderItemId == null || line.getQuantity() == null) {
                continue;
            }
            purchaseOrderApi.updateOrderItemReturnCount(purchaseOrderItemId, line.getQuantity());
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createRedFlushPayment(Long id) {
        // 1. æ ¡éªŒå­˜åœ¨ + å·²å®Œæˆ + å¤„理方式为退款
        MesWmReturnVendorDO returnVendor = validateReturnVendorFinished(id);
        if (!MesWmReturnVendorHandleTypeEnum.REFUND.getType().equals(returnVendor.getHandleType())) {
            throw exception(WM_RETURN_VENDOR_HANDLE_TYPE_INVALID, returnVendor.getHandleType());
        }
        // 2. å¹‚等:已生成过冲红单直接返回
        if (returnVendor.getRedFlushPaymentId() != null) {
            return returnVendor.getRedFlushPaymentId();
        }
        // 3. è®¡ç®—退款金额(未填写时按采购单价 Ã— é€€è´§æ•°é‡ï¼‰
        BigDecimal refundAmount = resolveRefundAmount(returnVendor);
        // 4. ç”Ÿæˆè´Ÿæ•°å†²çº¢ä»˜æ¬¾å•
        Long paymentId = financePaymentApi.createRedFlushPayment(new ErpFinancePaymentRedFlushCreateReqDTO()
                .setSupplierId(returnVendor.getVendorId())
                .setRefundAmount(refundAmount)
                .setSourceId(returnVendor.getId())
                .setSourceNo(returnVendor.getCode())
                .setFinanceUserId(getCurrentUserId())
                .setRemark("供应商退货单[" + returnVendor.getCode() + "]退款冲红,合同号[" + returnVendor.getContractNo() + "]"))
                .getCheckedData();
        // 5. å›žå†™å†²çº¢å•号与最终退款金额
        returnVendorMapper.updateById(new MesWmReturnVendorDO()
                .setId(id).setRefundAmount(refundAmount).setRedFlushPaymentId(paymentId));
        return paymentId;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createReplacementNotice(Long id) {
        // 1. æ ¡éªŒå­˜åœ¨ + å·²å®Œæˆ + å¤„理方式为换货
        MesWmReturnVendorDO returnVendor = validateReturnVendorFinished(id);
        if (!MesWmReturnVendorHandleTypeEnum.REPLACEMENT.getType().equals(returnVendor.getHandleType())) {
            throw exception(WM_RETURN_VENDOR_HANDLE_TYPE_INVALID, returnVendor.getHandleType());
        }
        // 2. å¹‚等:已生成过换货到货单直接返回
        if (returnVendor.getReplacementNoticeId() != null) {
            return returnVendor.getReplacementNoticeId();
        }
        // 3. ç”Ÿæˆæ¢è´§åˆ°è´§é€šçŸ¥å•(待质检,行自动创建 IQC)
        Long noticeId = arrivalNoticeService.createReplacementArrivalNotice(id);
        // 4. å›žå†™åˆ°è´§é€šçŸ¥å•编号
        returnVendorMapper.updateById(new MesWmReturnVendorDO()
                .setId(id).setReplacementNoticeId(noticeId));
        return noticeId;
    }
    /**
     * è§£æžé€€æ¬¾é‡‘额:优先取单据填写值;未填写时按「采购单价 Ã— é€€è´§æ•°é‡ã€é€è¡Œç´¯åŠ 
     */
    private BigDecimal resolveRefundAmount(MesWmReturnVendorDO returnVendor) {
        if (returnVendor.getRefundAmount() != null
                && returnVendor.getRefundAmount().compareTo(BigDecimal.ZERO) > 0) {
            return returnVendor.getRefundAmount();
        }
        if (returnVendor.getPurchaseOrderId() == null) {
            throw exception(WM_RETURN_VENDOR_REFUND_AMOUNT_REQUIRED,
                    returnVendor.getCode(), "未关联采购订单");
        }
        List<ErpPurchaseOrderItemRespDTO> orderItems = purchaseOrderApi
                .getPurchaseOrderItemList(returnVendor.getPurchaseOrderId()).getCheckedData();
        Map<Long, BigDecimal> priceMap = new HashMap<>();
        if (CollUtil.isNotEmpty(orderItems)) {
            orderItems.stream().filter(item -> item.getProductId() != null)
                    .forEach(item -> priceMap.putIfAbsent(item.getProductId(), item.getProductPrice()));
        }
        List<MesWmReturnVendorLineDO> lines = returnVendorLineService.getReturnVendorLineListByReturnId(returnVendor.getId());
        if (CollUtil.isEmpty(lines)) {
            throw exception(WM_RETURN_VENDOR_NO_LINE);
        }
        BigDecimal total = BigDecimal.ZERO;
        for (MesWmReturnVendorLineDO line : lines) {
            BigDecimal price = priceMap.get(line.getItemId());
            if (price == null) {
                throw exception(WM_RETURN_VENDOR_REFUND_AMOUNT_REQUIRED,
                        returnVendor.getCode(), "物料ID=" + line.getItemId() + " æœªåŒ¹é…åˆ°é‡‡è´­å•ä»·");
            }
            BigDecimal quantity = line.getQuantity() != null ? line.getQuantity() : BigDecimal.ZERO;
            total = total.add(price.multiply(quantity));
        }
        if (total.compareTo(BigDecimal.ZERO) <= 0) {
            throw exception(WM_RETURN_VENDOR_REFUND_AMOUNT_REQUIRED,
                    returnVendor.getCode(), "采购单价 Ã— é€€è´§æ•°é‡åˆè®¡ä¸º 0");
        }
        return total;
    }
    /**
     * æ ¡éªŒé€€è´§å•存在且已完成(费用类操作仅在完成后退货单上执行)
     */
    private MesWmReturnVendorDO validateReturnVendorFinished(Long id) {
        MesWmReturnVendorDO returnVendor = validateReturnVendorExists(id);
        if (!MesWmReturnVendorStatusEnum.FINISHED.getStatus().equals(returnVendor.getStatus())) {
            throw exception(WM_RETURN_VENDOR_FINISHED_REQUIRED, returnVendor.getCode(),
                    resolveStatusName(returnVendor.getStatus()));
        }
        return returnVendor;
    }
    /**
     * çŠ¶æ€å€¼è½¬åç§°ï¼Œè§£æžå¤±è´¥å›žé€€ä¸º ID å­—符串
     */
    private String resolveStatusName(Integer status) {
        for (MesWmReturnVendorStatusEnum value : MesWmReturnVendorStatusEnum.values()) {
            if (value.getStatus().equals(status)) {
                return value.getName();
            }
        }
        return "ID=" + status;
    }
    /**
     * èŽ·å–å½“å‰ç”¨æˆ·ç¼–å·ï¼ˆBPM æµç¨‹å‘起人、财务人员)
     */
    private Long getCurrentUserId() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null && authentication.getPrincipal() instanceof Long) {
            return (Long) authentication.getPrincipal();
        }
        return null;
    }
    private void createTransactionList(MesWmReturnVendorDO returnVendor, List<MesWmReturnVendorDetailDO> details) {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnvendor/listener/MesWmReturnVendorStatusListener.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,65 @@
package cn.iocoder.yudao.module.mes.service.wm.returnvendor.listener;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnvendor.MesWmReturnVendorDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.returnvendor.MesWmReturnVendorMapper;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmReturnVendorStatusEnum;
import cn.iocoder.yudao.module.mes.service.wm.returnvendor.MesWmReturnVendorService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
/**
 * ä¾›åº”商退货单 BPM å®¡æ‰¹çŠ¶æ€ç›‘å¬å™¨
 *
 * ç›‘听 BPM æµç¨‹å®žä¾‹çŠ¶æ€å˜æ›´äº‹ä»¶ï¼Œæ›´æ–°ä¾›åº”å•†é€€è´§å•ï¼ˆå®¡æ‰¹æµç¨‹ï¼šé‡‡è´­ -> è´¨é‡ -> ä»“库)的审批状态
 */
@Component
@Slf4j
public class MesWmReturnVendorStatusListener {
    @Resource
    private MesWmReturnVendorService returnVendorService;
    @Resource
    private MesWmReturnVendorMapper returnVendorMapper;
    @EventListener
    public void onApplicationEvent(BpmProcessInstanceStatusEvent event) {
        // 1. è§£æžä¸šåС䏻键
        Long returnVendorId;
        try {
            returnVendorId = Long.parseLong(event.getBusinessKey());
        } catch (NumberFormatException e) {
            log.warn("[onApplicationEvent] ä¸šåС䏻键({}) ä¸æ˜¯æœ‰æ•ˆçš„供应商退货单编号", event.getBusinessKey());
            return;
        }
        // 2. æ ¡éªŒé€€è´§å•存在
        MesWmReturnVendorDO returnVendor = returnVendorMapper.selectById(returnVendorId);
        if (returnVendor == null) {
            return;
        }
        // 3. æ ¡éªŒæµç¨‹å®žä¾‹åŒ¹é…ï¼šé¿å…å…¶å®ƒä¸šåŠ¡çš„æµç¨‹å›žè°ƒè¯¯æ›´æ–°æœ¬å•æ®
        if (StrUtil.isBlank(returnVendor.getProcessInstanceId())
                || !returnVendor.getProcessInstanceId().equals(event.getId())) {
            log.warn("[onApplicationEvent] é€€è´§å•({}) çš„æµç¨‹å®žä¾‹({}) ä¸Žäº‹ä»¶({}) ä¸åŒ¹é…ï¼Œå¿½ç•¥",
                    returnVendorId, returnVendor.getProcessInstanceId(), event.getId());
            return;
        }
        // 4. æ ¡éªŒé€€è´§å•状态为审批中
        if (!MesWmReturnVendorStatusEnum.BPM_APPROVING.getStatus().equals(returnVendor.getStatus())) {
            log.warn("[onApplicationEvent] é€€è´§å•({}) ä¸å¤„于审批中状态,当前状态:{}", returnVendorId, returnVendor.getStatus());
            return;
        }
        // 5. æ›´æ–°é€€è´§å•审批状态
        returnVendorService.updateReturnVendorAuditStatus(returnVendorId, event.getStatus());
        log.info("[onApplicationEvent] é€€è´§å•({}) å®¡æ‰¹çŠ¶æ€æ›´æ–°å®Œæˆï¼ŒBPM çŠ¶æ€ï¼š{}", returnVendorId, event.getStatus());
    }
}