Files
Aislo/B08_Quantity/B08_Quantity_Router_Material.py
T
eomsangdonandClaude Opus 5.5 8d4f4e8718 feat(B08): 사본 일위대가 자재 → 자재수량집계표 단위수량 재연결(143-4)
- 서버 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
2026-10-09 15:25:05 +09:00

157 lines
6.9 KiB
Python
Raw 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.
"""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)