| | |
| | | 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.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.dal.mysql.wm.warehouse.MesWmWarehouseAreaMapper; |
| | | 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.MesWmTransactionTypeEnum; |
| | | import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService; |
| | |
| | | |
| | | @Resource |
| | | private MesWmMaterialStockMapper materialStockMapper; |
| | | @Resource |
| | | private MesWmWarehouseAreaMapper warehouseAreaMapper; |
| | | |
| | | @Resource |
| | | private MesMdmItemService mdmItemService; |
| | |
| | | @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( |
| | | // 所有库存创建入口共用库位锁,避免与导入并发时创建重复台账。 |
| | | lockAreaAndValidateMixing(areaId, itemId, batchId); |
| | | MesWmMaterialStockDO stock = materialStockMapper.selectByCompositeKeyForUpdate( |
| | | itemId, warehouseId, locationId, areaId, batchId, vendorId); |
| | | if (stock != null) { |
| | | return stock; |
| | |
| | | } |
| | | } |
| | | |
| | | private void lockAreaAndValidateMixing(Long areaId, Long itemId, Long batchId) { |
| | | MesWmWarehouseAreaDO area = warehouseAreaMapper.selectOne(new LambdaQueryWrapperX<MesWmWarehouseAreaDO>() |
| | | .eq(MesWmWarehouseAreaDO::getId, areaId).last("FOR UPDATE")); |
| | | if (area == null) { |
| | | areaService.validateWarehouseAreaExists(areaId); |
| | | throw exception(WM_STOCK_IMPORT_INVALID, "库位不存在"); |
| | | } |
| | | List<MesWmMaterialStockDO> stocks = materialStockMapper.selectAreaStocksForUpdate(areaId); |
| | | if (Boolean.FALSE.equals(area.getAllowItemMixing()) && itemId != null |
| | | && CollUtil.anyMatch(stocks, stock -> ObjUtil.notEqual(itemId, stock.getItemId()))) { |
| | | throw exception(WM_MATERIAL_STOCK_AREA_ITEM_MIXING_NOT_ALLOWED, area.getName()); |
| | | } |
| | | if (Boolean.FALSE.equals(area.getAllowBatchMixing()) && batchId != null |
| | | && CollUtil.anyMatch(stocks, stock -> ObjUtil.notEqual(batchId, stock.getBatchId()))) { |
| | | throw exception(WM_MATERIAL_STOCK_AREA_BATCH_MIXING_NOT_ALLOWED, area.getName()); |
| | | } |
| | | } |
| | | @Override |
| | | public void checkAreaMixingRule(Long areaId, Long itemId, Long batchId) { |
| | | // 1.1 校验库位必须存在 |
| | |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public Long importMaterialStockIn(MesWmMaterialStockAdjustReqVO reqVO, Long importId, String reference) { |
| | | return doAdjustMaterialStock(reqVO, true, importId, reference); |
| | | } |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public Long adjustMaterialStockOut(MesWmMaterialStockAdjustReqVO reqVO) { |
| | | return doAdjustMaterialStock(reqVO, false); |
| | | } |
| | |
| | | * @return 库存记录ID |
| | | */ |
| | | private Long doAdjustMaterialStock(MesWmMaterialStockAdjustReqVO reqVO, boolean isInbound) { |
| | | return doAdjustMaterialStock(reqVO, isInbound, null, null); |
| | | } |
| | | |
| | | private Long doAdjustMaterialStock(MesWmMaterialStockAdjustReqVO reqVO, boolean isInbound, Long importId, String reference) { |
| | | // 1. 校验物料存在且启用 |
| | | MesMdmItemDO item = mdmItemService.validateItemExistsAndEnable(reqVO.getItemId()); |
| | | |
| | |
| | | } |
| | | |
| | | // 3. 校验库位混放规则 |
| | | checkAreaMixingRule(reqVO.getAreaId(), reqVO.getItemId(), batchId); |
| | | if (importId == null) { |
| | | checkAreaMixingRule(reqVO.getAreaId(), reqVO.getItemId(), batchId); |
| | | } |
| | | |
| | | // 4. 获取或创建库存记录 |
| | | MesWmMaterialStockDO stock = getOrCreateMaterialStock( |
| | | reqVO.getItemId(), |
| | | reqVO.getWarehouseId(), |
| | | reqVO.getLocationId(), |
| | | reqVO.getAreaId(), |
| | | batchId, |
| | | batchCode, |
| | | reqVO.getVendorId(), |
| | | LocalDateTime.now() |
| | | ); |
| | | |
| | | MesWmMaterialStockDO stock; |
| | | if (importId != null) { |
| | | lockAreaAndValidateMixing(reqVO.getAreaId(), reqVO.getItemId(), batchId); |
| | | List<MesWmMaterialStockDO> matches = reqVO.getVendorId() == null |
| | | ? materialStockMapper.selectImportBaseStocksForUpdate(reqVO.getItemId(), reqVO.getWarehouseId(), |
| | | reqVO.getLocationId(), reqVO.getAreaId(), batchId) |
| | | : materialStockMapper.selectImportStocksForUpdate(reqVO.getItemId(), reqVO.getWarehouseId(), |
| | | reqVO.getLocationId(), reqVO.getAreaId(), batchId, reqVO.getVendorId()); |
| | | if (matches.size() > 1) throw exception(WM_STOCK_IMPORT_INVALID, |
| | | reqVO.getVendorId() == null ? "该库存维度存在多条记录,请填写供应商编码并处理重复库存" : "库存维度存在重复记录,请先处理后导入"); |
| | | if (reqVO.getVendorId() == null && matches.size() == 1) reqVO.setVendorId(matches.get(0).getVendorId()); |
| | | if (matches.isEmpty()) { |
| | | stock = MesWmMaterialStockDO.builder().itemId(item.getId()).itemTypeId(item.getCategoryId()) |
| | | .warehouseId(reqVO.getWarehouseId()).locationId(reqVO.getLocationId()).areaId(reqVO.getAreaId()) |
| | | .batchId(batchId).batchCode(batchCode).vendorId(reqVO.getVendorId()) |
| | | .quantity(BigDecimal.ZERO).receiptTime(LocalDateTime.now()).frozen(false).build(); |
| | | materialStockMapper.insert(stock); |
| | | } else { |
| | | stock = matches.get(0); |
| | | } |
| | | } else { |
| | | 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; |
| | | int bizType = importId != null ? MesBizTypeConstants.WM_STOCK_IMPORT_IN : (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()) |
| | | .setBizId(importId != null ? importId : stock.getId()) |
| | | .setBizCode(importId != null ? reference : "STOCK_ADJUST_" + stock.getId()) |
| | | .setBizLineId(importId != null ? importId : stock.getId()) |
| | | .setItemId(reqVO.getItemId()) |
| | | .setQuantity(quantity) |
| | | .setBatchId(batchId) |
| | |
| | | .setAreaId(reqVO.getAreaId()) |
| | | .setVendorId(reqVO.getVendorId()) |
| | | .setReceiptTime(LocalDateTime.now()) |
| | | .setMaterialStockId(importId != null ? stock.getId() : null) |
| | | .setCheckFlag(!isInbound); |
| | | wmTransactionService.createTransaction(transaction); |
| | | |
| | |
| | | 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); |
| | | } |
| | | |
| | | } |