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.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.exception.ErrorCode; import cn.iocoder.yudao.framework.common.exception.ServiceException; 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.MesWmMaterialStockImportExcelVO; import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockImportRespVO; 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.dataobject.wm.warehouse.MesWmWarehouseLocationDO; 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.mdm.api.item.MdmItemApi; import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO; import cn.iocoder.yudao.module.srm.api.supplier.SrmSupplierApi; import cn.iocoder.yudao.module.srm.api.supplier.dto.SrmSupplierRespDTO; 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.MesWmWarehouseLocationService; 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.PlatformTransactionManager; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionTemplate; import org.springframework.validation.annotation.Validated; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.*; 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.convertSet; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; 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 MesWmWarehouseLocationService locationService; @Resource @Lazy private MesWmWarehouseService warehouseService; @Resource @Lazy private MesWmTransactionService wmTransactionService; @Resource @Lazy private MesWmBatchService batchService; @Resource private MdmItemApi mdmItemApi; @Resource private SrmSupplierApi srmSupplierApi; @Resource private PlatformTransactionManager transactionManager; @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); } @Override public MesWmMaterialStockImportRespVO importMaterialStockList(List importList) { // 操作类型取值:增加 / 减少 / 覆盖 final String OP_ADD = "增加"; final String OP_REDUCE = "减少"; final String OP_COVER = "覆盖"; // 1. 过滤全空行 importList = filterList(importList, item -> StrUtil.isNotBlank(item.getItemCode()) || StrUtil.isNotBlank(item.getBatchCode()) || item.getQuantity() != null); if (CollUtil.isEmpty(importList)) { throw exception(WM_MATERIAL_STOCK_IMPORT_LIST_IS_EMPTY); } // 2. 预加载 编码/名称 -> 实体 映射(编码优先,名称兜底) Map itemMap = mdmItemApi.getItemListByStatus(null).getCheckedData().stream() .collect(Collectors.toMap(MdmItemRespDTO::getCode, item -> item, (v1, v2) -> v1)); Map warehouseCodeMap = new HashMap<>(); Map warehouseNameMap = new HashMap<>(); for (MesWmWarehouseDO warehouse : warehouseService.getWarehouseList()) { warehouseCodeMap.putIfAbsent(warehouse.getCode(), warehouse); warehouseNameMap.putIfAbsent(warehouse.getName(), warehouse); } Map locationCodeMap = new HashMap<>(); Map locationNameMap = new HashMap<>(); for (MesWmWarehouseLocationDO location : locationService.getWarehouseLocationList((Long) null)) { locationCodeMap.putIfAbsent(location.getCode(), location); locationNameMap.putIfAbsent(location.getName(), location); } Map areaCodeMap = new HashMap<>(); Map areaNameMap = new HashMap<>(); for (MesWmWarehouseAreaDO area : areaService.getWarehouseAreaList((Long) null)) { areaCodeMap.putIfAbsent(area.getCode(), area); areaNameMap.putIfAbsent(area.getName(), area); } Map supplierCodeMap = new HashMap<>(); Map supplierNameMap = new HashMap<>(); for (SrmSupplierRespDTO supplier : srmSupplierApi.getSupplierSimpleList().getCheckedData()) { supplierCodeMap.putIfAbsent(supplier.getCode(), supplier); supplierNameMap.putIfAbsent(supplier.getName(), supplier); } // 3. 逐条处理 MesWmMaterialStockImportRespVO respVO = MesWmMaterialStockImportRespVO.builder() .createNames(new ArrayList<>()) .updateNames(new ArrayList<>()) .failureNames(new LinkedHashMap<>()) .build(); TransactionTemplate importTxTemplate = new TransactionTemplate(transactionManager); for (MesWmMaterialStockImportExcelVO importStock : importList) { String key = StrUtil.blankToDefault(importStock.getItemCode(), "") + "-" + StrUtil.blankToDefault(importStock.getWarehouse(), ""); try { importTxTemplate.executeWithoutResult(status -> { // 3.1 物料 MdmItemRespDTO item = resolveImportItem(itemMap, importStock.getItemCode()); // 3.2 仓库/库区/库位(编码优先、名称兜底,含父子关系校验) MesWmWarehouseDO warehouse = resolveImportWarehouse(warehouseCodeMap, warehouseNameMap, importStock.getWarehouse()); Long locationId = null; if (StrUtil.isNotBlank(importStock.getLocation())) { MesWmWarehouseLocationDO location = resolveImportLocationOrArea(locationCodeMap, locationNameMap, importStock.getLocation(), WM_MATERIAL_STOCK_IMPORT_LOCATION_NOT_EXISTS); if (ObjUtil.notEqual(location.getWarehouseId(), warehouse.getId())) { throw exception(WM_WAREHOUSE_AREA_WAREHOUSE_MISMATCH); } locationId = location.getId(); } Long areaId = null; if (StrUtil.isNotBlank(importStock.getArea())) { MesWmWarehouseAreaDO area = resolveImportLocationOrArea(areaCodeMap, areaNameMap, importStock.getArea(), WM_MATERIAL_STOCK_IMPORT_AREA_NOT_EXISTS); if (ObjUtil.notEqual(area.getLocationId(), locationId != null ? locationId : null)) { throw exception(WM_WAREHOUSE_AREA_RELATION_INVALID); } areaId = area.getId(); } // 3.3 批次(物料启用批次管理时必填,且批次需归属该物料) Long batchId = null; String batchCode = null; if (Boolean.TRUE.equals(item.getIsBatchManaged())) { if (StrUtil.isBlank(importStock.getBatchCode())) { throw exception(WM_TRANSACTION_BATCH_REQUIRED); } MesWmBatchDO batch = batchService.getBatchByCode(importStock.getBatchCode().trim()); if (batch == null || ObjUtil.notEqual(batch.getItemId(), item.getId())) { throw exception(WM_MATERIAL_STOCK_IMPORT_BATCH_NOT_EXISTS, importStock.getBatchCode().trim()); } batchId = batch.getId(); batchCode = batch.getCode(); } // 3.4 供应商(可选) Long vendorId = resolveImportVendor(supplierCodeMap, supplierNameMap, importStock.getVendor()); // 3.5 操作类型(必填:增加/减少/覆盖) String operationType = importStock.getOperationType() == null ? null : importStock.getOperationType().trim(); if (StrUtil.isBlank(operationType)) { throw exception(WM_MATERIAL_STOCK_IMPORT_FIELD_REQUIRED, "操作类型"); } if (!OP_ADD.equals(operationType) && !OP_REDUCE.equals(operationType) && !OP_COVER.equals(operationType)) { throw exception(WM_MATERIAL_STOCK_IMPORT_OPERATION_INVALID, operationType); } // 3.6 数量校验:增加/减少为变动量须为正数;覆盖为目标现有量须 >= 0 BigDecimal quantity = importStock.getQuantity(); boolean cover = OP_COVER.equals(operationType); if (quantity == null || quantity.compareTo(BigDecimal.ZERO) < 0 || (!cover && quantity.compareTo(BigDecimal.ZERO) == 0)) { throw exception(WM_MATERIAL_STOCK_IMPORT_QUANTITY_INVALID, quantity == null ? "空" : quantity); } // 3.7 按操作类型定位并处理库存记录(不存在则创建,存在则调整数量) MesWmMaterialStockDO existStock = materialStockMapper.selectByCompositeKey( item.getId(), warehouse.getId(), locationId, areaId, batchId, vendorId); Long wId = warehouse.getId(); if (OP_REDUCE.equals(operationType)) { // 减少:库存必须存在,扣减不足时由出库流水校验并失败 if (existStock == null) { throw exception(WM_MATERIAL_STOCK_IMPORT_STOCK_NOT_EXISTS); } createImportTransaction(existStock, item.getId(), batchId, batchCode, wId, locationId, areaId, vendorId, quantity.negate()); respVO.getUpdateNames().add(key); } else if (OP_ADD.equals(operationType)) { // 增加:不存在则建库存再入库;已存在则直接追加变动量 if (existStock == null) { MesWmMaterialStockDO newStock = getOrCreateMaterialStock( item.getId(), wId, locationId, areaId, batchId, batchCode, vendorId, LocalDateTime.now()); createImportTransaction(newStock, item.getId(), batchId, batchCode, wId, locationId, areaId, vendorId, quantity); respVO.getCreateNames().add(key); } else { createImportTransaction(existStock, item.getId(), batchId, batchCode, wId, locationId, areaId, vendorId, quantity); respVO.getUpdateNames().add(key); } } else { // 覆盖:数量为目标现有量;不存在则新建并置量,已存在按差额出入库 if (existStock == null) { MesWmMaterialStockDO newStock = getOrCreateMaterialStock( item.getId(), wId, locationId, areaId, batchId, batchCode, vendorId, LocalDateTime.now()); if (quantity.compareTo(BigDecimal.ZERO) > 0) { createImportTransaction(newStock, item.getId(), batchId, batchCode, wId, locationId, areaId, vendorId, quantity); } respVO.getCreateNames().add(key); } else { BigDecimal currentQty = existStock.getQuantity() != null ? existStock.getQuantity() : BigDecimal.ZERO; BigDecimal diff = quantity.subtract(currentQty); if (diff.compareTo(BigDecimal.ZERO) != 0) { createImportTransaction(existStock, item.getId(), batchId, batchCode, wId, locationId, areaId, vendorId, diff); } respVO.getUpdateNames().add(key); } } }); } catch (Exception ex) { respVO.getFailureNames().put(key, ex.getMessage()); } } return respVO; } private MdmItemRespDTO resolveImportItem(Map itemMap, String itemCode) { if (StrUtil.isBlank(itemCode)) { throw exception(WM_MATERIAL_STOCK_IMPORT_FIELD_REQUIRED, "物料编码"); } MdmItemRespDTO item = itemMap.get(itemCode.trim()); if (item == null) { throw exception(WM_MATERIAL_STOCK_IMPORT_ITEM_NOT_EXISTS, itemCode.trim()); } return item; } private MesWmWarehouseDO resolveImportWarehouse(Map codeMap, Map nameMap, String value) { if (StrUtil.isBlank(value)) { throw exception(WM_MATERIAL_STOCK_IMPORT_FIELD_REQUIRED, "仓库"); } String trimmed = value.trim(); MesWmWarehouseDO warehouse = codeMap.get(trimmed) != null ? codeMap.get(trimmed) : nameMap.get(trimmed); if (warehouse == null) { throw exception(WM_MATERIAL_STOCK_IMPORT_WAREHOUSE_NOT_EXISTS, trimmed); } return warehouse; } private T resolveImportLocationOrArea(Map codeMap, Map nameMap, String value, ErrorCode errorCode) { String trimmed = value.trim(); T result = codeMap.get(trimmed) != null ? codeMap.get(trimmed) : nameMap.get(trimmed); if (result == null) { throw exception(errorCode, trimmed); } return result; } private Long resolveImportVendor(Map codeMap, Map nameMap, String value) { if (StrUtil.isBlank(value)) { return null; } String trimmed = value.trim(); SrmSupplierRespDTO supplier = codeMap.get(trimmed) != null ? codeMap.get(trimmed) : nameMap.get(trimmed); if (supplier == null) { throw exception(WM_MATERIAL_STOCK_IMPORT_VENDOR_NOT_EXISTS, trimmed); } return supplier.getId(); } /** * 创建导入产生的库存事务流水(复用库存调整的流水模式) * * @param quantity 变动数量(正数=入库,负数=出库) */ private void createImportTransaction(MesWmMaterialStockDO stock, Long itemId, Long batchId, String batchCode, Long warehouseId, Long locationId, Long areaId, Long vendorId, BigDecimal quantity) { boolean inbound = quantity.compareTo(BigDecimal.ZERO) > 0; MesWmTransactionSaveReqDTO transaction = new MesWmTransactionSaveReqDTO() .setType(inbound ? MesWmTransactionTypeEnum.IN.getType() : MesWmTransactionTypeEnum.OUT.getType()) .setBizType(inbound ? MesBizTypeConstants.WM_STOCK_ADJUST_IN : MesBizTypeConstants.WM_STOCK_ADJUST_OUT) .setBizId(stock.getId()) .setBizCode("STOCK_IMPORT_" + stock.getId()) .setBizLineId(stock.getId()) .setItemId(itemId) .setQuantity(quantity.abs()) .setBatchId(batchId) .setBatchCode(batchCode) .setWarehouseId(warehouseId) .setLocationId(locationId) .setAreaId(areaId) .setVendorId(vendorId) .setReceiptTime(LocalDateTime.now()) .setCheckFlag(!inbound); wmTransactionService.createTransaction(transaction); } }