gaoluyang
9 天以前 8e4eee5ad4d74e1a4e6ab4de3a9c28000700dce0
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productissue/MesWmProductIssueServiceImpl.java
@@ -2,14 +2,20 @@
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;
@@ -18,12 +24,15 @@
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;
@@ -41,6 +50,7 @@
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -66,6 +76,8 @@
    @Resource
    private MesProWorkOrderService workOrderService;
    @Resource
    private MesProWorkOrderBomService workOrderBomService;
    @Resource
    private MesProTaskService taskService;
    @Resource
    private MesProTaskIssueService taskIssueService;
@@ -83,6 +95,8 @@
    private MesWmWarehouseAreaService areaService;
    @Resource
    private MesWmStockReserveService stockReserveService;
    @Resource
    private MesMdAutoCodeRecordService autoCodeRecordService;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -103,6 +117,146 @@
        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();
    }
@@ -248,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()));
@@ -398,6 +552,14 @@
     */
    private void validateProductIssueSaveData(MesWmProductIssueSaveReqVO reqVO) {
        validateCodeUnique(reqVO.getId(), reqVO.getCode());
        // 领料类型:为空按正常领料处理;补领辅料必须填写补领原因
        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 关联的工单已确认
@@ -410,4 +572,9 @@
        }
    }
    @Override
    public List<MesWmProductIssueDO> getFinishedIssuesByWorkOrderId(Long workOrderId) {
        return issueMapper.selectListByWorkOrderIdAndStatus(workOrderId, MesWmProductIssueStatusEnum.FINISHED.getStatus());
    }
}