| | |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.wm.itemconsume.MesWmItemConsumeMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstock.MesWmMaterialStockMapper; |
| | | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| | | import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants; |
| | | import cn.iocoder.yudao.module.mes.enums.wm.MesWmItemConsumeStatusEnum; |
| | | import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum; |
| | |
| | | import jakarta.annotation.Resource; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | import org.springframework.validation.annotation.Validated; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDateTime; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.HashMap; |
| | | import java.util.stream.Collectors; |
| | | |
| | | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| | | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; |
| | | import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_FEEDBACK_ROUTE_PROCESS_INVALID; |
| | | import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_ITEM_CONSUME_STOCK_INSUFFICIENT; |
| | | |
| | | /** |
| | | * MES 物料消耗记录 Service 实现类 |
| | |
| | | private MesWmWarehouseAreaService areaService; |
| | | @Resource |
| | | private MesWmMaterialStockService materialStockService; |
| | | @Resource |
| | | private MesWmMaterialStockMapper materialStockMapper; |
| | | @Autowired |
| | | private MesMdItemService mesMdItemService; |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public MesWmItemConsumeDO generateItemConsume(MesProFeedbackDO feedback) { |
| | | // 1.1 校验工单编号有效 |
| | | if (feedback.getWorkOrderId() == null) { |
| | |
| | | |
| | | // 3. 按线边库 FIFO 生成消耗明细 |
| | | List<MesWmItemConsumeDetailDO> allDetails = new ArrayList<>(); |
| | | Map<Long, BigDecimal> allocations = new HashMap<>(); |
| | | // 以库存 ID 固定顺序加锁,并重新读取冻结/占用数据。审批的外层事务持续持锁至扣减完成。 |
| | | Map<Long, List<MesWmMaterialStockDO>> stocksByItem = new HashMap<>(); |
| | | for (MesWmItemConsumeLineDO line : lines) { |
| | | if (stocksByItem.containsKey(line.getItemId())) continue; |
| | | MesMdItemDO item = mesMdItemService.validateItemExists(line.getItemId()); |
| | | Long stockItemId = item.getMdmItemId() != null ? item.getMdmItemId() : item.getId(); |
| | | MesWmMaterialStockListReqVO query = new MesWmMaterialStockListReqVO().setItemId(stockItemId) |
| | | .setWarehouseId(virtualWarehouse.getId()).setLocationId(virtualLocation.getId()).setAreaId(virtualArea.getId()); |
| | | stocksByItem.put(line.getItemId(), materialStockService.getMaterialStockList(query)); |
| | | } |
| | | Map<Long, MesWmMaterialStockDO> lockedStocks = new HashMap<>(); |
| | | stocksByItem.values().stream().flatMap(List::stream).map(MesWmMaterialStockDO::getId) |
| | | .filter(java.util.Objects::nonNull).distinct().sorted().forEach(id -> { |
| | | MesWmMaterialStockDO locked = materialStockMapper.selectOne(new LambdaQueryWrapperX<MesWmMaterialStockDO>() |
| | | .eq(MesWmMaterialStockDO::getId, id).last("FOR UPDATE")); |
| | | if (locked != null) lockedStocks.put(id, locked); |
| | | }); |
| | | for (MesWmItemConsumeLineDO line : lines) { |
| | | generateDetailForLine(line, consume.getId(), |
| | | virtualWarehouse, virtualLocation, virtualArea, allDetails); |
| | | virtualWarehouse, virtualLocation, virtualArea, allDetails, allocations, |
| | | stocksByItem.get(line.getItemId()).stream().map(s -> lockedStocks.get(s.getId())) |
| | | .filter(java.util.Objects::nonNull).toList()); |
| | | } |
| | | if (CollUtil.isNotEmpty(allDetails)) { |
| | | itemConsumeDetailService.createItemConsumeDetailBatch(allDetails); |
| | |
| | | * 规则: |
| | | * 1. 查询该物料在虚拟线边库中的库存记录,按入库时间升序(FIFO) |
| | | * 2. 依次从各批次分配,直到消耗量满足 |
| | | * 3. 如果线边库无库存或库存不足,剩余数量生成无批次明细(待后续盘库核销) |
| | | * 3. 扣除冻结、占用及本次已分配量后不足时中止报工审批 |
| | | */ |
| | | private void generateDetailForLine(MesWmItemConsumeLineDO line, Long consumeId, |
| | | MesWmWarehouseDO warehouse, MesWmWarehouseLocationDO location, MesWmWarehouseAreaDO area, |
| | | List<MesWmItemConsumeDetailDO> allDetails) { |
| | | List<MesWmItemConsumeDetailDO> allDetails, Map<Long, BigDecimal> allocations, |
| | | List<MesWmMaterialStockDO> lockedStocks) { |
| | | // 查询该物料在线边库中的库存(按 receiptTime 升序 = FIFO;已排除 quantity = 0) |
| | | MesWmMaterialStockListReqVO stockQuery = new MesWmMaterialStockListReqVO().setItemId(line.getItemId()) |
| | | .setWarehouseId(warehouse.getId()).setLocationId(location.getId()).setAreaId(area.getId()); |
| | | List<MesWmMaterialStockDO> stocks = materialStockService.getMaterialStockList(stockQuery) |
| | | .stream().filter(s -> s.getQuantity().compareTo(BigDecimal.ZERO) > 0).collect(Collectors.toList()); |
| | | |
| | | if (CollUtil.isEmpty(stocks)) { |
| | | // 线边库无该物料库存 → 生成不带批次的明细 |
| | | allDetails.add(buildDetail(consumeId, line.getId(), null, line.getItemId(), |
| | | line.getQuantity(), null, null, warehouse, location, area)); |
| | | return; |
| | | } |
| | | MesMdItemDO item = mesMdItemService.validateItemExists(line.getItemId()); |
| | | Long stockItemId = item.getMdmItemId() != null ? item.getMdmItemId() : item.getId(); |
| | | List<MesWmMaterialStockDO> stocks = lockedStocks |
| | | .stream().filter(s -> java.util.Objects.equals(s.getItemId(), stockItemId) |
| | | && java.util.Objects.equals(s.getWarehouseId(), warehouse.getId()) |
| | | && java.util.Objects.equals(s.getLocationId(), location.getId()) |
| | | && java.util.Objects.equals(s.getAreaId(), area.getId()) |
| | | && !Boolean.TRUE.equals(s.getFrozen()) && s.getQuantity() != null |
| | | && s.getQuantity().compareTo(BigDecimal.ZERO) > 0).collect(Collectors.toList()); |
| | | |
| | | // 按 FIFO 依次从各批次分配 |
| | | BigDecimal remaining = line.getQuantity(); |
| | |
| | | if (remaining.compareTo(BigDecimal.ZERO) <= 0) { |
| | | break; |
| | | } |
| | | BigDecimal allocate = stock.getQuantity().min(remaining); |
| | | BigDecimal available = stock.getQuantity() |
| | | .subtract(stock.getFrozenQuantity() == null ? BigDecimal.ZERO : stock.getFrozenQuantity()) |
| | | .subtract(stock.getReservedQuantity() == null ? BigDecimal.ZERO : stock.getReservedQuantity()) |
| | | .subtract(allocations.getOrDefault(stock.getId(), BigDecimal.ZERO)); |
| | | if (available.signum() <= 0) continue; |
| | | BigDecimal allocate = available.min(remaining); |
| | | allDetails.add(buildDetail(consumeId, line.getId(), stock.getId(), line.getItemId(), |
| | | allocate, stock.getBatchId(), stock.getBatchCode(), warehouse, location, area)); |
| | | remaining = remaining.subtract(allocate); |
| | | allocations.merge(stock.getId(), allocate, BigDecimal::add); |
| | | } |
| | | |
| | | // 库存不足时,剩余数量生成无批次明细(待后续盘库手工核销) |
| | | if (remaining.compareTo(BigDecimal.ZERO) > 0) { |
| | | allDetails.add(buildDetail(consumeId, line.getId(), null, line.getItemId(), |
| | | remaining, null, null, warehouse, location, area)); |
| | | throw exception(WM_ITEM_CONSUME_STOCK_INSUFFICIENT, item.getName()); |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void finishItemConsume(Long consumeId) { |
| | | // 1. 查询消耗明细(detail 级别,包含精确的批次/仓库信息) |
| | | List<MesWmItemConsumeDetailDO> details = itemConsumeDetailService |
| | |
| | | |
| | | // 2. 遍历明细,创建库存事务(按批次精确扣减线边库) |
| | | for (MesWmItemConsumeDetailDO detail : details) { |
| | | MesMdItemDO item = mesMdItemService.getItem(detail.getItemId()); |
| | | MesMdItemDO item = mesMdItemService.validateItemExists(detail.getItemId()); |
| | | wmTransactionService.createTransaction(new MesWmTransactionSaveReqDTO() |
| | | .setType(MesWmTransactionTypeEnum.OUT.getType()) |
| | | .setItemId(item.getMdmItemId()) |
| | | .setItemId(item.getMdmItemId() != null ? item.getMdmItemId() : item.getId()) |
| | | .setQuantity(detail.getQuantity().negate()) |
| | | .setBatchId(detail.getBatchId()).setBatchCode(detail.getBatchCode()) |
| | | .setWarehouseId(detail.getWarehouseId()) |
| | | .setLocationId(detail.getLocationId()) |
| | | .setAreaId(detail.getAreaId()) |
| | | .setCheckFlag(false) // 线边库允许负库存 |
| | | .setMaterialStockId(detail.getMaterialStockId()) |
| | | .setCheckFlag(true) |
| | | .setBizType(MesBizTypeConstants.WM_ITEM_CONSUME) |
| | | .setBizId(consumeId) |
| | | .setBizCode("") |