Files
SCGL/backend/app/routers/deduce_bom.py

126 lines
4.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""齐套性推演接口路由"""
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from decimal import Decimal
from collections import defaultdict
from app.database import get_db_inventory
from app.models import MaterialBase, BomTable, StockBuy
router = APIRouter(prefix="/api/pms", tags=["齐套性推演"])
def calculate_bom_requirements(
base_id: int,
quantity: int,
db: Session,
bom_no: str | None = None,
version: str | None = None,
visited: set | None = None,
) -> dict[int, Decimal]:
"""
递归计算BOM所需物料及用量
Args:
base_id: 目标成品物料ID
quantity: 目标数量
db: 数据库会话
bom_no: BOM编号精确匹配可为 None
version: BOM版本精确匹配可为 None
visited: 已访问物料ID集合防循环
"""
if visited is None:
visited = set()
if base_id in visited:
return {}
visited.add(base_id)
requirements = defaultdict(Decimal)
# 关键:按 bom_no / version 精确匹配 BOM 层级
bom_query = db.query(BomTable).filter(BomTable.parent_id == base_id)
if bom_no is not None:
bom_query = bom_query.filter(BomTable.bom_no == bom_no)
if version is not None:
bom_query = bom_query.filter(BomTable.version == version)
bom_items = bom_query.all()
if not bom_items:
visited.discard(base_id)
return {base_id: Decimal(quantity)}
for item in bom_items:
child_requirements = calculate_bom_requirements(
item.child_id,
int(quantity * item.dosage),
db,
bom_no=None,
version=None,
visited=visited,
)
for child_id, qty in child_requirements.items():
requirements[child_id] += qty
return dict(requirements)
@router.get("/deduce_bom")
async def deduce_bom(
target_base_id: int = Query(..., description="目标成品ID"),
target_quantity: int = Query(..., gt=0, description="计划生产数量"),
bom_no: str | None = Query(None, description="BOM编号精确匹配可为空"),
version: str | None = Query(None, description="BOM版本精确匹配可为空"),
db: Session = Depends(get_db_inventory),
):
"""
齐套性推演接口
- 若传 bom_no 和 version则只查对应版本 BOM
- 若不传 bom_no/version则查该成品所有可用 BOM兼容旧行为
- 返回 material_id / material_name / spec_model / unit / required_quantity / current_stock / shortage_quantity
"""
target_material = db.query(MaterialBase).filter(MaterialBase.id == target_base_id).first()
if not target_material:
raise HTTPException(status_code=404, detail=f"目标物料 ID={target_base_id} 不存在")
requirements = calculate_bom_requirements(
target_base_id, target_quantity, db,
bom_no=bom_no, version=version,
)
stock_records = {
r.base_id: r.stock_quantity
for r in db.query(StockBuy).filter(StockBuy.base_id.in_(requirements.keys())).all()
}
material_requirements = []
total_shortage = 0
for base_id, required_qty in sorted(requirements.items()):
material = db.query(MaterialBase).filter(MaterialBase.id == base_id).first()
current_stock = stock_records.get(base_id, Decimal("0"))
shortage = max(Decimal("0"), required_qty - current_stock)
if shortage > 0:
total_shortage += 1
material_requirements.append({
"material_id": base_id,
"material_name": material.name if material else f"未知物料({base_id})",
"spec_model": material.spec_model if material else None,
"unit": material.unit if material else None,
"required_quantity": required_qty,
"current_stock": current_stock,
"shortage_quantity": shortage,
"is_shortage": shortage > 0,
})
return {
"target_base_id": target_base_id,
"target_quantity": target_quantity,
"bom_no": bom_no,
"version": version,
"is_shortage": total_shortage > 0,
"total_shortage_count": total_shortage,
"material_requirements": material_requirements,
}