gaoluyang
9 天以前 8e4eee5ad4d74e1a4e6ab4de3a9c28000700dce0
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productissue/MesWmProductIssueServiceImpl.java
@@ -2,21 +2,37 @@
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.mes.controller.admin.wm.productissue.vo.MesWmProductIssuePageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.productissue.vo.MesWmProductIssueSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.productissue.vo.MesWmProductIssueSupplementReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.productissue.vo.line.MesWmProductIssueLineSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderBomDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskIssueDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productissue.MesWmProductIssueDetailDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productissue.MesWmProductIssueDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productissue.MesWmProductIssueLineDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.task.MesProTaskIssueMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.productissue.MesWmProductIssueMapper;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmProductIssueStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmProductIssueTypeEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmStockReserveBizTypeEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService;
import cn.iocoder.yudao.module.mes.service.pro.task.MesProTaskIssueService;
import cn.iocoder.yudao.module.mes.service.pro.task.MesProTaskService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderBomService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
import cn.iocoder.yudao.module.mes.service.wm.stockreserve.MesWmStockReserveService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
@@ -34,7 +50,9 @@
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Collections;
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.*;
@@ -58,6 +76,16 @@
    @Resource
    private MesProWorkOrderService workOrderService;
    @Resource
    private MesProWorkOrderBomService workOrderBomService;
    @Resource
    private MesProTaskService taskService;
    @Resource
    private MesProTaskIssueService taskIssueService;
    @Resource
    private MesProTaskIssueMapper taskIssueMapper;
    @Resource
    private MesMdItemService mdmItemService;
    @Resource
    private MesWmTransactionService wmTransactionService;
    @Resource
    private MesWmWarehouseService warehouseService;
