"""B09 원가계산 — ③ 단가산출서 `D` 층 (PLAN 9-1 · 9-3). **무엇인가** — 내역서 한 줄의 단가가 **어떻게 나왔는지** 보이는 표다. 실무 내역서는 줄마다 비고에 「단산 46 참조」처럼 **참조번호**를 적고, 그 번호의 산출서를 펴서 검산한다 (8-13 관측). STC 실측도 `D01341 절토(토사) 굴삭기0.7㎥ m³ 1,939` 처럼 **`D` 가 `B` (일위대가)를 참조하는 한 층 위**였다. D 단가산출 → B 일위대가 → X 시간당 사용료 → S·M·L 카탈로그 ⚠ **표를 세 벌 만들지 않는다** (PLAN 9-3). `PriceBook` 의 「제목 + 상세」 한 쌍에 `kind` 만 `PRICE_BASIS` 로 얹는다 — 일위대가와 같은 구조, 같은 화면 모양이다. ⚠ **번호는 코드에 박지 않는다.** 실무 참조번호(「단산 46」)는 **그 내역서 안에서의 차례**라 프로젝트마다 다르다. 코드(`D-FP-…`)는 공종을 가리키고, 번호는 조판할 때 매긴다. """ from __future__ import annotations from dataclasses import dataclass, field from decimal import Decimal from typing import Any from B09_Estimation.B09_Estimation_PriceBook import PriceDetail, PriceKind, PriceTitle from B09_Estimation.B09_Estimation_Rounding import OutputPlace, round_at from B09_Estimation.B09_Estimation_UnitPrice import UnitPriceBuild, cached_build _ONE = Decimal(1) @dataclass class PriceBasisEntry: """단가산출서 한 장 — 참조번호 + 그 줄이 무엇을 참조하는지.""" number: int code: str name: str spec: str unit: str unit_price_krw: Decimal #: 이 산출서가 참조하는 일위대가 코드. 화면에서 눌러 내려가는 자리. ref_code: str @property def label(self) -> str: """내역서 비고에 적는 문구 — 실무 서식 그대로 「단산 46 참조」.""" return f"단산 {self.number} 참조" def as_dict(self) -> dict[str, Any]: return { "number": self.number, "label": self.label, "code": self.code, "name": self.name, "spec": self.spec, "unit": self.unit, "unit_price_krw": str(self.unit_price_krw), "ref_code": self.ref_code, } @dataclass class PriceBasisSheet: """그 내역서에 딸린 단가산출서 한 벌.""" entries: list[PriceBasisEntry] = field(default_factory=list) def by_unit_price(self, ref_code: str) -> PriceBasisEntry | None: return next((entry for entry in self.entries if entry.ref_code == ref_code), None) def as_dict(self) -> dict[str, Any]: return {"entries": [entry.as_dict() for entry in self.entries]} def build_price_basis( unit_price_codes: list[str], build: UnitPriceBuild | None = None, ) -> PriceBasisSheet: """내역서에 쓰인 일위대가마다 산출서 한 장을 세운다. 번호는 **쓰인 차례**로 매긴다 — 실무 참조번호가 그 내역서 안의 차례이기 때문이다. 같은 일위대가가 두 줄에 쓰이면 **산출서는 한 장**이고 두 줄이 같은 번호를 가리킨다. """ prices = build or cached_build() sheet = PriceBasisSheet() seen: set[str] = set() for code in unit_price_codes: if not code or code in seen or code not in prices.book.titles: continue seen.add(code) title = prices.book.title(code) money = prices.book.resolve(code) basis_code = f"D-{code[2:]}" if code.startswith("B-") else f"D-{code}" if basis_code not in prices.book.titles: prices.book.add_title( PriceTitle( code=basis_code, kind=PriceKind.PRICE_BASIS, name=title.name, spec=title.spec, unit=title.unit, ) ) # ⚠ 지금은 **일위대가를 그대로 한 줄로** 참조한다. 할증·기타 비용이 붙는 # 자리가 생기면 여기에 줄이 는다 — 구조를 미리 열어 둔다. prices.book.add_detail(PriceDetail(basis_code, code, _ONE, note="일위대가 그대로")) sheet.entries.append( PriceBasisEntry( number=len(sheet.entries) + 1, code=basis_code, name=title.name, spec=title.spec, unit=title.unit, unit_price_krw=round_at(money.total, OutputPlace.UNIT_PRICE_ROW), ref_code=code, ) ) return sheet def price_basis_detail( code: str, build: UnitPriceBuild | None = None, ) -> dict[str, Any]: """산출서 한 장의 본표 — 무엇을 참조해 그 단가가 나왔는지. 일위대가 본표와 **같은 모양**이라 화면이 같은 표를 쓴다. """ from B09_Estimation.B09_Estimation_UnitPrice import detail_of prices = build or cached_build() title = prices.book.title(code) rows: list[dict[str, Any]] = [] for detail in prices.book.details.get(code, []): child = prices.book.title(detail.ref_code) money = prices.book.resolve(detail.ref_code).scaled(detail.quantity) rows.append( { "code": detail.ref_code, "name": child.name, "spec": child.spec, "unit": child.unit, "quantity": str(detail.quantity), "total": str(round_at(money.total, OutputPlace.UNIT_PRICE_ROW)), "drillable": True, "note": detail.note, } ) money = prices.book.resolve(code) return { "code": code, "name": title.name, "spec": title.spec, "unit": title.unit, "rows": rows, "total": str(round_at(money.total, OutputPlace.UNIT_PRICE_ROW)), "material": str(money.material), "labor": str(money.labor), "expense": str(money.expense), # 한 층 아래(일위대가) 본표를 그대로 딸려 보낸다 — 화면이 두 번 물어보지 않게. "unit_price": detail_of(prices, rows[0]["code"]) if rows else None, }