refactor(B08,B09): 폴더째 old_code 로 옮기고 빈 화면 둘만 남김 (PLAN 7-3)
B08_Quantity 86 · B09_Estimation 111 파일을 old_code/ 로 옮김(지우지 않음). 화면은 메뉴·주소·단계 막대만 남은 빈 틀 둘 — main.py 라우터 13 개는 끊음. B07 이 빌려 쓰던 비탈 길이·면적은 필요한 함수만 B07_DesignDetail_Engine_SlopeGeometry 로 옮겨 적음(면적 적분·노면 면적·측점 묶음은 안 옮김) · 시험 하나를 새로 둠. B07 구조물도 조립(Cad_StandardSheet)은 2026-09-13 에 이미 도면 목록에서 빠져 부르는 곳이 없어 old_code 로 같이 보냄 — 구조물 그림은 되살리지 않음. B06 구조물 몫 조회는 빈 값으로 두어 화면이 그대로 서게 함. B08·B09 를 부르던 시험 25 개도 old_code/resources/tester 로 옮김. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PAdA5ThqmtVSsbzjusk1cJ
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
"""B09 원가계산 — ③ 단가산출서 `D` 층 (PLAN 9-1 · 9-3 · 6장 층 차례).
|
||||
|
||||
**무엇인가** — 시공능력 **Q** 로 선 기계 몫의 단가가 **어떻게 나왔는지** 보이는 표다. 실무 내역서는
|
||||
줄마다 비고에 「단산 46 참조」처럼 **참조번호**를 적고, 그 번호의 산출서를 펴서 검산한다(8-13 관측).
|
||||
|
||||
X 시간당 중기사용료 → D 단가산출(Q 식: 시간당 성분 ÷ Q) → B 일위대가 → 내역
|
||||
|
||||
⚠⚠ **층 차례 정정(2026-09-13 브레인 판정 · 명세 3장)** — 종전엔 D 가 **B 를 그대로 부르는 껍데기**
|
||||
(D → B 수량 1)였고 B 가 X 를 바로 불렀음(뒤집힘). 이제 D 는 **Q 쓰는 자리에만** 서고
|
||||
(`PriceBook.add_output_detail`), B 가 D 를 수량 1 로 부름. 내역 코드 축은 B 하나(금액 같음).
|
||||
⚠ 그래서 「단산 N」은 **D 를 품은 내역 줄에만** 붙음 — 사람 품만 있는 일위대가엔 없는 근거라 안 붙음
|
||||
(STmate 도 D 가 있는 줄에만).
|
||||
|
||||
⚠ **표를 세 벌 만들지 않는다** (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 Money3, PriceBook, PriceKind
|
||||
from B09_Estimation.B09_Estimation_Rounding import OutputPlace, round_at
|
||||
from B09_Estimation.B09_Estimation_UnitPrice import UnitPriceBuild, cached_build
|
||||
|
||||
|
||||
@dataclass
|
||||
class PriceBasisEntry:
|
||||
"""단가산출서 한 장 — 참조번호 + 그 산출을 부르는 일위대가."""
|
||||
|
||||
number: int
|
||||
code: str
|
||||
name: str
|
||||
spec: str
|
||||
unit: str
|
||||
unit_price_krw: Decimal
|
||||
#: 이 산출서를 **처음 부른** 일위대가 코드 — 화면에서 눌러 내려가는 자리.
|
||||
ref_code: str
|
||||
#: 이 산출서를 부르는 내역 일위대가 전부(갈래·단계 합산 부모 포함).
|
||||
unit_price_codes: list[str] = field(default_factory=list)
|
||||
#: 성분 금액 — 목록표 「노무비·재료비·경비」 칸(머리 성분 원 미만 절사 값).
|
||||
money: Money3 = field(default_factory=Money3)
|
||||
|
||||
@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,
|
||||
"unit_price_codes": list(self.unit_price_codes),
|
||||
"material_krw": str(self.money.material),
|
||||
"labor_krw": str(self.money.labor),
|
||||
"expense_krw": str(self.money.expense),
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class PriceBasisSheet:
|
||||
"""그 내역서에 딸린 단가산출서 한 벌."""
|
||||
|
||||
entries: list[PriceBasisEntry] = field(default_factory=list)
|
||||
|
||||
def by_unit_price(self, ref_code: str) -> PriceBasisEntry | None:
|
||||
return next((e for e in self.entries if ref_code in e.unit_price_codes), None)
|
||||
|
||||
def label_for(self, ref_code: str) -> str:
|
||||
"""그 일위대가가 품은 산출서 번호 전부 — 단계 합산 부모는 둘 이상일 수 있음(「단산 3·4」)."""
|
||||
numbers = [str(e.number) for e in self.entries if ref_code in e.unit_price_codes]
|
||||
return f"단산 {'·'.join(numbers)} 참조" if numbers else ""
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
return {"entries": [entry.as_dict() for entry in self.entries]}
|
||||
|
||||
|
||||
def _bases_of(book: PriceBook, code: str, seen: tuple[str, ...] = ()) -> list[str]:
|
||||
"""일위대가가 품은 D — 단계 합산 부모(B → 잎 B)는 잎까지 내려가 찾음. 줄 차례 그대로."""
|
||||
found: list[str] = []
|
||||
for detail in book.details.get(code, []):
|
||||
ref = detail.ref_code
|
||||
title = book.titles.get(ref)
|
||||
if title is None or ref == code or ref in seen:
|
||||
continue
|
||||
if title.kind is PriceKind.PRICE_BASIS:
|
||||
found.append(ref)
|
||||
elif title.kind is PriceKind.UNIT_PRICE:
|
||||
found.extend(b for b in _bases_of(book, ref, (*seen, code)) if b not in found)
|
||||
return found
|
||||
|
||||
|
||||
def build_price_basis(
|
||||
unit_price_codes: list[str],
|
||||
build: UnitPriceBuild | None = None,
|
||||
) -> PriceBasisSheet:
|
||||
"""내역에 쓰인 일위대가가 품은 **단가산출(D)** 마다 한 장.
|
||||
|
||||
번호는 **쓰인 차례**로 매긴다 — 실무 참조번호가 그 내역서 안의 차례이기 때문이다.
|
||||
같은 D 를 두 줄이 부르면 **산출서는 한 장**이고 두 줄이 같은 번호를 가리킨다.
|
||||
⚠ D 가 없는 일위대가(사람 품만)는 산출서가 없음 — 없는 근거를 가리키지 않음.
|
||||
"""
|
||||
prices = build or cached_build()
|
||||
book = prices.book
|
||||
sheet = PriceBasisSheet()
|
||||
numbered: dict[str, PriceBasisEntry] = {}
|
||||
for code in unit_price_codes:
|
||||
if not code or code not in book.titles:
|
||||
continue
|
||||
for basis in _bases_of(book, code):
|
||||
entry = numbered.get(basis)
|
||||
if entry is None:
|
||||
title = book.title(basis)
|
||||
money = book.resolve(basis)
|
||||
entry = PriceBasisEntry(
|
||||
number=len(sheet.entries) + 1,
|
||||
code=basis,
|
||||
name=title.name,
|
||||
spec=title.spec,
|
||||
unit=title.unit,
|
||||
unit_price_krw=round_at(money.total, OutputPlace.UNIT_PRICE_ROW),
|
||||
ref_code=code,
|
||||
money=money,
|
||||
)
|
||||
sheet.entries.append(entry)
|
||||
numbered[basis] = entry
|
||||
if code not in entry.unit_price_codes:
|
||||
entry.unit_price_codes.append(code)
|
||||
return sheet
|
||||
|
||||
|
||||
def price_basis_detail(
|
||||
code: str,
|
||||
build: UnitPriceBuild | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""산출서 한 장의 본표 — Q 식 줄(시간당 중기사용료 × 1/Q)과 성분 합.
|
||||
|
||||
일위대가 본표와 **같은 모양**(`detail_of`)이라 화면이 같은 표를 쓴다.
|
||||
"""
|
||||
from B09_Estimation.B09_Estimation_UnitPrice import detail_of
|
||||
|
||||
prices = build or cached_build()
|
||||
if prices.book.title(code).kind is not PriceKind.PRICE_BASIS:
|
||||
raise LookupError(f"단가산출서 코드가 아닙니다: {code}")
|
||||
return detail_of(prices, code)
|
||||
Reference in New Issue
Block a user