liyong
5 天以前 f90d9f53beb844265c4153159aadb0baf81bafdb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
package cn.iocoder.yudao.module.mes.controller.admin.pro.workorder;
 
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.MesProWorkOrderItemRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.bom.MesProWorkOrderBomPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.bom.MesProWorkOrderBomRespVO;
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.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.materialstock.MesWmMaterialStockDO;
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.unitmeasure.MesMdUnitMeasureService;
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;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderProcessService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
 
import java.math.BigDecimal;
import java.util.*;
import java.util.Comparator;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
 
@Tag(name = "管理后台 - MES 生产工单 BOM")
@RestController
@RequestMapping("/mes/pro/work-order-bom")
@Validated
public class MesProWorkOrderBomController {
 
    @Resource
    private MesProWorkOrderBomService workOrderBomService;
    @Resource
    private MesProWorkOrderService workOrderService;
    @Resource
    private MesProWorkOrderProcessService workOrderProcessService;
    @Resource
    private MesMdItemService itemService;
    @Resource
    private MesMdUnitMeasureService unitMeasureService;
    @Resource
    private MesMdItemTypeService itemTypeService;
    @Resource
    private MesProRouteProductService routeProductService;
    @Resource
    private MesProRouteProductBomService routeProductBomService;
    @Resource
    private MesWmMaterialStockMapper materialStockMapper;
    @Resource
    private ErpPurchaseOrderApi purchaseOrderApi;
 
    @PostMapping("/create")
    @Operation(summary = "创建工单 BOM")
    @PreAuthorize("@ss.hasPermission('mes:pro-work-order:update')")
    public CommonResult<Long> createWorkOrderBom(@Valid @RequestBody MesProWorkOrderBomSaveReqVO createReqVO) {
        return success(workOrderBomService.createWorkOrderBom(createReqVO));
    }
 
    @PutMapping("/update")
    @Operation(summary = "更新工单 BOM")
    @PreAuthorize("@ss.hasPermission('mes:pro-work-order:update')")
    public CommonResult<Boolean> updateWorkOrderBom(@Valid @RequestBody MesProWorkOrderBomSaveReqVO updateReqVO) {
        workOrderBomService.updateWorkOrderBom(updateReqVO);
        return success(true);
    }
 
    @DeleteMapping("/delete")
    @Operation(summary = "删除工单 BOM")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('mes:pro-work-order:update')")
    public CommonResult<Boolean> deleteWorkOrderBom(@RequestParam("id") Long id) {
        workOrderBomService.deleteWorkOrderBom(id);
        return success(true);
    }
 
