| | |
| | | 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.controller.admin.wm.materialstock.vo.MesWmStockSummaryRespVO; |
| | | 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.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.MesWmStockStateEnum; |
| | | 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.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.materialstockbag.MesWmMaterialStockBagService; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstockbag.MesWmMaterialStockBagMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstockbag.MesWmMaterialStockBagDO; |
| | | 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 java.math.BigDecimal; |
| | | import java.time.LocalDateTime; |
| | | import java.util.*; |
| | | import java.util.function.BiConsumer; |
| | | import java.util.function.Function; |
| | | |
| | | 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.framework.common.util.collection.CollectionUtils.convertSet; |
| | | import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*; |
| | | |
| | |
| | | @Resource |
| | | private MesMdmItemService mdmItemService; |
| | | @Resource |
| | | private MesMdItemService mdItemService; |
| | | @Resource |
| | | private MesMdItemTypeService itemTypeService; |
| | | @Resource |
| | | private MesWmWarehouseAreaService areaService; |
| | |
| | | @Resource |
| | | @Lazy |
| | | private MesWmBatchService batchService; |
| | | |
| | | @Resource |
| | | private MesWmMaterialStockBagMapper materialStockBagMapper; |
| | | @Resource |
| | | private MesWmMaterialStockBagService materialStockBagService; |
| | | |
| | | @Override |
| | | public MesWmMaterialStockDO getMaterialStock(Long id) { |
| | |
| | | List<MesMdItemTypeDO> children = itemTypeService.getItemTypeChildrenList(pageReqVO.getItemTypeId()); |
| | | itemTypeIds.addAll(convertSet(children, MesMdItemTypeDO::getId)); |
| | | } |
| | | // 1.2 解析 itemId |
| | | // 1.2 解析 itemId:前端传的是 mes_md_item.id,库存表存的是 mdm_item_id,需要转换 |
| | | Set<Long> itemIds = null; |
| | | if (pageReqVO.getItemId() != null) { |
| | | itemIds = SetUtils.asSet(pageReqVO.getItemId()); |
| | | MesMdItemDO item = mdItemService.getItem(pageReqVO.getItemId()); |
| | | if (item != null && item.getMdmItemId() != null) { |
| | | itemIds = SetUtils.asSet(item.getMdmItemId()); |
| | | } |
| | | } |
| | | // 1.3 解析 virtualFilter:转换为虚拟仓 warehouseId |
| | | Long virtualWarehouseId = null; |
| | |
| | | Assert.notNull(virtualWarehouse, "虚拟仓库(WIP_VIRTUAL_WAREHOUSE)不存在"); |
| | | virtualWarehouseId = virtualWarehouse.getId(); |
| | | } |
| | | // 1.4 解析 warehouseType:转换为该类型仓库 warehouseIds(与 warehouseId / virtualFilter 并存,保留兼容) |
| | | List<Long> warehouseIds = resolveWarehouseIdsByType(pageReqVO.getWarehouseType()); |
| | | if (warehouseIds != null && warehouseIds.isEmpty()) { |
| | | return PageResult.empty(); |
| | | } |
| | | |
| | | // 2. 分页查询 |
| | | return materialStockMapper.selectPage(pageReqVO, itemTypeIds, itemIds, virtualWarehouseId); |
| | | return materialStockMapper.selectPage(pageReqVO, itemTypeIds, itemIds, virtualWarehouseId, warehouseIds); |
| | | } |
| | | |
| | | /** |
| | | * 将仓库类型解析为仓库 id 集合(warehouseType 为 null 时不加过滤,返回 null) |
| | | * |
| | | * @param warehouseType 仓库类型,见 {@link cn.iocoder.yudao.module.mes.enums.wm.MesWmWarehouseTypeEnum} |
| | | * @return 该类型仓库 id 集合;warehouseType 为 null 返回 null;该类型无仓库返回空集合 |
| | | */ |
| | | private List<Long> resolveWarehouseIdsByType(Integer warehouseType) { |
| | | if (warehouseType == null) { |
| | | return null; |
| | | } |
| | | return convertList(warehouseService.getWarehouseListByType(warehouseType), MesWmWarehouseDO::getId); |
| | | } |
| | | |
| | | @Override |
| | |
| | | |
| | | @Override |
| | | public List<MesWmMaterialStockDO> getMaterialStockList(MesWmMaterialStockListReqVO reqVO) { |
| | | return materialStockMapper.selectList(reqVO); |
| | | List<Long> warehouseIds = resolveWarehouseIdsByType(reqVO.getWarehouseType()); |
| | | if (warehouseIds != null && warehouseIds.isEmpty()) { |
| | | return Collections.emptyList(); |
| | | } |
| | | return materialStockMapper.selectList(reqVO, warehouseIds); |
| | | } |
| | | |
| | | @Override |
| | | public List<MesWmStockSummaryRespVO> getStockSummaryByItem(MesWmMaterialStockListReqVO reqVO) { |
| | | List<MesWmMaterialStockDO> stocks = getMaterialStockList(reqVO); |
| | | List<MesWmStockSummaryRespVO> list = buildStockSummary(stocks, |
| | | stock -> String.valueOf(stock.getItemId()), |
| | | (vo, stock) -> vo.setItemId(stock.getItemId())); |
| | | Map<Long, MesMdmItemDO> itemMap = mdmItemService.getItemMap( |
| | | convertSet(stocks, MesWmMaterialStockDO::getItemId)); |
| | | list.forEach(vo -> { |
| | | MesMdmItemDO item = itemMap.get(vo.getItemId()); |
| | | if (item != null) { |
| | | vo.setItemCode(item.getCode()).setItemName(item.getName()); |
| | | } |
| | | }); |
| | | return list; |
| | | } |
| | | |
| | | @Override |
| | | public List<MesWmStockSummaryRespVO> getStockSummaryByBatch(MesWmMaterialStockListReqVO reqVO) { |
| | | List<MesWmMaterialStockDO> stocks = getMaterialStockList(reqVO); |
| | | return buildStockSummary(stocks, |
| | | stock -> stock.getBatchId() + "|" + (stock.getBatchCode() != null ? stock.getBatchCode() : ""), |
| | | (vo, stock) -> vo.setBatchId(stock.getBatchId()).setBatchCode(stock.getBatchCode())); |
| | | } |
| | | |
| | | @Override |
| | | public List<MesWmStockSummaryRespVO> getStockSummaryByWarehouse(MesWmMaterialStockListReqVO reqVO) { |
| | | List<MesWmMaterialStockDO> stocks = getMaterialStockList(reqVO); |
| | | List<MesWmStockSummaryRespVO> list = buildStockSummary(stocks, |
| | | stock -> String.valueOf(stock.getWarehouseId()), |
| | | (vo, stock) -> vo.setWarehouseId(stock.getWarehouseId())); |
| | | Map<Long, MesWmWarehouseDO> warehouseMap = warehouseService.getWarehouseMap( |
| | | convertSet(stocks, MesWmMaterialStockDO::getWarehouseId)); |
| | | list.forEach(vo -> { |
| | | MesWmWarehouseDO warehouse = warehouseMap.get(vo.getWarehouseId()); |
| | | if (warehouse != null) { |
| | | vo.setWarehouseName(warehouse.getName()); |
| | | } |
| | | }); |
| | | return list; |
| | | } |
| | | |
| | | /** |
| | | * 内存聚合库存数量:按维度键 + 库存状态分组,累加在库数量 |
| | | */ |
| | | private List<MesWmStockSummaryRespVO> buildStockSummary(List<MesWmMaterialStockDO> stocks, |
| | | Function<MesWmMaterialStockDO, String> dimKeyFn, |
| | | BiConsumer<MesWmStockSummaryRespVO, MesWmMaterialStockDO> dimSetter) { |
| | | if (CollUtil.isEmpty(stocks)) { |
| | | return Collections.emptyList(); |
| | | } |
| | | Map<String, MesWmStockSummaryRespVO> map = new LinkedHashMap<>(); |
| | | for (MesWmMaterialStockDO stock : stocks) { |
| | | String key = dimKeyFn.apply(stock) + "|" + stock.getStockState(); |
| | | MesWmStockSummaryRespVO vo = map.computeIfAbsent(key, k -> { |
| | | MesWmStockSummaryRespVO newVo = new MesWmStockSummaryRespVO(); |
| | | dimSetter.accept(newVo, stock); |
| | | newVo.setStockState(stock.getStockState()); |
| | | newVo.setStockStateName(stockStateName(stock.getStockState())); |
| | | newVo.setTotalQuantity(BigDecimal.ZERO); |
| | | return newVo; |
| | | }); |
| | | BigDecimal qty = stock.getQuantity() != null ? stock.getQuantity() : BigDecimal.ZERO; |
| | | vo.setTotalQuantity(vo.getTotalQuantity().add(qty)); |
| | | } |
| | | return new ArrayList<>(map.values()); |
| | | } |
| | | |
| | | private String stockStateName(Integer state) { |
| | | if (state == null) { |
| | | return ""; |
| | | } |
| | | for (MesWmStockStateEnum e : MesWmStockStateEnum.values()) { |
| | | if (e.getState().equals(state)) { |
| | | return e.getName(); |
| | | } |
| | | } |
| | | return String.valueOf(state); |
| | | } |
| | | |
| | | @Override |
| | |
| | | .batchId(batchId).batchCode(batchCode).vendorId(vendorId) |
| | | .quantity(BigDecimal.ZERO) // 初始数量为 0,由 updateMaterialStockQuantity 更新 |
| | | .receiptTime(receiptTime != null ? receiptTime : LocalDateTime.now()) |
| | | .stockState(MesWmStockStateEnum.TEMP.getState()) |
| | | .frozen(false) |
| | | .build(); |
| | | materialStockMapper.insert(newStock); |
| | | return 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.selectQualifiedByCompositeKey(itemId, warehouseId, locationId, areaId, batchId, vendorId); |
| | | } |
| | | |
| | | @Override |
| | | public List<MesWmMaterialStockDO> getQualifiedMaterialStockListByItemAndBatch(Long itemId, Long batchId) { |
| | | List<Long> qualifiedWarehouseIds = warehouseService.getQualifiedWarehouseIds(); |
| | | if (CollUtil.isEmpty(qualifiedWarehouseIds)) { |
| | | return Collections.emptyList(); |
| | | } |
| | | return materialStockMapper.selectQualifiedListByItemAndBatch(itemId, batchId, qualifiedWarehouseIds); |
| | | } |
| | | |
| | | @Override |
| | | public List<MesWmMaterialStockDO> getMaterialStockListByItemAndBatch(Long itemId, Long batchId) { |
| | | return materialStockMapper.selectListByItemAndBatch(itemId, batchId); |
| | | } |
| | | |
| | | @Override |
| | |
| | | if (updatedRows == 0) { |
| | | throw exception(WM_MATERIAL_STOCK_INSUFFICIENT); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public void updateMaterialStockState(Long id, Integer stockState) { |
| | | materialStockMapper.updateById(new MesWmMaterialStockDO().setId(id).setStockState(stockState)); |
| | | } |
| | | |
| | | @Override |
| | |
| | | || ObjUtil.notEqual(stock.getAreaId(), areaId)) { |
| | | throw exception(WM_MATERIAL_STOCK_SELECTION_MISMATCH); |
| | | } |
| | | // 3. 校验库存数量充足(如果前端传了 quantity,则必须保证库存数量 >= quantity) |
| | | // 3. 校验库存记录必须是合格库(暂存库存未质检,不可用于出库/占用) |
| | | if (!MesWmStockStateEnum.QUALIFIED.getState().equals(stock.getStockState())) { |
| | | throw exception(WM_MATERIAL_STOCK_NOT_QUALIFIED); |
| | | } |
| | | // 4. 校验库存数量充足(如果前端传了 quantity,则必须保证库存数量 >= quantity) |
| | | if (quantity != null && stock.getQuantity() != null && stock.getQuantity().compareTo(quantity) < 0) { |
| | | throw exception(WM_MATERIAL_STOCK_INSUFFICIENT); |
| | | } |
| | |
| | | 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); |
| | | |
| | | // 9. 整位置移库时,逐袋迁移袋码明细位置(数量等于源库位活动袋数且为整数才视为整位移库) |
| | | MesWmMaterialStockDO finalTargetStock = materialStockMapper.selectByCompositeKeyIgnoreVendor( |
| | | sourceStock.getItemId(), reqVO.getToWarehouseId(), reqVO.getToLocationId(), |
| | | reqVO.getToAreaId(), sourceStock.getBatchId()); |
| | | relocateBagsForMove(sourceStock, reqVO.getQuantity(), inTransaction, finalTargetStock); |
| | | } |
| | | |
| | | /** |
| | | * 整位移库:将源库位的活动袋码明细迁移到目标库位。 |
| | | * <p> |
| | | * 仅当移库数量等于源库位活动袋数且为整数时执行(整位置搬移),将每袋明细的库存汇总、仓库、库区、库位 |
| | | * 更新为目标位置;部分移库/无袋码明细的旧库存不做袋码迁移,保持汇总库存变动即可。 |
| | | */ |
| | | private void relocateBagsForMove(MesWmMaterialStockDO sourceStock, BigDecimal quantity, |
| | | MesWmTransactionSaveReqDTO moveInReq, MesWmMaterialStockDO targetStock) { |
| | | List<MesWmMaterialStockBagDO> bags = materialStockBagMapper.selectActiveListByItemAndBatch( |
| | | sourceStock.getItemId(), sourceStock.getBatchId(), sourceStock.getStockState()); |
| | | if (CollUtil.isEmpty(bags) || targetStock == null || quantity == null |
| | | || quantity.compareTo(BigDecimal.valueOf(bags.size())) != 0) { |
| | | return; |
| | | } |
| | | for (MesWmMaterialStockBagDO bag : bags) { |
| | | moveInReq.setBagCodeId(bag.getBagCodeId()).setBagCode(bag.getBagCode()); |
| | | materialStockBagService.moveIn(moveInReq, targetStock, null); |
| | | } |
| | | } |
| | | |
| | | } |