package com.ruoyi.production.service.impl;
|
|
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.production.bean.vo.ProductionOrderPickVo;
|
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.pojo.ProductionOrder;
|
import com.ruoyi.production.pojo.ProductionOrderPick;
|
import com.ruoyi.production.pojo.ProductionOrderPickRecord;
|
import com.ruoyi.production.service.ProductionOrderPickService;
|
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.math.BigDecimal;
|
import java.util.*;
|
import java.util.function.Function;
|
import java.util.stream.Collectors;
|
|
/**
|
* 生产订单领料服务实现。
|
* 负责领料新增、更新、补料、退料及库存联动。
|
*/
|
@Service
|
@RequiredArgsConstructor
|
public class ProductionOrderPickServiceImpl extends ServiceImpl<ProductionOrderPickMapper, ProductionOrderPick> implements ProductionOrderPickService {
|
|
private static final byte PICK_TYPE_NORMAL = 1;
|
private static final byte PICK_TYPE_FEEDING = 2;
|
|
private final ProductionOrderMapper productionOrderMapper;
|
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(), storedBatchNo, resolvedDto.getPickQuantity(), rowNo);
|
|
// 保存领料主记录快照。
|
ProductionOrderPick orderPick = new ProductionOrderPick();
|
orderPick.setProductionOrderId(resolvedDto.getProductionOrderId());
|
orderPick.setProductModelId(resolvedDto.getProductModelId());
|
orderPick.setBatchNo(storedBatchNo);
|
orderPick.setQuantity(resolvedDto.getPickQuantity());
|
orderPick.setRemark(resolvedDto.getRemark());
|
orderPick.setOperationName(resolvedDto.getOperationName());
|
orderPick.setTechnologyOperationId(resolvedDto.getTechnologyOperationId());
|
orderPick.setDemandedQuantity(resolvedDto.getDemandedQuantity());
|
orderPick.setBom(resolvedDto.getBom());
|
orderPick.setReturned(false);
|
// 新增主记录。
|
baseMapper.insert(orderPick);
|
|
// 记录本次领料流水(before=0,after=本次领料量)。
|
insertPickRecord(orderPick.getId(),
|
resolvedDto.getProductionOrderId(),
|
resolvedDto.getProductionOperationTaskId(),
|
resolvedDto.getProductModelId(),
|
inventoryBatchNo,
|
resolvedDto.getPickQuantity(),
|
BigDecimal.ZERO,
|
resolvedDto.getPickQuantity(),
|
resolvedDto.getPickType(),
|
resolvedDto.getRemark(),
|
resolvedDto.getFeedingReason());
|
}
|
return true;
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public Boolean updatePick(ProductionOrderPickDto dto) {
|
// 领料更新入口(同接口兼容三类业务):
|
// 1) 普通领料改量/增删;
|
// 2) 补料(pickType=2);
|
// 3) 退料(returned=true)。
|
if (dto == null) {
|
throw new ServiceException("参数不能为空");
|
}
|
Long productionOrderId = resolveProductionOrderId(dto);
|
if (productionOrderId == null) {
|
throw new ServiceException("生产订单ID不能为空");
|
}
|
ProductionOrder productionOrder = productionOrderMapper.selectById(productionOrderId);
|
if (productionOrder == 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));
|
if (isEmptyUpdateItem(resolvedDto)) {
|
continue;
|
}
|
if (resolvedDto.getProductionOrderId() == null) {
|
resolvedDto.setProductionOrderId(productionOrderId);
|
}
|
validatePickParam(resolvedDto, rowNo);
|
|
if (resolvedDto.getId() == null) {
|
addNewPickInUpdate(resolvedDto, rowNo);
|
continue;
|
}
|
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();
|
}
|
List<ProductionOrderPickVo> detailList = baseMapper.listPickedDetailByOrderId(productionOrderId);
|
fillBatchNoList(detailList);
|
fillSelectableBatchNoList(detailList);
|
return detailList;
|
}
|
|
private void processDeletePickIds(ProductionOrderPickDto rootDto,
|
Map<Long, ProductionOrderPick> existingPickMap,
|
Long productionOrderId) {
|
// 处理前端显式删除ID:
|
// 1) 校验删除目标是否属于当前订单;
|
// 2) 回补库存;
|
// 3) 删除主记录;
|
// 4) 记录删除流水。
|
if (rootDto.getDeletePickIds() == null || rootDto.getDeletePickIds().isEmpty()) {
|
return;
|
}
|
Set<Long> deleteIdSet = new LinkedHashSet<>(rootDto.getDeletePickIds());
|
for (Long deleteId : deleteIdSet) {
|
if (deleteId == null) {
|
continue;
|
}
|
ProductionOrderPick existingPick = existingPickMap.get(deleteId);
|
if (existingPick == null || !Objects.equals(existingPick.getProductionOrderId(), productionOrderId)) {
|
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);
|
}
|
insertPickRecord(existingPick.getId(),
|
existingPick.getProductionOrderId(),
|
rootDto.getProductionOperationTaskId(),
|
existingPick.getProductModelId(),
|
oldBatchNo,
|
oldQuantity,
|
oldQuantity,
|
BigDecimal.ZERO,
|
rootDto.getPickType(),
|
rootDto.getRemark(),
|
rootDto.getFeedingReason());
|
existingPickMap.remove(deleteId);
|
}
|
}
|
|
private void processMissingPickItems(ProductionOrderPickDto rootDto,
|
Map<Long, ProductionOrderPick> existingPickMap,
|
Long productionOrderId,
|
Set<Long> keepPickIdSet) {
|
// 处理“前端未回传”的旧行:
|
// 对应场景是用户在前端删除行但未放入 deletePickIds。
|
// 这里兜底识别并执行回补库存 + 删除主记录 + 写流水。
|
if (rootDto.getPickList() == null) {
|
return;
|
}
|
List<ProductionOrderPick> missingPickList = existingPickMap.values().stream()
|
.filter(Objects::nonNull)
|
.filter(item -> item.getId() != null)
|
.filter(item -> Objects.equals(item.getProductionOrderId(), productionOrderId))
|
.filter(item -> !keepPickIdSet.contains(item.getId()))
|
.toList();
|
for (ProductionOrderPick missingPick : missingPickList) {
|
String oldBatchNo = resolveInventoryBatchNoFromStored(missingPick.getBatchNo());
|
BigDecimal oldQuantity = defaultDecimal(missingPick.getQuantity());
|
addInventory(missingPick.getProductModelId(), oldBatchNo, oldQuantity);
|
int affected = baseMapper.deleteById(missingPick.getId());
|
if (affected <= 0) {
|
throw new ServiceException("删除未回传领料记录失败,ID=" + missingPick.getId());
|
}
|
insertPickRecord(missingPick.getId(),
|
missingPick.getProductionOrderId(),
|
rootDto.getProductionOperationTaskId(),
|
missingPick.getProductModelId(),
|
oldBatchNo,
|
oldQuantity,
|
oldQuantity,
|
BigDecimal.ZERO,
|
rootDto.getPickType(),
|
rootDto.getRemark(),
|
rootDto.getFeedingReason());
|
existingPickMap.remove(missingPick.getId());
|
}
|
}
|
|
private void addNewPickInUpdate(ProductionOrderPickDto dto, int rowNo) {
|
// 更新场景下新增一条领料:扣库存 -> 新增主记录 -> 写流水。
|
List<String> batchNoList = resolveBatchNoList(dto);
|
String inventoryBatchNo = pickInventoryBatchNo(batchNoList);
|
String storedBatchNo = formatBatchNoStorage(batchNoList);
|
subtractInventory(dto.getProductModelId(), storedBatchNo, dto.getPickQuantity(), rowNo);
|
|
ProductionOrderPick orderPick = new ProductionOrderPick();
|
orderPick.setProductionOrderId(dto.getProductionOrderId());
|
orderPick.setProductModelId(dto.getProductModelId());
|
orderPick.setBatchNo(storedBatchNo);
|
orderPick.setQuantity(dto.getPickQuantity());
|
orderPick.setRemark(dto.getRemark());
|
orderPick.setOperationName(dto.getOperationName());
|
orderPick.setTechnologyOperationId(dto.getTechnologyOperationId());
|
orderPick.setDemandedQuantity(dto.getDemandedQuantity());
|
orderPick.setBom(dto.getBom());
|
orderPick.setReturned(false);
|
baseMapper.insert(orderPick);
|
|
insertPickRecord(orderPick.getId(),
|
dto.getProductionOrderId(),
|
dto.getProductionOperationTaskId(),
|
dto.getProductModelId(),
|
inventoryBatchNo,
|
dto.getPickQuantity(),
|
BigDecimal.ZERO,
|
dto.getPickQuantity(),
|
dto.getPickType(),
|
dto.getRemark(),
|
dto.getFeedingReason());
|
}
|
|
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;
|
ProductionOrderPickDto resolvedDto = mergeDto(rootDto, pickItems.get(i));
|
if (isEmptyUpdateItem(resolvedDto)) {
|
continue;
|
}
|
if (!isFeedingPick(resolvedDto)) {
|
throw new ServiceException("补料请求中存在非补料类型数据");
|
}
|
if (resolvedDto.getProductionOrderId() == null) {
|
resolvedDto.setProductionOrderId(productionOrderId);
|
}
|
validateFeedingParam(resolvedDto, rowNo);
|
|
ProductionOrderPick oldPick = existingPickMap.get(resolvedDto.getId());
|
if (oldPick == null || !Objects.equals(oldPick.getProductionOrderId(), productionOrderId)) {
|
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 + "行补料失败:产品规格与原领料记录不一致");
|
}
|
Long productModelId = oldPick.getProductModelId();
|
List<String> batchNoList = resolveBatchNoList(dto);
|
String inventoryBatchNo = batchNoList.isEmpty()
|
? resolveInventoryBatchNoFromStored(oldPick.getBatchNo())
|
: 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(),
|
dto.getProductionOrderId(),
|
dto.getProductionOperationTaskId(),
|
productModelId,
|
inventoryBatchNo,
|
feedingQuantity,
|
beforeFeedingQty,
|
afterFeedingQty,
|
PICK_TYPE_FEEDING,
|
dto.getRemark(),
|
dto.getFeedingReason());
|
|
ProductionOrderPick updatePick = new ProductionOrderPick();
|
updatePick.setId(oldPick.getId());
|
updatePick.setFeedingQty(afterFeedingQty);
|
updatePick.setActualQty(calculateActualQty(oldPick, afterFeedingQty));
|
// 回写主记录的补料累计值与实际用量。
|
int affected = baseMapper.updateById(updatePick);
|
if (affected <= 0) {
|
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;
|
ProductionOrderPickDto resolvedDto = mergeDto(rootDto, pickItems.get(i));
|
if (isEmptyUpdateItem(resolvedDto)) {
|
continue;
|
}
|
if (resolvedDto.getProductionOrderId() == null) {
|
resolvedDto.setProductionOrderId(productionOrderId);
|
}
|
validateReturnParam(resolvedDto, rowNo);
|
|
ProductionOrderPick oldPick = existingPickMap.get(resolvedDto.getId());
|
if (oldPick == null || !Objects.equals(oldPick.getProductionOrderId(), productionOrderId)) {
|
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.setActualQty(dto.getActualQty());
|
updatePick.setReturned(true);
|
int affected = baseMapper.updateById(updatePick);
|
if (affected <= 0) {
|
throw new ServiceException("第" + rowNo + "行退料失败:更新领料主记录失败");
|
}
|
oldPick.setReturnQty(updatePick.getReturnQty());
|
oldPick.setActualQty(updatePick.getActualQty());
|
oldPick.setReturned(true);
|
}
|
|
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 + "行更新失败:未找到对应的领料记录");
|
}
|
|
Long oldProductModelId = oldPick.getProductModelId();
|
String oldBatchNo = resolveInventoryBatchNoFromStored(oldPick.getBatchNo());
|
BigDecimal oldQuantity = defaultDecimal(oldPick.getQuantity());
|
|
Long newProductModelId = dto.getProductModelId();
|
List<String> newBatchNoList = resolveBatchNoList(dto);
|
String newBatchNo = pickInventoryBatchNo(newBatchNoList);
|
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, newStoredBatchNo, delta, rowNo);
|
} else if (delta.compareTo(BigDecimal.ZERO) < 0) {
|
addInventory(oldProductModelId, oldBatchNo, delta.abs());
|
}
|
} else {
|
// 规格或批次变化:先回补旧库存,再扣减新库存。
|
addInventory(oldProductModelId, oldBatchNo, oldQuantity);
|
subtractInventory(newProductModelId, newStoredBatchNo, newQuantity, rowNo);
|
}
|
|
oldPick.setProductModelId(newProductModelId);
|
oldPick.setBatchNo(newStoredBatchNo);
|
oldPick.setQuantity(newQuantity);
|
oldPick.setRemark(dto.getRemark());
|
oldPick.setOperationName(dto.getOperationName());
|
oldPick.setTechnologyOperationId(dto.getTechnologyOperationId());
|
if (dto.getDemandedQuantity() != null) {
|
oldPick.setDemandedQuantity(dto.getDemandedQuantity());
|
}
|
if (dto.getBom() != null) {
|
oldPick.setBom(dto.getBom());
|
}
|
int affected = baseMapper.updateById(oldPick);
|
if (affected <= 0) {
|
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(),
|
dto.getProductionOrderId(),
|
dto.getProductionOperationTaskId(),
|
newProductModelId,
|
newBatchNo,
|
recordQuantity,
|
oldQuantity,
|
newQuantity,
|
dto.getPickType(),
|
dto.getRemark(),
|
dto.getFeedingReason());
|
}
|
}
|
|
private void insertPickRecord(Long pickId,
|
Long productionOrderId,
|
Long productionOperationTaskId,
|
Long productModelId,
|
String batchNo,
|
BigDecimal pickQuantity,
|
BigDecimal beforeQuantity,
|
BigDecimal afterQuantity,
|
Byte pickType,
|
String remark,
|
String feedingReason) {
|
// 写领料流水记录:统一记录领料/补料/退料数量变化轨迹。
|
ProductionOrderPickRecord pickRecord = new ProductionOrderPickRecord();
|
pickRecord.setPickId(pickId);
|
pickRecord.setProductionOrderId(productionOrderId);
|
pickRecord.setProductionOperationTaskId(productionOperationTaskId);
|
pickRecord.setProductModelId(productModelId);
|
pickRecord.setBatchNo(batchNo);
|
pickRecord.setPickQuantity(defaultDecimal(pickQuantity));
|
pickRecord.setBeforeQuantity(defaultDecimal(beforeQuantity));
|
pickRecord.setAfterQuantity(defaultDecimal(afterQuantity));
|
pickRecord.setPickType(pickType == null ? PICK_TYPE_NORMAL : pickType);
|
pickRecord.setRemark(remark);
|
pickRecord.setFeedingReason(feedingReason);
|
productionOrderPickRecordMapper.insert(pickRecord);
|
}
|
|
private void subtractInventory(Long productModelId, String batchNo, BigDecimal quantity, int rowNo) {
|
// 扣减库存总流程:
|
// 1) 解析批次列表;
|
// 2) 计算每个批次可用量与总可用量;
|
// 3) 按批次顺序逐笔扣减,直到扣完目标数量;
|
// 4) 任一步失败即抛错并回滚事务。
|
BigDecimal deductQuantity = defaultDecimal(quantity);
|
// 领料数量小于等于0时,不需要执行库存扣减。
|
if (deductQuantity.compareTo(BigDecimal.ZERO) <= 0) {
|
return;
|
}
|
|
List<String> batchNoList = parseBatchNoValue(batchNo);
|
if (batchNoList.isEmpty()) {
|
batchNoList = Collections.singletonList(null);
|
}
|
|
// 先计算各批次可用量,避免边扣边算导致判断不一致。
|
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;
|
}
|
StockInventoryDto stockInventoryDto = new StockInventoryDto();
|
stockInventoryDto.setProductModelId(productModelId);
|
stockInventoryDto.setBatchNo(batchNo);
|
stockInventoryDto.setQualitity(addQuantity);
|
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("参数不能为空");
|
}
|
if (dto.getPickList() != null && !dto.getPickList().isEmpty()) {
|
return dto.getPickList();
|
}
|
return Collections.singletonList(dto);
|
}
|
|
private List<ProductionOrderPickDto> resolveUpdateItems(ProductionOrderPickDto dto) {
|
// 解析更新场景的领料明细集合。
|
if (dto.getPickList() != null) {
|
return dto.getPickList();
|
}
|
if (isEmptyUpdateItem(dto)) {
|
return Collections.emptyList();
|
}
|
return Collections.singletonList(dto);
|
}
|
|
private boolean isEmptyUpdateItem(ProductionOrderPickDto dto) {
|
// 判断更新行是否为空白占位行。
|
return dto.getId() == null
|
&& dto.getProductModelId() == null
|
&& dto.getPickQuantity() == null
|
&& StringUtils.isEmpty(dto.getBatchNo())
|
&& (dto.getBatchNoList() == null || dto.getBatchNoList().isEmpty())
|
&& dto.getPickType() == null
|
&& dto.getFeedingQuantity() == null
|
&& StringUtils.isEmpty(dto.getFeedingReason())
|
&& dto.getReturnQty() == null
|
&& dto.getActualQty() == null
|
&& dto.getReturned() == null
|
&& dto.getProductionOperationTaskId() == null
|
&& dto.getTechnologyOperationId() == null
|
&& StringUtils.isEmpty(dto.getOperationName())
|
&& dto.getDemandedQuantity() == null
|
&& dto.getBom() == null
|
&& StringUtils.isEmpty(dto.getRemark());
|
}
|
|
private Long resolveProductionOrderId(ProductionOrderPickDto dto) {
|
// 优先从主DTO解析订单ID,不存在时再从子项中回退查找。
|
if (dto.getProductionOrderId() != null) {
|
return dto.getProductionOrderId();
|
}
|
if (dto.getPickList() == null || dto.getPickList().isEmpty()) {
|
return null;
|
}
|
return dto.getPickList().stream()
|
.filter(Objects::nonNull)
|
.map(ProductionOrderPickDto::getProductionOrderId)
|
.filter(Objects::nonNull)
|
.findFirst()
|
.orElse(null);
|
}
|
|
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());
|
merged.setProductionOperationTaskId(itemDto.getProductionOperationTaskId());
|
merged.setProductModelId(itemDto.getProductModelId());
|
merged.setBatchNo(itemDto.getBatchNo());
|
merged.setBatchNoList(itemDto.getBatchNoList());
|
merged.setPickQuantity(itemDto.getPickQuantity());
|
merged.setPickType(itemDto.getPickType());
|
merged.setRemark(itemDto.getRemark());
|
merged.setFeedingReason(itemDto.getFeedingReason());
|
merged.setFeedingQuantity(itemDto.getFeedingQuantity());
|
merged.setTechnologyOperationId(itemDto.getTechnologyOperationId());
|
merged.setOperationName(itemDto.getOperationName());
|
merged.setDemandedQuantity(itemDto.getDemandedQuantity());
|
merged.setBom(itemDto.getBom());
|
merged.setReturnQty(itemDto.getReturnQty());
|
merged.setActualQty(itemDto.getActualQty());
|
merged.setReturned(itemDto.getReturned());
|
}
|
if (merged.getId() == null) {
|
merged.setId(rootDto.getId());
|
}
|
if (merged.getProductionOrderId() == null) {
|
merged.setProductionOrderId(rootDto.getProductionOrderId());
|
}
|
if (merged.getProductionOperationTaskId() == null) {
|
merged.setProductionOperationTaskId(rootDto.getProductionOperationTaskId());
|
}
|
if (merged.getProductModelId() == null) {
|
merged.setProductModelId(rootDto.getProductModelId());
|
}
|
if (merged.getBatchNo() == null) {
|
merged.setBatchNo(rootDto.getBatchNo());
|
}
|
if (merged.getBatchNoList() == null || merged.getBatchNoList().isEmpty()) {
|
merged.setBatchNoList(rootDto.getBatchNoList());
|
}
|
if (merged.getPickQuantity() == null) {
|
merged.setPickQuantity(rootDto.getPickQuantity());
|
}
|
if (merged.getPickType() == null) {
|
merged.setPickType(rootDto.getPickType());
|
}
|
if (merged.getRemark() == null) {
|
merged.setRemark(rootDto.getRemark());
|
}
|
if (merged.getFeedingReason() == null) {
|
merged.setFeedingReason(rootDto.getFeedingReason());
|
}
|
if (merged.getFeedingQuantity() == null) {
|
merged.setFeedingQuantity(rootDto.getFeedingQuantity());
|
}
|
if (merged.getTechnologyOperationId() == null) {
|
merged.setTechnologyOperationId(rootDto.getTechnologyOperationId());
|
}
|
if (merged.getOperationName() == null) {
|
merged.setOperationName(rootDto.getOperationName());
|
}
|
if (merged.getDemandedQuantity() == null) {
|
merged.setDemandedQuantity(rootDto.getDemandedQuantity());
|
}
|
if (merged.getBom() == null) {
|
merged.setBom(rootDto.getBom());
|
}
|
if (merged.getReturnQty() == null) {
|
merged.setReturnQty(rootDto.getReturnQty());
|
}
|
if (merged.getActualQty() == null) {
|
merged.setActualQty(rootDto.getActualQty());
|
}
|
if (merged.getReturned() == null) {
|
merged.setReturned(rootDto.getReturned());
|
}
|
return merged;
|
}
|
|
private void validatePickParam(ProductionOrderPickDto dto, int rowNo) {
|
// 校验普通领料参数(订单、规格、数量、类型)。
|
if (dto.getProductionOrderId() == null) {
|
throw new ServiceException("第" + rowNo + "行参数错误:生产订单ID不能为空");
|
}
|
if (dto.getProductModelId() == null) {
|
throw new ServiceException("第" + rowNo + "行参数错误:产品规格不能为空");
|
}
|
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(补料)");
|
}
|
}
|
|
private void validateFeedingParam(ProductionOrderPickDto dto, int rowNo) {
|
// 校验补料参数(订单、领料ID、补料数量、类型)。
|
if (dto.getProductionOrderId() == null) {
|
throw new ServiceException("第" + rowNo + "行参数错误:生产订单ID不能为空");
|
}
|
if (dto.getId() == null) {
|
throw new ServiceException("第" + rowNo + "行参数错误:领料记录ID不能为空");
|
}
|
if (dto.getFeedingQuantity() == null || dto.getFeedingQuantity().compareTo(BigDecimal.ZERO) < 0) {
|
throw new ServiceException("第" + rowNo + "行参数错误:补料数量不能小于0");
|
}
|
if (!isFeedingPick(dto)) {
|
throw new ServiceException("第" + rowNo + "行参数错误:补料场景下领料类型必须为2");
|
}
|
}
|
|
private void validateReturnParam(ProductionOrderPickDto dto, int rowNo) {
|
// 校验退料参数(订单、领料ID、退料量、实际量)。
|
if (dto.getProductionOrderId() == null) {
|
throw new ServiceException("第" + rowNo + "行参数错误:生产订单ID不能为空");
|
}
|
if (dto.getId() == null) {
|
throw new ServiceException("第" + rowNo + "行参数错误:领料记录ID不能为空");
|
}
|
if (dto.getReturnQty() == null || dto.getReturnQty().compareTo(BigDecimal.ZERO) < 0) {
|
throw new ServiceException("第" + rowNo + "行参数错误:退料数量不能小于0");
|
}
|
if (dto.getActualQty() == null || dto.getActualQty().compareTo(BigDecimal.ZERO) < 0) {
|
throw new ServiceException("第" + rowNo + "行参数错误:实际数量不能小于0");
|
}
|
}
|
|
private boolean isFeedingRequest(ProductionOrderPickDto dto) {
|
// 判断当前请求是否属于补料流程。
|
if (isFeedingPick(dto)) {
|
return true;
|
}
|
if (dto.getPickList() == null || dto.getPickList().isEmpty()) {
|
return false;
|
}
|
return dto.getPickList().stream()
|
.filter(Objects::nonNull)
|
.anyMatch(this::isFeedingPick);
|
}
|
|
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;
|
}
|
if (dto.getPickList() == null || dto.getPickList().isEmpty()) {
|
return false;
|
}
|
return dto.getPickList().stream()
|
.filter(Objects::nonNull)
|
.anyMatch(this::isReturnPick);
|
}
|
|
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)
|
.eq(ProductionOrderPickRecord::getPickId, pickId)
|
.eq(ProductionOrderPickRecord::getPickType, PICK_TYPE_FEEDING));
|
return feedingRecords.stream()
|
.map(ProductionOrderPickRecord::getPickQuantity)
|
.map(this::defaultDecimal)
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
}
|
|
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;
|
}
|
String trimBatchNo = batchNo.trim();
|
return trimBatchNo.isEmpty() ? null : trimBatchNo;
|
}
|
private List<String> resolveBatchNoList(ProductionOrderPickDto dto) {
|
// 优先解析 batchNoList,空则回退解析 batchNo 字符串。
|
List<String> normalizedBatchNoList = normalizeBatchNoList(dto.getBatchNoList());
|
if (!normalizedBatchNoList.isEmpty()) {
|
return normalizedBatchNoList;
|
}
|
return parseBatchNoValue(dto.getBatchNo());
|
}
|
|
private String pickInventoryBatchNo(List<String> batchNoList) {
|
// 从批次集合中取库存扣减使用的批次。
|
if (batchNoList == null || batchNoList.isEmpty()) {
|
return null;
|
}
|
return batchNoList.get(0);
|
}
|
|
private String resolveInventoryBatchNoFromStored(String storedBatchNo) {
|
// 从数据库存储批次字段中反解可用批次。
|
return pickInventoryBatchNo(parseBatchNoValue(storedBatchNo));
|
}
|
|
private String formatBatchNoStorage(List<String> batchNoList) {
|
// 将批次集合格式化为数据库存储值。
|
if (batchNoList == null || batchNoList.isEmpty()) {
|
return null;
|
}
|
if (batchNoList.size() == 1) {
|
return batchNoList.get(0);
|
}
|
return String.join(",", batchNoList);
|
}
|
|
private List<String> normalizeBatchNoList(List<String> batchNoList) {
|
// 批量标准化批次号并去重。
|
if (batchNoList == null || batchNoList.isEmpty()) {
|
return Collections.emptyList();
|
}
|
LinkedHashSet<String> normalizedSet = new LinkedHashSet<>();
|
for (String batchNo : batchNoList) {
|
String normalizedBatchNo = normalizeBatchNo(batchNo);
|
if (!StringUtils.isEmpty(normalizedBatchNo)) {
|
normalizedSet.add(normalizedBatchNo);
|
}
|
}
|
return new ArrayList<>(normalizedSet);
|
}
|
|
private void fillBatchNoList(List<ProductionOrderPickVo> detailList) {
|
// 将同订单+同规格+同工序的数据按组聚合批次,便于前端统一展示。
|
if (detailList == null || detailList.isEmpty()) {
|
return;
|
}
|
Map<String, LinkedHashSet<String>> batchNoGroupMap = new HashMap<>();
|
for (ProductionOrderPickVo detail : detailList) {
|
String key = buildBatchNoGroupKey(detail);
|
LinkedHashSet<String> batchSet = batchNoGroupMap.computeIfAbsent(key, k -> new LinkedHashSet<>());
|
batchSet.addAll(parseBatchNoValue(detail.getBatchNo()));
|
if (detail.getBatchNoList() != null && !detail.getBatchNoList().isEmpty()) {
|
batchSet.addAll(normalizeBatchNoList(detail.getBatchNoList()));
|
}
|
}
|
for (ProductionOrderPickVo detail : detailList) {
|
String key = buildBatchNoGroupKey(detail);
|
LinkedHashSet<String> batchSet = batchNoGroupMap.get(key);
|
detail.setBatchNoList(batchSet == null ? Collections.emptyList() : new ArrayList<>(batchSet));
|
}
|
}
|
|
private void fillSelectableBatchNoList(List<ProductionOrderPickVo> detailList) {
|
// 合并“已选批次”和“库存可选批次”,用于前端下拉。
|
if (detailList == null || detailList.isEmpty()) {
|
return;
|
}
|
// 先收集明细中涉及的规格ID,批量查询库存批次。
|
Set<Long> productModelIdSet = detailList.stream()
|
.map(ProductionOrderPickVo::getProductModelId)
|
.filter(Objects::nonNull)
|
.collect(Collectors.toSet());
|
if (productModelIdSet.isEmpty()) {
|
return;
|
}
|
List<StockInventory> stockBatchList = stockInventoryMapper.listSelectableBatchNoByProductModelIds(
|
new ArrayList<>(productModelIdSet));
|
Map<Long, LinkedHashSet<String>> stockBatchMap = new HashMap<>();
|
for (StockInventory stockInventory : stockBatchList) {
|
if (stockInventory == null || stockInventory.getProductModelId() == null) {
|
continue;
|
}
|
String normalizedBatchNo = normalizeBatchNo(stockInventory.getBatchNo());
|
if (StringUtils.isEmpty(normalizedBatchNo)) {
|
continue;
|
}
|
stockBatchMap.computeIfAbsent(stockInventory.getProductModelId(), k -> new LinkedHashSet<>())
|
.add(normalizedBatchNo);
|
}
|
for (ProductionOrderPickVo detail : detailList) {
|
LinkedHashSet<String> mergedBatchSet = new LinkedHashSet<>();
|
mergedBatchSet.addAll(normalizeBatchNoList(detail.getBatchNoList()));
|
LinkedHashSet<String> selectableBatchSet = stockBatchMap.get(detail.getProductModelId());
|
if (selectableBatchSet != null) {
|
mergedBatchSet.addAll(selectableBatchSet);
|
}
|
detail.setBatchNoList(new ArrayList<>(mergedBatchSet));
|
}
|
}
|
|
private String buildBatchNoGroupKey(ProductionOrderPickVo detail) {
|
// 构建批次聚合分组键。
|
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 != 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.trim().replace("\"", "").replace("'", ""))
|
.collect(Collectors.toList());
|
return normalizeBatchNoList(parsed);
|
}
|
if (normalizedValue != null && normalizedValue.contains(",")) {
|
List<String> parsed = Arrays.stream(normalizedValue.split(","))
|
.map(item -> item.trim())
|
.collect(Collectors.toList());
|
return normalizeBatchNoList(parsed);
|
}
|
return Collections.singletonList(normalizedValue);
|
}
|
|
private LambdaQueryWrapper<StockInventory> buildStockWrapper(Long productModelId, String batchNo) {
|
// 构建库存查询条件(规格 + 批次)。
|
LambdaQueryWrapper<StockInventory> wrapper = Wrappers.<StockInventory>lambdaQuery()
|
.eq(StockInventory::getProductModelId, productModelId);
|
if (StringUtils.isEmpty(batchNo)) {
|
wrapper.isNull(StockInventory::getBatchNo);
|
} else {
|
wrapper.eq(StockInventory::getBatchNo, batchNo);
|
}
|
return wrapper;
|
}
|
|
private BigDecimal defaultDecimal(BigDecimal value) {
|
// BigDecimal 空值兜底,统一按0处理。
|
return value == null ? BigDecimal.ZERO : value;
|
}
|
|
private String formatQuantity(BigDecimal value) {
|
// 数量格式化输出(去除末尾无效0)。
|
return defaultDecimal(value).stripTrailingZeros().toPlainString();
|
}
|
}
|