gaoluyang
7 小时以前 cea8396d93b31fe6037a0558febcabb2169363a8
feat(wm): 入库单和领料单支持手工填写关联信息

- 采购入库单新增 notice_code、vendor_name 冗余字段,供应商和到货通知单改为非必填
- 生产领料单新增 work_order_code、task_code、workstation_name 冗余字段,工单、任务、工作站改为非必填
- 查询接口优先使用单据上的手工文本,为空时回退按编号反查,兼容历史数据
- 保存校验逻辑调整:未选择主数据时跳过供应商存在性校验和通知单一致性比对
已修改16个文件
268 ■■■■■ 文件已修改
docs/project-business/data/objects.json 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sql/config_export_all_20260928.sql 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/itemreceipt/MesWmItemReceiptController.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/itemreceipt/vo/MesWmItemReceiptPageReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/itemreceipt/vo/MesWmItemReceiptSaveReqVO.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/productissue/MesWmProductIssueController.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/productissue/vo/MesWmProductIssueRespVO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/productissue/vo/MesWmProductIssueSaveReqVO.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/itemreceipt/MesWmItemReceiptDO.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/productissue/MesWmProductIssueDO.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/itemreceipt/MesWmItemReceiptMapper.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemreceipt/MesWmItemReceiptServiceImpl.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productissue/MesWmProductIssueLineServiceImpl.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productissue/MesWmProductIssueServiceImpl.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/wm/itemreceipt/MesWmItemReceiptServiceImplTest.java 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/test/resources/sql/create_tables.sql 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/project-business/data/objects.json
@@ -26,8 +26,8 @@
    {"name": "报工单", "module": "生产", "table": "mes_pro_feedback", "doc": "草稿0→审批中2→待检3/完成4,按工序顺序校验(上道工序报满才可报),联动倒冲消耗/产出入库/IPQC", "upstream": ["生产任务"], "downstream": ["产品产出", "物料消耗", "IPQC"], "keywords": "报工 feedback"},
    {"name": "工艺路线", "module": "生产", "table": "mes_pro_route", "doc": "工序顺序+质检/关键/倒冲标记,驱动工单任务与报工规则", "upstream": [], "downstream": ["生产工单"], "keywords": "工艺 路线 route 工序"},
    {"name": "到货通知单", "module": "仓储", "table": "mes_wm_arrival_notice", "doc": "采购订单确认收货生成,草稿0→待质检2→待入库3→完成4", "upstream": ["采购订单"], "downstream": ["IQC", "采购入库"], "keywords": "到货 收货 arrival"},
    {"name": "采购入库单", "module": "仓储", "table": "mes_wm_item_receipt", "doc": "草稿0→待上架2→待执行3→完成4,完成后库存+并回写 ERP inCount/inStatus", "upstream": ["到货通知"], "downstream": ["库存台账", "采购订单(回写)"], "keywords": "采购入库 收货入库 receipt"},
    {"name": "生产领料单", "module": "仓储", "table": "mes_wm_product_issue", "doc": "草稿0→待拣货2→待执行3→完成4,库存-", "upstream": ["生产工单"], "downstream": ["库存台账"], "keywords": "领料 issue"},
    {"name": "采购入库单", "module": "仓储", "table": "mes_wm_item_receipt", "doc": "草稿0→待上架2→待执行3→完成4,完成后库存+并回写 ERP inCount/inStatus;北方科技branch:到货通知单号(notice_code)与供应商名称(vendor_name)改为手工填写且非必填,可不依赖前置到货通知单", "upstream": ["到货通知(可选)"], "downstream": ["库存台账", "采购订单(回写)"], "keywords": "采购入库 收货入库 receipt 手工填写 供应商 到货通知单"},
    {"name": "生产领料单", "module": "仓储", "table": "mes_wm_product_issue", "doc": "草稿0→待拣货2→待执行3→完成4,库存-;北方科技branch:生产订单号(work_order_code)、生产任务号(task_code)、工作站名称(workstation_name)改为手工填写且非必填,不再联动带出;无工单ID时跳过BOM防错料校验、不写任务投料记录", "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"},
