- 서버 snapshot_units: 칸마다 그 칸을 낸 조합의 연결된 사본 셈(자재 MT 코드 · 로직 재료 이름|단위) × 칸 값 ÷ 열 계 - 짝 계산(py · ts): 자재 열 = 사본 자재 + 관경 · 2공구 고정 열은 손 값 있을 때만 · 손 값 이김 · 관매설 → 파형강관 1 그대로 - 콘크리트소요 · 총괄 끌어옴 = 이름 · 규격 맞춤 · 안 맞는 사본 자재는 알림 「맞춤 없음」 - 구조물 집계표 결과에 칸 출처(sources) · 채집석 공제는 그대로 남김 - 시험: test_143_4 새로(서버 셈 · 로직 재료 · 거울) · test_121 기대 고침 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015sDZSjFvoiDjdGpgj3muc3
157 lines
6.9 KiB
Python
157 lines
6.9 KiB
Python
"""B08 자재 집계표 셋 길(PLAN 121-1) — 총괄자재 · 콘크리트소요자재 · 자재수량 집계표.
|
||
|
||
GET /api/projects/{id}/quantity/materials
|
||
구조물 집계표(98 `structure_summary` 그대로 부름) + 저장된 사용자 값 → 세 표
|
||
PUT /api/projects/{id}/quantity/materials {overrides}
|
||
[저장] — 사용자 값을 프로젝트 설정 quantity › material_sheets 에 · 서버가 정본으로 다시 셈
|
||
|
||
계산 = `B08_Quantity_Material_Calc.py`(화면 짝 `.ts` 와 같은 식) · 총괄 모래 · 자갈 · 시멘트 할증 =
|
||
좌측 패널 값(`surcharge_pct` · 저장은 옵션 길 PUT …/quantity/options)
|
||
기본값은 파일에 안 씀(읽을 때 채움).
|
||
단위수량 기본(143-4) = 연결된 사본 셈(`M02_Item_Copy.calc` = 층 셈 한 벌)의 일위대가 자재 ·
|
||
로직 재료(`snapshot_units` · 서버 단독 — 브라우저는 받은 값으로 짝 계산).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
from pathlib import Path
|
||
from typing import Any
|
||
from uuid import UUID
|
||
|
||
from fastapi import APIRouter, Body
|
||
|
||
from B08_Quantity.B08_Quantity_Material_Calc import (
|
||
PANEL_KEY,
|
||
_num,
|
||
clean_overrides,
|
||
compute,
|
||
panel_surcharge,
|
||
round6,
|
||
)
|
||
from B08_Quantity.B08_Quantity_Router_Hydro import _project_root
|
||
from B08_Quantity.B08_Quantity_Router_Summary import _combo, structure_summary
|
||
from common_util.common_util_project_settings import quantity_settings, save_section
|
||
from common_util.common_util_summary_item import GROUP_JOIN
|
||
|
||
router = APIRouter(prefix="/api/projects", tags=["B08 Material"])
|
||
|
||
SETTINGS_KEY = "material_sheets"
|
||
|
||
|
||
def _folder_units(root: str | Path, folder: str, meta: dict[str, dict]) -> dict[str, float]:
|
||
"""사본 하나 = 집계 항목 1 단위의 자재 `{키: 수량}` — 일위대가 자재(키 = MT 코드) + 로직 재료
|
||
(키 = 이름|단위 · 재료 수량 × 줄 수량 ÷ 배) · 줄 수량 빈칸 · 0 · 파형강관은 안 담음 · 셈 못 하면
|
||
빈 것. ponytail: 참조 줄(승격 ·
|
||
사전조합) 안 재료는 안 셈 — 펼친 재료가 셈 답에 오면 그때."""
|
||
from M02_MasterTemplete import M02_Item_Copy as copies
|
||
from M02_MasterTemplete.M02_Item_Source import PRICE_FILE, read_json
|
||
|
||
try:
|
||
got = copies.calc(root, folder)
|
||
bundle = read_json(copies.folder_of(root, folder) / PRICE_FILE) or {}
|
||
except (LookupError, ValueError):
|
||
return {}
|
||
rows = {
|
||
row.get("키"): row
|
||
for data in bundle.values()
|
||
if isinstance(data, dict)
|
||
for row in data.get("줄") or data.get("표") or []
|
||
if isinstance(row, dict)
|
||
}
|
||
out: dict[str, float] = {}
|
||
|
||
def put(key: str, name: str, spec: str, unit: str, qty: float) -> None:
|
||
if not qty or name.startswith("파형강관"): # 0 · 관 몫(관매설 → 파형강관 1 · 관경 열)
|
||
return
|
||
meta.setdefault(key, {"key": key, "name": name, "spec": spec, "unit": unit})
|
||
out[key] = out.get(key, 0.0) + qty
|
||
|
||
for line in got.get("줄") or []:
|
||
qty, times = _num(line.get("수량")), _num(line.get("배")) or 1.0
|
||
for mat in line.get("재료") or [] if qty is not None else []:
|
||
if (per := _num(mat.get("수량"))) is not None:
|
||
name, unit = str(mat.get("이름") or ""), str(mat.get("단위") or "")
|
||
put(f"{name}|{unit}", name, "", unit, per * qty / times)
|
||
for mat in got.get("자재") or []:
|
||
code, qty = str(mat.get("코드") or ""), _num(mat.get("수량"))
|
||
if code and qty is not None:
|
||
row = rows.get(code) or {}
|
||
put(
|
||
code,
|
||
str(row.get("이름") or code),
|
||
str(row.get("규격") or ""),
|
||
str(row.get("단위") or ""),
|
||
qty,
|
||
)
|
||
return out
|
||
|
||
|
||
def snapshot_units(root: str | Path, summary: dict[str, Any]) -> dict[str, Any]:
|
||
"""연결된 사본 자재 → 집계표 열마다 단위수량 기본(143-4) — 칸마다(측점 · 시설) 그 칸을 낸
|
||
조합의 사본 자재 × 칸 값을 모아 열 계로 나눔(시설마다 사본이 달라도 계가 맞음) · 사본 없으면
|
||
안 담음."""
|
||
folders: dict[tuple[str, str | None], list[str]] = {}
|
||
for owner_id, entry in (summary.get("linked") or {}).items():
|
||
owner = owner_id.partition(GROUP_JOIN)[0]
|
||
folders.setdefault((owner, _combo(entry.get("항목"))), []).append(entry["폴더"])
|
||
meta: dict[str, dict] = {}
|
||
per_folder: dict[str, dict[str, float]] = {}
|
||
sums: dict[str, dict[str, float]] = {}
|
||
for cid, value, owner, combos in summary.get("sources") or []:
|
||
for folder in (f for combo in combos for f in folders.get((owner, combo), [])):
|
||
if folder not in per_folder:
|
||
per_folder[folder] = _folder_units(root, folder, meta)
|
||
cell = sums.setdefault(cid, {})
|
||
for key, qty in per_folder[folder].items():
|
||
cell[key] = cell.get(key, 0.0) + value * qty
|
||
total = summary.get("total") or {}
|
||
units = {
|
||
cid: {key: round6(qty / total[cid]) for key, qty in cells.items()}
|
||
for cid, cells in sums.items()
|
||
if cells and total.get(cid)
|
||
}
|
||
used = {key for cells in units.values() for key in cells}
|
||
return {"materials": [m for key, m in meta.items() if key in used], "units": units}
|
||
|
||
|
||
async def _sheets(project_id: UUID, root: str | Path, overrides: dict[str, Any]) -> dict[str, Any]:
|
||
summary = await structure_summary(project_id)
|
||
source = {
|
||
"columns": summary["columns"],
|
||
"total": summary["total"],
|
||
"snapshot": await asyncio.to_thread(snapshot_units, root, summary),
|
||
}
|
||
panel = panel_surcharge(quantity_settings(root).get(PANEL_KEY))
|
||
sheets = compute(source, overrides, panel)
|
||
notes = list(summary.get("notes") or [])
|
||
if lost := sheets["quantity"]["unmatched"]:
|
||
notes.append(
|
||
f"사본 자재 {len(lost)}종 — 2공구 열 맞춤 없음(콘크리트소요 · 총괄로 안 끌어옴)"
|
||
)
|
||
return {
|
||
"source": source,
|
||
"overrides": overrides,
|
||
"surcharge_pct": panel,
|
||
# 구조물 집계표 알림(「집수정 모양 기록 없음 n곳」 등) — 총괄 · 콘크리트 위 빈 까닭(131)
|
||
"notes": notes,
|
||
"sheets": sheets,
|
||
}
|
||
|
||
|
||
@router.get("/{project_id}/quantity/materials")
|
||
async def material_sheets(project_id: UUID) -> dict[str, Any]:
|
||
root = await _project_root(project_id)
|
||
overrides = clean_overrides(quantity_settings(root).get(SETTINGS_KEY))
|
||
return await _sheets(project_id, root, overrides)
|
||
|
||
|
||
@router.put("/{project_id}/quantity/materials")
|
||
async def save_material_sheets(
|
||
project_id: UUID, body: dict[str, Any] = Body(...)
|
||
) -> dict[str, Any]:
|
||
root = await _project_root(project_id)
|
||
overrides = clean_overrides(body.get("overrides"))
|
||
save_section(root, "quantity", {SETTINGS_KEY: overrides}, replace_keys=(SETTINGS_KEY,))
|
||
return await _sheets(project_id, root, overrides)
|