云
2 天以前 ac2e2b5af3726ee64dc37c3ebf26989a0346f22a
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemconsume/MesWmItemConsumeServiceImpl.java
@@ -14,6 +14,8 @@
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;
@@ -29,17 +31,21 @@
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 实现类
@@ -69,10 +75,13 @@
    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) {
@@ -105,9 +114,29 @@
        // 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);
@@ -121,23 +150,22 @@
     * 规则:
     * 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();
@@ -145,16 +173,20 @@
            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());
        }
    }
@@ -173,6 +205,7 @@
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void finishItemConsume(Long consumeId) {
        // 1. 查询消耗明细(detail 级别,包含精确的批次/仓库信息)
        List<MesWmItemConsumeDetailDO> details = itemConsumeDetailService
@@ -180,16 +213,17 @@
        // 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("")