feat: P0-4 按库存优化+切割准备清单(缺料告警)
This commit is contained in:
@@ -54,45 +54,103 @@ async def save_render_setting(body: RenderSettingBody, user: CurrentUser, db: Db
|
||||
class CutSolutionBody(BaseModel):
|
||||
contract_id: int
|
||||
kerf: int = Field(default=5, ge=0, le=50)
|
||||
use_stock: bool = Field(default=False, description="使用仓库实际库存原料优化(不足部分输出缺口清单)")
|
||||
stock_length: int = Field(default=6000, ge=1000, le=12000, description="非库存模式的标准原料长度(mm)")
|
||||
|
||||
|
||||
@router.post("/cut_solution")
|
||||
async def cut_solution(body: CutSolutionBody, user: CurrentUser, db: DbSession):
|
||||
"""合同下料方案:汇总合同下订单的算料需求,执行一维优化并保存参数。"""
|
||||
contract = await db.get(Contract, body.contract_id)
|
||||
if contract is None or contract.company_id != user.company_id:
|
||||
return fail("合同不存在")
|
||||
stmt = select(Order).where(Order.contract_id == body.contract_id,
|
||||
Order.company_id == user.company_id)
|
||||
orders = list((await db.execute(stmt)).scalars())
|
||||
demands: dict[tuple, float] = {}
|
||||
def _contract_demands(orders) -> dict[tuple, int]:
|
||||
"""汇总订单图纸的型材需求 {(length, note): qty}。"""
|
||||
from huamenchuang.core.drawing import parse_drawing
|
||||
from huamenchuang.services.quote_service import takeoff
|
||||
demands: dict[tuple, int] = {}
|
||||
for o in orders:
|
||||
if not o.drawing_data:
|
||||
continue
|
||||
from huamenchuang.core.drawing import parse_drawing
|
||||
from huamenchuang.services.quote_service import takeoff
|
||||
t = takeoff(parse_drawing(o.drawing_data))
|
||||
for b in t.bars:
|
||||
if b.kind == "glass":
|
||||
continue
|
||||
key = (b.length, b.note)
|
||||
demands[key] = demands.get(key, 0) + b.qty
|
||||
return demands
|
||||
|
||||
|
||||
async def _stock_bars(db, company_id: int) -> list:
|
||||
"""仓库型材库存 → StockBar 列表(按单根长度分组)。"""
|
||||
from huamenchuang.models.contract import WarehouseItem
|
||||
from huamenchuang.services.optimize_service import StockBar
|
||||
stmt = select(WarehouseItem).where(
|
||||
WarehouseItem.company_id == company_id,
|
||||
WarehouseItem.material == "profile",
|
||||
WarehouseItem.length > 0,
|
||||
WarehouseItem.count > 0)
|
||||
items = list((await db.execute(stmt)).scalars())
|
||||
return [StockBar(length=int(i.length), qty=int(float(i.count)), label=i.name or f"{int(i.length)}mm型材")
|
||||
for i in items]
|
||||
|
||||
|
||||
@router.post("/cut_solution")
|
||||
async def cut_solution(body: CutSolutionBody, user: CurrentUser, db: DbSession):
|
||||
"""合同下料方案:汇总合同下订单的算料需求,执行一维优化并保存(可选按库存)。"""
|
||||
contract = await db.get(Contract, body.contract_id)
|
||||
if contract is None or contract.company_id != user.company_id:
|
||||
return fail("合同不存在")
|
||||
stmt = select(Order).where(Order.contract_id == body.contract_id,
|
||||
Order.company_id == user.company_id)
|
||||
orders = list((await db.execute(stmt)).scalars())
|
||||
demands = _contract_demands(orders)
|
||||
if not demands:
|
||||
return fail("该合同下没有可算料的订单图纸")
|
||||
from huamenchuang.services.optimize_service import BarDemand, StockBar, optimize_1d
|
||||
dem = [BarDemand(length=k[0], qty=int(v), label=k[1]) for k, v in demands.items()]
|
||||
stocks = [StockBar(length=6000, qty=max(1, sum(d.qty for d in dem) // 3 + 1), label="6米型材")]
|
||||
if body.use_stock:
|
||||
stocks = await _stock_bars(db, user.company_id)
|
||||
if not stocks:
|
||||
return fail("仓库中没有可用的型材库存(需 material=profile 且 length>0)")
|
||||
else:
|
||||
total_pieces = sum(d.qty for d in dem)
|
||||
stocks = [StockBar(length=body.stock_length, qty=max(1, total_pieces // 3 + 1),
|
||||
label=f"{body.stock_length}mm型材")]
|
||||
result = optimize_1d(dem, stocks, body.kerf)
|
||||
# 按库存模式:缺料清单 = 未能排入的件(需求超出库存)
|
||||
shortage = []
|
||||
if body.use_stock and result["unplaced"]:
|
||||
agg: dict[tuple, int] = {}
|
||||
for u in result["unplaced"]:
|
||||
agg[(u["length"], u["label"])] = agg.get((u["length"], u["label"]), 0) + 1
|
||||
shortage = [{"length": k[0], "label": k[1], "qty": v} for k, v in
|
||||
sorted(agg.items(), key=lambda x: -x[0][0])]
|
||||
# 领料/切割准备清单:按原料长度聚合根数
|
||||
preparation = _preparation(result)
|
||||
payload = {"demands": [[k[0], int(v), k[1]] for k, v in demands.items()],
|
||||
"kerf": body.kerf, "use_stock": body.use_stock,
|
||||
"stock_length": body.stock_length, "result": result,
|
||||
"shortage": shortage, "preparation": preparation}
|
||||
await _save_param(db, user.company_id, f"cut_solution_{body.contract_id}",
|
||||
__import__("json").dumps({"demands": [[k[0], int(v), k[1]] for k, v in demands.items()],
|
||||
"kerf": body.kerf}, ensure_ascii=False))
|
||||
return ok(data={"contract_no": contract.contract_no, "order_count": len(orders), **result},
|
||||
__import__("json").dumps(payload, ensure_ascii=False))
|
||||
return ok(data={"contract_no": contract.contract_no, "order_count": len(orders),
|
||||
"use_stock": body.use_stock, **result,
|
||||
"shortage": shortage, "preparation": preparation},
|
||||
message="下料方案已生成")
|
||||
|
||||
|
||||
def _preparation(result: dict) -> list[dict]:
|
||||
"""切割准备清单:按原料(长度,标签)聚合根数 + 每根切割件数/余料。"""
|
||||
agg: dict[tuple, dict] = {}
|
||||
for plan in result["plans"]:
|
||||
key = (plan["stock_length"], plan["stock_label"])
|
||||
if key not in agg:
|
||||
agg[key] = {"stock_length": plan["stock_length"], "stock_label": plan["stock_label"],
|
||||
"bars": 0, "pieces": 0, "waste": 0}
|
||||
agg[key]["bars"] += 1
|
||||
agg[key]["pieces"] += len(plan["pieces"])
|
||||
agg[key]["waste"] += plan["waste"]
|
||||
return sorted(agg.values(), key=lambda x: -x["stock_length"])
|
||||
|
||||
|
||||
@router.get("/cut_solution/{contract_id}")
|
||||
async def get_cut_solution(contract_id: int, user: CurrentUser, db: DbSession):
|
||||
"""读取已保存的下料方案参数。"""
|
||||
"""读取已保存的下料方案(含优化结果/缺口/领料清单)。"""
|
||||
p = await _get_param(db, user.company_id, f"cut_solution_{contract_id}")
|
||||
if p is None:
|
||||
return fail("尚未生成下料方案")
|
||||
|
||||
Reference in New Issue
Block a user