@@ -67,6 +95,8 @@
    private MesWmWarehouseAreaService areaService;
    @Resource
    private MesWmStockReserveService stockReserveService;
    @Resource
    private MesMdAutoCodeRecordService autoCodeRecordService;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -74,10 +104,159 @@
        // 1. 校验关联数据
        validateProductIssueSaveData(createReqVO);
        // 2. 插入主表
        // 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());
        }
        // 3. 插入主表
        MesWmProductIssueDO issue = BeanUtils.toBean(createReqVO, MesWmProductIssueDO.class);
        issue.setStatus(MesWmProductIssueStatusEnum.PREPARE.getStatus());
        issueMapper.insert(issue);
        return issue.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long generateByWorkOrder(Long workOrderId) {
        // 1. 工单必须已确认:草稿未开工、已完成、已取消都不需要再生成领料任务
        MesProWorkOrderDO workOrder = workOrderService.validateWorkOrderConfirmed(workOrderId);
        // 2. BOM 物料清单为空时无法推算应领数量
        List<MesProWorkOrderBomDO> boms = workOrderBomService.getWorkOrderBomListByWorkOrderId(workOrderId);
        if (CollUtil.isEmpty(boms)) {
            throw new ServiceException(WM_PRODUCT_ISSUE_GENERATE_BOM_EMPTY.getCode(),
                    "生产工单[" + workOrder.getCode() + "]的 BOM 物料清单为空,无法生成领料任务。"
                            + "请先在该工单的 BOM 物料中维护需要领用的原辅料及用量");
        }
        // 3. 统计该工单下已领数量(排除已取消的领料单,避免把作废单据算成已领)
        Map<Long, BigDecimal> issuedQuantityMap = getIssuedQuantityMap(workOrderId);
        BigDecimal planQuantity = workOrder.getQuantity() != null ? workOrder.getQuantity() : BigDecimal.ZERO;
        // 4. 逐物料计算本次应领数量 = BOM 单耗 × 工单计划数量 - 已领数量
        List<MesWmProductIssueLineSaveReqVO> lines = new java.util.ArrayList<>();
        for (MesProWorkOrderBomDO bom : boms) {
            if (bom.getQuantity() == null) {
                continue;
            }
            BigDecimal required = bom.getQuantity().multiply(planQuantity);
            BigDecimal issued = issuedQuantityMap.getOrDefault(bom.getItemId(), BigDecimal.ZERO);
            BigDecimal remain = required.subtract(issued);
            if (remain.compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
            lines.add(new MesWmProductIssueLineSaveReqVO()
                    .setItemId(bom.getItemId()).setQuantity(remain)
                    .setRemark("工单应领 " + required.stripTrailingZeros().toPlainString()
                            + ",已领 " + issued.stripTrailingZeros().toPlainString()
                            + ",本次补足 " + remain.stripTrailingZeros().toPlainString()));
        }
        if (lines.isEmpty()) {
            throw new ServiceException(WM_PRODUCT_ISSUE_GENERATE_ALL_ISSUED.getCode(),
                    "生产工单[" + workOrder.getCode() + "]按 BOM 应领的 " + boms.size() + " 种物料已全部领过,"
                            + "无需重复生成领料任务。如需继续领用,请使用【补领辅料】并填写补领原因");
        }
        // 5. 取该工单下的第一道生产任务,作为领料单的任务与工作站归属
        List<MesProTaskDO> tasks = taskService.getTaskListByWorkOrderIds(Collections.singletonList(workOrderId));
        MesProTaskDO firstTask = CollUtil.isEmpty(tasks) ? null : tasks.get(0);
        Long taskId = firstTask != null ? firstTask.getId() : null;
        Long workstationId = firstTask != null ? firstTask.getWorkstationId() : null;
        // 6. 生成草稿领料出库单
        MesWmProductIssueDO issue = new MesWmProductIssueDO()
                .setCode(autoCodeRecordService.generateAutoCode("WM_PRODUCT_ISSUE_CODE"))
                .setName("领料-" + workOrder.getCode())
                .setIssueType(MesWmProductIssueTypeEnum.NORMAL.getType())
                .setWorkOrderId(workOrder.getId())
                .setTaskId(taskId)
                .setWorkstationId(workstationId)
                .setRequiredTime(LocalDateTime.now())
                .setStatus(MesWmProductIssueStatusEnum.PREPARE.getStatus())
                .setRemark("根据生产工单[" + workOrder.getCode() + "]的 BOM 物料清单自动生成");
        issueMapper.insert(issue);
        // 7. 生成领料行
        for (MesWmProductIssueLineSaveReqVO line : lines) {
            line.setIssueId(issue.getId()).setTaskId(taskId);
            issueLineService.createProductIssueLine(line);
        }
        return issue.getId();
    }
    /**
     * 统计某工单下各物料已领数量(只统计未取消的领料单)
     */
    private Map<Long, BigDecimal> getIssuedQuantityMap(Long workOrderId) {
        List<MesWmProductIssueDO> issues = issueMapper.selectListByWorkOrderId(workOrderId);
        List<Long> issueIds = issues.stream()
                .filter(issue -> ObjUtil.notEqual(issue.getStatus(), MesWmProductIssueStatusEnum.CANCELED.getStatus()))
                .map(MesWmProductIssueDO::getId).toList();
        if (CollUtil.isEmpty(issueIds)) {
            return java.util.Collections.emptyMap();
        }
        List<MesWmProductIssueLineDO> lines = issueLineService.getProductIssueLineListByIssueIds(issueIds);
        Map<Long, BigDecimal> result = new java.util.HashMap<>();
        for (MesWmProductIssueLineDO line : lines) {
            if (line.getQuantity() == null) {
                continue;
            }
            result.merge(line.getItemId(), line.getQuantity(), BigDecimal::add);
        }
        return result;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createSupplementIssue(MesWmProductIssueSupplementReqVO reqVO) {
        // 1. 校验工单已确认(未确认的工单不允许补领,避免补领挂到不存在的生产任务上)
        MesProWorkOrderDO workOrder = workOrderService.validateWorkOrderConfirmed(reqVO.getWorkOrderId());
        // 2.1 补领原因必填
        if (StrUtil.isBlank(reqVO.getReason())) {
            throw exception(WM_PRODUCT_ISSUE_SUPPLEMENT_REASON_REQUIRED);
        }
        // 2.2 物料必须存在且启用
        mdmItemService.validateItemExistsAndEnable(reqVO.getItemId());
        // 3.1 未指定任务时,取该工单下的第一道生产任务
        Long taskId = reqVO.getTaskId();
        Long workstationId = reqVO.getWorkstationId();
        if (taskId == null) {
            List<MesProTaskDO> tasks = taskService.getTaskListByWorkOrderIds(
                    Collections.singletonList(reqVO.getWorkOrderId()));
            MesProTaskDO firstTask = CollUtil.isEmpty(tasks) ? null : tasks.get(0);
            if (firstTask != null) {
                taskId = firstTask.getId();
                if (workstationId == null) {
                    workstationId = firstTask.getWorkstationId();
                }
            }
        } else {
            MesProTaskDO task = taskService.validateTaskExists(taskId);
            if (workstationId == null) {
                workstationId = task.getWorkstationId();
            }
        }
        // 4.1 创建补领主表(草稿,后续走 提交 → 拣货 → 完成)
        MesWmProductIssueDO issue = new MesWmProductIssueDO()
                .setCode(autoCodeRecordService.generateAutoCode("WM_PRODUCT_ISSUE_CODE"))
                .setName("补领-" + workOrder.getCode())
                .setIssueType(MesWmProductIssueTypeEnum.SUPPLEMENT.getType())
                .setSupplementReason(reqVO.getReason())
                .setWorkOrderId(workOrder.getId())
                .setTaskId(taskId)
                .setWorkstationId(workstationId)
                .setRequiredTime(reqVO.getRequiredTime() != null ? reqVO.getRequiredTime() : LocalDateTime.now())
                .setStatus(MesWmProductIssueStatusEnum.PREPARE.getStatus())
                .setRemark(reqVO.getRemark());
        issueMapper.insert(issue);
        // 4.2 创建补领行
        issueLineService.createProductIssueLine(new MesWmProductIssueLineSaveReqVO()
                .setIssueId(issue.getId()).setTaskId(taskId)
                .setItemId(reqVO.getItemId()).setQuantity(reqVO.getQuantity())
                .setRemark("补领原因:" + reqVO.getReason()));
        return issue.getId();
    }
@@ -89,7 +268,16 @@
        // 1.2 校验关联数据
        validateProductIssueSaveData(updateReqVO);
        // 2. 更新主表
        // 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());
        }
        // 3. 更新主表
        MesWmProductIssueDO updateObj = BeanUtils.toBean(updateReqVO, MesWmProductIssueDO.class);
        issueMapper.updateById(updateObj);
    }
