| | |
| | | import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService; |
| | | import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderProcessService; |
| | | import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductService; |
| | | import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProcessService; |
| | | import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService; |
| | | import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService; |
| | | import cn.iocoder.yudao.module.mes.service.wm.productreceipt.MesWmProductReceiptService; |
| | | import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService; |
| | |
| | | import org.springframework.validation.annotation.Validated; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | import java.util.ArrayList; |
| | | import java.util.Arrays; |
| | | import java.util.Collections; |
| | | import java.util.HashMap; |
| | | 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.*; |
| | |
| | | @Resource |
| | | private MesWmProductProduceLineService productProduceLineService; |
| | | @Resource |
| | | private cn.iocoder.yudao.module.mes.dal.mysql.wm.productproduce.MesWmProductProduceLineMapper produceLineMapper; |
| | | @Resource |
| | | private MesWmProductProduceDetailService productProduceDetailService; |
| | | @Resource |
| | | private MesProWorkOrderService workOrderService; |
| | |
| | | private MesProWorkOrderProcessService workOrderProcessService; |
| | | @Resource |
| | | private MesProRouteProductService routeProductService; |
| | | @Resource |
| | | private MesProRouteProcessService routeProcessService; |
| | | @Resource |
| | | private MesWmProductReceiptService productReceiptService; |
| | | @Resource |
| | |
| | | private MesWmWarehouseLocationService locationService; |
| | | @Resource |
| | | private MesWmWarehouseAreaService areaService; |
| | | @Resource |
| | | private MesMdItemService itemService; |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | |
| | | productProduceMapper.updateById(new MesWmProductProduceDO() |
| | | .setId(id).setStatus(MesWmProductProduceStatusEnum.FINISHED.getStatus())); |
| | | |
| | | // 4. 只有末道工序的合格产出才生成产品入库单,避免中间工序重复生成成品入库 |
| | | MesProWorkOrderProcessDO lastProcess = workOrderProcessService |
| | | .getLastWorkOrderProcessByWorkOrderId(produce.getWorkOrderId()); |
| | | if (lastProcess != null && ObjUtil.equal(lastProcess.getProcessId(), produce.getProcessId())) { |
| | | productReceiptService.generateProductReceipt(produce, lines); |
| | | // 4. 末道工序生成成品入库单;中间工序产出到实际仓时生成半成品入库单,避免「有库存无单据」 |
| | | // 产出到虚拟线边库属于内部在制,不生成入库单 |
| | | boolean lastProcess = isLastProcess(produce.getWorkOrderId(), produce.getProcessId(), produce.getFeedbackId()); |
| | | if (lastProcess || hasRealWarehouseOutput(lines)) { |
| | | productReceiptService.generateProductReceipt(produce, lines, lastProcess); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 判断合格产出是否落在实际仓(非虚拟线边库) |
| | | */ |
| | | private boolean hasRealWarehouseOutput(List<MesWmProductProduceLineDO> lines) { |
| | | MesWmWarehouseDO virtualWarehouse = warehouseService.getWarehouseByCode(MesWmWarehouseDO.WIP_VIRTUAL_WAREHOUSE); |
| | | Long virtualWarehouseId = virtualWarehouse != null ? virtualWarehouse.getId() : null; |
| | | for (MesWmProductProduceLineDO line : lines) { |
| | | if (!ObjUtil.equal(line.getQualityStatus(), MesWmQualityStatusEnum.PASS.getStatus())) { |
| | | continue; |
| | | } |
| | | List<MesWmProductProduceDetailDO> details = productProduceDetailService |
| | | .getProductProduceDetailListByLineId(line.getId()); |
| | | if (CollUtil.isEmpty(details)) { |
| | | continue; |
| | | } |
| | | if (!ObjUtil.equal(details.get(0).getWarehouseId(), virtualWarehouseId)) { |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | private void createTransactionList(MesWmProductProduceDO produce, List<MesWmProductProduceLineDO> lines) { |
| | |
| | | ? feedbackMapper.selectById(produce.getFeedbackId()) : null; |
| | | String bizCode = feedback != null && cn.hutool.core.util.StrUtil.isNotBlank(feedback.getCode()) |
| | | ? feedback.getCode() : "PRODUCE-" + produce.getId(); |
| | | MesWmWarehouseDO virtualWarehouse = warehouseService.getWarehouseByCode(MesWmWarehouseDO.WIP_VIRTUAL_WAREHOUSE); |
| | | Long virtualWarehouseId = virtualWarehouse != null ? virtualWarehouse.getId() : null; |
| | | for (MesWmProductProduceLineDO line : lines) { |
| | | // 不合格品保留产出记录供质检处置,不计入可用库存。 |
| | | if (ObjUtil.equal(line.getQualityStatus(), MesWmQualityStatusEnum.FAIL.getStatus())) { |
| | | continue; |
| | | } |
| | | if (!ObjUtil.equal(line.getQualityStatus(), MesWmQualityStatusEnum.PASS.getStatus())) { |
| | | throw exception(WM_PRODUCT_PRODUCE_STATUS_INVALID); |
| | | } |
| | | // 1. 校验每行明细数量之和等于行数量 |
| | | List<MesWmProductProduceDetailDO> details = productProduceDetailService.getProductProduceDetailListByLineId( |
| | | line.getId()); |
| | |
| | | if (line.getQuantity() != null && totalDetailQty.compareTo(line.getQuantity()) != 0) { |
| | | throw exception(WM_PRODUCT_PRODUCE_DETAIL_QUANTITY_MISMATCH); |
| | | } |
| | | // 2. 按明细创建库存事务(产品产出到线边库) |
| | | // 2. 按明细创建库存事务 |
| | | for (MesWmProductProduceDetailDO detail : details) { |
| | | // 虚拟线边库产出属于车间在制,需立即记账供下道工序倒冲领用; |
| | | // 实际仓库的成品/半成品留到产品入库单「执行入库」时记账,避免未上架就先有库存流水 |
| | | if (!ObjUtil.equal(detail.getWarehouseId(), virtualWarehouseId)) { |
| | | continue; |
| | | } |
| | | var item = itemService.validateItemExists(detail.getItemId()); |
| | | Long stockItemId = item.getMdmItemId() != null ? item.getMdmItemId() : item.getId(); |
| | | wmTransactionService.createTransaction(new MesWmTransactionSaveReqDTO() |
| | | .setType(MesWmTransactionTypeEnum.IN.getType()).setItemId(detail.getItemId()) |
| | | .setType(MesWmTransactionTypeEnum.IN.getType()).setItemId(stockItemId) |
| | | .setQuantity(detail.getQuantity()) |
| | | .setBatchId(detail.getBatchId()).setBatchCode(detail.getBatchCode()) |
| | | .setWarehouseId(detail.getWarehouseId()).setLocationId(detail.getLocationId()).setAreaId(detail.getAreaId()) |
| | |
| | | .build(); |
| | | productProduceMapper.insert(produce); |
| | | |
| | | // 2. 获取或生成批次(审批请求中的批次属性优先) |
| | | // 2. 解析本次报工的产出列表:主产出为报工物料,联产出按工单工序快照顺序并回查路线配比 |
| | | List<OutputPlan> outputs = resolveOutputPlans(feedback, workOrder); |
| | | |
| | | // 3. 根据是否需要检验分支处理 |
| | | if (checkFlag) { |
| | | // 3.1 需要检验:每个产出创建一条「待检验」行,不生成明细 |
| | | for (OutputPlan output : outputs) { |
| | | BatchInfo batch = resolveBatch(feedback, workOrder, output.itemId(), approveReqVO); |
| | | MesWmProductProduceLineDO line = buildProduceLine(produce, feedback, output.itemId(), |
| | | batch.id(), batch.code(), scaleQuantity(feedback.getFeedbackQuantity(), output.ratio()), |
| | | MesWmQualityStatusEnum.PENDING.getStatus()); |
| | | productProduceLineService.createProductProduceLine(line); |
| | | } |
| | | // 注意:checkFlag=true 时不生成明细行,等 IPQC 检验完成后由 splitPendingAndFinishProduce 按质量结果拆行+生成明细 |
| | | } else { |
| | | // 3.2 无需检验:每个产出按合格品/不合格品各生成一行 + 明细 |
| | | BigDecimal qualifiedQty = ObjectUtil.defaultIfNull(feedback.getQualifiedQuantity(), BigDecimal.ZERO); |
| | | BigDecimal unqualifiedQty = ObjectUtil.defaultIfNull(feedback.getUnqualifiedQuantity(), BigDecimal.ZERO); |
| | | for (OutputPlan output : outputs) { |
| | | BigDecimal outputQualifiedQty = scaleQuantity(qualifiedQty, output.ratio()); |
| | | BigDecimal outputUnqualifiedQty = scaleQuantity(unqualifiedQty, output.ratio()); |
| | | BatchInfo batch = resolveBatch(feedback, workOrder, output.itemId(), approveReqVO); |
| | | // 产出仓库:工单工序配置优先,其次该产出物料的路线产品仓库,中间工序可回退虚拟线边库 |
| | | WarehouseTarget outputTarget = resolveOutputTarget(feedback.getWorkOrderId(), |
| | | feedback.getProcessId(), output.itemId(), feedback.getId()); |
| | | // 3.2.1 不合格品行 + 明细 |
| | | if (outputUnqualifiedQty.compareTo(BigDecimal.ZERO) > 0) { |
| | | MesWmProductProduceLineDO unqualifiedLine = buildProduceLine(produce, feedback, output.itemId(), |
| | | batch.id(), batch.code(), outputUnqualifiedQty, MesWmQualityStatusEnum.FAIL.getStatus()); |
| | | productProduceLineService.createProductProduceLine(unqualifiedLine); |
| | | MesWmProductProduceDetailDO unqualifiedDetail = buildProduceDetail(produce, output.itemId(), |
| | | batch.id(), batch.code(), unqualifiedLine.getId(), outputUnqualifiedQty, |
| | | outputTarget.warehouseId(), outputTarget.locationId(), outputTarget.areaId()); |
| | | productProduceDetailService.createProductProduceDetail(unqualifiedDetail); |
| | | } |
| | | // 3.2.2 合格品行 + 明细 |
| | | if (outputQualifiedQty.compareTo(BigDecimal.ZERO) > 0) { |
| | | MesWmProductProduceLineDO qualifiedLine = buildProduceLine(produce, feedback, output.itemId(), |
| | | batch.id(), batch.code(), outputQualifiedQty, MesWmQualityStatusEnum.PASS.getStatus()); |
| | | productProduceLineService.createProductProduceLine(qualifiedLine); |
| | | MesWmProductProduceDetailDO qualifiedDetail = buildProduceDetail(produce, output.itemId(), |
| | | batch.id(), batch.code(), qualifiedLine.getId(), outputQualifiedQty, |
| | | outputTarget.warehouseId(), outputTarget.locationId(), outputTarget.areaId()); |
| | | productProduceDetailService.createProductProduceDetail(qualifiedDetail); |
| | | } |
| | | } |
| | | } |
| | | return produce; |
| | | } |
| | | |
| | | /** |
| | | * 解析报工产出列表:主产出固定为报工物料(配比 1),联产出按工单工序快照顺序,配比回查工艺路线配置。 |
| | | */ |
| | | private List<OutputPlan> resolveOutputPlans(MesProFeedbackDO feedback, MesProWorkOrderDO workOrder) { |
| | | List<OutputPlan> single = List.of(new OutputPlan(feedback.getItemId(), BigDecimal.ONE)); |
| | | if (feedback.getWorkOrderId() == null || feedback.getProcessId() == null) { |
| | | return single; |
| | | } |
| | | MesProWorkOrderProcessDO workOrderProcess = workOrderProcessService |
| | | .getWorkOrderProcessByWorkOrderIdAndProcessId(feedback.getWorkOrderId(), feedback.getProcessId()); |
| | | List<Long> snapshotItemIds = parseItemIds(workOrderProcess != null ? workOrderProcess.getOutputItemId() : null); |
| | | if (snapshotItemIds.size() <= 1) { |
| | | return single; |
| | | } |
| | | // 回查路线产出配比:找不到配置时按 1 处理 |
| | | Map<Long, BigDecimal> ratioMap = new HashMap<>(); |
| | | Long routeId = feedback.getRouteId() != null ? feedback.getRouteId() |
| | | : (workOrder != null ? workOrder.getRouteId() : null); |
| | | if (routeId != null) { |
| | | var routeProcess = routeProcessService.getRouteProcessByRouteIdAndProcessId(routeId, feedback.getProcessId()); |
| | | if (routeProcess != null) { |
| | | for (var config : routeProcessService.getProcessOutputs(routeProcess.getId(), |
| | | workOrder != null ? workOrder.getProductId() : null)) { |
| | | if (config.getItemId() != null && config.getQuantity() != null) { |
| | | ratioMap.putIfAbsent(config.getItemId(), config.getQuantity()); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | List<OutputPlan> plans = new ArrayList<>(); |
| | | plans.add(new OutputPlan(feedback.getItemId(), BigDecimal.ONE)); |
| | | for (Long itemId : snapshotItemIds) { |
| | | if (itemId == null || itemId.equals(feedback.getItemId())) { |
| | | continue; |
| | | } |
| | | plans.add(new OutputPlan(itemId, ratioMap.getOrDefault(itemId, BigDecimal.ONE))); |
| | | } |
| | | return plans; |
| | | } |
| | | |
| | | private List<Long> parseItemIds(String csv) { |
| | | if (csv == null || csv.isBlank()) { |
| | | return Collections.emptyList(); |
| | | } |
| | | return Arrays.stream(csv.split(",")) |
| | | .map(String::trim).filter(value -> !value.isEmpty()) |
| | | .map(Long::valueOf).distinct().toList(); |
| | | } |
| | | |
| | | /** |
| | | * 产出数量 = 基准数量 × 配比,保留 6 位小数,与产出单数量字段精度一致。 |
| | | */ |
| | | private BigDecimal scaleQuantity(BigDecimal base, BigDecimal ratio) { |
| | | BigDecimal baseValue = base != null ? base : BigDecimal.ZERO; |
| | | BigDecimal ratioValue = ratio != null ? ratio : BigDecimal.ONE; |
| | | return baseValue.multiply(ratioValue).setScale(6, RoundingMode.HALF_UP); |
| | | } |
| | | |
| | | private BatchInfo resolveBatch(MesProFeedbackDO feedback, MesProWorkOrderDO workOrder, |
| | | Long itemId, MesProFeedbackApproveReqVO approveReqVO) { |
| | | MesWmBatchGenerateReqVO batchReqVO = new MesWmBatchGenerateReqVO() |
| | | .setItemId(feedback.getItemId()) |
| | | .setItemId(itemId) |
| | | .setProduceDate(LocalDate.now().atStartOfDay()) // 截断到当天零点,确保同一天的报工生成相同批次号 |
| | | .setExpireDate(feedback.getExpireDate()) |
| | | .setWorkOrderId(feedback.getWorkOrderId()) |
| | |
| | | if (approveReqVO.getQualityStatus() != null) batchReqVO.setQualityStatus(approveReqVO.getQualityStatus()); |
| | | } |
| | | MesWmBatchDO batch = batchService.getOrGenerateBatchCode(batchReqVO); |
| | | Long batchId = batch != null ? batch.getId() : null; |
| | | String batchCode = batch != null ? batch.getCode() : null; |
| | | |
| | | // 1.5 优先使用工单工序配置的报工仓库(下单时可编辑),其次工艺路线产品,最后回退虚拟线边库 |
| | | WarehouseTarget outputTarget = resolveOutputTarget(feedback.getWorkOrderId(), |
| | | feedback.getProcessId(), feedback.getItemId()); |
| | | |
| | | // 3. 根据是否需要检验分支处理 |
| | | if (checkFlag) { |
| | | // 3.1 需要检验:创建一条行(质量状态=待检验),不生成明细 |
| | | MesWmProductProduceLineDO line = buildProduceLine(produce, feedback, batchId, batchCode, |
| | | feedback.getFeedbackQuantity(), MesWmQualityStatusEnum.PENDING.getStatus()); |
| | | productProduceLineService.createProductProduceLine(line); |
| | | // 注意:checkFlag=true 时不生成明细行,等 IPQC 检验完成后由 splitPendingAndFinishProduce 按质量结果拆行+生成明细 |
| | | } else { |
| | | // 3.2 无需检验:按合格品/不合格品各生成一行 + 明细 |
| | | BigDecimal qualifiedQty = ObjectUtil.defaultIfNull(feedback.getQualifiedQuantity(), BigDecimal.ZERO); |
| | | BigDecimal unqualifiedQty = ObjectUtil.defaultIfNull(feedback.getUnqualifiedQuantity(), BigDecimal.ZERO); |
| | | // 3.2.1 不合格品行 + 明细 |
| | | if (unqualifiedQty.compareTo(BigDecimal.ZERO) > 0) { |
| | | MesWmProductProduceLineDO unqualifiedLine = buildProduceLine(produce, feedback, batchId, batchCode, |
| | | unqualifiedQty, MesWmQualityStatusEnum.FAIL.getStatus()); |
| | | productProduceLineService.createProductProduceLine(unqualifiedLine); |
| | | MesWmProductProduceDetailDO unqualifiedDetail = buildProduceDetail(produce, feedback, batchId, batchCode, |
| | | unqualifiedLine.getId(), unqualifiedQty, |
| | | outputTarget.warehouseId(), outputTarget.locationId(), outputTarget.areaId()); |
| | | productProduceDetailService.createProductProduceDetail(unqualifiedDetail); |
| | | } |
| | | // 3.2.2 合格品行 + 明细 |
| | | if (qualifiedQty.compareTo(BigDecimal.ZERO) > 0) { |
| | | MesWmProductProduceLineDO qualifiedLine = buildProduceLine(produce, feedback, batchId, batchCode, |
| | | qualifiedQty, MesWmQualityStatusEnum.PASS.getStatus()); |
| | | productProduceLineService.createProductProduceLine(qualifiedLine); |
| | | MesWmProductProduceDetailDO qualifiedDetail = buildProduceDetail(produce, feedback, batchId, batchCode, |
| | | qualifiedLine.getId(), qualifiedQty, |
| | | outputTarget.warehouseId(), outputTarget.locationId(), outputTarget.areaId()); |
| | | productProduceDetailService.createProductProduceDetail(qualifiedDetail); |
| | | } |
| | | } |
| | | return produce; |
| | | return new BatchInfo(batch != null ? batch.getId() : null, batch != null ? batch.getCode() : null); |
| | | } |
| | | |
| | | private MesWmProductProduceLineDO buildProduceLine(MesWmProductProduceDO produce, MesProFeedbackDO feedback, |
| | | Long batchId, String batchCode, |
| | | Long itemId, Long batchId, String batchCode, |
| | | BigDecimal quantity, Integer qualityStatus) { |
| | | return MesWmProductProduceLineDO.builder() |
| | | .produceId(produce.getId()).feedbackId(feedback.getId()) |
| | | .itemId(feedback.getItemId()).quantity(quantity) |
| | | .itemId(itemId).quantity(quantity) |
| | | .batchId(batchId).batchCode(batchCode) |
| | | .expireDate(feedback.getExpireDate()).lotNumber(feedback.getLotNumber()) |
| | | .qualityStatus(qualityStatus) |
| | | .build(); |
| | | } |
| | | |
| | | private MesWmProductProduceDetailDO buildProduceDetail(MesWmProductProduceDO produce, MesProFeedbackDO feedback, |
| | | private MesWmProductProduceDetailDO buildProduceDetail(MesWmProductProduceDO produce, Long itemId, |
| | | Long batchId, String batchCode, |
| | | Long lineId, BigDecimal quantity, |
| | | Long warehouseId, Long locationId, Long areaId) { |
| | | return MesWmProductProduceDetailDO.builder() |
| | | .produceId(produce.getId()).lineId(lineId) |
| | | .itemId(feedback.getItemId()).quantity(quantity) |
| | | .itemId(itemId).quantity(quantity) |
| | | .batchId(batchId).batchCode(batchCode) |
| | | .warehouseId(warehouseId).locationId(locationId).areaId(areaId) |
| | | .build(); |
| | | } |
| | | |
| | | private record OutputPlan(Long itemId, BigDecimal ratio) { |
| | | } |
| | | |
| | | private record BatchInfo(Long id, String code) { |
| | | } |
| | | |
| | | @Override |
| | |
| | | throw exception(WM_PRODUCT_PRODUCE_NOT_EXISTS); |
| | | } |
| | | // 2. 通过 sourceLineId 直接定位待检验行 |
| | | MesWmProductProduceLineDO pendingLine = productProduceLineService.validateProductProduceLineExists(sourceLineId); |
| | | WarehouseTarget outputTarget = resolveOutputTarget(produce.getWorkOrderId(), |
| | | produce.getProcessId(), pendingLine.getItemId()); |
| | | MesWmProductProduceLineDO pendingLine = produceLineMapper.selectByIdForUpdate(sourceLineId); |
| | | if (pendingLine == null) throw exception(WM_PRODUCT_PRODUCE_LINE_NOT_EXISTS); |
| | | if (!java.util.Objects.equals(pendingLine.getProduceId(), produce.getId()) |
| | | || !java.util.Objects.equals(pendingLine.getFeedbackId(), feedbackId)) { |
| | | throw exception(QC_IPQC_SOURCE_LINE_NOT_BELONG); |
| | | } |
| | | if (!java.util.Objects.equals(pendingLine.getQualityStatus(), MesWmQualityStatusEnum.PENDING.getStatus())) { |
| | | throw exception(QC_IPQC_SOURCE_LINE_NOT_PENDING); |
| | | } |
| | | if (qualifiedQty == null || unqualifiedQty == null || qualifiedQty.signum() < 0 || unqualifiedQty.signum() < 0 |
| | | || qualifiedQty.add(unqualifiedQty).signum() <= 0 || pendingLine.getQuantity() == null |
| | | || qualifiedQty.add(unqualifiedQty).compareTo(pendingLine.getQuantity()) != 0) { |
| | | throw exception(QC_IPQC_SOURCE_QUANTITY_INVALID); |
| | | } |
| | | // 3. 只拆分当前检验的产出行;同一报工的其它产出物料各自建检验单、独立检验 |
| | | applyQualitySplit(produce, pendingLine, qualifiedQty, unqualifiedQty); |
| | | |
| | | // 3A. 情况一:存在不合格品数量,需要拆分行 |
| | | // 4. 全部产出物料检验完成后才完成产出单(创建库存事务 + 更新状态为已完成) |
| | | boolean allInspected = productProduceLineService.getProductProduceLineListByProduceId(produce.getId()) |
| | | .stream().noneMatch(line -> ObjUtil.equal(line.getQualityStatus(), MesWmQualityStatusEnum.PENDING.getStatus())); |
| | | if (allInspected) { |
| | | finishProductProduce(produce.getId()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 按合格/不合格数量拆分一条待检行:生成明细,必要时新增不合格行。 |
| | | */ |
| | | private void applyQualitySplit(MesWmProductProduceDO produce, MesWmProductProduceLineDO pendingLine, |
| | | BigDecimal qualifiedQty, BigDecimal unqualifiedQty) { |
| | | WarehouseTarget outputTarget = resolveOutputTarget(produce.getWorkOrderId(), |
| | | produce.getProcessId(), pendingLine.getItemId(), produce.getFeedbackId()); |
| | | |
| | | if (qualifiedQty.signum() == 0) { |
| | | pendingLine.setQualityStatus(MesWmQualityStatusEnum.FAIL.getStatus()); |
| | | productProduceLineService.updateProductProduceLine(pendingLine); |
| | | productProduceDetailService.createProductProduceDetail(MesWmProductProduceDetailDO.builder() |
| | | .produceId(produce.getId()).lineId(pendingLine.getId()) |
| | | .itemId(pendingLine.getItemId()).quantity(unqualifiedQty) |
| | | .batchId(pendingLine.getBatchId()).batchCode(pendingLine.getBatchCode()) |
| | | .warehouseId(outputTarget.warehouseId()).locationId(outputTarget.locationId()).areaId(outputTarget.areaId()) |
| | | .build()); |
| | | return; |
| | | } |
| | | |
| | | // 情况一:存在不合格品数量,需要拆分行 |
| | | if (unqualifiedQty != null && unqualifiedQty.compareTo(BigDecimal.ZERO) > 0) { |
| | | // 3A.1 不合格品:新建一行 + 明细 |
| | | // 不合格品:新建一行 + 明细 |
| | | MesWmProductProduceLineDO unqualifiedLine = MesWmProductProduceLineDO.builder() |
| | | .produceId(produce.getId()).feedbackId(feedbackId) |
| | | .produceId(produce.getId()).feedbackId(pendingLine.getFeedbackId()) |
| | | .itemId(pendingLine.getItemId()).quantity(unqualifiedQty) |
| | | .batchId(pendingLine.getBatchId()).batchCode(pendingLine.getBatchCode()) |
| | | .expireDate(pendingLine.getExpireDate()).lotNumber(pendingLine.getLotNumber()) |
| | |
| | | .batchId(pendingLine.getBatchId()).batchCode(pendingLine.getBatchCode()) |
| | | .warehouseId(outputTarget.warehouseId()).locationId(outputTarget.locationId()).areaId(outputTarget.areaId()) |
| | | .build()); |
| | | // 3A.2 合格品:复用原待检行,更新数量和状态 + 明细 |
| | | // 合格品:复用原待检行,更新数量和状态 + 明细 |
| | | pendingLine.setQuantity(ObjectUtil.defaultIfNull(qualifiedQty, BigDecimal.ZERO)); |
| | | pendingLine.setQualityStatus(MesWmQualityStatusEnum.PASS.getStatus()); |
| | | productProduceLineService.updateProductProduceLine(pendingLine); |
| | |
| | | .build()); |
| | | } |
| | | } else { |
| | | // 3B. 情况二:全部合格,直接更新行状态 + 明细 |
| | | // 情况二:全部合格,直接更新行状态 + 明细 |
| | | pendingLine.setQualityStatus(MesWmQualityStatusEnum.PASS.getStatus()); |
| | | productProduceLineService.updateProductProduceLine(pendingLine); |
| | | productProduceDetailService.createProductProduceDetail(MesWmProductProduceDetailDO.builder() |
| | |
| | | .warehouseId(outputTarget.warehouseId()).locationId(outputTarget.locationId()).areaId(outputTarget.areaId()) |
| | | .build()); |
| | | } |
| | | |
| | | // 4. 完成产出单(创建库存事务 + 更新状态为已完成) |
| | | finishProductProduce(produce.getId()); |
| | | } |
| | | |
| | | private WarehouseTarget resolveOutputTarget(Long workOrderId, Long processId, Long itemId) { |
| | | private WarehouseTarget resolveOutputTarget(Long workOrderId, Long processId, Long itemId, Long feedbackId) { |
| | | boolean lastProcess = isLastProcess(workOrderId, processId, feedbackId); |
| | | // 1. 工单工序配置(下单时可编辑) |
| | | if (workOrderId != null && processId != null) { |
| | | MesProWorkOrderProcessDO workOrderProcess = workOrderProcessService |
| | |
| | | && workOrderProcess.getLocationId() != null && workOrderProcess.getAreaId() != null) { |
| | | areaService.validateWarehouseAreaExists(workOrderProcess.getWarehouseId(), |
| | | workOrderProcess.getLocationId(), workOrderProcess.getAreaId()); |
| | | validateFinalWarehouse(lastProcess, workOrderProcess.getWarehouseId()); |
| | | return new WarehouseTarget(workOrderProcess.getWarehouseId(), |
| | | workOrderProcess.getLocationId(), workOrderProcess.getAreaId()); |
| | | } |
| | |
| | | && routeProduct.getLocationId() != null && routeProduct.getAreaId() != null) { |
| | | areaService.validateWarehouseAreaExists(routeProduct.getWarehouseId(), |
| | | routeProduct.getLocationId(), routeProduct.getAreaId()); |
| | | validateFinalWarehouse(lastProcess, routeProduct.getWarehouseId()); |
| | | return new WarehouseTarget(routeProduct.getWarehouseId(), routeProduct.getLocationId(), routeProduct.getAreaId()); |
| | | } |
| | | if (lastProcess) { |
| | | throw exception(WM_PRODUCT_PRODUCE_OUTPUT_WAREHOUSE_REQUIRED); |
| | | } |
| | | MesWmWarehouseDO warehouse = warehouseService.getWarehouseByCode(MesWmWarehouseDO.WIP_VIRTUAL_WAREHOUSE); |
| | | MesWmWarehouseLocationDO location = locationService.getWarehouseLocationByCode( |
| | |
| | | return new WarehouseTarget(warehouse.getId(), location.getId(), area.getId()); |
| | | } |
| | | |
| | | private boolean isLastProcess(Long workOrderId, Long processId, Long feedbackId) { |
| | | MesProWorkOrderProcessDO last = workOrderProcessService.getLastWorkOrderProcessByWorkOrderId(workOrderId); |
| | | if (last != null) return ObjUtil.equal(last.getProcessId(), processId); |
| | | MesProFeedbackDO feedback = feedbackId == null ? null : feedbackMapper.selectById(feedbackId); |
| | | if (feedback == null || feedback.getRouteId() == null) { |
| | | throw exception(PRO_FEEDBACK_PARAM_INVALID, "无法确定末道工序,请检查工单和工艺路线的工序顺序"); |
| | | } |
| | | var processes = routeProcessService.getRouteProcessListByRouteId(feedback.getRouteId()); |
| | | if (CollUtil.isEmpty(processes) || processes.stream().anyMatch(p -> p.getSort() == null)) { |
| | | throw exception(PRO_FEEDBACK_PARAM_INVALID, "无法确定末道工序,请检查工单和工艺路线的工序顺序"); |
| | | } |
| | | int maxSort = processes.stream().mapToInt(p -> p.getSort()).max().orElseThrow(); |
| | | var lastProcesses = processes.stream().filter(p -> p.getSort() == maxSort).toList(); |
| | | if (lastProcesses.size() != 1 || lastProcesses.get(0).getProcessId() == null) { |
| | | throw exception(PRO_FEEDBACK_PARAM_INVALID, "无法唯一确定末道工序,请检查工艺路线的工序顺序"); |
| | | } |
| | | return ObjUtil.equal(lastProcesses.get(0).getProcessId(), processId); |
| | | } |
| | | |
| | | private void validateFinalWarehouse(boolean lastProcess, Long warehouseId) { |
| | | if (!lastProcess) return; |
| | | MesWmWarehouseDO virtualWarehouse = warehouseService.getWarehouseByCode(MesWmWarehouseDO.WIP_VIRTUAL_WAREHOUSE); |
| | | if (virtualWarehouse != null && ObjUtil.equal(warehouseId, virtualWarehouse.getId())) { |
| | | throw exception(WM_PRODUCT_PRODUCE_OUTPUT_WAREHOUSE_REQUIRED); |
| | | } |
| | | } |
| | | |
| | | private record WarehouseTarget(Long warehouseId, Long locationId, Long areaId) { |
| | | } |
| | | |