feat: 部位级材料选择(对应部位使用对应材料: 估算器overrides+series-materials聚合端点+QuoteDialog部位选材面板实时重算)

This commit is contained in:
HHY
2026-10-06 05:58:10 -07:00
parent 07eda3ff35
commit 5abfe61beb
6 changed files with 143 additions and 14 deletions
+39
View File
@@ -57,6 +57,45 @@ async def list_series(user: CurrentUser, db: DbSession):
return ok(data=[_brief(t) for t in rows])
@router.get("/costbook/series-materials")
async def series_materials(user: CurrentUser, db: DbSession):
"""聚合全部系列模板的用料清单,按部位分组(frame/mullion/sash/pressline/glass/hardware)。
供商家「按部位选材」下拉:跨系列混搭(如外框用 85 系列的 33框、扇用 75 的内开扇)。
"""
import json as _json
from huamenchuang.services.costbook_service import _profile_role
rows = list((await db.execute(select(CostSeriesTemplate).where(
CostSeriesTemplate.company_id == user.company_id, CostSeriesTemplate.enabled == 1)
.order_by(CostSeriesTemplate.series))).scalars())
groups: dict[str, list] = {"frame": [], "mullion": [], "sash": [], "pressline": [], "glass": [], "hardware": []}
seen: set = set()
def push(role: str, item: dict):
key = (role, item.get("name"), item.get("unit_price"))
if key in seen:
return
seen.add(key)
groups[role].append(item)
for t in rows:
try:
data = _json.loads(t.data)
except Exception:
continue
for p in data.get("profile", []):
role = _profile_role(p.get("name", ""))
if role:
push(role, {"name": p.get("name"), "unit_price": p.get("unit_price"),
"weight_per_m": p.get("weight_per_m"), "series": t.name})
for g in data.get("glass", []):
push("glass", {"name": g.get("config"), "unit_price": g.get("unit_price"), "series": t.name})
for hw in data.get("hardware", []):
push("hardware", {"name": hw.get("name"), "unit_price": hw.get("unit_price"), "series": t.name})
return ok(data=groups)
class EstimateBody(BaseModel):
series_id: int
width: float = Field(gt=0, description="窗宽 mm")
+3 -1
View File
@@ -15,6 +15,7 @@ class QuoteRequest(BaseModel):
template_id: int | None = Field(default=None, description="或直接指定样板")
order_id: int | None = Field(default=None, description="或对已有订单报价")
series_id: int | None = Field(default=None, description="材料系列(成本系列模板): 附加该系列的成本估算供商家选材")
materials: dict | None = Field(default=None, description="部位级材料覆盖 {frame,mullion,sash,glass,hardware: {name,unit_price,weight_per_m}}——对应部位使用对应材料(可跨系列)")
prices: dict | None = Field(default=None, description="价格本覆盖 {frame,mullion,sash,glass,hardware}")
use_price_book: bool = Field(default=False, description="从产品管理价格本取价(按材料类别均价)")
use_scripts: bool = Field(default=False, description="启用价格脚本计价(公司启用脚本覆盖默认单价/总价)")
@@ -88,12 +89,13 @@ async def offer_price(body: QuoteRequest, user: CurrentUser, db: DbSession):
if tpl is not None and tpl.company_id == user.company_id:
w = result.get("width_mm") or 1600
h = result.get("height_mm") or 1600
est = _svc.estimate(tpl, w, h, 1)
est = _svc.estimate(tpl, w, h, 1, material_overrides=body.materials)
result["series"] = {"id": tpl.id, "name": tpl.name, "series": tpl.series, "wall_thick": tpl.wall_thick}
result["series_estimate"] = {
"width": w, "height": h, "total": est["total"], "per_sqm": est["per_sqm"],
"profile_cost": est["profile_cost"], "glass_cost": est["glass_cost"],
"hardware_cost": est["hardware_cost"], "labor_cost": est["labor_cost"],
"margin_ladder": est["margin_ladder"],
"overrides": est.get("overrides"),
}
return ok(data=result, message="报价成功")