@@ -192,7 +380,10 @@
        // 3. 遍历所有明细,创建库存事务(扣减库存 + 记录流水)
        createTransactionList(issue);
        // 4. 更新出库单状态 + 领料日期
        // 4. 自动创建任务投料记录
        createTaskIssueRecords(issue);
        // 5. 更新出库单状态 + 领料日期
        issueMapper.updateById(new MesWmProductIssueDO()
                .setId(id).setStatus(MesWmProductIssueStatusEnum.FINISHED.getStatus())
                .setIssueDate(LocalDateTime.now()));
@@ -211,7 +402,7 @@
            Long outTransactionId = wmTransactionService.createTransaction(new MesWmTransactionSaveReqDTO()
                    .setType(MesWmTransactionTypeEnum.OUT.getType()).setItemId(detail.getItemId())
                    .setQuantity(detail.getQuantity().negate()) // 库存减少
                    .setBatchId(detail.getBatchId()).setBatchCode(detail.getBatchCode())
                    .setBatchId(detail.getBatchId()).setBatchCode(detail.getBatchCode()).setMaterialStockId(detail.getMaterialStockId())
                    .setWarehouseId(detail.getWarehouseId()).setLocationId(detail.getLocationId()).setAreaId(detail.getAreaId())
                    .setBizType(MesBizTypeConstants.WM_ISSUE).setBizId(issue.getId())
                    .setBizCode(issue.getCode()).setBizLineId(detail.getLineId()));
