| | |
| | | import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.bom.MesProWorkOrderBomSaveReqVO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemTypeDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdProductBomDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductBomDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderBomDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderProcessDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeLineDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.wm.itemconsume.MesWmItemConsumeLineMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstock.MesWmMaterialStockMapper; |
| | | import cn.iocoder.yudao.module.erp.api.purchase.ErpPurchaseOrderApi; |
| | | import cn.iocoder.yudao.module.erp.api.purchase.dto.ErpPurchaseOrderItemRespDTO; |
| | | import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService; |
| | | import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemTypeService; |
| | | import cn.iocoder.yudao.module.mes.service.md.item.MesMdProductBomService; |
| | | import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService; |
| | | import cn.iocoder.yudao.module.mes.service.pro.cost.MesProMaterialPriceService; |
| | | import cn.iocoder.yudao.module.mes.service.pro.cost.dto.MesProMaterialPriceRespDTO; |
| | | import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductBomService; |
| | | import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductService; |
| | | import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderBomService; |
| | |
| | | @Resource |
| | | private MesWmMaterialStockMapper materialStockMapper; |
| | | @Resource |
| | | private MesWmItemConsumeLineMapper consumeLineMapper; |
| | | @Resource |
| | | private MesMdProductBomService productBomService; |
| | | @Resource |
| | | private MesProMaterialPriceService materialPriceService; |
| | | @Resource |
| | | private ErpPurchaseOrderApi purchaseOrderApi; |
| | | |
| | | @PostMapping("/create") |
| | |
| | | @GetMapping("/get") |
| | | @Operation(summary = "获得工单 BOM") |
| | | @Parameter(name = "id", description = "编号", required = true, example = "1024") |
| | | @PreAuthorize("@ss.hasPermission('mes:pro-work-order:query')") |
| | | public CommonResult<MesProWorkOrderBomRespVO> getWorkOrderBom(@RequestParam("id") Long id) { |
| | | MesProWorkOrderBomDO workOrderBom = workOrderBomService.getWorkOrderBom(id); |
| | | if (workOrderBom == null) { |
| | |
| | | |
| | | @GetMapping("/page") |
| | | @Operation(summary = "获得工单 BOM 分页") |
| | | @PreAuthorize("@ss.hasPermission('mes:pro-work-order:query')") |
| | | public CommonResult<PageResult<MesProWorkOrderBomRespVO>> getWorkOrderBomPage(@Valid MesProWorkOrderBomPageReqVO pageReqVO) { |
| | | PageResult<MesProWorkOrderBomDO> pageResult = workOrderBomService.getWorkOrderBomPage(pageReqVO); |
| | | return success(new PageResult<>(buildWorkOrderBomRespVOList(pageResult.getList()), pageResult.getTotal())); |
| | |
| | | @GetMapping("/item-list-by-work-order-id") |
| | | @Operation(summary = "获得工单物料需求列表") |
| | | @Parameter(name = "workOrderId", description = "工单编号", required = true) |
| | | @PreAuthorize("@ss.hasPermission('mes:pro-work-order:query')") |
| | | public CommonResult<List<MesProWorkOrderItemRespVO>> getWorkOrderBomItemListByWorkOrderId( |
| | | @RequestParam("workOrderId") Long workOrderId) { |
| | | // 1. 获取工单 |
| | |
| | | return success(Collections.emptyList()); |
| | | } |
| | | |
| | | // 2. 获取工单工序列表(含各工序的投料明细与产出品) |
| | | // 2. 获取工单工序列表与工单 BOM |
| | | List<MesProWorkOrderProcessDO> processList = workOrderProcessService |
| | | .getWorkOrderProcessListByWorkOrderId(workOrderId); |
| | | if (CollUtil.isEmpty(processList)) { |
| | | |
| | | return success(Collections.emptyList()); |
| | | } |
| | | |
| | | // 3. 获取工单 BOM 行(各工序的投料记录,按 processId 分组使用) |
| | | List<MesProWorkOrderBomDO> bomList = workOrderBomService.getWorkOrderBomListByWorkOrderId(workOrderId); |
| | | if (CollUtil.isEmpty(bomList)) { |
| | | |
| | | // 3. 计算本次生产的总物料需求 |
| | | Map<Long, BigDecimal> totalRequirements = buildTotalRequirements(workOrder, processList, bomList); |
| | | if (CollUtil.isEmpty(totalRequirements)) { |
| | | return success(Collections.emptyList()); |
| | | } |
| | | |
| | | // 4. 通过产品ID反查工艺路线(工单 routeId 可能为空) |
| | | Long routeId = null; |
| | | if (workOrder.getProductId() != null) { |
| | | MesProRouteProductDO routeProduct = routeProductService.getRouteProductByItemId(workOrder.getProductId()); |
| | | if (routeProduct != null) { |
| | | routeId = routeProduct.getRouteId(); |
| | | } |
| | | } |
| | | |
| | | // 5. 按工序产出链逐层展开 BOM,得到叶子物料(每单位成品的用量) |
| | | Map<Long, BigDecimal> leafItems = buildWorkOrderItems(processList, bomList, routeId); |
| | | if (CollUtil.isEmpty(leafItems)) { |
| | | return success(Collections.emptyList()); |
| | | } |
| | | |
| | | // 6. 乘以工单生产数量,得到总需求 |
| | | BigDecimal orderQty = workOrder.getQuantity() != null ? workOrder.getQuantity() : BigDecimal.ONE; |
| | | Map<Long, BigDecimal> totalRequirements = new LinkedHashMap<>(); |
| | | for (Map.Entry<Long, BigDecimal> entry : leafItems.entrySet()) { |
| | | totalRequirements.put(entry.getKey(), entry.getValue().multiply(orderQty)); |
| | | } |
| | | |
| | | // 7. 查询来源为本工单的采购订单明细,按产品汇总在途采购数 |
| | | // 4. 查询来源为本工单的采购订单明细,按产品汇总在途采购数 |
| | | // sourceType=1 表示来源类型为"生产工单" |
| | | Map<Long, BigDecimal> purchaseOrderQtyMap = new HashMap<>(); |
| | | List<ErpPurchaseOrderItemRespDTO> poItems = purchaseOrderApi |
| | |
| | | } |
| | | } |
| | | |
| | | // 8. 查询库存,拼接 VO |
| | | return success(buildItemRespVOList(totalRequirements, purchaseOrderQtyMap)); |
| | | // 5. 查询本工单累计消耗,拼接 VO |
| | | Map<Long, BigDecimal> actualQuantityMap = buildActualQuantityMap(workOrderId); |
| | | return success(buildItemRespVOList(totalRequirements, purchaseOrderQtyMap, actualQuantityMap)); |
| | | } |
| | | |
| | | /** |
| | | * 计算工单总物料需求。 |
| | | * |
| | | * 优先级: |
| | | * 1. 工序产出链展开(有工序、有工单 BOM 时) |
| | | * 2. 产品 BOM × 工单数量(未配置工序/工单 BOM 时的兜底,保证能按单量算料) |
| | | * 3. 工单 BOM 兜底(无工序行已按工单数量放大;有工序行为单位用量,需乘工单数量) |
| | | */ |
| | | private Map<Long, BigDecimal> buildTotalRequirements(MesProWorkOrderDO workOrder, |
| | | List<MesProWorkOrderProcessDO> processList, |
| | | List<MesProWorkOrderBomDO> bomList) { |
| | | BigDecimal orderQty = workOrder.getQuantity() != null ? workOrder.getQuantity() : BigDecimal.ONE; |
| | | // 1. 工序产出链展开 |
| | | if (CollUtil.isNotEmpty(processList) && CollUtil.isNotEmpty(bomList)) { |
| | | Long routeId = null; |
| | | if (workOrder.getProductId() != null) { |
| | | MesProRouteProductDO routeProduct = routeProductService |
| | | .getRouteProductByItemId(workOrder.getProductId()); |
| | | if (routeProduct != null) { |
| | | routeId = routeProduct.getRouteId(); |
| | | } |
| | | } |
| | | Map<Long, BigDecimal> leafItems = buildWorkOrderItems(processList, bomList, routeId); |
| | | if (CollUtil.isNotEmpty(leafItems)) { |
| | | return multiplyQuantity(leafItems, orderQty); |
| | | } |
| | | } |
| | | // 2. 产品 BOM × 工单数量 |
| | | List<MesMdProductBomDO> productBomList = productBomService |
| | | .getProductBomListByItemId(workOrder.getProductId()); |
| | | if (CollUtil.isNotEmpty(productBomList)) { |
| | | Map<Long, BigDecimal> requirements = new LinkedHashMap<>(); |
| | | for (MesMdProductBomDO bom : productBomList) { |
| | | if (bom.getBomItemId() != null && bom.getQuantity() != null) { |
| | | requirements.merge(bom.getBomItemId(), |
| | | bom.getQuantity().multiply(orderQty), BigDecimal::add); |
| | | } |
| | | } |
| | | return requirements; |
| | | } |
| | | // 3. 工单 BOM 兜底 |
| | | if (CollUtil.isNotEmpty(bomList)) { |
| | | Map<Long, BigDecimal> requirements = new LinkedHashMap<>(); |
| | | for (MesProWorkOrderBomDO bom : bomList) { |
| | | if (bom.getItemId() == null || bom.getQuantity() == null) { |
| | | continue; |
| | | } |
| | | BigDecimal quantity = bom.getProcessId() == null |
| | | ? bom.getQuantity() : bom.getQuantity().multiply(orderQty); |
| | | requirements.merge(bom.getItemId(), quantity, BigDecimal::add); |
| | | } |
| | | return requirements; |
| | | } |
| | | return Collections.emptyMap(); |
| | | } |
| | | |
| | | private static Map<Long, BigDecimal> multiplyQuantity(Map<Long, BigDecimal> source, BigDecimal factor) { |
| | | Map<Long, BigDecimal> result = new LinkedHashMap<>(); |
| | | for (Map.Entry<Long, BigDecimal> entry : source.entrySet()) { |
| | | result.put(entry.getKey(), entry.getValue().multiply(factor)); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private Map<Long, BigDecimal> buildActualQuantityMap(Long workOrderId) { |
| | | Map<Long, BigDecimal> result = new HashMap<>(); |
| | | for (MesWmItemConsumeLineDO line : consumeLineMapper.selectListByWorkOrderId(workOrderId)) { |
| | | if (line.getItemId() != null && line.getQuantity() != null) { |
| | | result.merge(line.getItemId(), line.getQuantity(), BigDecimal::add); |
| | | } |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | // ==================== 拼接 VO ==================== |
| | |
| | | } |
| | | List<MesProWorkOrderBomDO> items = new ArrayList<>(); |
| | | for (MesProWorkOrderBomDO bom : bomList) { |
| | | if (bom.getProcessId().equals(process.getProcessId())) { |
| | | // 跳过无工序编号的投料行(工单 BOM 中仅按工序维护投料明细,无工序的行不参与展开, |
| | | // 且 getProcessId 可能为 null,直接 equals 会 NPE) |
| | | if (Objects.equals(bom.getProcessId(), process.getProcessId())) { |
| | | items.add(bom); |
| | | } |
| | | } |
| | |
| | | // 3. 以最后一道工序的投料为起点 |
| | | Map<Long, BigDecimal> currentLayer = new LinkedHashMap<>(); |
| | | for (MesProWorkOrderBomDO bom : bomList) { |
| | | if (bom.getProcessId().equals(lastProcess.getProcessId())) { |
| | | if (Objects.equals(bom.getProcessId(), lastProcess.getProcessId())) { |
| | | currentLayer.merge(bom.getItemId(), bom.getQuantity(), BigDecimal::add); |
| | | } |
| | | } |
| | |
| | | // ==================== 拼接物料需求 VO(含库存校验) ==================== |
| | | |
| | | private List<MesProWorkOrderItemRespVO> buildItemRespVOList(Map<Long, BigDecimal> requirements, |
| | | Map<Long, BigDecimal> purchaseOrderQtyMap) { |
| | | // 1. 批量获取物料基础信息 |
| | | Map<Long, BigDecimal> purchaseOrderQtyMap, |
| | | Map<Long, BigDecimal> actualQuantityMap) { |
| | | // 1. 批量获取物料基础信息 + 物料价格 |
| | | Map<Long, MesMdItemDO> itemMap = itemService.getItemMap(requirements.keySet()); |
| | | Map<Long, MesMdUnitMeasureDO> unitMeasureMap = unitMeasureService.getUnitMeasureMap( |
| | | convertSet(itemMap.values(), MesMdItemDO::getUnitMeasureId)); |
| | | Map<Long, MesMdItemTypeDO> itemTypeMap = itemTypeService.getItemTypeMap( |
| | | convertSet(itemMap.values(), MesMdItemDO::getItemTypeId)); |
| | | Map<Long, MesProMaterialPriceRespDTO> priceMap = materialPriceService |
| | | .getMaterialPriceMap(requirements.keySet()); |
| | | |
| | | // 2. 批量查询库存,按 itemId 汇总可用量(在库 - 冻结 - 占用) |
| | | Map<Long, BigDecimal> stockMap = new HashMap<>(); |
| | |
| | | // 充足判断 = 可用库存 + 在途采购数 >= 需求数量 |
| | | BigDecimal totalSupply = stockQty.add(purchaseQty); |
| | | |
| | | MesProMaterialPriceRespDTO price = priceMap.get(item.getId()); |
| | | BigDecimal unitPrice = price != null && price.getUnitPrice() != null |
| | | ? price.getUnitPrice() : BigDecimal.ZERO; |
| | | BigDecimal amount = requiredQty.multiply(unitPrice); |
| | | |
| | | MesProWorkOrderItemRespVO vo = new MesProWorkOrderItemRespVO() |
| | | .setItemId(item.getId()).setQuantity(requiredQty) |
| | | .setItemCode(item.getCode()).setItemName(item.getName()).setItemSpecification(item.getSpecification()) |
| | | .setActualQuantity(actualQuantityMap.getOrDefault(item.getId(), BigDecimal.ZERO)) |
| | | .setUnitPrice(unitPrice) |
| | | .setAmount(amount) |
| | | .setPriceMissing(price == null || Boolean.TRUE.equals(price.getPriceMissing())) |
| | | .setStockQuantity(stockQty) |
| | | .setPurchaseOrderQuantity(purchaseQty) |
| | | .setStockSufficient(totalSupply.compareTo(requiredQty) >= 0); |