package cn.iocoder.yudao.module.mes.service.wm.materialstock; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ObjUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.collection.SetUtils; import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockAdjustReqVO; import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockFreezeReqVO; import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockListReqVO; import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockMoveReqVO; import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockPageReqVO; import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO; import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO; import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemTypeDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseAreaDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO; import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstock.MesWmMaterialStockMapper; import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants; import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum; import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService; import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService; import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemTypeService; import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService; import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchGenerateReqVO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO; import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService; import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO; import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService; import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService; import jakarta.annotation.Resource; import org.springframework.context.annotation.Lazy; 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.*; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*; /** * MES 库存台账 Service 实现类 */ @Service @Validated public class MesWmMaterialStockServiceImpl implements MesWmMaterialStockService { @Resource private MesWmMaterialStockMapper materialStockMapper; @Resource private MesMdmItemService mdmItemService; @Resource private MesMdItemService mdItemService; @Resource private MesMdItemTypeService itemTypeService; @Resource private MesWmWarehouseAreaService areaService; @Resource @Lazy private MesWmWarehouseService warehouseService; @Resource @Lazy private MesWmTransactionService wmTransactionService; @Resource @Lazy private MesWmBatchService batchService; @Override public MesWmMaterialStockDO getMaterialStock(Long id) { return materialStockMapper.selectById(id); } @Override public List getMaterialStockListByAreaId(Long areaId) { return materialStockMapper.selectList(MesWmMaterialStockDO::getAreaId, areaId); } public MesWmMaterialStockDO validateMaterialStockExists(Long id) { MesWmMaterialStockDO stock = materialStockMapper.selectById(id); if (stock == null) { throw exception(WM_MATERIAL_STOCK_NOT_EXISTS); } return stock; } @Override public PageResult getMaterialStockPage(MesWmMaterialStockPageReqVO pageReqVO) { // 1.1 解析 itemTypeId:包含子分类 Set itemTypeIds = null; if (pageReqVO.getItemTypeId() != null) { itemTypeIds = new HashSet<>(); itemTypeIds.add(pageReqVO.getItemTypeId()); List children = itemTypeService.getItemTypeChildrenList(pageReqVO.getItemTypeId()); itemTypeIds.addAll(convertSet(children, MesMdItemTypeDO::getId)); } // 1.2 解析 itemId:前端传的是 mes_md_item.id,库存表存的是 mdm_item_id,需要转换 Set itemIds = null; if (pageReqVO.getItemId() != null) { MesMdItemDO item = mdItemService.getItem(pageReqVO.getItemId()); if (item != null && item.getMdmItemId() != null) { itemIds = SetUtils.asSet(item.getMdmItemId()); } } // 1.3 解析 virtualFilter:转换为虚拟仓 warehouseId Long virtualWarehouseId = null; String virtualFilter = pageReqVO.getVirtualFilter(); if (MesWmMaterialStockPageReqVO.VIRTUAL_FILTER_EXCLUDE.equals(virtualFilter) || MesWmMaterialStockPageReqVO.VIRTUAL_FILTER_ONLY.equals(virtualFilter)) { MesWmWarehouseDO virtualWarehouse = warehouseService.getWarehouseByCode( MesWmWarehouseDO.WIP_VIRTUAL_WAREHOUSE); Assert.notNull(virtualWarehouse, "虚拟仓库(WIP_VIRTUAL_WAREHOUSE)不存在"); virtualWarehouseId = virtualWarehouse.getId(); } // 2. 分页查询 return materialStockMapper.selectPage(pageReqVO, itemTypeIds, itemIds, virtualWarehouseId); } @Override public void updateMaterialStockFrozen(MesWmMaterialStockFreezeReqVO updateReqVO) { // 校验存在 validateMaterialStockExists(updateReqVO.getId()); // 更新冻结状态 materialStockMapper.updateById(new MesWmMaterialStockDO() .setId(updateReqVO.getId()).setFrozen(updateReqVO.getFrozen())); } @Override public void updateMaterialStockFrozen(Collection ids, boolean frozen) { if (CollUtil.isEmpty(ids)) { return; } materialStockMapper.updateByIds(ids, new MesWmMaterialStockDO().setFrozen(frozen)); } @Override public Long getMaterialStockCountByWarehouseId(Long warehouseId) { return materialStockMapper.selectCountByWarehouseId(warehouseId); } @Override public Long getMaterialStockCountByLocationId(Long locationId) { return materialStockMapper.selectCountByLocationId(locationId); } @Override public Long getMaterialStockCountByAreaId(Long areaId) { return materialStockMapper.selectCountByAreaId(areaId); } @Override public List getMaterialStockList(Collection ids) { if (CollUtil.isEmpty(ids)) { return Collections.emptyList(); } return materialStockMapper.selectListByIds(ids); } @Override public List getMaterialStockList(MesWmMaterialStockListReqVO reqVO) { return materialStockMapper.selectList(reqVO); } @Override public MesWmMaterialStockDO getOrCreateMaterialStock(Long itemId, Long warehouseId, Long locationId, Long areaId, Long batchId, String batchCode, Long vendorId, LocalDateTime receiptTime) { // 1. 查找已有库存记录(vendorId 参与唯一键) MesWmMaterialStockDO stock = materialStockMapper.selectByCompositeKey( itemId, warehouseId, locationId, areaId, batchId, vendorId); if (stock != null) { return stock; } // 2. 不存在则新建 MesMdmItemDO item = mdmItemService.validateItemExists(itemId); MesWmMaterialStockDO newStock = MesWmMaterialStockDO.builder() .itemId(itemId).itemTypeId(item.getCategoryId()) .warehouseId(warehouseId).locationId(locationId).areaId(areaId) .batchId(batchId).batchCode(batchCode).vendorId(vendorId) .quantity(BigDecimal.ZERO) // 初始数量为 0,由 updateMaterialStockQuantity 更新 .receiptTime(receiptTime != null ? receiptTime : LocalDateTime.now()) .frozen(false) .build(); materialStockMapper.insert(newStock); // 3. 重新查询,确保拿到数据库生成的完整记录(id、createTime 等) MesWmMaterialStockDO inserted = materialStockMapper.selectById(newStock.getId()); return inserted != null ? inserted : newStock; } @Override public MesWmMaterialStockDO getMaterialStockByCompositeKey(Long itemId, Long warehouseId, Long locationId, Long areaId, Long batchId, Long vendorId) { return materialStockMapper.selectByCompositeKey(itemId, warehouseId, locationId, areaId, batchId, vendorId); } @Override public void updateMaterialStockQuantity(Long id, BigDecimal quantity, boolean checkFlag) { // 更新库存数量(Mapper 层 CAS 防负库存;checkFlag=false 时允许负数) int updatedRows = materialStockMapper.updateQuantity(id, quantity, checkFlag); if (updatedRows == 0) { throw exception(WM_MATERIAL_STOCK_INSUFFICIENT); } } @Override public void checkAreaMixingRule(Long areaId, Long itemId, Long batchId) { // 1.1 校验库位必须存在 MesWmWarehouseAreaDO area = areaService.validateWarehouseAreaExists(areaId); // 2. 查询当前库位上已有的库存记录 List stockList = getMaterialStockListByAreaId(areaId); if (CollUtil.isEmpty(stockList)) { return; } // 2.1 检查物料混放 if (Boolean.FALSE.equals(area.getAllowItemMixing()) && itemId != null) { if (CollUtil.anyMatch(stockList, item -> ObjUtil.notEqual(itemId, item.getItemId()))) { throw exception(WM_MATERIAL_STOCK_AREA_ITEM_MIXING_NOT_ALLOWED, area.getName()); } } // 2.2 检查批次混放 if (Boolean.FALSE.equals(area.getAllowBatchMixing()) && batchId != null) { if (CollUtil.anyMatch(stockList, item -> ObjUtil.notEqual(batchId, item.getBatchId()))) { throw exception(WM_MATERIAL_STOCK_AREA_BATCH_MIXING_NOT_ALLOWED, area.getName()); } } } @Override public MesWmMaterialStockDO validateSelectedStock(Long materialStockId, Long itemId, Long batchId, String batchCode, Long warehouseId, Long locationId, Long areaId, BigDecimal quantity) { // 1. 校验库存记录必须存在 if (materialStockId == null) { throw exception(WM_MATERIAL_STOCK_REQUIRED); } MesWmMaterialStockDO stock = validateMaterialStockExists(materialStockId); // 2. 校验库存记录的物料、批次、仓库、库区、库位等信息与前端选择的一致,避免串单或越权提交 if (ObjUtil.notEqual(stock.getItemId(), itemId) || ObjUtil.notEqual(stock.getBatchId(), batchId) || (batchCode != null && ObjUtil.notEqual(stock.getBatchCode(), batchCode)) || ObjUtil.notEqual(stock.getWarehouseId(), warehouseId) || ObjUtil.notEqual(stock.getLocationId(), locationId) || ObjUtil.notEqual(stock.getAreaId(), areaId)) { throw exception(WM_MATERIAL_STOCK_SELECTION_MISMATCH); } // 3. 校验库存数量充足(如果前端传了 quantity,则必须保证库存数量 >= quantity) if (quantity != null && stock.getQuantity() != null && stock.getQuantity().compareTo(quantity) < 0) { throw exception(WM_MATERIAL_STOCK_INSUFFICIENT); } return stock; } @Override @Transactional(rollbackFor = Exception.class) public Long adjustMaterialStockIn(MesWmMaterialStockAdjustReqVO reqVO) { return doAdjustMaterialStock(reqVO, true); } @Override @Transactional(rollbackFor = Exception.class) public Long adjustMaterialStockOut(MesWmMaterialStockAdjustReqVO reqVO) { return doAdjustMaterialStock(reqVO, false); } /** * 执行库存调整 * * @param reqVO 调整请求 * @param isInbound 是否为入库(true=入库,false=出库) * @return 库存记录ID */ private Long doAdjustMaterialStock(MesWmMaterialStockAdjustReqVO reqVO, boolean isInbound) { // 1. 校验物料存在且启用 MesMdmItemDO item = mdmItemService.validateItemExistsAndEnable(reqVO.getItemId()); // 2. 处理批次(仅入库时需要处理批次) Long batchId = reqVO.getBatchId(); String batchCode = reqVO.getBatchCode(); if (isInbound) { MesWmBatchDO batch = resolveBatch(reqVO, item); if (batch != null) { batchId = batch.getId(); batchCode = batch.getCode(); } } else { // 出库时校验批次存在 if (batchId != null) { batchService.validateBatchExists(batchId, reqVO.getItemId()); } } // 3. 校验库位混放规则 checkAreaMixingRule(reqVO.getAreaId(), reqVO.getItemId(), batchId); // 4. 获取或创建库存记录 MesWmMaterialStockDO stock = getOrCreateMaterialStock( reqVO.getItemId(), reqVO.getWarehouseId(), reqVO.getLocationId(), reqVO.getAreaId(), batchId, batchCode, reqVO.getVendorId(), LocalDateTime.now() ); // 5. 计算变动数量:入库为正,出库为负 BigDecimal quantity = isInbound ? reqVO.getQuantity() : reqVO.getQuantity().negate(); // 6. 创建库存事务流水(内部会更新库存数量) int bizType = isInbound ? MesBizTypeConstants.WM_STOCK_ADJUST_IN : MesBizTypeConstants.WM_STOCK_ADJUST_OUT; MesWmTransactionSaveReqDTO transaction = new MesWmTransactionSaveReqDTO() .setType(isInbound ? MesWmTransactionTypeEnum.IN.getType() : MesWmTransactionTypeEnum.OUT.getType()) .setBizType(bizType) .setBizId(stock.getId()) .setBizCode("STOCK_ADJUST_" + stock.getId()) .setBizLineId(stock.getId()) .setItemId(reqVO.getItemId()) .setQuantity(quantity) .setBatchId(batchId) .setBatchCode(batchCode) .setWarehouseId(reqVO.getWarehouseId()) .setLocationId(reqVO.getLocationId()) .setAreaId(reqVO.getAreaId()) .setVendorId(reqVO.getVendorId()) .setReceiptTime(LocalDateTime.now()) .setCheckFlag(!isInbound); wmTransactionService.createTransaction(transaction); return stock.getId(); } /** * 解析批次 *

* 入库时的批次处理逻辑: * 1. 物料未启用批次管理:返回 null * 2. 传入 batchId:校验并使用已有批次 * 3. 未传 batchId:根据 batchCode 等参数自动创建批次 * * @param reqVO 入库请求 * @param item 物料信息 * @return 批次记录(物料未启用批次管理时返回 null) */ private MesWmBatchDO resolveBatch(MesWmMaterialStockAdjustReqVO reqVO, MesMdmItemDO item) { // 1. 物料未启用批次管理,返回 null if (Boolean.FALSE.equals(item.getIsBatchManaged())) { return null; } // 2. 传入 batchId,校验并返回已有批次 if (reqVO.getBatchId() != null) { return batchService.validateBatchExists(reqVO.getBatchId(), reqVO.getItemId()); } // 3. 未传 batchId,自动创建批次 MesWmBatchGenerateReqVO batchReqVO = new MesWmBatchGenerateReqVO() .setItemId(reqVO.getItemId()) .setBatchCode(reqVO.getBatchCode()) .setVendorId(reqVO.getVendorId()) .setProduceDate(reqVO.getProduceDate() != null ? reqVO.getProduceDate().atStartOfDay() : null) .setExpireDate(reqVO.getExpireDate() != null ? reqVO.getExpireDate().atStartOfDay() : null) .setReceiptDate(LocalDateTime.now()) .setLotNumber(reqVO.getLotNumber()) .setQualityStatus(parseQualityStatus(reqVO.getQualityStatus())); return batchService.getOrGenerateBatchCode(batchReqVO); } /** * 解析质量状态(支持数字或字符串) */ private Integer parseQualityStatus(Object qualityStatus) { if (qualityStatus == null) { return null; } if (qualityStatus instanceof Integer) { return (Integer) qualityStatus; } if (qualityStatus instanceof String) { String str = (String) qualityStatus; // 支持数字字符串 if (str.matches("\\d+")) { return Integer.parseInt(str); } // 支持常见字符串映射 switch (str.toUpperCase()) { case "PENDING": case "待检验": case "待检": return 0; case "PASS": case "QUALIFIED": case "合格": return 1; case "FAIL": case "UNQUALIFIED": case "不合格": return 2; default: throw new IllegalArgumentException("无效的质量状态: " + str); } } throw new IllegalArgumentException("无效的质量状态类型: " + qualityStatus.getClass().getName()); } @Override @Transactional(rollbackFor = Exception.class) public void moveMaterialStock(MesWmMaterialStockMoveReqVO reqVO) { // 1. 查询原库存记录 MesWmMaterialStockDO sourceStock = validateMaterialStockExists(reqVO.getMaterialStockId()); // 2. 校验库存数量充足(可用量 = 在库数量 - 冻结数量 - 占用量) BigDecimal availableQty = sourceStock.getQuantity() != null ? sourceStock.getQuantity() : BigDecimal.ZERO; if (sourceStock.getFrozenQuantity() != null) { availableQty = availableQty.subtract(sourceStock.getFrozenQuantity()); } if (sourceStock.getReservedQuantity() != null) { availableQty = availableQty.subtract(sourceStock.getReservedQuantity()); } if (availableQty.compareTo(reqVO.getQuantity()) < 0) { throw exception(WM_MATERIAL_STOCK_INSUFFICIENT); } // 3. 校验库存未冻结 if (Boolean.TRUE.equals(sourceStock.getFrozen())) { throw exception(WM_MATERIAL_STOCK_FROZEN); } // 4. 校验目标库位混放规则 checkAreaMixingRule(reqVO.getToAreaId(), sourceStock.getItemId(), sourceStock.getBatchId()); // 5. 生成移库单号 String moveCode = "MOVE_" + System.currentTimeMillis(); // 6. 创建调拨移出事务(负数) MesWmTransactionSaveReqDTO outTransaction = new MesWmTransactionSaveReqDTO() .setType(MesWmTransactionTypeEnum.MOVE_OUT.getType()) .setBizType(MesBizTypeConstants.WM_TRANSFER_OUT) .setBizId(sourceStock.getId()) .setBizCode(moveCode) .setBizLineId(sourceStock.getId()) .setItemId(sourceStock.getItemId()) .setQuantity(reqVO.getQuantity().negate()) .setBatchId(sourceStock.getBatchId()) .setBatchCode(sourceStock.getBatchCode()) .setWarehouseId(sourceStock.getWarehouseId()) .setLocationId(sourceStock.getLocationId()) .setAreaId(sourceStock.getAreaId()) .setVendorId(sourceStock.getVendorId()) .setMaterialStockId(sourceStock.getId()) .setCheckFlag(true); Long outTransactionId = wmTransactionService.createTransaction(outTransaction); // 7. 查找目标库位的库存记录(忽略供应商),用于合并库存 MesWmMaterialStockDO targetStock = materialStockMapper.selectByCompositeKeyIgnoreVendor( sourceStock.getItemId(), reqVO.getToWarehouseId(), reqVO.getToLocationId(), reqVO.getToAreaId(), sourceStock.getBatchId() ); // 8. 创建调拨移入事务(正数),关联出库事务 // 使用目标库位已有库存记录的 vendorId(如果有),否则不传 MesWmTransactionSaveReqDTO inTransaction = new MesWmTransactionSaveReqDTO() .setType(MesWmTransactionTypeEnum.MOVE_IN.getType()) .setBizType(MesBizTypeConstants.WM_TRANSFER_IN) .setBizId(sourceStock.getId()) .setBizCode(moveCode) .setBizLineId(sourceStock.getId()) .setItemId(sourceStock.getItemId()) .setQuantity(reqVO.getQuantity()) .setBatchId(sourceStock.getBatchId()) .setBatchCode(sourceStock.getBatchCode()) .setWarehouseId(reqVO.getToWarehouseId()) .setLocationId(reqVO.getToLocationId()) .setAreaId(reqVO.getToAreaId()) .setVendorId(targetStock != null ? targetStock.getVendorId() : null) .setRelatedTransactionId(outTransactionId) .setCheckFlag(false); wmTransactionService.createTransaction(inTransaction); } }