| | |
| | | import com.ruoyi.common.utils.StringUtils; |
| | | import com.ruoyi.production.bean.dto.ProductionOrderPickDto; |
| | | import com.ruoyi.production.bean.vo.ProductionOrderPickVo; |
| | | import com.ruoyi.production.mapper.ProductionBomStructureMapper; |
| | | import com.ruoyi.production.mapper.ProductionOperationTaskMapper; |
| | | import com.ruoyi.production.mapper.ProductionOrderMapper; |
| | | import com.ruoyi.production.mapper.ProductionOrderPickMapper; |
| | | import com.ruoyi.production.mapper.ProductionOrderPickRecordMapper; |
| | | import com.ruoyi.production.mapper.ProductionOrderRoutingOperationMapper; |
| | | import com.ruoyi.production.pojo.ProductionBomStructure; |
| | | import com.ruoyi.production.pojo.ProductionOperationTask; |
| | | import com.ruoyi.production.pojo.ProductionOrder; |
| | | import com.ruoyi.production.pojo.ProductionOrderPick; |
| | | import com.ruoyi.production.pojo.ProductionOrderPickRecord; |
| | | import com.ruoyi.production.pojo.ProductionOrderRoutingOperation; |
| | | import com.ruoyi.production.service.ProductionOrderPickService; |
| | | import com.ruoyi.production.service.ProductionOrderService; |
| | | import com.ruoyi.stock.dto.StockInventoryDto; |
| | | import com.ruoyi.stock.mapper.StockInventoryMapper; |
| | | import com.ruoyi.stock.pojo.StockInRecord; |
| | |
| | | import com.ruoyi.stock.service.StockInventoryService; |
| | | import com.ruoyi.stock.service.StockInRecordService; |
| | | import com.ruoyi.stock.service.StockOutRecordService; |
| | | import com.ruoyi.technology.mapper.TechnologyOperationMapper; |
| | | import com.ruoyi.technology.pojo.TechnologyOperation; |
| | | import lombok.RequiredArgsConstructor; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | |
| | | private final ProductionOrderMapper productionOrderMapper; |
| | | private final ProductionOperationTaskMapper productionOperationTaskMapper; |
| | | private final ProductionOrderPickRecordMapper productionOrderPickRecordMapper; |
| | | private final ProductionOrderRoutingOperationMapper productionOrderRoutingOperationMapper; |
| | | private final ProductionBomStructureMapper productionBomStructureMapper; |
| | | private final ProductionOrderService productionOrderService; |
| | | private final TechnologyOperationMapper technologyOperationMapper; |
| | | private final StockInventoryMapper stockInventoryMapper; |
| | | private final StockInventoryService stockInventoryService; |
| | | private final StockInRecordService stockInRecordService; |
| | |
| | | // 保存领料主记录快照。 |
| | | ProductionOrderPick orderPick = new ProductionOrderPick(); |
| | | orderPick.setProductionOrderId(resolvedDto.getProductionOrderId()); |
| | | orderPick.setProductionOperationTaskId(resolvedDto.getProductionOperationTaskId()); |
| | | orderPick.setProductModelId(resolvedDto.getProductModelId()); |
| | | orderPick.setBatchNo(storedBatchNo); |
| | | orderPick.setQuantity(resolvedDto.getPickQuantity()); |
| | |
| | | List<ProductionOrderPickVo> detailList = baseMapper.listPickedDetailByOrderId(productionOrderId); |
| | | fillBatchNoList(detailList); |
| | | fillSelectableBatchNoList(detailList); |
| | | fillPickedFlagAndEffectiveQuantity(detailList); |
| | | return detailList; |
| | | } |
| | | |
| | | /** |
| | | * 工序物料清单 = 订单BOM快照中属于该工序的可领用物料建议 + 该工单已领料记录。 |
| | | * 建议行 picked=false(id 为空),历史领料行 picked=true。 |
| | | */ |
| | | @Override |
| | | public List<ProductionOrderPickVo> listTaskMaterials(Long productionOperationTaskId) { |
| | | if (productionOperationTaskId == null) { |
| | | return Collections.emptyList(); |
| | | } |
| | | TaskOperationContext context = resolveTaskOperationContext(productionOperationTaskId); |
| | | |
| | | List<ProductionOrderPickVo> result = new ArrayList<>(); |
| | | // BOM 建议物料:按当前工序过滤,不带任何需求数量 |
| | | List<ProductionOrderPickVo> suggestionList = |
| | | productionOrderService.pick(context.productionOrderId(), context.technologyOperationId()); |
| | | for (ProductionOrderPickVo suggestion : suggestionList) { |
| | | suggestion.setId(null); |
| | | suggestion.setPicked(false); |
| | | suggestion.setProductionOrderId(context.productionOrderId()); |
| | | suggestion.setProductionOperationTaskId(productionOperationTaskId); |
| | | suggestion.setTechnologyOperationId(context.technologyOperationId()); |
| | | if (StringUtils.isEmpty(suggestion.getOperationName())) { |
| | | suggestion.setOperationName(context.operationName()); |
| | | } |
| | | suggestion.setWorkOrderNo(context.workOrderNo()); |
| | | result.add(suggestion); |
| | | } |
| | | |
| | | // 该工单已产生的领料记录 |
| | | List<ProductionOrderPickVo> pickedList = baseMapper.listPickedDetailByTaskId(productionOperationTaskId); |
| | | fillBatchNoList(pickedList); |
| | | fillSelectableBatchNoList(pickedList); |
| | | fillPickedFlagAndEffectiveQuantity(pickedList); |
| | | result.addAll(pickedList); |
| | | return result; |
| | | } |
| | | |
| | | /** |
| | | * 工序统一领料: |
| | | * 1) 由工单反查订单与工序,不信任前端传入的归属字段; |
| | | * 2) 逐行校验物料、数量与批次; |
| | | * 3) 复用出库申请 + 立即审批的库存扣减流程; |
| | | * 4) 写入领料主记录与流水,重复领料即新增记录,不区分初领与补料。 |
| | | */ |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public Boolean savePickByTask(ProductionOrderPickDto dto) { |
| | | if (dto == null) { |
| | | throw new ServiceException("参数不能为空"); |
| | | } |
| | | Long productionOperationTaskId = dto.getProductionOperationTaskId(); |
| | | if (productionOperationTaskId == null) { |
| | | throw new ServiceException("工单ID不能为空"); |
| | | } |
| | | TaskOperationContext context = resolveTaskOperationContext(productionOperationTaskId); |
| | | |
| | | List<ProductionOrderPickDto> pickItems = resolvePickItems(dto).stream() |
| | | .filter(Objects::nonNull) |
| | | .filter(item -> item.getProductModelId() != null || item.getPickQuantity() != null) |
| | | .collect(Collectors.toList()); |
| | | if (pickItems.isEmpty()) { |
| | | throw new ServiceException("请至少填写一条领料物料"); |
| | | } |
| | | |
| | | for (int i = 0; i < pickItems.size(); i++) { |
| | | int rowNo = i + 1; |
| | | ProductionOrderPickDto resolvedDto = mergeDto(dto, pickItems.get(i)); |
| | | if (resolvedDto.getProductModelId() == null) { |
| | | throw new ServiceException("第" + rowNo + "行参数错误:产品规格不能为空"); |
| | | } |
| | | if (resolvedDto.getPickQuantity() == null |
| | | || resolvedDto.getPickQuantity().compareTo(BigDecimal.ZERO) <= 0) { |
| | | throw new ServiceException("第" + rowNo + "行参数错误:领料数量必须大于0"); |
| | | } |
| | | |
| | | List<String> batchNoList = resolveBatchNoList(resolvedDto); |
| | | String inventoryBatchNo = pickInventoryBatchNo(batchNoList); |
| | | String storedBatchNo = formatBatchNoStorage(batchNoList); |
| | | |
| | | ProductionOrderPick orderPick = new ProductionOrderPick(); |
| | | orderPick.setProductionOrderId(context.productionOrderId()); |
| | | orderPick.setProductionOperationTaskId(productionOperationTaskId); |
| | | orderPick.setProductModelId(resolvedDto.getProductModelId()); |
| | | orderPick.setBatchNo(storedBatchNo); |
| | | orderPick.setQuantity(resolvedDto.getPickQuantity()); |
| | | orderPick.setRemark(resolvedDto.getRemark()); |
| | | orderPick.setOperationName(context.operationName()); |
| | | orderPick.setTechnologyOperationId(context.technologyOperationId()); |
| | | // 扁平BOM不配置数量,领料不再有需求数量约束 |
| | | orderPick.setDemandedQuantity(null); |
| | | orderPick.setBom(isBomMaterial(context, resolvedDto.getProductModelId())); |
| | | orderPick.setReturned(false); |
| | | baseMapper.insert(orderPick); |
| | | |
| | | // 立即扣减库存,失败则整个事务回滚 |
| | | subtractInventory(orderPick.getId(), |
| | | resolvedDto.getProductModelId(), |
| | | storedBatchNo, |
| | | resolvedDto.getPickQuantity(), |
| | | rowNo, |
| | | PICK_STOCK_OUT_RECORD_TYPE); |
| | | |
| | | insertPickRecord(orderPick.getId(), |
| | | context.productionOrderId(), |
| | | productionOperationTaskId, |
| | | resolvedDto.getProductModelId(), |
| | | inventoryBatchNo, |
| | | resolvedDto.getPickQuantity(), |
| | | BigDecimal.ZERO, |
| | | resolvedDto.getPickQuantity(), |
| | | PICK_TYPE_NORMAL, |
| | | resolvedDto.getRemark(), |
| | | null); |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * 当前工序是否已有有效领料:任一物料的净领料数量(领料+补料-退料)大于0。 |
| | | */ |
| | | @Override |
| | | public boolean hasEffectivePick(Long productionOperationTaskId) { |
| | | if (productionOperationTaskId == null) { |
| | | return false; |
| | | } |
| | | List<ProductionOrderPick> pickList = baseMapper.selectList( |
| | | Wrappers.<ProductionOrderPick>lambdaQuery() |
| | | .eq(ProductionOrderPick::getProductionOperationTaskId, productionOperationTaskId)); |
| | | for (ProductionOrderPick pick : pickList) { |
| | | if (pick == null) { |
| | | continue; |
| | | } |
| | | if (resolveEffectiveQuantity(pick).compareTo(BigDecimal.ZERO) > 0) { |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * 由工单解析出订单、工序快照与工序名称,并校验归属关系。 |
| | | */ |
| | | private TaskOperationContext resolveTaskOperationContext(Long productionOperationTaskId) { |
| | | ProductionOperationTask task = productionOperationTaskMapper.selectById(productionOperationTaskId); |
| | | if (task == null) { |
| | | throw new ServiceException("工单不存在"); |
| | | } |
| | | if (task.getProductionOrderId() == null) { |
| | | throw new ServiceException("工单未关联生产订单"); |
| | | } |
| | | ProductionOrder productionOrder = productionOrderMapper.selectById(task.getProductionOrderId()); |
| | | if (productionOrder == null) { |
| | | throw new ServiceException("生产订单不存在"); |
| | | } |
| | | ProductionOrderRoutingOperation routingOperation = |
| | | productionOrderRoutingOperationMapper.selectById(task.getProductionOrderRoutingOperationId()); |
| | | if (routingOperation == null) { |
| | | throw new ServiceException("工单对应的订单工序不存在"); |
| | | } |
| | | if (routingOperation.getProductionOrderId() != null |
| | | && !Objects.equals(routingOperation.getProductionOrderId(), task.getProductionOrderId())) { |
| | | throw new ServiceException("工单与订单工序归属不一致"); |
| | | } |
| | | if (routingOperation.getTechnologyOperationId() == null) { |
| | | throw new ServiceException("订单工序未关联工艺工序"); |
| | | } |
| | | String operationName = routingOperation.getOperationName(); |
| | | if (StringUtils.isEmpty(operationName)) { |
| | | TechnologyOperation technologyOperation = |
| | | technologyOperationMapper.selectById(routingOperation.getTechnologyOperationId()); |
| | | operationName = technologyOperation == null ? null : technologyOperation.getName(); |
| | | } |
| | | return new TaskOperationContext( |
| | | task.getId(), |
| | | task.getProductionOrderId(), |
| | | routingOperation.getTechnologyOperationId(), |
| | | operationName, |
| | | task.getWorkOrderNo()); |
| | | } |
| | | |
| | | /** |
| | | * 判断物料是否来自订单BOM快照中的当前工序。 |
| | | */ |
| | | private boolean isBomMaterial(TaskOperationContext context, Long productModelId) { |
| | | if (productModelId == null) { |
| | | return false; |
| | | } |
| | | return productionBomStructureMapper.selectCount( |
| | | Wrappers.<ProductionBomStructure>lambdaQuery() |
| | | .eq(ProductionBomStructure::getProductionOrderId, context.productionOrderId()) |
| | | .eq(ProductionBomStructure::getTechnologyOperationId, context.technologyOperationId()) |
| | | .eq(ProductionBomStructure::getProductModelId, productModelId)) > 0; |
| | | } |
| | | |
| | | private void fillPickedFlagAndEffectiveQuantity(List<ProductionOrderPickVo> detailList) { |
| | | if (detailList == null || detailList.isEmpty()) { |
| | | return; |
| | | } |
| | | for (ProductionOrderPickVo detail : detailList) { |
| | | if (detail == null) { |
| | | continue; |
| | | } |
| | | detail.setPicked(detail.getId() != null); |
| | | detail.setEffectiveQuantity(resolveEffectiveQuantity(detail)); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 净领料数量 = 领料数量 + 补料数量 - 退料数量。 |
| | | */ |
| | | private BigDecimal resolveEffectiveQuantity(ProductionOrderPick pick) { |
| | | if (pick == null) { |
| | | return BigDecimal.ZERO; |
| | | } |
| | | return defaultDecimal(pick.getQuantity()) |
| | | .add(defaultDecimal(pick.getFeedingQty())) |
| | | .subtract(defaultDecimal(pick.getReturnQty())); |
| | | } |
| | | |
| | | /** |
| | | * 工单对应的领料上下文,避免依赖前端传入的订单/工序字段。 |
| | | */ |
| | | private record TaskOperationContext(Long productionOperationTaskId, |
| | | Long productionOrderId, |
| | | Long technologyOperationId, |
| | | String operationName, |
| | | String workOrderNo) { |
| | | } |
| | | |
| | | private void processDeletePickIds(ProductionOrderPickDto rootDto, |
| | |
| | | |
| | | ProductionOrderPick orderPick = new ProductionOrderPick(); |
| | | orderPick.setProductionOrderId(dto.getProductionOrderId()); |
| | | orderPick.setProductionOperationTaskId(dto.getProductionOperationTaskId()); |
| | | orderPick.setProductModelId(dto.getProductModelId()); |
| | | orderPick.setBatchNo(storedBatchNo); |
| | | orderPick.setQuantity(dto.getPickQuantity()); |