gaoluyang
5 天以前 a0b4de611e3b1986b2713c6a161cc1613ec31a5b
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,20 +258,219 @@
            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() : "空")
                                + "]不一致,无法确认入库。请在物料行上核对入库数量,或取消该单据后按简化流程重新生成。");
            }
        }
    }
    /**
@@ -433,13 +654,19 @@
        // 6. 遍历行创建入库单行
        for (MesWmArrivalNoticeLineDO noticeLine : lines) {
            BigDecimal receivable = noticeLine.getQualifiedQuantity() != null
                    ? noticeLine.getQualifiedQuantity() : noticeLine.getArrivalQuantity();
            // 整行已退供应商时不生成零数量入库行;其余行仍正常收货。
            if (receivable == null || receivable.signum() <= 0) {
                continue;
            }
            MesWmItemReceiptLineDO receiptLine = new MesWmItemReceiptLineDO();
            receiptLine.setReceiptId(receipt.getId());
            receiptLine.setArrivalNoticeLineId(noticeLine.getId());
            receiptLine.setPurchaseOrderItemId(noticeLine.getPurchaseOrderItemId());
            receiptLine.setItemId(noticeLine.getItemId());
            // 使用合格数量(如果有 IQC 则是合格数量,否则是到货数量)
            receiptLine.setReceivedQuantity(noticeLine.getQualifiedQuantity() != null ? noticeLine.getQualifiedQuantity() : noticeLine.getArrivalQuantity());
            receiptLine.setReceivedQuantity(receivable);
            // 按采购订单行单价快照金额,供来票/对账使用
            fillReceiptLinePrice(receiptLine);
            itemReceiptLineService.createItemReceiptLineFromArrivalNoticeLine(receiptLine);