@@ -224,6 +415,64 @@
                    .setBizType(MesBizTypeConstants.WM_ISSUE).setBizId(issue.getId())
                    .setBizCode(issue.getCode()).setBizLineId(detail.getLineId())
                    .setRelatedTransactionId(outTransactionId)); // 关联出库事务
        }
    }
    /**
     * 领料出库完成时,自动创建任务投料记录
     * 按行遍历,每行创建一条 task_issue 记录
     */
    private void createTaskIssueRecords(MesWmProductIssueDO issue) {
        List<MesWmProductIssueLineDO> lines = issueLineService.getProductIssueLineListByIssueId(issue.getId());
        if (CollUtil.isEmpty(lines)) {
            return;
        }
        // 批量查询物料信息(获取 unitMeasureId)
        Map<Long, MesMdItemDO> itemMap = mdmItemService.getItemMap(
                CollectionUtils.convertSet(lines, MesWmProductIssueLineDO::getItemId));
        // 获取明细列表,用于提取 batchCode
        List<MesWmProductIssueDetailDO> details = issueDetailService.getProductIssueDetailListByIssueId(issue.getId());
        Map<Long, List<MesWmProductIssueDetailDO>> detailMapByLineId = CollectionUtils.convertMultiMap(
                details, MesWmProductIssueDetailDO::getLineId);
        for (MesWmProductIssueLineDO line : lines) {
            // 行的 taskId 优先,否则取主表的 taskId
            Long taskId = line.getTaskId() != null ? line.getTaskId() : issue.getTaskId();
            if (taskId == null) {
                continue;
            }
            // 获取物料单位
            MesMdItemDO item = itemMap.get(line.getItemId());
            Long unitMeasureId = item != null ? item.getUnitMeasureId() : null;
            // 获取批次号:取该行第一个明细的 batchCode
            String batchCode = null;
            List<MesWmProductIssueDetailDO> lineDetails = detailMapByLineId.get(line.getId());
            if (CollUtil.isNotEmpty(lineDetails)) {
                MesWmProductIssueDetailDO firstDetail = lineDetails.get(0);
                batchCode = firstDetail.getBatchCode();
            }
            // 创建任务投料记录
            MesProTaskIssueDO taskIssue = MesProTaskIssueDO.builder()
                    .taskId(taskId)
                    .workOrderId(issue.getWorkOrderId())
                    .workstationId(issue.getWorkstationId())
                    .sourceDocType("PRODUCT_ISSUE")
                    .sourceDocId(issue.getId())
                    .sourceLineId(line.getId())
                    .sourceDocCode(issue.getCode())
                    .batchCode(batchCode)
                    .itemId(line.getItemId())
                    .unitMeasureId(unitMeasureId)
                    .issuedQuantity(line.getQuantity())
                    .availableQuantity(line.getQuantity()) // 初始全可用
                    .usedQuantity(BigDecimal.ZERO)
                    .build();
            taskIssueMapper.insert(taskIssue);
        }
    }
@@ -241,6 +490,9 @@
        // 释放库存占用
        stockReserveService.releaseReserve(MesWmStockReserveBizTypeEnum.PRODUCT_ISSUE.getType(), id);
        // 删除通过该领料单创建的任务投料记录
        taskIssueService.deleteTaskIssueBySourceDoc("PRODUCT_ISSUE", id);
        // 取消
        issueMapper.updateById(new MesWmProductIssueDO()
@@ -300,10 +552,29 @@
     */
    private void validateProductIssueSaveData(MesWmProductIssueSaveReqVO reqVO) {
        validateCodeUnique(reqVO.getId(), reqVO.getCode());
        workOrderService.validateWorkOrderConfirmed(reqVO.getWorkOrderId());
        // 领料类型:为空按正常领料处理;补领辅料必须填写补领原因
        if (reqVO.getIssueType() == null) {
            reqVO.setIssueType(MesWmProductIssueTypeEnum.NORMAL.getType());
        }
        if (MesWmProductIssueTypeEnum.SUPPLEMENT.getType().equals(reqVO.getIssueType())
                && StrUtil.isBlank(reqVO.getSupplementReason())) {
            throw exception(WM_PRODUCT_ISSUE_SUPPLEMENT_REASON_REQUIRED);
        }
        // 校验 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.getWorkstationId() != null) {
            workstationService.validateWorkstationExistsAndEnable(reqVO.getWorkstationId());
        }
    }
    @Override
    public List<MesWmProductIssueDO> getFinishedIssuesByWorkOrderId(Long workOrderId) {
        return issueMapper.selectListByWorkOrderIdAndStatus(workOrderId, MesWmProductIssueStatusEnum.FINISHED.getStatus());
    }
}