docs/project-business/data/flows.json
@@ -67,11 +67,11 @@ "name": "采购流程", "mermaid": "mermaid/03-purchase-flow.mmd", "color": "#5fc79e", "summary": "供应商档案(多联系人/多银行·含银行行号) → 询价比价(产品×供应商横向报价,按产品选中标) → 确认后按中标供应商拆采购订单草稿并走现有 BPM → 确认收货生成 MES 到货通知 → IQC(合格/特采入库·退货/报废) → 采购入库回写订单与申请 → 来票(AI OCR) → 付款单 BPM → 供应商往来对账。", "summary": "供应商档案(多联系人/多银行·含银行行号) → 采购申请(不记价格·不填供应商)审批通过后,采购员在询价比价页的待处理列表决定:发起询价比价确认价格,或选供应商直接生成零金额台账草稿 → 确认收货生成 MES 到货通知 → IQC(合格/特采入库·退货/报废) → 采购入库回写订单与申请 → 来票(AI OCR) → 付款单 BPM → 供应商往来对账。", "steps": [ { "stage": "寻源", "action": "供应商档案维护多个联系人与多个银行账户(含银行行号);新增询价单时一并录入产品×参与供应商报价并选定每个产品的中标供应商,列表直接确认后按中标供应商生成采购订单草稿", "action": "供应商档案维护多个联系人与多个银行账户(含银行行号);询价单可按采购申请明细一键发起,也可独立新建;录入产品×参与供应商报价并选定每个产品的中标供应商,审核通过后确认转订单", "roles": [ "采购员", "供应商管理员" @@ -79,7 +79,15 @@ }, { "stage": "申请", "action": "采购申请(CGSQ)提交 BPM,驳回可重置回草稿;通过且有供应商自动生成订单", "action": "采购申请(CGSQ)只记产品/单位/数量/需求日期,不含任何价格字段,也不填供应商与是否质检;提交 BPM,驳回可重置回草稿;审批通过仅改状态,不自动生成台账", "roles": [ "采购员", "审批人" ] }, { "stage": "价格确认", "action": "审批通过后采购员在「询价比价」页的「采购申请待处理」页签(带数量角标,有待处理时默认展示)中二选一:①需要比价→发起询价,生成询价单草稿并回写申请;询价审核通过后确认转订单,按中标供应商拆分生成台账,价格取中标报价;②不需要比价→直接生成零金额、无供应商台账草稿,由采购员在台账补录供应商/单价/税率;台账提交审批时后端校验供应商、付款日期与每条明细单价,缺哪条就报哪条,补齐后才能提交。采购申请页本身不再提供这两个按钮。询价取消或删除会清空申请的询价关联,申请重新出现在待处理列表", "roles": [ "采购员", "审批人" @@ -94,7 +102,7 @@ }, { "stage": "收货", "action": "确认收货生成到货通知(0→待质检2/待入库3→4);需检物料自动创建 IQC", "action": "确认收货生成到货通知(0→待质检2/待入库3→4);需检物料由库管在到货通知点「执行质检」才生成 IQC,不点则不生成、也不能入库;无需检验的提交即自动生成采购入库单", "roles": [ "仓管", "IQC 质检员" @@ -102,7 +110,7 @@ }, { "stage": "入库", "action": "IQC 合格/特采→采购入库(待上架→待执行→完成)库存+,回写订单 inCount/inStatus 与申请明细", "action": "IQC 合格/特采→系统自动生成采购入库单(待入库0)→库管核对数量、选入库仓库/库区/库位后点「确认入库」一步完成(→完成4),库存+,回写订单 inCount/inStatus 与申请明细;不再有提交/执行上架/到货通知执行入库步骤", "roles": [ "仓管" ] docs/project-business/data/objects.json
@@ -199,8 +199,9 @@ "name": "询价比价", "module": "供应商管理", "table": "srm_inquiry", "doc": "XJ编号,草稿0→已转订单20→已取消30(10询价中为历史状态);新增/编辑时一并录入产品行×参与供应商报价(srm_inquiry_quote)并选定每个产品的中标供应商,无需单独的报价/比价步骤,确认后按中标供应商拆分生成采购订单草稿并记录 srm_inquiry_order", "doc": "XJ编号,草稿0→询价中10→审核中40→审核通过50→已转订单20/已取消30/审核未通过60;可由采购申请明细一键发起(记录 purchaseRequestId/purchaseRequestNo 并回写申请 inquiryId/inquiryNo),也可独立新建;新增/编辑时一并录入产品行×参与供应商报价(srm_inquiry_quote)并选定每个产品的中标供应商,审核通过后确认转订单,按中标供应商拆分生成采购台账草稿并记录 srm_inquiry_order;取消或删除会清空采购申请的询价关联", "upstream": [ "采购申请(可选)", "供应商", "主数据(物料)" ], @@ -227,12 +228,13 @@ "name": "采购申请", "module": "进销存", "table": "erp_purchase_request", "doc": "CGSQ编号,BPM 审批,通过后有供应商自动生成订单", "doc": "CGSQ编号,只记产品/单位/数量/需求日期,表头与明细均不含价格字段,不填供应商、不填是否质检(质检在台账环节确定);BPM 审批,通过后仅改状态,采购员到「询价比价」页的「采购申请待处理」中决定「发起询价」或「直接生成台账」:发起询价时回写 inquiryId/inquiryNo,询价确认后生成台账并回写 orderId/orderNo;直接生成时为零金额台账草稿,价格由采购员补录", "upstream": [ "生产缺口(人工)" ], "downstream": [ "采购订单" "询价比价(可选)", "采购台账" ], "keywords": "采购申请 请购 request" }, @@ -240,7 +242,7 @@ "name": "采购订单", "module": "进销存", "table": "erp_purchase_order", "doc": "菜单名「采购台账」,CGDD编号,BPM 审批(草稿0→审批中15→已审核20),入库状态:未0部分1全部2;表头含采购类型(生产/办公用品/其他)、合同号(手工输入不自动生成)、项目名称、付款日期(必填,超30天账期标红)、付款方式(电汇/汇票/现金/手机支付);明细产品编号快照 MDM 物料编码;来源可为采购申请、询价比价确认生成的草稿或销售订单/生产工单(来源单号回溯);单价为含税单价,价税合计=单价×数量,税额从含税金额中反算(不再额外加税)", "doc": "菜单名「采购台账」,CGDD编号,BPM 审批(草稿0→审批中15→已审核20),入库状态:未0部分1全部2;表头含采购类型(生产/办公用品/其他)、合同号(手工输入不自动生成)、项目名称、付款日期(必填,超30天账期标红)、付款方式(电汇/汇票/现金/手机支付);明细产品编号快照 MDM 物料编码;来源可为采购申请、询价比价确认生成的草稿或销售订单/生产工单(来源单号回溯);单价为含税单价,价税合计=单价×数量,税额从含税金额中反算(不再额外加税);采购申请直接生成的草稿为零金额且无供应商,提交审批前必须补齐供应商、付款日期与明细单价(后端逐项校验并报具体缺口)", "upstream": [ "采购申请", "询价比价(确认中标)" @@ -497,7 +499,7 @@ "name": "到货通知单", "module": "仓储", "table": "mes_wm_arrival_notice", "doc": "采购订单确认收货生成,草稿0→待质检2→待入库3→完成4;表头带采购合同号(选择采购订单自动带入,换货到货沿用原退货单合同号),可按合同号检索", "doc": "采购订单确认收货生成,草稿0→待质检2→待入库3→完成4;无需检验或 IQC 完成后自动生成采购入库单;表头带采购合同号(选择采购订单自动带入,换货到货沿用原退货单合同号),可按合同号检索", "upstream": [ "采购订单", "供应商退货(换货)" @@ -512,7 +514,7 @@ "name": "采购入库单", "module": "仓储", "table": "mes_wm_item_receipt", "doc": "草稿0→待上架2→待执行3→完成4,完成后库存+并回写 ERP inCount/inStatus", "doc": "到货通知提交或 IQC 合格后自动生成(待入库0),库管核对数量并选择仓库/库区/库位后一步「确认入库」(完成4),完成后库存+并回写 ERP inCount/inStatus;旧流程的待上架2/待执行3 单据仍可确认入库", "upstream": [ "到货通知" ], docs/project-business/index.html
@@ -1544,7 +1544,7 @@ "name": "到货通知单", "module": "仓储", "table": "mes_wm_arrival_notice", "doc": "采购订单确认收货生成,草稿0→待质检2→待入库3→完成4;表头带采购合同号(选择采购订单自动带入,换货到货沿用原退货单合同号),可按合同号检索", "doc": "采购订单确认收货生成,草稿0→待质检2→待入库3→完成4;无需检验或 IQC 完成后自动生成采购入库单;表头带采购合同号(选择采购订单自动带入,换货到货沿用原退货单合同号),可按合同号检索", "upstream": [ "采购订单", "供应商退货(换货)" @@ -1559,7 +1559,7 @@ "name": "采购入库单", "module": "仓储", "table": "mes_wm_item_receipt", "doc": "草稿0→待上架2→待执行3→完成4,完成后库存+并回写 ERP inCount/inStatus", "doc": "到货通知提交或 IQC 合格后自动生成(待入库0),库管核对数量并选择仓库/库区/库位后一步「确认入库」(完成4),完成后库存+并回写 ERP inCount/inStatus;旧流程的待上架2/待执行3 单据仍可确认入库", "upstream": [ "到货通知" ], @@ -2032,7 +2032,7 @@ }, { "stage": "收货", "action": "确认收货生成到货通知(0→待质检2/待入库3→4);需检物料自动创建 IQC", "action": "确认收货生成到货通知(0→待质检2/待入库3→4);需检物料由库管在到货通知点「执行质检」才生成 IQC,不点则不生成、也不能入库;无需检验的提交即自动生成采购入库单", "roles": [ "仓管", "IQC 质检员" @@ -2040,7 +2040,7 @@ }, { "stage": "入库", "action": "IQC 合格/特采→采购入库(待上架→待执行→完成)库存+,回写订单 inCount/inStatus 与申请明细", "action": "IQC 合格/特采→系统自动生成采购入库单(待入库0)→库管核对数量、选入库仓库/库区/库位后点「确认入库」一步完成(→完成4),库存+,回写订单 inCount/inStatus 与申请明细;不再有提交/执行上架/到货通知执行入库步骤", "roles": [ "仓管" ] @@ -2855,10 +2855,12 @@ C4 --> C3 C3 -->|确认收货| C5[到货通知单·草稿0→待质检2→待入库3→完成4] C5 -->|物料需检验·自动创建| C6[IQC 来料检验] C6 -->|合格1或特采2 可入库| C7[采购入库单·草稿0→待上架2→待执行3→完成4] C5 -.->|无需检验·提交即自动生成入库单| C7 C6 -->|合格1或特采2·全部需检行完成后自动生成入库单| C7[采购入库单·待入库0→完成4·库管核对数量后一步确认入库] C6 -->|不合格退货3| C8[供应商退货·草稿→审批中(BPM 采购→质量→仓库)→待拣货→待执行→完成·库存扣减;退款→负数冲红付款单;换货→到货通知+IQC 重新入库] C6 -->|不合格报废4| C9[NCR 不合格品处置] C7 -->|回写 inCount 与 inStatus 未入库0部分1全部2| C3 C7 -.->|确认入库时选整单入库仓库/库区/库位·自动按行生成入库明细与批次·不再执行上架| C7NOTE[库存事务·库存+·到货通知完成] C7 -->|回写申请明细 inCount| C1 C3 -->|订单总价控制| C10[采购来票 FPD·累计金额≤订单总价] C10 -->|付款单必须关联来票| C11[付款单 FKD·审批中15→已审核20·累计付款≤来票金额] docs/project-business/mermaid/03-purchase-flow.mmd
@@ -1,21 +1,32 @@ flowchart TD S1[供应商档案 srm_supplier·多联系人/多银行含银行行号] --> S2[询价比价 srm_inquiry·草稿0→已转订单20] S2 -->|新增时一并录入产品×供应商报价并选定中标·列表直接确认| S3[按中标供应商拆分生成采购订单草稿] S3 -->|提交| C3 C1[采购申请 CGSQ·草稿0→审批中10] -->|提交BPM| C2{审批} S1[供应商档案 srm_supplier·多联系人/多银行含银行行号] --> S2[询价比价 srm_inquiry·草稿0→询价中10→审核中40→审核通过50→已转订单20/已取消30] C1[采购申请 CGSQ·只记产品/数量/需求日期·不记价格·不填供应商·不填是否质检·草稿0→审批中10] -->|提交BPM| C2{审批} C2 -->|驳回30→可重置回草稿| C1 C2 -->|通过20且有供应商·自动生成| C3[采购台账 CGDD·采购类型/合同号/项目/付款日期/付款方式·审批中15→已审核20] C2 -->|通过20无供应商| C4[手动指定供应商生成订单] C2 -->|通过20·仅改状态不生成台账| C2A{采购员在“询价比价”页的“采购申请待处理”页签(带角标·有待处理时默认展示)中决定} C2A -->|需要询价比价·发起询价| S2 S2 -->|按申请明细生成询价单草稿·回写申请 inquiryId/inquiryNo| S2 S2 -->|补充供应商×产品报价并选定中标·提交审核| S2B{询价审核} S2B -->|通过50·确认转订单| C3 S2B -->|驳回60→可改后重提| S2 S2 -->|取消/删除·清空申请询价关联| C2A C2A -->|不需要比价·生成台账| C4[直接生成零金额·无供应商台账草稿] C4 --> C3 C3[采购台账 CGDD·价格来自中标报价或采购员补录·采购类型/合同号/项目/付款日期/付款方式·草稿0→审批中15→已审核20] C3 -->|确认收货| C5[到货通知单·草稿0→待质检2→待入库3→完成4] C5 -->|物料需检验·自动创建| C6[IQC 来料检验] C6 -->|合格1或特采2 可入库| C7[采购入库单·草稿0→待上架2→待执行3→完成4] C5 -->|物料需检验·库管点「执行质检」才创建| C6[IQC 来料检验] C5 -.->|不点执行质检则不生成 IQC·单据停在待质检·不能入库| C6 C5 -.->|无需检验·提交即自动生成入库单| C7 C6 -->|合格1或特采2·全部需检行完成后自动生成入库单| C7[采购入库单·待入库0→完成4·库管核对数量后一步确认入库] C6 -->|不合格退货3| C8[供应商退货·草稿→审批中(BPM 采购→质量→仓库)→待拣货→待执行→完成·库存扣减;退款→负数冲红付款单;换货→到货通知+IQC 重新入库] C6 -->|不合格报废4| C9[NCR 不合格品处置] C7 -->|回写 inCount 与 inStatus 未入库0部分1全部2| C3 C7 -.->|确认入库时选整单入库仓库/库区/库位·自动按行生成入库明细与批次·不再执行上架| C7NOTE[库存事务·库存+·到货通知完成] C7 -->|回写申请明细 inCount| C1 C3 -->|订单总价控制| C10[采购来票 FPD·累计金额≤订单总价] C10 -->|付款单必须关联来票| C11[付款单 FKD·审批中15→已审核20·累计付款≤来票金额] C3 --> S6[供应商往来·订单流水含已开票/未开票] C10 --> S6 C11 --> S6 C2 -.->|任务分配到审批人即发站内信·铃铛点击直达待办任务| N1[BPM 待办通知 bpm_task_assigned·审批中心·所有流程通用·任务自动通过/已结束不发] S2B -.->|同上| N1 C3 -.->|同上| N1 docs/project-business/mermaid/05-quality-flow.mmd
@@ -1,5 +1,6 @@ flowchart TD T1[到货通知单 或 外协入库单] --> A1[IQC 来料检验·草稿0→完成4] T1 -.->|到货通知:库管点「执行质检」才生成 IQC·不点不生成也不能入库| A1 T2[报工后产出行·由质量部门手工新建 IPQC] --> A2[IPQC 过程检验·草稿0→完成4] T2 -.->|新建时绑定生产工单·工序由工单工艺路线带出·半成品由该工序产出物料带出·盘号手工录入| A2 A2 -.->|新建/编辑·选工位按工序带出该工序维护的自定义工资字段·数值/文本/下拉/布尔控件按字段类型决定| QWF[工资字段填写·由质检填写·报工审批重算工资优先取此值·未填回退工序固定值] yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/arrivalnotice/MesWmArrivalNoticeController.java
@@ -114,6 +114,14 @@ return success(true); } @PostMapping("/create-iqc") @Operation(summary = "执行质检:为到货通知单的需检行生成来料检验单") @Parameter(name = "id", description = "到货通知单编号", required = true) @PreAuthorize("@ss.hasPermission('mes:wm-arrival-notice:update')") public CommonResult<Integer> createArrivalNoticeIqc(@RequestParam("id") Long id) { return success(arrivalNoticeService.createIqcForArrivalNotice(id)); } @PostMapping("/generate-from-purchase-order") @Operation(summary = "根据采购订单生成到货通知单") @Parameter(name = "purchaseOrderId", description = "采购订单编号", required = true) yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/itemreceipt/MesWmItemReceiptController.java
@@ -127,14 +127,30 @@ } @PutMapping("/finish") @Operation(summary = "执行入库") @Operation(summary = "执行入库(旧流程,已由确认入库取代)") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('mes:wm-item-receipt:finish')") @Deprecated public CommonResult<Boolean> finishItemReceipt(@RequestParam("id") Long id) { itemReceiptService.finishItemReceipt(id); return success(true); } @PutMapping("/confirm") @Operation(summary = "确认入库:核对数量后一步完成入库") @Parameter(name = "id", description = "采购入库单编号", required = true) @Parameter(name = "warehouseId", description = "入库仓库编号", required = true) @Parameter(name = "locationId", description = "入库库区编号", required = true) @Parameter(name = "areaId", description = "入库库位编号", required = true) @PreAuthorize("@ss.hasPermission('mes:wm-item-receipt:finish')") public CommonResult<Boolean> confirmItemReceipt(@RequestParam("id") Long id, @RequestParam(value = "warehouseId", required = false) Long warehouseId, @RequestParam(value = "locationId", required = false) Long locationId, @RequestParam(value = "areaId", required = false) Long areaId) { itemReceiptService.confirmItemReceipt(id, warehouseId, locationId, areaId); return success(true); } @PutMapping("/cancel") @Operation(summary = "取消采购入库单") @Parameter(name = "id", description = "编号", required = true) yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -590,6 +590,9 @@ ErrorCode WM_ARRIVAL_NOTICE_NO_LINE = new ErrorCode(1_040_704_006, "至少需要一条行项目"); ErrorCode WM_ARRIVAL_NOTICE_VENDOR_MISMATCH = new ErrorCode(1_040_704_007, "到货通知单的供应商与当前单据不一致"); ErrorCode WM_ARRIVAL_NOTICE_ALREADY_EXISTS = new ErrorCode(1_040_704_008, "该采购订单已生成到货通知单,不能重复生成"); ErrorCode WM_ARRIVAL_NOTICE_NO_IQC_LINE = new ErrorCode(1_040_704_009, "到货通知单没有需要检验的明细行,无需生成来料检验单"); ErrorCode WM_ARRIVAL_NOTICE_IQC_ALREADY_CREATED = new ErrorCode(1_040_704_010, "到货通知单的需检行已全部生成来料检验单"); ErrorCode WM_ARRIVAL_NOTICE_IQC_CREATE_FAILED = new ErrorCode(1_040_704_011, "生成来料检验单失败"); ErrorCode WM_ARRIVAL_NOTICE_LINE_NOT_EXISTS = new ErrorCode(1_040_704_100, "到货通知单行不存在"); ErrorCode WM_ARRIVAL_NOTICE_LINE_NOT_MATCH = new ErrorCode(1_040_704_101, "到货通知单行不属于指定的到货通知单"); @@ -613,6 +616,12 @@ ErrorCode WM_ITEM_RECEIPT_DETAIL_NOT_EXISTS = new ErrorCode(1_040_705_200, "采购入库明细不存在"); ErrorCode WM_ITEM_RECEIPT_DETAIL_QUANTITY_EXCEED = new ErrorCode(1_040_705_202, "上架明细总数量不能超过行入库数量"); ErrorCode WM_ITEM_RECEIPT_BATCH_REQUIRED = new ErrorCode(1_040_705_203, "物料【{}-{}】已开启批次管理,上架明细的批次号不能为空"); ErrorCode WM_ITEM_RECEIPT_CONFIRM_STATUS_ERROR = new ErrorCode(1_040_705_009, "采购入库单当前状态不允许确认入库"); ErrorCode WM_ITEM_RECEIPT_CONFIRM_LOCATION_REQUIRED = new ErrorCode(1_040_705_010, "确认入库必须选择入库仓库、库区和库位"); ErrorCode WM_ITEM_RECEIPT_CONFIRM_LOCATION_MISMATCH = new ErrorCode(1_040_705_011, "入库仓库、库区、库位的层级不匹配"); ErrorCode WM_ITEM_RECEIPT_CONFIRM_QUANTITY_INVALID = new ErrorCode(1_040_705_013, "入库数量必须大于 0"); ErrorCode WM_ITEM_RECEIPT_CONFIRM_BATCH_GENERATE_FAILED = new ErrorCode(1_040_705_014, "生成物料批次失败"); ErrorCode WM_ITEM_RECEIPT_CONFIRM_DETAIL_MISSING = new ErrorCode(1_040_705_015, "旧流程的待执行入库单缺少上架明细"); // ========== MES 仓库管理-领料申请单(1-040-706-000) ========== ErrorCode WM_MATERIAL_REQUEST_NOT_EXISTS = new ErrorCode(1_040_706_000, "领料申请单不存在"); yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/arrivalnotice/MesWmArrivalNoticeService.java
@@ -64,6 +64,14 @@ void submitArrivalNotice(Long id); /** * 执行质检:为待质检的到货通知单的需检行生成来料检验单(IQC) * * @param id 到货通知单编号 * @return 本次新建的来料检验单数量 */ Integer createIqcForArrivalNotice(Long id); /** * IQC 检验完成后回写到货通知单(更新行 + 尝试推进主表状态) * * @param id 到货通知单编号 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/arrivalnotice/MesWmArrivalNoticeServiceImpl.java
@@ -2,6 +2,7 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjUtil; 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; @@ -22,6 +23,7 @@ import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService; import cn.iocoder.yudao.module.mes.service.qc.iqc.MesQcIqcService; import cn.iocoder.yudao.module.mes.enums.qc.MesQcCheckResultEnum; import cn.iocoder.yudao.module.mes.service.wm.itemreceipt.MesWmItemReceiptService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Lazy; @@ -30,6 +32,7 @@ import org.springframework.validation.annotation.Validated; import java.math.BigDecimal; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -73,6 +76,10 @@ @Resource @Lazy private MesQcIqcService iqcService; @Resource @Lazy private MesWmItemReceiptService itemReceiptService; @Override public Long createArrivalNotice(MesWmArrivalNoticeSaveReqVO createReqVO) { @@ -161,34 +168,72 @@ } // 2. 检查所有行的 iqcCheckFlag:如果有需要检验的行,进入待质检状态 // 注意:来料检验单不在提交时生成,由库管点击「执行质检」时通过 createIqcForArrivalNotice 生成 boolean needCheck = CollectionUtils.anyMatch(lines, line -> Boolean.TRUE.equals(line.getIqcCheckFlag())); if (needCheck) { // 2.1 需要检验,进入待质检 arrivalNoticeMapper.updateById(new MesWmArrivalNoticeDO() .setId(id).setStatus(MesWmArrivalNoticeStatusEnum.PENDING_QC.getStatus())); // 2.2 为需要质检的行自动创建 IQC 检验单 for (MesWmArrivalNoticeLineDO line : lines) { if (Boolean.TRUE.equals(line.getIqcCheckFlag()) && line.getIqcId() == null) { try { Long iqcId = iqcService.createIqcFromArrivalNotice( id, line, notice.getCode(), notice.getVendorId()); // 更新行上的 iqcId arrivalNoticeLineService.updateArrivalNoticeLineIqcId(line.getId(), iqcId); log.info("[submitArrivalNotice][到货通知单({}) 行({}) 自动创建 IQC 检验单({})]", id, line.getId(), iqcId); } catch (Exception e) { log.warn("[submitArrivalNotice][到货通知单({}) 行({}) 创建 IQC 失败: {}]", id, line.getId(), e.getMessage()); // 继续处理其他行,不中断流程 } } } return; } // 不需要检验,直接进入待入库 // 不需要检验,直接进入待入库,并自动生成采购入库单交给库管确认入库 arrivalNoticeMapper.updateById(new MesWmArrivalNoticeDO() .setId(id).setStatus(MesWmArrivalNoticeStatusEnum.PENDING_RECEIPT.getStatus())); autoGenerateItemReceipt(notice); } @Override @Transactional(rollbackFor = Exception.class) public Integer createIqcForArrivalNotice(Long id) { // 1. 校验到货通知单存在且为待质检状态 MesWmArrivalNoticeDO notice = validateArrivalNoticeExists(id); if (ObjUtil.notEqual(MesWmArrivalNoticeStatusEnum.PENDING_QC.getStatus(), notice.getStatus())) { String statusName = Arrays.stream(MesWmArrivalNoticeStatusEnum.values()) .filter(item -> ObjUtil.equal(item.getStatus(), notice.getStatus())) .map(MesWmArrivalNoticeStatusEnum::getName).findFirst() .orElse(String.valueOf(notice.getStatus())); throw new ServiceException(WM_ARRIVAL_NOTICE_STATUS_NOT_PENDING_QC.getCode(), "到货通知单[" + notice.getCode() + "]当前状态为[" + statusName + "],只有「待质检」状态才能执行质检。" + "请刷新列表确认单据状态;若该单据已执行过质检,请直接到【质量管理 - IQC检验】录入检测结果。"); } // 2. 校验需检行 List<MesWmArrivalNoticeLineDO> lines = arrivalNoticeLineService.getArrivalNoticeLineListByNoticeId(id); List<MesWmArrivalNoticeLineDO> needCheckLines = lines.stream() .filter(line -> Boolean.TRUE.equals(line.getIqcCheckFlag())) .toList(); if (needCheckLines.isEmpty()) { throw new ServiceException(WM_ARRIVAL_NOTICE_NO_IQC_LINE.getCode(), "到货通知单[" + notice.getCode() + "]的明细行均为「不需检验」,无需生成来料检验单。" + "请直接到该单据执行入库;若本应检验,请核对采购订单行的检验标识(该标识由采购订单带入,到货通知单上不可修改)。"); } List<MesWmArrivalNoticeLineDO> pendingLines = needCheckLines.stream() .filter(line -> line.getIqcId() == null) .toList(); if (pendingLines.isEmpty()) { throw new ServiceException(WM_ARRIVAL_NOTICE_IQC_ALREADY_CREATED.getCode(), "到货通知单[" + notice.getCode() + "]的 " + needCheckLines.size() + " 条需检行均已生成来料检验单,无需重复生成。请直接到【质量管理 - IQC检验】录入检测结果;" + "如需重新生成,请先删除原检验单。"); } // 3. 逐行生成来料检验单 int createdCount = 0; for (MesWmArrivalNoticeLineDO line : pendingLines) { Long iqcId; try { iqcId = iqcService.createIqcFromArrivalNotice(id, line, notice.getCode(), notice.getVendorId()); } catch (Exception ex) { log.error("[createIqcForArrivalNotice][到货通知单({}) 行({}) 创建 IQC 失败]", id, line.getId(), ex); throw new ServiceException(WM_ARRIVAL_NOTICE_IQC_CREATE_FAILED.getCode(), "到货通知单[" + notice.getCode() + "]的明细行(物料ID=" + line.getItemId() + ")生成来料检验单失败:" + ex.getMessage() + "。请稍后重试;若持续失败,请到【质量管理 - IQC检验】手工新建检验单。"); } arrivalNoticeLineService.updateArrivalNoticeLineIqcId(line.getId(), iqcId); createdCount++; log.info("[createIqcForArrivalNotice][到货通知单({}) 行({}) 生成 IQC 检验单({})]", id, line.getId(), iqcId); } return createdCount; } @Override @@ -206,6 +251,47 @@ // 同时认可仓库已确认的来料退货;保留原始不合格结论。 arrivalDispositionService.refresh(id); // 3.1 判断是否所有需检行都已完成质检 List<MesWmArrivalNoticeLineDO> lines = arrivalNoticeLineService.getArrivalNoticeLineListByNoticeId(id); boolean hasUnchecked = CollectionUtils.anyMatch(lines, line -> Boolean.TRUE.equals(line.getIqcCheckFlag()) && line.getIqcCheckResult() == null); if (hasUnchecked) { log.info("[updateArrivalNoticeWhenIqcFinish][到货通知单({}) 还有未完成检验的行,暂不更新状态]", id); return; } // 3.2 判断是否所有需检行都可以入库(合格或特采) boolean hasNonInbound = CollectionUtils.anyMatch(lines, line -> Boolean.TRUE.equals(line.getIqcCheckFlag()) && !MesQcCheckResultEnum.canInbound(line.getIqcCheckResult())); if (hasNonInbound) { log.info("[updateArrivalNoticeWhenIqcFinish][到货通知单({}) 存在不可入库的行(退货/报废),状态保持待质检]", id); return; } // 3.3 所有行检验完成且可入库,更新主表状态 PENDING_QC → PENDING_RECEIPT arrivalNoticeMapper.updateById(new MesWmArrivalNoticeDO() .setId(id).setStatus(MesWmArrivalNoticeStatusEnum.PENDING_RECEIPT.getStatus())); // 3.4 质检合格后直接生成采购入库单(待入库),库管只需核对数量并确认入库 autoGenerateItemReceipt(notice); } /** * 到货通知单进入「待入库」后自动生成采购入库单。 * 库管无需再回到到货通知点「执行入库」,直接到【采购入库】核对数量并确认入库即可。 */ private void autoGenerateItemReceipt(MesWmArrivalNoticeDO notice) { try { Long receiptId = itemReceiptService.generateItemReceiptFromArrivalNotice(notice.getId()); log.info("[autoGenerateItemReceipt][到货通知单({}) 已自动生成采购入库单({})]", notice.getId(), receiptId); } catch (ServiceException ex) { log.error("[autoGenerateItemReceipt][到货通知单({}) 自动生成采购入库单失败]", notice.getId(), ex); throw new ServiceException(ex.getCode(), "到货通知单[" + notice.getCode() + "]转入待入库后自动生成采购入库单失败:" + ex.getMessage() + "。本次操作未生效,请稍后重试;若持续失败请联系管理员。"); } } @Override @@ -362,21 +448,11 @@ arrivalNoticeLineService.createArrivalNoticeLineFromPurchaseOrderItem(line); } // 7. 如果有需要质检的行,更新状态为待质检并自动创建 IQC 单 // 7. 如果有需要质检的行,更新状态为待质检 // 注意:来料检验单不在生成时创建,由库管点击「执行质检」时通过 createIqcForArrivalNotice 生成 if (hasNeedQc) { arrivalNoticeMapper.updateById(new MesWmArrivalNoticeDO() .setId(notice.getId()).setStatus(MesWmArrivalNoticeStatusEnum.PENDING_QC.getStatus())); // 为需要质检的行自动创建 IQC 检验单 List<MesWmArrivalNoticeLineDO> lines = arrivalNoticeLineService.getArrivalNoticeLineListByNoticeId(notice.getId()); for (MesWmArrivalNoticeLineDO line : lines) { if (Boolean.TRUE.equals(line.getIqcCheckFlag())) { Long iqcId = iqcService.createIqcFromArrivalNotice( notice.getId(), line, notice.getCode(), notice.getVendorId()); arrivalNoticeLineService.updateArrivalNoticeLineIqcId(line.getId(), iqcId); log.info("[generateArrivalNoticeFromPurchaseOrder][到货通知单({}) 行({}) 自动创建 IQC 检验单({})]", notice.getId(), line.getId(), iqcId); } } } return notice.getId(); yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemreceipt/MesWmItemReceiptLineService.java
@@ -67,6 +67,15 @@ void deleteItemReceiptLineByReceiptId(Long receiptId); /** * 回写入库单行的批次(确认入库时自动生成批次后调用,不校验父单据状态) * * @param id 行编号 * @param batchId 批次编号 * @param batchCode 批次编码 */ void updateItemReceiptLineBatch(Long id, Long batchId, String batchCode); /** * 根据到货通知单行创建入库单行(内部方法,不校验) * * @param line 入库单行 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemreceipt/MesWmItemReceiptLineServiceImpl.java
@@ -134,6 +134,13 @@ } @Override public void updateItemReceiptLineBatch(Long id, Long batchId, String batchCode) { // 内部方法:确认入库时自动生成批次后回写,不做父单据状态校验 itemReceiptLineMapper.updateById(new MesWmItemReceiptLineDO() .setId(id).setBatchId(batchId).setBatchCode(batchCode)); } @Override public void createItemReceiptLineFromArrivalNoticeLine(MesWmItemReceiptLineDO line) { // 内部方法,不做校验,直接插入 itemReceiptLineMapper.insert(line); yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemreceipt/MesWmItemReceiptService.java
@@ -73,9 +73,23 @@ * * @param id 编号 */ @Deprecated void finishItemReceipt(Long id); /** * 确认入库(简化流程):核对数量后一步完成入库 * * 允许从草稿、旧流程的待上架(非审批中)、待执行入库 直接完成: * 逐行补齐批次与上架明细,按所选仓库/库区/库位生成库存事务,并把单据置为已完成。 * * @param id 采购入库单编号 * @param warehouseId 入库仓库编号 * @param locationId 入库库区编号 * @param areaId 入库库位编号 */ void confirmItemReceipt(Long id, Long warehouseId, Long locationId, Long areaId); /** * 取消采购入库单(任意非已完成/已取消状态 → 已取消) * * @param id 编号 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemreceipt/MesWmItemReceiptServiceImpl.java
@@ -4,17 +4,28 @@ import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; 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.mes.controller.admin.wm.batch.vo.MesWmBatchGenerateReqVO; import cn.iocoder.yudao.module.mes.controller.admin.wm.itemreceipt.vo.MesWmItemReceiptPageReqVO; import cn.iocoder.yudao.module.mes.controller.admin.wm.itemreceipt.vo.MesWmItemReceiptSaveReqVO; import cn.iocoder.yudao.module.mes.controller.admin.wm.itemreceipt.vo.detail.MesWmItemReceiptDetailSaveReqVO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.arrivalnotice.MesWmArrivalNoticeDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemreceipt.MesWmItemReceiptDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemreceipt.MesWmItemReceiptDetailDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemreceipt.MesWmItemReceiptLineDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseAreaDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO; import cn.iocoder.yudao.module.mes.dal.mysql.wm.itemreceipt.MesWmItemReceiptMapper; import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService; import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService; import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseLocationService; import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService; import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants; import cn.iocoder.yudao.module.mes.enums.wm.MesWmItemReceiptStatusEnum; import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum; @@ -42,6 +53,7 @@ import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; @@ -94,6 +106,15 @@ @Resource private MdmItemApi mdmItemApi; @Resource private MesWmWarehouseService warehouseService; @Resource private MesWmWarehouseLocationService warehouseLocationService; @Resource private MesWmWarehouseAreaService warehouseAreaService; @Resource private MesWmBatchService batchService; @Override public Long createItemReceipt(MesWmItemReceiptSaveReqVO createReqVO) { @@ -229,6 +250,7 @@ @Override @Transactional(rollbackFor = Exception.class) @Deprecated public void finishItemReceipt(Long id) { // 1. 校验存在 MesWmItemReceiptDO receipt = validateItemReceiptExists(id); @@ -236,23 +258,222 @@ throw exception(WM_ITEM_RECEIPT_STATUS_ERROR); } // 2. 遍历所有明细,创建库存事务(增加库存 + 记录流水) // 2. 完成入库(库存事务 + 状态 + 到货通知 + 采购订单回写) completeItemReceipt(receipt); } @Override @Transactional(rollbackFor = Exception.class) public void confirmItemReceipt(Long id, Long warehouseId, Long locationId, Long areaId) { // 1. 校验单据存在 MesWmItemReceiptDO receipt = validateItemReceiptExists(id); // 2. 校验状态:草稿、旧流程的待上架(且不在 BPM 审批中)、待执行入库 均允许确认 validateItemReceiptConfirmable(receipt); // 3. 校验行 List<MesWmItemReceiptLineDO> lines = itemReceiptLineService.getItemReceiptLineListByReceiptId(id); if (CollUtil.isEmpty(lines)) { throw exception(WM_ITEM_RECEIPT_NO_LINE); } // 4. 校验入库位置(仓库 / 库区 / 库位)的层级关系 validateConfirmLocation(receipt.getCode(), warehouseId, locationId, areaId); // 5. 逐行校验入库数量,并为启用批次管理但还没批次的物料补齐批次 Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap( convertList(lines, MesWmItemReceiptLineDO::getItemId)); for (MesWmItemReceiptLineDO line : lines) { validateConfirmLineQuantity(receipt.getCode(), line, itemMap); fillLineBatch(receipt, line, itemMap); } // 6. 补齐上架明细:没有明细的行按“行入库数量 + 所选位置”生成一条;已有明细的保持原样 createDetailsForConfirm(receipt, lines, warehouseId, locationId, areaId); // 7. 完成入库(库存事务 + 状态 + 到货通知 + 采购订单回写) completeItemReceipt(receipt); } /** * 完成入库:创建库存事务、更新单据与关联单据状态 */ private void completeItemReceipt(MesWmItemReceiptDO receipt) { // 1. 遍历所有明细,创建库存事务(增加库存 + 记录流水) createTransactionList(receipt); // 3. 更新入库单状态 // 2. 更新入库单状态 itemReceiptMapper.updateById(new MesWmItemReceiptDO() .setId(id).setStatus(MesWmItemReceiptStatusEnum.FINISHED.getStatus())); .setId(receipt.getId()).setStatus(MesWmItemReceiptStatusEnum.FINISHED.getStatus())); // 4. 更新关联的到货通知单状态 // 3. 更新关联的到货通知单状态 if (receipt.getNoticeId() != null) { arrivalNoticeService.finishArrivalNotice(receipt.getNoticeId()); } // 5. 回写 ERP 采购订单的入库数量 // 4. 回写 ERP 采购订单的入库数量 updateErpPurchaseOrderInCount(receipt); } /** * 校验采购入库单是否允许确认入库 * * 草稿(新流程)、旧流程的待上架(无 BPM 审批)、待执行入库(旧流程已上架)都允许; * 审批中的单据必须等审批结束,已完成/已取消的不允许。 */ private void validateItemReceiptConfirmable(MesWmItemReceiptDO receipt) { MesWmItemReceiptStatusEnum status = Arrays.stream(MesWmItemReceiptStatusEnum.values()) .filter(item -> ObjUtil.equal(item.getStatus(), receipt.getStatus())) .findFirst().orElse(null); String statusName = status != null ? status.getName() : String.valueOf(receipt.getStatus()); // 1. BPM 审批中的单据不允许直接入库,否则会绕过审批 if (ObjUtil.equal(MesWmItemReceiptStatusEnum.APPROVING.getStatus(), receipt.getStatus()) && StrUtil.isNotBlank(receipt.getProcessInstanceId())) { throw new ServiceException(WM_ITEM_RECEIPT_CONFIRM_STATUS_ERROR.getCode(), "采购入库单[" + receipt.getCode() + "]正在审批中(当前状态:" + statusName + "),不能确认入库。" + "请等待审批通过后再确认入库;若审批已驳回,请按审批意见修改后重新提交。"); } // 2. 已完成 / 已取消不允许重复确认 if (ObjUtil.equal(MesWmItemReceiptStatusEnum.FINISHED.getStatus(), receipt.getStatus()) || ObjUtil.equal(MesWmItemReceiptStatusEnum.CANCELED.getStatus(), receipt.getStatus())) { throw new ServiceException(WM_ITEM_RECEIPT_CONFIRM_STATUS_ERROR.getCode(), "采购入库单[" + receipt.getCode() + "]当前状态为[" + statusName + "],不能确认入库。" + "只有草稿、待上架或待执行入库的单据可以确认入库,请刷新列表确认单据状态。"); } } /** * 校验确认入库时选择的仓库 / 库区 / 库位 */ private void validateConfirmLocation(String receiptCode, Long warehouseId, Long locationId, Long areaId) { if (warehouseId == null || locationId == null || areaId == null) { throw new ServiceException(WM_ITEM_RECEIPT_CONFIRM_LOCATION_REQUIRED.getCode(), "采购入库单[" + receiptCode + "]确认入库前必须选择入库仓库、库区和库位(当前" + "仓库=" + (warehouseId != null ? warehouseId : "未选择") + ",库区=" + (locationId != null ? locationId : "未选择") + ",库位=" + (areaId != null ? areaId : "未选择") + ")。请点击「确认入库」,在弹窗中依次选择仓库、库区、库位后重新提交。"); } MesWmWarehouseDO warehouse = warehouseService.getWarehouse(warehouseId); if (warehouse == null) { throw new ServiceException(WM_ITEM_RECEIPT_CONFIRM_LOCATION_REQUIRED.getCode(), "所选入库仓库(ID=" + warehouseId + ")不存在或已被删除,无法确认入库。" + "请刷新页面后重新选择仓库、库区、库位。"); } MesWmWarehouseLocationDO location = warehouseLocationService.getWarehouseLocation(locationId); if (location == null) { throw new ServiceException(WM_ITEM_RECEIPT_CONFIRM_LOCATION_REQUIRED.getCode(), "所选入库库区(ID=" + locationId + ")不存在或已被删除,无法确认入库。" + "请刷新页面后重新选择仓库、库区、库位。"); } MesWmWarehouseAreaDO area = warehouseAreaService.getWarehouseArea(areaId); if (area == null) { throw new ServiceException(WM_ITEM_RECEIPT_CONFIRM_LOCATION_REQUIRED.getCode(), "所选入库库位(ID=" + areaId + ")不存在或已被删除,无法确认入库。" + "请刷新页面后重新选择仓库、库区、库位。"); } // 仓库 → 库区 → 库位 的层级必须一致 if (ObjUtil.notEqual(location.getWarehouseId(), warehouse.getId())) { MesWmWarehouseDO locationWarehouse = warehouseService.getWarehouse(location.getWarehouseId()); throw new ServiceException(WM_ITEM_RECEIPT_CONFIRM_LOCATION_MISMATCH.getCode(), "入库仓库与库区不匹配:所选仓库[" + warehouse.getName() + "],库区[" + location.getName() + "]属于仓库[" + (locationWarehouse != null ? locationWarehouse.getName() : "ID=" + location.getWarehouseId()) + "]。请重新选择同一仓库下的库区。"); } if (ObjUtil.notEqual(area.getLocationId(), location.getId())) { MesWmWarehouseLocationDO areaLocation = warehouseLocationService.getWarehouseLocation(area.getLocationId()); throw new ServiceException(WM_ITEM_RECEIPT_CONFIRM_LOCATION_MISMATCH.getCode(), "入库库区与库位不匹配:所选库区[" + location.getName() + "],库位[" + area.getName() + "]属于库区[" + (areaLocation != null ? areaLocation.getName() : "ID=" + area.getLocationId()) + "]。请重新选择同一库区下的库位。"); } } /** * 校验单行入库数量 */ private void validateConfirmLineQuantity(String receiptCode, MesWmItemReceiptLineDO line, Map<Long, MdmItemRespDTO> itemMap) { if (line.getReceivedQuantity() != null && line.getReceivedQuantity().compareTo(BigDecimal.ZERO) > 0) { return; } MdmItemRespDTO item = itemMap.get(line.getItemId()); String itemText = item != null ? item.getCode() + " " + item.getName() : "ID=" + line.getItemId(); throw new ServiceException(WM_ITEM_RECEIPT_CONFIRM_QUANTITY_INVALID.getCode(), "采购入库单[" + receiptCode + "]的物料行[" + itemText + "]入库数量为[" + (line.getReceivedQuantity() != null ? line.getReceivedQuantity().toPlainString() : "空") + "],必须大于 0 才能入库。请编辑该物料行,核对并填写实际收货数量后重试。"); } /** * 为启用批次管理但还没有批次的物料行补齐批次 * * 由到货通知单生成的入库行不会预先生成批次,这里在确认入库时统一生成, * 避免库管逐行打开物料行保存一次。 */ private void fillLineBatch(MesWmItemReceiptDO receipt, MesWmItemReceiptLineDO line, Map<Long, MdmItemRespDTO> itemMap) { MdmItemRespDTO item = itemMap.get(line.getItemId()); if (item == null || !Boolean.TRUE.equals(item.getIsBatchManaged()) || line.getBatchId() != null) { return; } MesWmBatchGenerateReqVO batchReqVO = new MesWmBatchGenerateReqVO(); batchReqVO.setItemId(line.getItemId()).setProduceDate(line.getProductionDate()) .setExpireDate(line.getExpireDate()).setLotNumber(line.getLotNumber()) .setVendorId(receipt.getVendorId()).setReceiptDate(receipt.getReceiptDate()) .setPurchaseOrderCode(receipt.getPurchaseOrderCode()); MesWmBatchDO batch; try { batch = batchService.getOrGenerateBatchCode(batchReqVO); } catch (ServiceException ex) { // 批次配置要求的字段(生产日期、有效期、生产批号等)没有填:把行上下文补进提示 throw new ServiceException(WM_ITEM_RECEIPT_CONFIRM_BATCH_GENERATE_FAILED.getCode(), "物料[" + item.getCode() + " " + item.getName() + "]生成批次失败:" + ex.getMessage() + "。请编辑采购入库单里该物料的物料行,补充批次要求的字段(生产日期、有效期、生产批号)后重试。"); } if (batch == null) { return; } line.setBatchId(batch.getId()).setBatchCode(batch.getCode()); itemReceiptLineService.updateItemReceiptLineBatch(line.getId(), batch.getId(), batch.getCode()); } /** * 确认入库时补齐上架明细:没有明细的行按行入库数量生成一条明细,已有明细的校验数量合计 */ private void createDetailsForConfirm(MesWmItemReceiptDO receipt, List<MesWmItemReceiptLineDO> lines, Long warehouseId, Long locationId, Long areaId) { for (MesWmItemReceiptLineDO line : lines) { List<MesWmItemReceiptDetailDO> details = itemReceiptDetailService .getItemReceiptDetailListByLineId(line.getId()); if (CollUtil.isEmpty(details)) { // 旧流程“待执行入库”的单据应当已经有上架明细,缺了说明数据不完整 if (ObjUtil.equal(MesWmItemReceiptStatusEnum.APPROVED.getStatus(), receipt.getStatus())) { throw new ServiceException(WM_ITEM_RECEIPT_CONFIRM_DETAIL_MISSING.getCode(), "采购入库单[" + receipt.getCode() + "]处于旧流程的「待执行入库」状态,但物料行(ID=" + line.getId() + ")没有上架明细,无法确认入库。" + "请将该单据取消后重新生成,或联系管理员补齐上架明细。"); } MesWmItemReceiptDetailSaveReqVO detailReqVO = new MesWmItemReceiptDetailSaveReqVO(); detailReqVO.setReceiptId(receipt.getId()).setLineId(line.getId()) .setItemId(line.getItemId()).setQuantity(line.getReceivedQuantity()) .setBatchId(line.getBatchId()) .setWarehouseId(warehouseId).setLocationId(locationId).setAreaId(areaId); itemReceiptDetailService.createItemReceiptDetail(detailReqVO); continue; } // 已有上架明细(旧流程):数量合计必须与行入库数量一致 BigDecimal totalDetailQty = CollectionUtils.getSumValue(details, MesWmItemReceiptDetailDO::getQuantity, BigDecimal::add, BigDecimal.ZERO); if (line.getReceivedQuantity() == null || totalDetailQty.compareTo(line.getReceivedQuantity()) != 0) { MdmItemRespDTO item = mdmItemApi.getItem(line.getItemId()).getCheckedData(); String itemText = item != null ? item.getCode() + " " + item.getName() : "ID=" + line.getItemId(); throw new ServiceException(WM_ITEM_RECEIPT_DETAIL_QUANTITY_MISMATCH.getCode(), "采购入库单[" + receipt.getCode() + "]的物料行[" + itemText + "]上架明细数量合计为[" + totalDetailQty.toPlainString() + "],与行入库数量[" + (line.getReceivedQuantity() != null ? line.getReceivedQuantity().toPlainString() : "空") + "]不一致,无法确认入库。请在物料行上核对入库数量,或取消该单据后按简化流程重新生成。"); } } } /** * 回写 ERP 采购订单的入库数量 */ private void updateErpPurchaseOrderInCount(MesWmItemReceiptDO receipt) {