    @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) {
            return success(null);
        }
        return success(buildWorkOrderBomRespVOList(ListUtil.of(workOrderBom)).get(0));
    }
 
    @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. 获取工单
        MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(workOrderId);
        if (workOrder == null) {
            return success(Collections.emptyList());
        }
 
        // 2. 获取工单工序列表(含各工序的投料明细与产出品)
        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)) {
            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. 查询来源为本工单的采购订单明细,按产品汇总在途采购数
        //    sourceType=1 表示来源类型为"生产工单"
        Map<Long, BigDecimal> purchaseOrderQtyMap = new HashMap<>();
        List<ErpPurchaseOrderItemRespDTO> poItems = purchaseOrderApi
                .getPurchaseOrderItemListBySource(1, workOrderId)
                .getCheckedData();
        if (CollUtil.isNotEmpty(poItems)) {
            for (ErpPurchaseOrderItemRespDTO poItem : poItems) {
                // 在途 = 订购数量 - 已入库数量
                BigDecimal inCount = poItem.getInCount() != null ? poItem.getInCount() : BigDecimal.ZERO;
                BigDecimal inTransit = poItem.getCount().subtract(inCount);
                if (inTransit.compareTo(BigDecimal.ZERO) > 0) {
                    purchaseOrderQtyMap.merge(poItem.getProductId(), inTransit, BigDecimal::add);
                }
            }
        }
 
        // 8. 查询库存,拼接 VO
        return success(buildItemRespVOList(totalRequirements, purchaseOrderQtyMap));
    }
 
    // ==================== 拼接 VO ====================
 
    private List<MesProWorkOrderBomRespVO> buildWorkOrderBomRespVOList(List<MesProWorkOrderBomDO> list) {
        if (CollUtil.isEmpty(list)) {
            return Collections.emptyList();
        }
        // 1. 批量获取关联数据
        Map<Long, MesMdItemDO> itemMap = itemService.getItemMap(
                convertSet(list, MesProWorkOrderBomDO::getItemId));
        Map<Long, MesMdUnitMeasureDO> unitMeasureMap = unitMeasureService.getUnitMeasureMap(
                convertSet(itemMap.values(), MesMdItemDO::getUnitMeasureId));
        Map<Long, MesMdItemTypeDO> itemTypeMap = itemTypeService.getItemTypeMap(
                convertSet(itemMap.values(), MesMdItemDO::getItemTypeId));
        // 2. 拼接 VO
        return BeanUtils.toBean(list, MesProWorkOrderBomRespVO.class, vo -> {
            MapUtils.findAndThen(itemMap, vo.getItemId(), item -> {
                vo.setItemName(item.getName()).setItemCode(item.getCode())
                        .setItemSpecification(item.getSpecification());
                MapUtils.findAndThen(itemTypeMap, item.getItemTypeId(),
                        itemType -> vo.setItemOrProduct(itemType.getItemOrProduct()));
                MapUtils.findAndThen(unitMeasureMap, item.getUnitMeasureId(),
                        unitMeasure -> vo.setUnitMeasureName(unitMeasure.getName()));
            });
        });
    }
 
    // ==================== 物料需求展开(工序产出链 + 路由产品 BOM) ====================
 
    /**
     * 按工序产出链逐层展开物料需求。
     *
     * <p><b>核心逻辑:</b></p>
     * <ol>
     *   <li>找出关键工序(keyFlag=true),以它的投料为展开起点</li>
     *   <li>构建产出品→工序投料的映射({@code outputItemId → per-unit bomItems})</li>
     *   <li>逐层展开:若物料是某工序的产出品 → 用该工序的投料配方替代;
     *       否则查路由产品 BOM → 用 BOM 子项替代;
     *       两者都没有 → 叶子物料(外购件),累加到结果</li>
     * </ol>
     *
     * <p>这样避免了把中间工序的投料直接纳入起点导致的重复计算问题。</p>
     *
     * @param processList 工单工序列表(按 sort 升序)
     * @param bomList     工单 BOM 行(所有工序的投料明细)
     * @param routeId     工艺路线编号(可为 null,null 时跳过路由 BOM 查询)
     * @return 叶子物料 Map:itemId → 需求数量
     */
    private Map<Long, BigDecimal> buildWorkOrderItems(List<MesProWorkOrderProcessDO> processList,
                                                       List<MesProWorkOrderBomDO> bomList,
                                                       Long routeId) {
        // 1. 构建产出品 → 工序投料配方(per-unit)
        //    outputItemId → [{itemId, quantity}, ...]
        Map<Long, List<MesProWorkOrderBomDO>> processBomMap = new HashMap<>();
        for (MesProWorkOrderProcessDO process : processList) {
            if (process.getOutputItemId() == null) {
                continue;
            }
            List<MesProWorkOrderBomDO> items = new ArrayList<>();
            for (MesProWorkOrderBomDO bom : bomList) {
                if (bom.getProcessId().equals(process.getProcessId())) {
                    items.add(bom);
                }
            }
            processBomMap.put(process.getOutputItemId(), items);
        }
 
        // 2. 取最后一道工序(sort 最大)作为展开起点,逐层向前递推
        MesProWorkOrderProcessDO lastProcess = processList.stream()
                .max(Comparator.comparing(MesProWorkOrderProcessDO::getSort))
                .orElse(null);
        if (lastProcess == null) {
            return Collections.emptyMap();
        }
 
        // 3. 以最后一道工序的投料为起点
        Map<Long, BigDecimal> currentLayer = new LinkedHashMap<>();
        for (MesProWorkOrderBomDO bom : bomList) {
            if (bom.getProcessId().equals(lastProcess.getProcessId())) {
                currentLayer.merge(bom.getItemId(), bom.getQuantity(), BigDecimal::add);
            }
        }
        if (CollUtil.isEmpty(currentLayer)) {
            return Collections.emptyMap();
        }
 
        // 4. 逐层展开
        Map<Long, BigDecimal> leafItems = new LinkedHashMap<>();
        for (int i = 0; i < Byte.MAX_VALUE && CollUtil.isNotEmpty(currentLayer); i++) {
            Map<Long, BigDecimal> nextLayer = new LinkedHashMap<>();
 
            // 4.1 批量查询路由产品 BOM(用于非工序产出的物料展开)
            Map<Long, List<MesProRouteProductBomDO>> routeSubBomMap;
            if (routeId != null) {
                List<MesProRouteProductBomDO> routeSubBomList = routeProductBomService
                        .getRouteProductBomListByProductIds(routeId, currentLayer.keySet());
                routeSubBomMap = convertMultiMap(routeSubBomList, MesProRouteProductBomDO::getProductId);
            } else {
                routeSubBomMap = Collections.emptyMap();
            }
 
            for (Map.Entry<Long, BigDecimal> entry : currentLayer.entrySet()) {
                Long itemId = entry.getKey();
                BigDecimal qty = entry.getValue();
 
                // 情况一:物料是某工序的产出品 → 用工序配方替代
                List<MesProWorkOrderBomDO> processBom = processBomMap.get(itemId);
                if (CollUtil.isNotEmpty(processBom)) {
                    for (MesProWorkOrderBomDO child : processBom) {
                        nextLayer.merge(child.getItemId(), qty.multiply(child.getQuantity()), BigDecimal::add);
                    }
                    continue;
                }
 
                // 情况二:路由产品 BOM 中有下级 → 展开
                List<MesProRouteProductBomDO> routeChildren = routeSubBomMap.get(itemId);
                if (CollUtil.isNotEmpty(routeChildren)) {
                    for (MesProRouteProductBomDO child : routeChildren) {
                        nextLayer.merge(child.getItemId(), qty.multiply(child.getQuantity()), BigDecimal::add);
                    }
                    continue;
                }
 
                // 情况三:叶子物料
                leafItems.merge(itemId, qty, BigDecimal::add);
            }
            currentLayer = nextLayer;
        }
        return leafItems;
    }
 
    // ==================== 拼接物料需求 VO(含库存校验) ====================
 
    private List<MesProWorkOrderItemRespVO> buildItemRespVOList(Map<Long, BigDecimal> requirements,
                                                                  Map<Long, BigDecimal> purchaseOrderQtyMap) {
        // 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));
 
        // 2. 批量查询库存,按 itemId 汇总可用量(在库 - 冻结 - 占用)
        Map<Long, BigDecimal> stockMap = new HashMap<>();
        List<MesWmMaterialStockDO> stockList = materialStockMapper.selectListByItemIds(requirements.keySet());
        for (MesWmMaterialStockDO stock : stockList) {
            BigDecimal frozen = stock.getFrozenQuantity() != null ? stock.getFrozenQuantity() : BigDecimal.ZERO;
            BigDecimal reserved = stock.getReservedQuantity() != null ? stock.getReservedQuantity() : BigDecimal.ZERO;
            BigDecimal available = stock.getQuantity().subtract(frozen).subtract(reserved);
            if (available.compareTo(BigDecimal.ZERO) > 0) {
                stockMap.merge(stock.getItemId(), available, BigDecimal::add);
            }
        }
 
        // 3. 拼接 VO
        List<MesProWorkOrderItemRespVO> result = new ArrayList<>(requirements.size());
        for (Map.Entry<Long, BigDecimal> entry : requirements.entrySet()) {
            MesMdItemDO item = itemMap.get(entry.getKey());
            if (item == null) {
                continue;
            }
            BigDecimal requiredQty = entry.getValue();
            BigDecimal stockQty = stockMap.getOrDefault(item.getId(), BigDecimal.ZERO);
            BigDecimal purchaseQty = purchaseOrderQtyMap.getOrDefault(item.getId(), BigDecimal.ZERO);
 
            // 充足判断 = 可用库存 + 在途采购数 >= 需求数量
            BigDecimal totalSupply = stockQty.add(purchaseQty);
 
            MesProWorkOrderItemRespVO vo = new MesProWorkOrderItemRespVO()
                    .setItemId(item.getId()).setQuantity(requiredQty)
                    .setItemCode(item.getCode()).setItemName(item.getName()).setItemSpecification(item.getSpecification())
                    .setStockQuantity(stockQty)
                    .setPurchaseOrderQuantity(purchaseQty)
                    .setStockSufficient(totalSupply.compareTo(requiredQty) >= 0);
            MapUtils.findAndThen(unitMeasureMap, item.getUnitMeasureId(),
                    unitMeasure -> vo.setUnitMeasureName(unitMeasure.getName()));
            MapUtils.findAndThen(itemTypeMap, item.getItemTypeId(),
                    itemType -> vo.setItemOrProduct(itemType.getItemOrProduct()));
            result.add(vo);
        }
        return result;
    }
 
}