docs/sql/config_export_all_20260928.sql
@@ -6744,9 +6744,11 @@
  `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '名称',
  `iqc_id` bigint DEFAULT NULL COMMENT '来料检验单编号',
  `notice_id` bigint DEFAULT NULL COMMENT '到货通知单编号',
  `notice_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '到货通知单编码(手工填写/冗余)',
  `purchase_order_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '采购订单号',
  `purchase_order_id` bigint DEFAULT NULL COMMENT '采购订单编号',
  `vendor_id` bigint DEFAULT NULL COMMENT '供应商编号',
  `vendor_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '供应商名称(手工填写/冗余)',
  `receipt_date` datetime DEFAULT NULL COMMENT '退库日期',
  `status` int DEFAULT NULL COMMENT '状态',
  `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
@@ -7129,7 +7131,9 @@
  `code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '单号',
  `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '名称',
  `work_order_id` bigint DEFAULT NULL COMMENT '生产工单 ID',
  `work_order_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '生产工单编号(手工填写/冗余)',
  `task_id` bigint DEFAULT NULL COMMENT '生产任务ID',
  `task_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '生产任务编码(手工填写/冗余)',
  `issue_date` datetime DEFAULT NULL COMMENT '领料日期',
  `required_time` datetime DEFAULT NULL COMMENT '需求时间',
  `status` int DEFAULT NULL COMMENT '状态',
@@ -7142,6 +7146,7 @@
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
  `workstation_id` int DEFAULT NULL,
  `workstation_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '工作站名称(手工填写/冗余)',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='MES 生产领料单';
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/itemreceipt/MesWmItemReceiptController.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.itemreceipt;
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;
@@ -181,12 +182,17 @@
                convertSet(list, MesWmItemReceiptDO::getNoticeId));
        // 2. 构建结果
        return BeanUtils.toBean(list, MesWmItemReceiptRespVO.class, vo -> {
            MapUtils.findAndThen(supplierMap, vo.getVendorId(),
                    supplier -> vo.setVendorName(supplier.getName()));
            // 供应商名称、到货通知单编码以单据上的文本为准(支持手工填写);为空时才回退到按编号反查,兼容历史数据
            if (StrUtil.isEmpty(vo.getVendorName())) {
                MapUtils.findAndThen(supplierMap, vo.getVendorId(),
                        supplier -> vo.setVendorName(supplier.getName()));
            }
            MapUtils.findAndThen(iqcMap, vo.getIqcId(),
                    iqc -> vo.setIqcCode(iqc.getCode()));
            MapUtils.findAndThen(noticeMap, vo.getNoticeId(), notice ->
                    vo.setNoticeCode(notice.getCode()));
            if (StrUtil.isEmpty(vo.getNoticeCode())) {
                MapUtils.findAndThen(noticeMap, vo.getNoticeId(), notice ->
                        vo.setNoticeCode(notice.getCode()));
            }
        });
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/itemreceipt/vo/MesWmItemReceiptPageReqVO.java
@@ -26,6 +26,9 @@
    @Schema(description = "供应商编号", example = "1")
    private Long vendorId;
    @Schema(description = "供应商名称", example = "某供应商")
    private String vendorName;
    @Schema(description = "入库日期")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime[] receiptDate;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/itemreceipt/vo/MesWmItemReceiptSaveReqVO.java
