| | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.baomidou.mybatisplus.core.toolkit.Wrappers; |
| | | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; |
| | | import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum; |
| | | import com.ruoyi.common.exception.ServiceException; |
| | | import com.ruoyi.common.utils.StringUtils; |
| | | import com.ruoyi.production.bean.dto.ProductionOrderPickDto; |
| | |
| | | import com.ruoyi.stock.dto.StockInventoryDto; |
| | | import com.ruoyi.stock.mapper.StockInventoryMapper; |
| | | import com.ruoyi.stock.pojo.StockInventory; |
| | | import com.ruoyi.stock.service.StockInventoryService; |
| | | import lombok.RequiredArgsConstructor; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | |
| | | import java.util.stream.Collectors; |
| | | |
| | | /** |
| | | * <p> |
| | | * 璁㈠崟棰嗘枡绾胯竟浠?鏈嶅姟瀹炵幇绫? |
| | | * </p> |
| | | * |
| | | * @author 鑺杞欢锛堟睙鑻忥級鏈夐檺鍏徃 |
| | | * @since 2026-04-21 03:55:52 |
| | | * 生产订单领料服务实现。 |
| | | * 负责领料新增、更新、补料、退料及库存联动。 |
| | | */ |
| | | @Service |
| | | @RequiredArgsConstructor |
| | |
| | | private final ProductionOperationTaskMapper productionOperationTaskMapper; |
| | | private final ProductionOrderPickRecordMapper productionOrderPickRecordMapper; |
| | | private final StockInventoryMapper stockInventoryMapper; |
| | | private final StockInventoryService stockInventoryService; |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public Boolean savePick(ProductionOrderPickDto dto) { |
| | | // 领料新增总流程: |
| | | // 1) 解析前端行数据并逐行合并参数; |
| | | // 2) 校验参数与批次; |
| | | // 3) 先扣减库存,再落库领料主记录; |
| | | // 4) 写入领料流水,记录数量变化轨迹。 |
| | | List<ProductionOrderPickDto> pickItems = resolvePickItems(dto); |
| | | // 逐行处理领料数据,行号用于拼装精确的报错信息。 |
| | | for (int i = 0; i < pickItems.size(); i++) { |
| | | int rowNo = i + 1; |
| | | ProductionOrderPickDto resolvedDto = mergeDto(dto, pickItems.get(i)); |
| | | // 每行都做完整校验,异常信息带行号。 |
| | | validatePickParam(resolvedDto, rowNo); |
| | | |
| | | // 统一处理批次(支持单批次/多批次)。 |
| | | List<String> batchNoList = resolveBatchNoList(resolvedDto); |
| | | String inventoryBatchNo = pickInventoryBatchNo(batchNoList); |
| | | String storedBatchNo = formatBatchNoStorage(batchNoList); |
| | | subtractInventory(resolvedDto.getProductModelId(), inventoryBatchNo, resolvedDto.getPickQuantity(), rowNo); |
| | | subtractInventory(resolvedDto.getProductModelId(), storedBatchNo, resolvedDto.getPickQuantity(), rowNo); |
| | | |
| | | // 保存领料主记录快照。 |
| | | ProductionOrderPick orderPick = new ProductionOrderPick(); |
| | | orderPick.setProductionOrderId(resolvedDto.getProductionOrderId()); |
| | | orderPick.setProductModelId(resolvedDto.getProductModelId()); |
| | |
| | | orderPick.setDemandedQuantity(resolvedDto.getDemandedQuantity()); |
| | | orderPick.setBom(resolvedDto.getBom()); |
| | | orderPick.setReturned(false); |
| | | // 新增主记录。 |
| | | baseMapper.insert(orderPick); |
| | | |
| | | // 记录本次领料流水(before=0,after=本次领料量)。 |
| | | insertPickRecord(orderPick.getId(), |
| | | resolvedDto.getProductionOrderId(), |
| | | resolvedDto.getProductionOperationTaskId(), |
| | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public Boolean updatePick(ProductionOrderPickDto dto) { |
| | | // 领料更新入口(同接口兼容三类业务): |
| | | // 1) 普通领料改量/增删; |
| | | // 2) 补料(pickType=2); |
| | | // 3) 退料(returned=true)。 |
| | | if (dto == null) { |
| | | throw new ServiceException("变更参数不能为空"); |
| | | throw new ServiceException("参数不能为空"); |
| | | } |
| | | Long productionOrderId = resolveProductionOrderId(dto); |
| | | if (productionOrderId == null) { |
| | |
| | | throw new ServiceException("生产订单不存在"); |
| | | } |
| | | |
| | | // 查询订单下现有领料记录并构建ID索引。 |
| | | List<ProductionOrderPick> existingPickList = baseMapper.selectList( |
| | | Wrappers.<ProductionOrderPick>lambdaQuery() |
| | | .eq(ProductionOrderPick::getProductionOrderId, productionOrderId)); |
| | | // 转成Map便于后续按ID快速校验与更新。 |
| | | Map<Long, ProductionOrderPick> existingPickMap = existingPickList.stream() |
| | | .filter(item -> item.getId() != null) |
| | | .collect(Collectors.toMap(ProductionOrderPick::getId, Function.identity(), (a, b) -> a)); |
| | | |
| | | // 补料请求单独走补料分支。 |
| | | if (isFeedingRequest(dto)) { |
| | | processFeedingPickItems(dto, existingPickMap, productionOrderId); |
| | | return true; |
| | | } |
| | | // 退料请求单独走退料分支。 |
| | | if (isReturnRequest(dto)) { |
| | | processReturnPickItems(dto, existingPickMap, productionOrderId); |
| | | return true; |
| | | } |
| | | |
| | | // 普通更新场景先处理显式删除。 |
| | | processDeletePickIds(dto, existingPickMap, productionOrderId); |
| | | |
| | | List<ProductionOrderPickDto> pickItems = resolveUpdateItems(dto); |
| | | Set<Long> keepPickIdSet = new HashSet<>(); |
| | | // keepPickIdSet 用于标记本次前端仍然保留的旧记录,后续用于识别“未回传即删除”的行。 |
| | | for (int i = 0; i < pickItems.size(); i++) { |
| | | int rowNo = i + 1; |
| | | ProductionOrderPickDto resolvedDto = mergeDto(dto, pickItems.get(i)); |
| | |
| | | keepPickIdSet.add(resolvedDto.getId()); |
| | | updateExistingPick(resolvedDto, rowNo, existingPickMap); |
| | | } |
| | | // 清理前端未回传旧行并回补库存。 |
| | | processMissingPickItems(dto, existingPickMap, productionOrderId, keepPickIdSet); |
| | | return true; |
| | | } |
| | | |
| | | @Override |
| | | public List<ProductionOrderPickVo> listPickedDetail(Long productionOrderId) { |
| | | // 查询订单领料明细,并补齐批次展示字段。 |
| | | if (productionOrderId == null) { |
| | | return Collections.emptyList(); |
| | | } |
| | |
| | | private void processDeletePickIds(ProductionOrderPickDto rootDto, |
| | | Map<Long, ProductionOrderPick> existingPickMap, |
| | | Long productionOrderId) { |
| | | // 处理前端显式删除ID: |
| | | // 1) 校验删除目标是否属于当前订单; |
| | | // 2) 回补库存; |
| | | // 3) 删除主记录; |
| | | // 4) 记录删除流水。 |
| | | if (rootDto.getDeletePickIds() == null || rootDto.getDeletePickIds().isEmpty()) { |
| | | return; |
| | | } |
| | |
| | | } |
| | | ProductionOrderPick existingPick = existingPickMap.get(deleteId); |
| | | if (existingPick == null || !Objects.equals(existingPick.getProductionOrderId(), productionOrderId)) { |
| | | throw new ServiceException("要删除的领料记录不存在或不属于当前订单,ID=" + deleteId); |
| | | throw new ServiceException("删除失败:领料记录不存在或不属于当前订单,ID=" + deleteId); |
| | | } |
| | | String oldBatchNo = resolveInventoryBatchNoFromStored(existingPick.getBatchNo()); |
| | | BigDecimal oldQuantity = defaultDecimal(existingPick.getQuantity()); |
| | | addInventory(existingPick.getProductModelId(), oldBatchNo, oldQuantity); |
| | | int affected = baseMapper.deleteById(deleteId); |
| | | if (affected <= 0) { |
| | | throw new ServiceException("删除领料失败,ID=" + deleteId); |
| | | throw new ServiceException("删除领料记录失败,ID=" + deleteId); |
| | | } |
| | | insertPickRecord(existingPick.getId(), |
| | | existingPick.getProductionOrderId(), |
| | |
| | | Map<Long, ProductionOrderPick> existingPickMap, |
| | | Long productionOrderId, |
| | | Set<Long> keepPickIdSet) { |
| | | // 处理“前端未回传”的旧行: |
| | | // 对应场景是用户在前端删除行但未放入 deletePickIds。 |
| | | // 这里兜底识别并执行回补库存 + 删除主记录 + 写流水。 |
| | | if (rootDto.getPickList() == null) { |
| | | return; |
| | | } |
| | |
| | | addInventory(missingPick.getProductModelId(), oldBatchNo, oldQuantity); |
| | | int affected = baseMapper.deleteById(missingPick.getId()); |
| | | if (affected <= 0) { |
| | | throw new ServiceException("删除领料失败,ID=" + missingPick.getId()); |
| | | throw new ServiceException("删除未回传领料记录失败,ID=" + missingPick.getId()); |
| | | } |
| | | insertPickRecord(missingPick.getId(), |
| | | missingPick.getProductionOrderId(), |
| | |
| | | } |
| | | |
| | | private void addNewPickInUpdate(ProductionOrderPickDto dto, int rowNo) { |
| | | // 更新场景下新增一条领料:扣库存 -> 新增主记录 -> 写流水。 |
| | | List<String> batchNoList = resolveBatchNoList(dto); |
| | | String inventoryBatchNo = pickInventoryBatchNo(batchNoList); |
| | | String storedBatchNo = formatBatchNoStorage(batchNoList); |
| | | subtractInventory(dto.getProductModelId(), inventoryBatchNo, dto.getPickQuantity(), rowNo); |
| | | subtractInventory(dto.getProductModelId(), storedBatchNo, dto.getPickQuantity(), rowNo); |
| | | |
| | | ProductionOrderPick orderPick = new ProductionOrderPick(); |
| | | orderPick.setProductionOrderId(dto.getProductionOrderId()); |
| | |
| | | private void processFeedingPickItems(ProductionOrderPickDto rootDto, |
| | | Map<Long, ProductionOrderPick> existingPickMap, |
| | | Long productionOrderId) { |
| | | // 补料流程入口: |
| | | // 逐行校验补料参数,校验原领料归属,再执行补料库存扣减和主记录回写。 |
| | | List<ProductionOrderPickDto> pickItems = resolveUpdateItems(rootDto); |
| | | for (int i = 0; i < pickItems.size(); i++) { |
| | | int rowNo = i + 1; |
| | |
| | | continue; |
| | | } |
| | | if (!isFeedingPick(resolvedDto)) { |
| | | throw new ServiceException("补料请求中的领料类型必须全部为2"); |
| | | throw new ServiceException("补料请求中存在非补料类型数据"); |
| | | } |
| | | if (resolvedDto.getProductionOrderId() == null) { |
| | | resolvedDto.setProductionOrderId(productionOrderId); |
| | |
| | | |
| | | ProductionOrderPick oldPick = existingPickMap.get(resolvedDto.getId()); |
| | | if (oldPick == null || !Objects.equals(oldPick.getProductionOrderId(), productionOrderId)) { |
| | | throw new ServiceException("第" + rowNo + "条领料记录不存在或不属于当前订单"); |
| | | throw new ServiceException("第" + rowNo + "行补料失败:未找到对应的领料记录"); |
| | | } |
| | | addFeedingPick(resolvedDto, oldPick, rowNo); |
| | | } |
| | | } |
| | | |
| | | private void addFeedingPick(ProductionOrderPickDto dto, ProductionOrderPick oldPick, int rowNo) { |
| | | // 补料核心: |
| | | // 1) 校验规格一致; |
| | | // 2) 扣减补料库存; |
| | | // 3) 写补料流水; |
| | | // 4) 回写主单累计补料量和实际量。 |
| | | if (dto.getProductModelId() != null && !Objects.equals(dto.getProductModelId(), oldPick.getProductModelId())) { |
| | | throw new ServiceException("第" + rowNo + "条补料产品规格与领料记录不一致"); |
| | | throw new ServiceException("第" + rowNo + "行补料失败:产品规格与原领料记录不一致"); |
| | | } |
| | | Long productModelId = oldPick.getProductModelId(); |
| | | List<String> batchNoList = resolveBatchNoList(dto); |
| | | String inventoryBatchNo = batchNoList.isEmpty() |
| | | ? resolveInventoryBatchNoFromStored(oldPick.getBatchNo()) |
| | | : pickInventoryBatchNo(batchNoList); |
| | | : formatBatchNoStorage(batchNoList); |
| | | BigDecimal feedingQuantity = dto.getFeedingQuantity(); |
| | | |
| | | subtractInventory(productModelId, inventoryBatchNo, feedingQuantity, rowNo); |
| | | |
| | | // 计算补料前后数量并写补料流水。 |
| | | BigDecimal beforeFeedingQty = sumFeedingQuantity(dto.getProductionOrderId(), oldPick.getId()); |
| | | BigDecimal afterFeedingQty = beforeFeedingQty.add(feedingQuantity); |
| | | insertPickRecord(oldPick.getId(), |
| | |
| | | updatePick.setId(oldPick.getId()); |
| | | updatePick.setFeedingQty(afterFeedingQty); |
| | | updatePick.setActualQty(calculateActualQty(oldPick, afterFeedingQty)); |
| | | // 回写主记录的补料累计值与实际用量。 |
| | | int affected = baseMapper.updateById(updatePick); |
| | | if (affected <= 0) { |
| | | throw new ServiceException("第" + rowNo + "条补料总量更新失败"); |
| | | throw new ServiceException("第" + rowNo + "行补料失败:更新领料主记录失败"); |
| | | } |
| | | oldPick.setFeedingQty(afterFeedingQty); |
| | | oldPick.setActualQty(updatePick.getActualQty()); |
| | |
| | | private void processReturnPickItems(ProductionOrderPickDto rootDto, |
| | | Map<Long, ProductionOrderPick> existingPickMap, |
| | | Long productionOrderId) { |
| | | // 退料流程入口: |
| | | // 逐行校验退料参数与领料归属,再更新退料量与实际量字段。 |
| | | List<ProductionOrderPickDto> pickItems = resolveUpdateItems(rootDto); |
| | | for (int i = 0; i < pickItems.size(); i++) { |
| | | int rowNo = i + 1; |
| | |
| | | |
| | | ProductionOrderPick oldPick = existingPickMap.get(resolvedDto.getId()); |
| | | if (oldPick == null || !Objects.equals(oldPick.getProductionOrderId(), productionOrderId)) { |
| | | throw new ServiceException("第" + rowNo + "条领料记录不存在或不属于当前订单"); |
| | | throw new ServiceException("第" + rowNo + "行退料失败:未找到对应的领料记录"); |
| | | } |
| | | updateReturnPick(resolvedDto, oldPick, rowNo); |
| | | } |
| | | } |
| | | |
| | | private void updateReturnPick(ProductionOrderPickDto dto, ProductionOrderPick oldPick, int rowNo) { |
| | | // 退料更新只改主领料记录中的退料字段与实际量。 |
| | | ProductionOrderPick updatePick = new ProductionOrderPick(); |
| | | updatePick.setId(oldPick.getId()); |
| | | updatePick.setReturnQty(dto.getReturnQty()); |
| | |
| | | updatePick.setReturned(true); |
| | | int affected = baseMapper.updateById(updatePick); |
| | | if (affected <= 0) { |
| | | throw new ServiceException("第" + rowNo + "条退料信息更新失败"); |
| | | throw new ServiceException("第" + rowNo + "行退料失败:更新领料主记录失败"); |
| | | } |
| | | oldPick.setReturnQty(updatePick.getReturnQty()); |
| | | oldPick.setActualQty(updatePick.getActualQty()); |
| | |
| | | private void updateExistingPick(ProductionOrderPickDto dto, |
| | | int rowNo, |
| | | Map<Long, ProductionOrderPick> existingPickMap) { |
| | | // 普通更新单行核心流程: |
| | | // 1) 校验旧记录存在且属于当前订单; |
| | | // 2) 比较新旧“规格+批次”,决定库存处理策略; |
| | | // 3) 更新主记录; |
| | | // 4) 写变更流水(记录前后数量变化)。 |
| | | ProductionOrderPick oldPick = existingPickMap.get(dto.getId()); |
| | | if (oldPick == null || !Objects.equals(oldPick.getProductionOrderId(), dto.getProductionOrderId())) { |
| | | throw new ServiceException("第" + rowNo + "条领料记录不存在或不属于当前订单"); |
| | | throw new ServiceException("第" + rowNo + "行更新失败:未找到对应的领料记录"); |
| | | } |
| | | |
| | | Long oldProductModelId = oldPick.getProductModelId(); |
| | |
| | | String newStoredBatchNo = formatBatchNoStorage(newBatchNoList); |
| | | BigDecimal newQuantity = dto.getPickQuantity(); |
| | | |
| | | // 判断规格+批次是否变化,决定库存处理策略。 |
| | | boolean sameStockKey = Objects.equals(oldProductModelId, newProductModelId) |
| | | && Objects.equals(oldBatchNo, newBatchNo); |
| | | if (sameStockKey) { |
| | | // 规格与批次不变:只按差值增减库存。 |
| | | BigDecimal delta = newQuantity.subtract(oldQuantity); |
| | | if (delta.compareTo(BigDecimal.ZERO) > 0) { |
| | | subtractInventory(newProductModelId, newBatchNo, delta, rowNo); |
| | | subtractInventory(newProductModelId, newStoredBatchNo, delta, rowNo); |
| | | } else if (delta.compareTo(BigDecimal.ZERO) < 0) { |
| | | addInventory(oldProductModelId, oldBatchNo, delta.abs()); |
| | | } |
| | | } else { |
| | | // 规格或批次变化:先回补旧库存,再扣减新库存。 |
| | | addInventory(oldProductModelId, oldBatchNo, oldQuantity); |
| | | subtractInventory(newProductModelId, newBatchNo, newQuantity, rowNo); |
| | | subtractInventory(newProductModelId, newStoredBatchNo, newQuantity, rowNo); |
| | | } |
| | | |
| | | oldPick.setProductModelId(newProductModelId); |
| | |
| | | } |
| | | int affected = baseMapper.updateById(oldPick); |
| | | if (affected <= 0) { |
| | | throw new ServiceException("第" + rowNo + "条领料更新失败"); |
| | | throw new ServiceException("第" + rowNo + "行更新失败:更新领料记录失败"); |
| | | } |
| | | |
| | | // 写入更新流水,保留本次数量变化轨迹。 |
| | | BigDecimal recordQuantity = sameStockKey ? oldQuantity.subtract(newQuantity).abs() : newQuantity; |
| | | if (recordQuantity.compareTo(BigDecimal.ZERO) > 0 || oldQuantity.compareTo(newQuantity) != 0 || !sameStockKey) { |
| | | insertPickRecord(oldPick.getId(), |
| | |
| | | Byte pickType, |
| | | String remark, |
| | | String feedingReason) { |
| | | // 写领料流水记录:统一记录领料/补料/退料数量变化轨迹。 |
| | | ProductionOrderPickRecord pickRecord = new ProductionOrderPickRecord(); |
| | | pickRecord.setPickId(pickId); |
| | | pickRecord.setProductionOrderId(productionOrderId); |
| | |
| | | } |
| | | |
| | | private void subtractInventory(Long productModelId, String batchNo, BigDecimal quantity, int rowNo) { |
| | | StockInventory stockInventory = stockInventoryMapper.selectOne(buildStockWrapper(productModelId, batchNo)); |
| | | if (stockInventory == null) { |
| | | throw new ServiceException("第" + rowNo + "条领料对应库存不存在"); |
| | | // 扣减库存总流程: |
| | | // 1) 解析批次列表; |
| | | // 2) 计算每个批次可用量与总可用量; |
| | | // 3) 按批次顺序逐笔扣减,直到扣完目标数量; |
| | | // 4) 任一步失败即抛错并回滚事务。 |
| | | BigDecimal deductQuantity = defaultDecimal(quantity); |
| | | // 领料数量小于等于0时,不需要执行库存扣减。 |
| | | if (deductQuantity.compareTo(BigDecimal.ZERO) <= 0) { |
| | | return; |
| | | } |
| | | BigDecimal availableQuantity = defaultDecimal(stockInventory.getQualitity()) |
| | | .subtract(defaultDecimal(stockInventory.getLockedQuantity())); |
| | | if (quantity.compareTo(availableQuantity) > 0) { |
| | | throw new ServiceException("第" + rowNo + "条领料可用库存不足"); |
| | | |
| | | List<String> batchNoList = parseBatchNoValue(batchNo); |
| | | if (batchNoList.isEmpty()) { |
| | | batchNoList = Collections.singletonList(null); |
| | | } |
| | | StockInventoryDto stockInventoryDto = new StockInventoryDto(); |
| | | stockInventoryDto.setProductModelId(productModelId); |
| | | stockInventoryDto.setBatchNo(batchNo); |
| | | stockInventoryDto.setQualitity(quantity); |
| | | int affected = stockInventoryMapper.updateSubtractStockInventory(stockInventoryDto); |
| | | if (affected <= 0) { |
| | | throw new ServiceException("第" + rowNo + "条领料扣减库存失败"); |
| | | |
| | | // 先计算各批次可用量,避免边扣边算导致判断不一致。 |
| | | Map<String, BigDecimal> availableQuantityMap = new LinkedHashMap<>(); |
| | | BigDecimal totalAvailableQuantity = BigDecimal.ZERO; |
| | | // 遍历批次,计算每个批次可用库存。 |
| | | for (String currentBatchNo : batchNoList) { |
| | | // 查询当前规格+批次的库存记录。 |
| | | StockInventory stockInventory = stockInventoryMapper.selectOne(buildStockWrapper(productModelId, currentBatchNo)); |
| | | BigDecimal availableQuantity = BigDecimal.ZERO; |
| | | if (stockInventory != null) { |
| | | availableQuantity = defaultDecimal(stockInventory.getQualitity()) |
| | | .subtract(defaultDecimal(stockInventory.getLockedQuantity())); |
| | | if (availableQuantity.compareTo(BigDecimal.ZERO) < 0) { |
| | | availableQuantity = BigDecimal.ZERO; |
| | | } |
| | | } |
| | | availableQuantityMap.put(currentBatchNo, availableQuantity); |
| | | totalAvailableQuantity = totalAvailableQuantity.add(availableQuantity); |
| | | } |
| | | |
| | | if (deductQuantity.compareTo(totalAvailableQuantity) > 0) { |
| | | BigDecimal shortQuantity = deductQuantity.subtract(totalAvailableQuantity); |
| | | throw new ServiceException("第" + rowNo + "行扣减库存失败:可用库存不足,当前可用" |
| | | + formatQuantity(totalAvailableQuantity) + ",仍缺少" + formatQuantity(shortQuantity)); |
| | | } |
| | | |
| | | // 按批次顺序逐笔扣减库存。 |
| | | BigDecimal remainingQuantity = deductQuantity; |
| | | for (Map.Entry<String, BigDecimal> entry : availableQuantityMap.entrySet()) { |
| | | if (remainingQuantity.compareTo(BigDecimal.ZERO) <= 0) { |
| | | break; |
| | | } |
| | | BigDecimal availableQuantity = defaultDecimal(entry.getValue()); |
| | | if (availableQuantity.compareTo(BigDecimal.ZERO) <= 0) { |
| | | continue; |
| | | } |
| | | BigDecimal currentDeductQuantity = remainingQuantity.min(availableQuantity); |
| | | StockInventoryDto stockInventoryDto = new StockInventoryDto(); |
| | | stockInventoryDto.setProductModelId(productModelId); |
| | | stockInventoryDto.setBatchNo(entry.getKey()); |
| | | stockInventoryDto.setQualitity(currentDeductQuantity); |
| | | int affected = stockInventoryMapper.updateSubtractStockInventory(stockInventoryDto); |
| | | if (affected <= 0) { |
| | | throw new ServiceException("第" + rowNo + "行扣减库存失败:库存更新失败"); |
| | | } |
| | | remainingQuantity = remainingQuantity.subtract(currentDeductQuantity); |
| | | } |
| | | |
| | | if (remainingQuantity.compareTo(BigDecimal.ZERO) > 0) { |
| | | throw new ServiceException("第" + rowNo + "行扣减库存失败:仍有未扣减数量" + formatQuantity(remainingQuantity)); |
| | | } |
| | | } |
| | | |
| | | private void addInventory(Long productModelId, String batchNo, BigDecimal quantity) { |
| | | // 回补库存(用于删除领料、改小领料、切换批次等场景)。 |
| | | BigDecimal addQuantity = defaultDecimal(quantity); |
| | | if (addQuantity.compareTo(BigDecimal.ZERO) <= 0) { |
| | | return; |
| | | } |
| | | StockInventory stockInventory = stockInventoryMapper.selectOne(buildStockWrapper(productModelId, batchNo)); |
| | | if (stockInventory == null) { |
| | | StockInventory newStockInventory = new StockInventory(); |
| | | newStockInventory.setProductModelId(productModelId); |
| | | newStockInventory.setBatchNo(batchNo); |
| | | newStockInventory.setQualitity(addQuantity); |
| | | newStockInventory.setLockedQuantity(BigDecimal.ZERO); |
| | | newStockInventory.setVersion(1); |
| | | stockInventoryMapper.insert(newStockInventory); |
| | | return; |
| | | } |
| | | StockInventoryDto stockInventoryDto = new StockInventoryDto(); |
| | | stockInventoryDto.setProductModelId(productModelId); |
| | | stockInventoryDto.setBatchNo(batchNo); |
| | | stockInventoryDto.setQualitity(addQuantity); |
| | | int affected = stockInventoryMapper.updateAddStockInventory(stockInventoryDto); |
| | | if (affected <= 0) { |
| | | throw new ServiceException("库存回退失败,产品规格ID=" + productModelId); |
| | | } |
| | | stockInventoryDto.setRecordType(String.valueOf(StockInQualifiedRecordTypeEnum.PICK_RETURN_IN.getCode())); |
| | | stockInventoryDto.setRecordId(0L); |
| | | stockInventoryService.addStockInRecordOnly(stockInventoryDto); |
| | | } |
| | | |
| | | private List<ProductionOrderPickDto> resolvePickItems(ProductionOrderPickDto dto) { |
| | | // 解析新增场景的领料明细集合。 |
| | | if (dto == null) { |
| | | throw new ServiceException("领料参数不能为空"); |
| | | throw new ServiceException("参数不能为空"); |
| | | } |
| | | if (dto.getPickList() != null && !dto.getPickList().isEmpty()) { |
| | | return dto.getPickList(); |
| | |
| | | } |
| | | |
| | | private List<ProductionOrderPickDto> resolveUpdateItems(ProductionOrderPickDto dto) { |
| | | // 解析更新场景的领料明细集合。 |
| | | if (dto.getPickList() != null) { |
| | | return dto.getPickList(); |
| | | } |
| | |
| | | } |
| | | |
| | | private boolean isEmptyUpdateItem(ProductionOrderPickDto dto) { |
| | | // 判断更新行是否为空白占位行。 |
| | | return dto.getId() == null |
| | | && dto.getProductModelId() == null |
| | | && dto.getPickQuantity() == null |
| | |
| | | } |
| | | |
| | | private Long resolveProductionOrderId(ProductionOrderPickDto dto) { |
| | | // 优先从主DTO解析订单ID,不存在时再从子项中回退查找。 |
| | | if (dto.getProductionOrderId() != null) { |
| | | return dto.getProductionOrderId(); |
| | | } |
| | |
| | | } |
| | | |
| | | private ProductionOrderPickDto mergeDto(ProductionOrderPickDto rootDto, ProductionOrderPickDto itemDto) { |
| | | // 合并规则: |
| | | // - itemDto 优先承载行级输入; |
| | | // - itemDto 缺失字段从 rootDto 兜底继承; |
| | | // - 输出 merged 作为统一业务入参。 |
| | | ProductionOrderPickDto merged = new ProductionOrderPickDto(); |
| | | // 先拷贝行级字段。 |
| | | if (itemDto != null) { |
| | | merged.setId(itemDto.getId()); |
| | | merged.setProductionOrderId(itemDto.getProductionOrderId()); |
| | |
| | | } |
| | | |
| | | private void validatePickParam(ProductionOrderPickDto dto, int rowNo) { |
| | | // 校验普通领料参数(订单、规格、数量、类型)。 |
| | | if (dto.getProductionOrderId() == null) { |
| | | throw new ServiceException("第" + rowNo + "条生产订单ID不能为空"); |
| | | throw new ServiceException("第" + rowNo + "行参数错误:生产订单ID不能为空"); |
| | | } |
| | | if (dto.getProductModelId() == null) { |
| | | throw new ServiceException("第" + rowNo + "条产品规格ID不能为空"); |
| | | throw new ServiceException("第" + rowNo + "行参数错误:产品规格不能为空"); |
| | | } |
| | | if (dto.getPickQuantity() == null || dto.getPickQuantity().compareTo(BigDecimal.ZERO) <= 0) { |
| | | throw new ServiceException("第" + rowNo + "条领料数量必须大于0"); |
| | | if (dto.getPickQuantity() == null || dto.getPickQuantity().compareTo(BigDecimal.ZERO) < 0) { |
| | | throw new ServiceException("第" + rowNo + "行参数错误:领料数量不能小于0"); |
| | | } |
| | | if (dto.getPickType() != null && dto.getPickType() != PICK_TYPE_NORMAL && dto.getPickType() != PICK_TYPE_FEEDING) { |
| | | throw new ServiceException("第" + rowNo + "条领料类型只能是1或2"); |
| | | throw new ServiceException("第" + rowNo + "行参数错误:领料类型仅支持1(领料)或2(补料)"); |
| | | } |
| | | } |
| | | |
| | | private void validateFeedingParam(ProductionOrderPickDto dto, int rowNo) { |
| | | // 校验补料参数(订单、领料ID、补料数量、类型)。 |
| | | if (dto.getProductionOrderId() == null) { |
| | | throw new ServiceException("第" + rowNo + "条生产订单ID不能为空"); |
| | | throw new ServiceException("第" + rowNo + "行参数错误:生产订单ID不能为空"); |
| | | } |
| | | if (dto.getId() == null) { |
| | | throw new ServiceException("第" + rowNo + "条领料ID不能为空"); |
| | | throw new ServiceException("第" + rowNo + "行参数错误:领料记录ID不能为空"); |
| | | } |
| | | if (dto.getFeedingQuantity() == null || dto.getFeedingQuantity().compareTo(BigDecimal.ZERO) <= 0) { |
| | | throw new ServiceException("第" + rowNo + "条本次补料数量必须大于0"); |
| | | if (dto.getFeedingQuantity() == null || dto.getFeedingQuantity().compareTo(BigDecimal.ZERO) < 0) { |
| | | throw new ServiceException("第" + rowNo + "行参数错误:补料数量不能小于0"); |
| | | } |
| | | if (!isFeedingPick(dto)) { |
| | | throw new ServiceException("第" + rowNo + "条补料类型必须为2"); |
| | | throw new ServiceException("第" + rowNo + "行参数错误:补料场景下领料类型必须为2"); |
| | | } |
| | | } |
| | | |
| | | private void validateReturnParam(ProductionOrderPickDto dto, int rowNo) { |
| | | // 校验退料参数(订单、领料ID、退料量、实际量)。 |
| | | if (dto.getProductionOrderId() == null) { |
| | | throw new ServiceException("第" + rowNo + "条生产订单ID不能为空"); |
| | | throw new ServiceException("第" + rowNo + "行参数错误:生产订单ID不能为空"); |
| | | } |
| | | if (dto.getId() == null) { |
| | | throw new ServiceException("第" + rowNo + "条领料ID不能为空"); |
| | | throw new ServiceException("第" + rowNo + "行参数错误:领料记录ID不能为空"); |
| | | } |
| | | if (dto.getReturnQty() == null || dto.getReturnQty().compareTo(BigDecimal.ZERO) < 0) { |
| | | throw new ServiceException("第" + rowNo + "条退料数量不能为空且不能小于0"); |
| | | throw new ServiceException("第" + rowNo + "行参数错误:退料数量不能小于0"); |
| | | } |
| | | if (dto.getActualQty() == null || dto.getActualQty().compareTo(BigDecimal.ZERO) < 0) { |
| | | throw new ServiceException("第" + rowNo + "条实际数量不能为空且不能小于0"); |
| | | throw new ServiceException("第" + rowNo + "行参数错误:实际数量不能小于0"); |
| | | } |
| | | } |
| | | |
| | | private boolean isFeedingRequest(ProductionOrderPickDto dto) { |
| | | // 判断当前请求是否属于补料流程。 |
| | | if (isFeedingPick(dto)) { |
| | | return true; |
| | | } |
| | |
| | | } |
| | | |
| | | private boolean isFeedingPick(ProductionOrderPickDto dto) { |
| | | // 判断当前行是否为补料类型。 |
| | | return dto != null && Objects.equals(dto.getPickType(), PICK_TYPE_FEEDING); |
| | | } |
| | | |
| | | private boolean isReturnRequest(ProductionOrderPickDto dto) { |
| | | // 判断当前请求是否属于退料流程。 |
| | | if (isReturnPick(dto)) { |
| | | return true; |
| | | } |
| | |
| | | } |
| | | |
| | | private boolean isReturnPick(ProductionOrderPickDto dto) { |
| | | // 判断当前行是否为退料类型。 |
| | | return dto != null && Boolean.TRUE.equals(dto.getReturned()); |
| | | } |
| | | |
| | | private BigDecimal sumFeedingQuantity(Long productionOrderId, Long pickId) { |
| | | // 汇总指定领料单的历史补料总量。 |
| | | List<ProductionOrderPickRecord> feedingRecords = productionOrderPickRecordMapper.selectList( |
| | | Wrappers.<ProductionOrderPickRecord>lambdaQuery() |
| | | .eq(ProductionOrderPickRecord::getProductionOrderId, productionOrderId) |
| | |
| | | } |
| | | |
| | | private BigDecimal calculateActualQty(ProductionOrderPick pick, BigDecimal feedingQty) { |
| | | // 按“领料+补料-退料”计算实际用量。 |
| | | return defaultDecimal(pick.getQuantity()) |
| | | .add(defaultDecimal(feedingQty)) |
| | | .subtract(defaultDecimal(pick.getReturnQty())); |
| | | } |
| | | |
| | | private String normalizeBatchNo(String batchNo) { |
| | | // 标准化批次号(去空白、空串转null)。 |
| | | if (StringUtils.isEmpty(batchNo)) { |
| | | return null; |
| | | } |
| | |
| | | return trimBatchNo.isEmpty() ? null : trimBatchNo; |
| | | } |
| | | private List<String> resolveBatchNoList(ProductionOrderPickDto dto) { |
| | | // 优先解析 batchNoList,空则回退解析 batchNo 字符串。 |
| | | List<String> normalizedBatchNoList = normalizeBatchNoList(dto.getBatchNoList()); |
| | | if (!normalizedBatchNoList.isEmpty()) { |
| | | return normalizedBatchNoList; |
| | |
| | | } |
| | | |
| | | private String pickInventoryBatchNo(List<String> batchNoList) { |
| | | // 从批次集合中取库存扣减使用的批次。 |
| | | if (batchNoList == null || batchNoList.isEmpty()) { |
| | | return null; |
| | | } |
| | |
| | | } |
| | | |
| | | private String resolveInventoryBatchNoFromStored(String storedBatchNo) { |
| | | // 从数据库存储批次字段中反解可用批次。 |
| | | return pickInventoryBatchNo(parseBatchNoValue(storedBatchNo)); |
| | | } |
| | | |
| | | private String formatBatchNoStorage(List<String> batchNoList) { |
| | | // 将批次集合格式化为数据库存储值。 |
| | | if (batchNoList == null || batchNoList.isEmpty()) { |
| | | return null; |
| | | } |
| | |
| | | } |
| | | |
| | | private List<String> normalizeBatchNoList(List<String> batchNoList) { |
| | | // 批量标准化批次号并去重。 |
| | | if (batchNoList == null || batchNoList.isEmpty()) { |
| | | return Collections.emptyList(); |
| | | } |
| | |
| | | } |
| | | |
| | | private void fillBatchNoList(List<ProductionOrderPickVo> detailList) { |
| | | // 将同订单+同规格+同工序的数据按组聚合批次,便于前端统一展示。 |
| | | if (detailList == null || detailList.isEmpty()) { |
| | | return; |
| | | } |
| | |
| | | } |
| | | |
| | | private void fillSelectableBatchNoList(List<ProductionOrderPickVo> detailList) { |
| | | // 合并“已选批次”和“库存可选批次”,用于前端下拉。 |
| | | if (detailList == null || detailList.isEmpty()) { |
| | | return; |
| | | } |
| | | // 先收集明细中涉及的规格ID,批量查询库存批次。 |
| | | Set<Long> productModelIdSet = detailList.stream() |
| | | .map(ProductionOrderPickVo::getProductModelId) |
| | | .filter(Objects::nonNull) |
| | |
| | | } |
| | | |
| | | private String buildBatchNoGroupKey(ProductionOrderPickVo detail) { |
| | | return String.valueOf(detail.getProductionOrderId()) + "|" |
| | | + String.valueOf(detail.getProductModelId()) + "|" |
| | | + String.valueOf(detail.getTechnologyOperationId()) + "|" |
| | | + String.valueOf(detail.getOperationName()); |
| | | // 构建批次聚合分组键。 |
| | | return detail.getProductionOrderId() + "|" |
| | | + detail.getProductModelId() + "|" |
| | | + detail.getTechnologyOperationId() + "|" |
| | | + detail.getOperationName(); |
| | | } |
| | | |
| | | private List<String> parseBatchNoValue(String rawBatchNoValue) { |
| | | // 批次解析兼容三种格式: |
| | | // 1) 单值:A001 |
| | | // 2) 逗号分隔:A001,A002 |
| | | // 3) 类JSON数组字符串:["A001","A002"] |
| | | String normalizedValue = normalizeBatchNo(rawBatchNoValue); |
| | | if (StringUtils.isEmpty(normalizedValue)) { |
| | | return Collections.emptyList(); |
| | | } |
| | | if (normalizedValue.startsWith("[") && normalizedValue.endsWith("]")) { |
| | | if (normalizedValue != null && normalizedValue.startsWith("[") && normalizedValue.endsWith("]")) { |
| | | String value = normalizedValue.substring(1, normalizedValue.length() - 1); |
| | | if (StringUtils.isEmpty(value)) { |
| | | return Collections.emptyList(); |
| | | } |
| | | List<String> parsed = Arrays.stream(value.split(",")) |
| | | .map(item -> item == null ? null : item.trim().replace("\"", "").replace("'", "")) |
| | | .map(item -> item.trim().replace("\"", "").replace("'", "")) |
| | | .collect(Collectors.toList()); |
| | | return normalizeBatchNoList(parsed); |
| | | } |
| | | if (normalizedValue.contains(",")) { |
| | | if (normalizedValue != null && normalizedValue.contains(",")) { |
| | | List<String> parsed = Arrays.stream(normalizedValue.split(",")) |
| | | .map(item -> item == null ? null : item.trim()) |
| | | .map(item -> item.trim()) |
| | | .collect(Collectors.toList()); |
| | | return normalizeBatchNoList(parsed); |
| | | } |
| | |
| | | } |
| | | |
| | | private LambdaQueryWrapper<StockInventory> buildStockWrapper(Long productModelId, String batchNo) { |
| | | // 构建库存查询条件(规格 + 批次)。 |
| | | LambdaQueryWrapper<StockInventory> wrapper = Wrappers.<StockInventory>lambdaQuery() |
| | | .eq(StockInventory::getProductModelId, productModelId); |
| | | if (StringUtils.isEmpty(batchNo)) { |
| | |
| | | } |
| | | |
| | | private BigDecimal defaultDecimal(BigDecimal value) { |
| | | // BigDecimal 空值兜底,统一按0处理。 |
| | | return value == null ? BigDecimal.ZERO : value; |
| | | } |
| | | |
| | | private String formatQuantity(BigDecimal value) { |
| | | // 数量格式化输出(去除末尾无效0)。 |
| | | return defaultDecimal(value).stripTrailingZeros().toPlainString(); |
| | | } |
| | | } |