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<MesWmMaterialStockDO> 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<MesWmMaterialStockDO> getMaterialStockPage(MesWmMaterialStockPageReqVO pageReqVO) {
|
// 1.1 解析 itemTypeId:包含子分类
|
Set<Long> itemTypeIds = null;
|
if (pageReqVO.getItemTypeId() != null) {
|
itemTypeIds = new HashSet<>();
|
itemTypeIds.add(pageReqVO.getItemTypeId());
|
List<MesMdItemTypeDO> children = itemTypeService.getItemTypeChildrenList(pageReqVO.getItemTypeId());
|
itemTypeIds.addAll(convertSet(children, MesMdItemTypeDO::getId));
|
}
|
// 1.2 解析 itemId:前端传的是 mes_md_item.id,库存表存的是 mdm_item_id,需要转换
|
Set<Long> 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<Long> 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<MesWmMaterialStockDO> getMaterialStockList(Collection<Long> ids) {
|
if (CollUtil.isEmpty(ids)) {
|
return Collections.emptyList();
|
}
|
return materialStockMapper.selectListByIds(ids);
|
}
|
|
@Override
|
public List<MesWmMaterialStockDO> 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. 不存在则新建
|
return createMaterialStock(itemId, warehouseId, locationId, areaId, batchId, batchCode, vendorId, receiptTime);
|
}
|
|
@Override
|
public MesWmMaterialStockDO createMaterialStock(Long itemId, Long warehouseId, Long locationId, Long areaId,
|
Long batchId, String batchCode, Long vendorId, LocalDateTime receiptTime) {
|
// 始终创建新库存记录
|
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);
|
// 重新查询,确保拿到数据库生成的完整记录(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<MesWmMaterialStockDO> 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();
|
}
|
|
/**
|
* 解析批次
|
* <p>
|
* 入库时的批次处理逻辑:
|
* 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<MesWmMaterialStockImportExcelVO> 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<String, MdmItemRespDTO> itemMap = mdmItemApi.getItemListByStatus(null).getCheckedData().stream()
|
.collect(Collectors.toMap(MdmItemRespDTO::getCode, item -> item, (v1, v2) -> v1));
|
Map<String, MesWmWarehouseDO> warehouseCodeMap = new HashMap<>();
|
Map<String, MesWmWarehouseDO> warehouseNameMap = new HashMap<>();
|
for (MesWmWarehouseDO warehouse : warehouseService.getWarehouseList()) {
|
warehouseCodeMap.putIfAbsent(warehouse.getCode(), warehouse);
|
warehouseNameMap.putIfAbsent(warehouse.getName(), warehouse);
|
}
|
Map<String, MesWmWarehouseLocationDO> locationCodeMap = new HashMap<>();
|
Map<String, MesWmWarehouseLocationDO> locationNameMap = new HashMap<>();
|
for (MesWmWarehouseLocationDO location : locationService.getWarehouseLocationList((Long) null)) {
|
locationCodeMap.putIfAbsent(location.getCode(), location);
|
locationNameMap.putIfAbsent(location.getName(), location);
|
}
|
Map<String, MesWmWarehouseAreaDO> areaCodeMap = new HashMap<>();
|
Map<String, MesWmWarehouseAreaDO> areaNameMap = new HashMap<>();
|
for (MesWmWarehouseAreaDO area : areaService.getWarehouseAreaList((Long) null)) {
|
areaCodeMap.putIfAbsent(area.getCode(), area);
|
areaNameMap.putIfAbsent(area.getName(), area);
|
}
|
Map<String, SrmSupplierRespDTO> supplierCodeMap = new HashMap<>();
|
Map<String, SrmSupplierRespDTO> 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<String, MdmItemRespDTO> 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<String, MesWmWarehouseDO> codeMap,
|
Map<String, MesWmWarehouseDO> 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> T resolveImportLocationOrArea(Map<String, T> codeMap, Map<String, T> 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<String, SrmSupplierRespDTO> codeMap,
|
Map<String, SrmSupplierRespDTO> 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);
|
}
|
|
}
|