@@ -28,10 +28,15 @@
    @Schema(description = "到货通知单编号", example = "1")
    private Long noticeId;
    @Schema(description = "供应商编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "供应商不能为空")
    @Schema(description = "到货通知单编码", example = "AN2026020001")
    private String noticeCode;
    @Schema(description = "供应商编号", example = "1")
    private Long vendorId;
    @Schema(description = "供应商名称", example = "某供应商")
    private String vendorName;
    @Schema(description = "入库日期", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "入库日期不能为空")
    private LocalDateTime receiptDate;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/productissue/MesWmProductIssueController.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.productissue;
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;
@@ -176,15 +177,25 @@
                convertSet(workOrderMap.values(), MesProWorkOrderDO::getClientId));
        // 2. 构建结果
        return BeanUtils.toBean(list, MesWmProductIssueRespVO.class, vo -> {
            MapUtils.findAndThen(workstationMap, vo.getWorkstationId(),
                    workstation -> vo.setWorkstationName(workstation.getName()));
            MapUtils.findAndThen(taskMap, vo.getTaskId(), task ->
                    vo.setTaskCode(task.getCode()).setTaskName(task.getName()));
            MapUtils.findAndThen(workOrderMap, vo.getWorkOrderId(), workOrder -> {
                vo.setWorkOrderCode(workOrder.getCode());
                MapUtils.findAndThen(clientMap, workOrder.getClientId(), client ->
                        vo.setClientName(client.getName()));
            });
            // 工作站名称以单据上的文本为准(支持手工填写);为空时才按编号反查,兼容历史数据
            if (StrUtil.isEmpty(vo.getWorkstationName())) {
                MapUtils.findAndThen(workstationMap, vo.getWorkstationId(),
                        workstation -> vo.setWorkstationName(workstation.getName()));
            }
            // 生产任务编码同理;taskName 不落库,仍按编号反查
            if (StrUtil.isEmpty(vo.getTaskCode())) {
                MapUtils.findAndThen(taskMap, vo.getTaskId(), task ->
                        vo.setTaskCode(task.getCode()).setTaskName(task.getName()));
            }
            // 生产工单编号同理
            if (StrUtil.isEmpty(vo.getWorkOrderCode())) {
                MapUtils.findAndThen(workOrderMap, vo.getWorkOrderId(), workOrder ->
                        vo.setWorkOrderCode(workOrder.getCode()));
            }
            // 客户名称:仍按 workOrderId → workOrder.clientId 反查;手填单没有工单编号时保持为空
            MapUtils.findAndThen(workOrderMap, vo.getWorkOrderId(), workOrder ->
                    MapUtils.findAndThen(clientMap, workOrder.getClientId(), client ->
                            vo.setClientName(client.getName())));
        });
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/productissue/vo/MesWmProductIssueRespVO.java
@@ -18,10 +18,10 @@
    @Schema(description = "领料单名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "生产领料")
    private String name;
    @Schema(description = "生产工单 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @Schema(description = "生产工单 ID", example = "1")
    private Long workOrderId;
    @Schema(description = "生产工单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "WO20250226001")
    @Schema(description = "生产工单编号", example = "WO20250226001")
    private String workOrderCode;
    @Schema(description = "工作站 ID", example = "1")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/productissue/vo/MesWmProductIssueSaveReqVO.java
@@ -22,16 +22,24 @@
    @NotBlank(message = "领料单名称不能为空")
    private String name;
    @Schema(description = "生产任务 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "生产任务 ID 不能为空")
    @Schema(description = "生产任务 ID", example = "1")
    private Long taskId;
    @Schema(description = "生产任务编码", example = "TASK001")
    private String taskCode;
    @Schema(description = "生产工单 ID", example = "1")
    private Long workOrderId;
    @Schema(description = "生产工单编号", example = "WO20250226001")
    private String workOrderCode;
    @Schema(description = "工作站 ID", example = "1")
    private Long workstationId;
    @Schema(description = "工作站名称", example = "装配工作站")
    private String workstationName;
    @Schema(description = "需求时间", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "需求时间不能为空")
    private LocalDateTime requiredTime;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/itemreceipt/MesWmItemReceiptDO.java
@@ -51,6 +51,12 @@
     */
    private Long noticeId;
    /**
     * 到货通知单编码
     *
     * 冗余自 {@link MesWmArrivalNoticeDO#getCode()},也可以手动填写
     */
    private String noticeCode;
    /**
     * 采购订单号
     *
     * 冗余自 {@link MesWmArrivalNoticeDO#getPurchaseOrderCode()},也可以手动填写
@@ -70,6 +76,12 @@
     */
    private Long vendorId;
    /**
     * 供应商名称
     *
     * 冗余自 ERP 供应商,也可以手动填写
     */
    private String vendorName;
    /**
     * 入库日期
     */
    private LocalDateTime receiptDate;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/productissue/MesWmProductIssueDO.java
@@ -46,18 +46,36 @@
     */
    private Long workstationId;
    /**
     * 工作站名称
     *
     * 冗余自 {@link MesMdWorkstationDO#getName()},也可以手动填写
     */
    private String workstationName;
    /**
     * 生产工单 ID
     *
     * 关联 {@link MesProWorkOrderDO#getId()}
     */
    private Long workOrderId;
    /**
     * 生产工单编号
     *
     * 冗余自 {@link MesProWorkOrderDO#getCode()},也可以手动填写
     */
    private String workOrderCode;
    /**
     * 生产任务 ID
     *
     * 关联 {@link MesProTaskDO#getId()}
     */
    private Long taskId;
    /**
     * 生产任务编码
     *
     * 冗余自 {@link MesProTaskDO#getCode()},也可以手动填写
     */
    private String taskCode;
    /**
     * 领料日期
     */
    private LocalDateTime issueDate;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/itemreceipt/MesWmItemReceiptMapper.java
@@ -20,6 +20,7 @@
                .likeIfPresent(MesWmItemReceiptDO::getCode, reqVO.getCode())
                .likeIfPresent(MesWmItemReceiptDO::getName, reqVO.getName())
                .eqIfPresent(MesWmItemReceiptDO::getVendorId, reqVO.getVendorId())
                .likeIfPresent(MesWmItemReceiptDO::getVendorName, reqVO.getVendorName())
                .betweenIfPresent(MesWmItemReceiptDO::getReceiptDate, reqVO.getReceiptDate())
                .orderByDesc(MesWmItemReceiptDO::getId));
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemreceipt/MesWmItemReceiptServiceImpl.java
@@ -113,12 +113,15 @@
    private void validateItemReceiptSaveData(MesWmItemReceiptSaveReqVO reqVO) {
        // 校验编码唯一
        validateCodeUnique(reqVO.getId(), reqVO.getCode());
        // 校验供应商存在(采购入库使用 ERP 供应商)
        srmSupplierApi.getSupplier(reqVO.getVendorId());
        // 校验供应商存在(采购入库使用 ERP 供应商);供应商已改为可选,允许手工填写名称,此时不校验主数据
        if (reqVO.getVendorId() != null) {
            srmSupplierApi.getSupplier(reqVO.getVendorId());
        }
        // 校验到货通知单存在
        if (reqVO.getNoticeId() != null) {
            MesWmArrivalNoticeDO notice = arrivalNoticeService.validateArrivalNoticeReadyForReceipt(reqVO.getNoticeId());
            if (ObjUtil.notEqual(notice.getVendorId(), reqVO.getVendorId())) {
            // 仅在选择了供应商主数据时才比对与到货通知单的一致性;手填供应商名称时跳过
            if (reqVO.getVendorId() != null && ObjUtil.notEqual(notice.getVendorId(), reqVO.getVendorId())) {
                throw exception(WM_ARRIVAL_NOTICE_VENDOR_MISMATCH);
            }
        }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productissue/MesWmProductIssueLineServiceImpl.java
@@ -141,6 +141,10 @@
    private void validateItemInWorkOrderBom(Long issueId, Long itemId) {
        // 校验领料单存在,并获取工单编号
        MesWmProductIssueDO issue = issueService.validateProductIssueExists(issueId);
        // 未关联工单(工单改为手工填写、未选主数据)时没有 BOM 可校验,直接放行
        if (issue.getWorkOrderId() == null) {
            return;
        }
        // 校验物料是否在工单 BOM 中(防错料)
        List<MesProWorkOrderBomDO> workOrderBoms = workOrderBomService.getWorkOrderBomListByWorkOrderId(issue.getWorkOrderId());
        if (CollUtil.isEmpty(workOrderBoms)) {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productissue/MesWmProductIssueServiceImpl.java
@@ -90,13 +90,16 @@
        // 1. 校验关联数据
        validateProductIssueSaveData(createReqVO);
        // 2. 从 task 自动带入 workOrderId 和 workstationId
        MesProTaskDO task = taskService.validateTaskExists(createReqVO.getTaskId());
        if (createReqVO.getWorkOrderId() == null) {
            createReqVO.setWorkOrderId(task.getWorkOrderId());
        }
        if (createReqVO.getWorkstationId() == null) {
            createReqVO.setWorkstationId(task.getWorkstationId());
        // 2. 从 task 自动带入 workOrderId 和 workstationId;
        //    生产任务改为可选:手填单据不传 taskId,工单与工作站由人工填写,不自动带入
        if (createReqVO.getTaskId() != null) {
            MesProTaskDO task = taskService.validateTaskExists(createReqVO.getTaskId());
            if (createReqVO.getWorkOrderId() == null) {
                createReqVO.setWorkOrderId(task.getWorkOrderId());
            }
            if (createReqVO.getWorkstationId() == null) {
                createReqVO.setWorkstationId(task.getWorkstationId());
            }
        }
        // 3. 插入主表
@@ -114,13 +117,16 @@
        // 1.2 校验关联数据
        validateProductIssueSaveData(updateReqVO);
        // 2. 从 task 自动带入 workOrderId 和 workstationId
        MesProTaskDO task = taskService.validateTaskExists(updateReqVO.getTaskId());
        if (updateReqVO.getWorkOrderId() == null) {
            updateReqVO.setWorkOrderId(task.getWorkOrderId());
        }
        if (updateReqVO.getWorkstationId() == null) {
            updateReqVO.setWorkstationId(task.getWorkstationId());
        // 2. 从 task 自动带入 workOrderId 和 workstationId;
        //    生产任务改为可选:手填单据不传 taskId,工单与工作站由人工填写,不自动带入
        if (updateReqVO.getTaskId() != null) {
            MesProTaskDO task = taskService.validateTaskExists(updateReqVO.getTaskId());
            if (updateReqVO.getWorkOrderId() == null) {
                updateReqVO.setWorkOrderId(task.getWorkOrderId());
            }
            if (updateReqVO.getWorkstationId() == null) {
                updateReqVO.setWorkstationId(task.getWorkstationId());
            }
        }
        // 3. 更新主表
@@ -398,12 +404,17 @@
     */
    private void validateProductIssueSaveData(MesWmProductIssueSaveReqVO reqVO) {
        validateCodeUnique(reqVO.getId(), reqVO.getCode());
        // 校验 task 存在
        MesProTaskDO task = taskService.validateTaskExists(reqVO.getTaskId());
        // 校验 task 关联的工单已确认
        Long workOrderId = reqVO.getWorkOrderId() != null ? reqVO.getWorkOrderId() : task.getWorkOrderId();
        if (workOrderId != null) {
            workOrderService.validateWorkOrderConfirmed(workOrderId);
        // 生产任务改为可选:传了才校验任务存在,并按任务带出的工单校验已确认
        if (reqVO.getTaskId() != null) {
            MesProTaskDO task = taskService.validateTaskExists(reqVO.getTaskId());
            // 校验 task 关联的工单已确认
            Long workOrderId = reqVO.getWorkOrderId() != null ? reqVO.getWorkOrderId() : task.getWorkOrderId();
            if (workOrderId != null) {
                workOrderService.validateWorkOrderConfirmed(workOrderId);
            }
        } else if (reqVO.getWorkOrderId() != null) {
            // 没传 taskId 但传了 workOrderId,仍校验工单已确认
            workOrderService.validateWorkOrderConfirmed(reqVO.getWorkOrderId());
        }
        if (reqVO.getWorkstationId() != null) {
            workstationService.validateWorkstationExistsAndEnable(reqVO.getWorkstationId());
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/wm/itemreceipt/MesWmItemReceiptServiceImplTest.java
@@ -1,13 +1,20 @@
package cn.iocoder.yudao.module.mes.service.wm.itemreceipt;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
import cn.iocoder.yudao.module.erp.api.purchase.ErpPurchaseOrderApi;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mes.controller.admin.wm.itemreceipt.vo.MesWmItemReceiptSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.arrivalnotice.MesWmArrivalNoticeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemreceipt.MesWmItemReceiptDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemreceipt.MesWmItemReceiptDetailDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.itemreceipt.MesWmItemReceiptMapper;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmItemReceiptStatusEnum;
import cn.iocoder.yudao.module.mes.service.qc.iqc.MesQcIqcService;
import cn.iocoder.yudao.module.mes.service.wm.arrivalnotice.MesWmArrivalNoticeLineService;
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.srm.api.supplier.SrmSupplierApi;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
@@ -15,6 +22,7 @@
import jakarta.annotation.Resource;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collections;
@@ -24,6 +32,7 @@
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
@@ -51,6 +60,85 @@
    private MesQcIqcService iqcService;
    @MockitoBean
    private MesWmTransactionService wmTransactionService;
    @MockitoBean
    private MesWmArrivalNoticeLineService arrivalNoticeLineService;
    @MockitoBean
    private SrmSupplierApi srmSupplierApi;
    @MockitoBean
    private BpmProcessInstanceApi processInstanceApi;
    @MockitoBean
    private ErpPurchaseOrderApi purchaseOrderApi;
    @MockitoBean
    private MdmItemApi mdmItemApi;
    // ========== createItemReceipt:供应商 / 到货通知单支持手工填写 ==========
    @Test
    public void testCreateItemReceipt_success_withManualVendorNameAndNoticeCode() {
        // 准备参数:只手工填写供应商名称与到货通知单号,不选供应商主数据、不选到货通知单
        MesWmItemReceiptSaveReqVO reqVO = new MesWmItemReceiptSaveReqVO();
        reqVO.setCode("IR-MANUAL-" + randomLongId());
        reqVO.setName("手工供应商入库单");
        reqVO.setVendorId(null);
        reqVO.setVendorName("某某贸易有限公司");
        reqVO.setNoticeId(null);
        reqVO.setNoticeCode("AN2026020001");
        reqVO.setReceiptDate(LocalDateTime.now());
        // 调用
        Long id = itemReceiptService.createItemReceipt(reqVO);
        // 断言 1:保存成功,手工填写的内容落库
        MesWmItemReceiptDO saved = itemReceiptMapper.selectById(id);
        assertNotNull(saved);
        assertNull(saved.getVendorId());
        assertEquals("某某贸易有限公司", saved.getVendorName());
        assertNull(saved.getNoticeId());
        assertEquals("AN2026020001", saved.getNoticeCode());
        // 断言 2:未选择供应商主数据时不校验供应商
        verify(srmSupplierApi, never()).getSupplier(anyLong());
    }
    @Test
    public void testCreateItemReceipt_success_withNoticeIdButManualVendor() {
        // 准备参数:关联了到货通知单(兼容历史单据),但供应商是手工填写
        Long noticeId = randomLongId();
        when(arrivalNoticeService.validateArrivalNoticeReadyForReceipt(eq(noticeId)))
                .thenReturn(randomPojo(MesWmArrivalNoticeDO.class, o -> o.setVendorId(randomLongId())));
        MesWmItemReceiptSaveReqVO reqVO = new MesWmItemReceiptSaveReqVO();
        reqVO.setCode("IR-MANUAL-" + randomLongId());
        reqVO.setName("手工供应商入库单");
        reqVO.setNoticeId(noticeId);
        reqVO.setVendorId(null);
        reqVO.setVendorName("某某贸易有限公司");
        reqVO.setReceiptDate(LocalDateTime.now());
        // 调用:不应因「供应商与到货通知单不一致」而报错
        Long id = itemReceiptService.createItemReceipt(reqVO);
        // 断言:保存成功
        assertNotNull(itemReceiptMapper.selectById(id));
    }
    @Test
    public void testCreateItemReceipt_success_withVendorId() {
        // 准备参数:仍可选择供应商主数据
        Long vendorId = randomLongId();
        MesWmItemReceiptSaveReqVO reqVO = new MesWmItemReceiptSaveReqVO();
        reqVO.setCode("IR-MANUAL-" + randomLongId());
        reqVO.setName("选择供应商入库单");
        reqVO.setVendorId(vendorId);
        reqVO.setReceiptDate(LocalDateTime.now());
        // 调用
        Long id = itemReceiptService.createItemReceipt(reqVO);
        // 断言 1:保存成功
        assertNotNull(itemReceiptMapper.selectById(id));
        // 断言 2:选择了供应商主数据时仍会校验供应商存在
        verify(srmSupplierApi).getSupplier(eq(vendorId));
    }
    // ========== finishItemReceipt ==========
yudao-module-mes/src/test/resources/sql/create_tables.sql
@@ -543,10 +543,15 @@
    "name" varchar(255) DEFAULT NULL,
    "iqc_id" bigint DEFAULT NULL,
    "notice_id" bigint DEFAULT NULL,
    "notice_code" varchar(64) DEFAULT NULL,
    "vendor_id" bigint DEFAULT NULL,
    "vendor_name" varchar(128) DEFAULT NULL,
    "purchase_order_code" varchar(64) DEFAULT NULL,
    "purchase_order_id" bigint DEFAULT NULL,
    "receipt_date" timestamp DEFAULT NULL,
    "status" tinyint NOT NULL DEFAULT 0,
    "process_instance_id" varchar(64) DEFAULT NULL,
    "process_definition_key" varchar(64) DEFAULT NULL,
    "remark" varchar(500) DEFAULT NULL,
    "creator" varchar(64) DEFAULT '',
    "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,