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;
|
}
|
|
}
|