"""B09 원가 화면의 **근거 사전** — 어느 숫자가 어디서 와서 어떻게 나왔나 (PLAN 8-36 ④). ⚠⚠ **개발 전용.** 사전은 `provenance_payload()` 를 거쳐 나가고, 개발환경이 아니면 `None` 이라 응답에 칸 자체가 안 생긴다. 화면에서 숨기는 것이 아니라 **안 보내는 것**이다. 왜 이 파일인가 「식」과 「원천」의 정답은 값을 낳는 엔진이 안다. 화면 TS 에 손으로 적어 두면 엔진을 고칠 때 설명만 옛것으로 남는다. 엔진 옆에 두어 같이 눈에 들어오게 한다. ⚠ `B09_Estimation_UI_Page.ts`(1415줄)·`B09_Estimation_UnitPrice.py`(1173줄) 가 이미 700줄을 크게 넘어 **새 파일로 뺐다**(PLAN 8-36 끝 ⚠). ⚠ **열 단위로 적는다.** 줄마다 갈리는 사유는 줄이 `notes` 로 들고 오고(내역서는 그 사유가 **닿는 열 키**까지 함께 들고 온다 — `BillRow.notes`), 화면이 그 열의 칸에만 덧붙인다. 토적표와 맞대 본 것 (데스크탑 메인 요청) · B08 토적표에는 `final` 이 **한 열도 없었다** — 중간 장부이기 때문이다. · B09 는 반대로 `final` 이 분명히 있다 — 내역서 금액·자재대 금액이 그 자리다. ⇒ 등급 여섯은 **한 장이 아니라 두 장을 합쳐야** 다 쓰인다. · 그래도 **원가계산서 「금액」은 열 단위로 `final` 을 못 붙였다.** 같은 열 안에서 중간줄(간접노무비 따위)과 마지막줄(총원가·도급금액·총계)의 성격이 갈리는데 등급은 **열에 하나**뿐이라서다. `calc` 로 두고 `rule` 에 어느 줄이 `final` 인지 적었다. ⇒ 이 어긋남은 PLAN 8-36 ① 에 남긴다(칸 단위 등급이 필요한 첫 자리). """ from __future__ import annotations from typing import Any from common_util.common_util_provenance import ( TIER_CALC, TIER_EXCLUDED, TIER_FINAL, TIER_STANDARD, TIER_SURVEY, ColumnProvenance, provenance_payload, sheet_provenance, ) from B09_Estimation.B09_Estimation_Provenance_Common import _label, _note_column from B09_Estimation.B09_Estimation_Provenance_Sources import ( base_reference_fuel_sheet, base_reference_labor_sheet, basis_sheet_tables, material_comparison_sheet, price_basis_sheet, ) #: 요율이 어디서 오는지 — 원가계산서 여러 열이 같은 문장을 쓴다. _RATE_SOURCE = ( "요율 판 `resources/data_cost_input_value/rates_2026.json` — " "공사금액·공사기간 구간으로 골라 씀(`B09_Estimation_Rates.py:180 select_bracket`). " "어느 판으로 섰는지는 좌측 패널 「요율 판」과 산출기초 ① 에 지문까지 남음" ) # ============================================================================= # ① 공사원가계산서 # ============================================================================= def cost_sheet() -> dict[str, Any]: """열 키는 화면 `buildCostSheetTable` 이 심는 낱말과 같아야 한다.""" return sheet_provenance( [ ColumnProvenance( key="name", label="비목", tier=TIER_STANDARD, formula="법이 정한 비목 이름 (차례도 법이 정함)", source="법정경비 14 비목은 `B09_Estimation_Statutory.py:58 STATUTORY_ITEMS` " "— 그 차례가 곧 원가계산서 줄 차례", code="B09_Estimation_Engine_Cost.py:115 CostLine.name", ), ColumnProvenance( key="amount_krw", label="금액", tier=TIER_CALC, formula="밑수 × 요율% (+ 정액) 을 원 단위로 버림", source="밑수는 비목마다 다름 — 「산출근거」 칸에 그 줄의 실제 밑수가 적힘. " "버림은 `B09_Estimation_Engine_Cost.py:44 floor_won`", rule="⚠ **총원가·도급금액·총계 줄은 `final`** — 계약으로 나가는 값이다. " "등급이 열에 하나뿐이라 그 셋을 따로 못 적었다(PLAN 8-36 ①). " "도급금액만 천원 단위 **올림**(`:49 ceil_thousand`)이라 끝자리가 다르다", code="B09_Estimation_Engine_Cost.py:175 _emitter", ), ColumnProvenance( key="rate_percent", label="요율", tier=TIER_STANDARD, formula="공사금액·공사기간이 든 구간의 요율을 그대로 씀", source=_RATE_SOURCE, code="B09_Estimation_Rates.py:180 select_bracket", ), ColumnProvenance( key="formula_text", label="산출근거", tier=TIER_CALC, formula="그 줄이 실제로 쓴 밑수와 요율을 사람이 읽게 적은 한 줄", source="엔진이 셈하면서 같이 지음 — 화면이 따로 짓지 않는다", rule="⚠ **산업안전보건관리비만 「× %」 꼴이 아니다** — A(요율식)·B(대상액×1.2) " "중 **작은 쪽**을 쓰므로 값 안에 고름이 숨어 있다" "(`B09_Estimation_Statutory.py:154 safety_management_cost`)", code="B09_Estimation_Engine_Cost.py:128 CostLine.formula_text", ), _note_column("B09_Estimation_Engine_Cost.py:125 CostLine.note"), ] ) # ============================================================================= # ② 설계내역서 # ============================================================================= def boq_sheet() -> dict[str, Any]: return sheet_provenance( [ _label("item_no", "No.", "마스터 목차가 매긴 번호"), _label("name", "공종", "B08 이 보낸 이름, 없으면 마스터 이름"), ColumnProvenance( key="spec", label="규격", tier=TIER_STANDARD, formula="마스터 규격 (갈래가 정해진 줄은 갈래 이름을 뒤에 이음)", source="갈래를 어떻게 골랐는지는 그 줄의 사유에 적힘 — B08 문구를 그대로 옮김", code="B09_Estimation_BillOfQuantities_Rows.py:149", ), _label("unit", "단위"), ColumnProvenance( key="quantity", label="수량", tier=TIER_SURVEY, formula="B08 이 보낸 값을 그대로 씀 (여기서 다시 곱하지 않음)", source="⚠ **반영률은 B08 이 이미 곱했다** — 여기서 또 곱하면 두 번 곱해진다. " "찍는 자리수는 품셈 1-2-2 종목별(`B09_Estimation_QuantityDigits.py`)이고 " "값 자체는 전정밀로 남는다", code="B09_Estimation_BillOfQuantities_Rows.py:151", ), ColumnProvenance( key="unit_price_krw", label="단가", tier=TIER_CALC, formula="그 공종의 일위대가 본표 합계 (1단위 값)", source="일위대가 탭에서 같은 표를 그대로 봄. 묶음 줄은 조각들의 " "`단가 × 조각수량` 을 더한 값", rule="⚠ **못 세우면 0 으로 때우지 않고 비운다.** 일위대가가 없음·성분이 빠짐·" "밑수를 모름·일부만 섬·단위가 안 맞음 — 사유는 그 줄의 사유에 적히고 " "「금액을 못 세운 줄」 목록에도 오른다", code="B09_Estimation_BillOfQuantities_Rows.py:134 _leaf_row", ), ColumnProvenance( key="amount_krw", label="금액", tier=TIER_FINAL, formula="수량 × 단가", source="이 값들의 합이 공사원가계산서의 직접비로 나간다 — 화면 밖으로 나가는 값", rule="단가가 안 선 줄은 **금액도 안 세운다**. 단위가 안 맞는 줄도 비운다 — " "곱하면 조용히 틀린 금액이 내역서에 든다", code="B09_Estimation_BillOfQuantities_Rows.py:134 _leaf_row", ), _note_column("B09_Estimation_BillOfQuantities.py:BillRow.note"), ] ) # ============================================================================= # ③ 일위대가 # ============================================================================= def unit_price_list_sheet() -> dict[str, Any]: """목록표 — 「무엇이 있나」.""" common = "단가판(`PriceBook`)이 성분을 풀어 낸 값 — 본표를 열면 줄마다 보인다" return sheet_provenance( [ _label("name", "명칭", "단가판 제목"), _label("unit", "단위", "단가판 기준 단위"), ColumnProvenance( key="material", label="재료비", tier=TIER_CALC, formula="본표 재료비 줄의 합", source=common, code="B09_Estimation_UnitPrice_View.py:149 list_unit_prices", ), ColumnProvenance( key="labor", label="노무비", tier=TIER_CALC, formula="본표 노무비 줄의 합", source=common, code="B09_Estimation_UnitPrice_View.py:149 list_unit_prices", ), ColumnProvenance( key="expense", label="경비", tier=TIER_CALC, formula="본표 경비 줄의 합", source=common, code="B09_Estimation_UnitPrice_View.py:149 list_unit_prices", ), ColumnProvenance( key="total", label="합계", tier=TIER_CALC, formula="재료비 + 노무비 + 경비", source=common, code="B09_Estimation_UnitPrice_View.py:149 list_unit_prices", ), ] ) def unit_price_detail_sheet() -> dict[str, Any]: """본표 — 「무엇으로 이루어졌나」.""" money = ( "성분 단위값 × 수량을 성분별로 자른 값. ⚠ 행마다 자르므로 **전정밀 합과 끝자리가 " "어긋난다 — 정상이다.** 숨기면 나중에 「합계가 안 맞는다」며 계산을 고치려 든다" ) return sheet_provenance( [ _label("name", "명칭", "성분 이름. 제잡비·공구손료는 품셈 [주]가 만든 줄"), _label("spec", "규격", "성분 규격. 비율 줄은 「노무비의 N%」 꼴"), ColumnProvenance( key="source", label="원천", tier=TIER_STANDARD, formula="그 성분이 어느 판에서 왔는지 + 그 판에서의 순번", source="자재 · 노임 · 기계경비 · 일위대가 · 단가산출 · 일식견적 여섯 중 하나" "(`B09_Estimation_UnitPrice.py:1026 SOURCE_LABEL`)", rule="기계경비·일위대가·단가산출 줄은 **눌러서 한 층 아래로 내려갈 수 있다**" "(`:1037 DRILLABLE_KINDS`)", code="B09_Estimation_UnitPrice_View.py:257", ), _label("unit", "단위"), ColumnProvenance( key="quantity", label="수량", tier=TIER_STANDARD, formula="품셈 표가 정한 1단위당 소요량", source="비율 줄(제잡비·공구손료)은 수량 칸에 **퍼센트**가 들어간다", code="B09_Estimation_UnitPrice_View.py:259", ), ColumnProvenance( key="material", label="재료비", tier=TIER_CALC, formula="성분 단위 재료비 × 수량", source=money, code="B09_Estimation_UnitPrice_View.py:264", ), ColumnProvenance( key="labor", label="노무비", tier=TIER_CALC, formula="성분 단위 노무비 × 수량", source=money, code="B09_Estimation_UnitPrice_View.py:265", ), ColumnProvenance( key="expense", label="경비", tier=TIER_CALC, formula="성분 단위 경비 × 수량", source=money, code="B09_Estimation_UnitPrice_View.py:266", ), ColumnProvenance( key="total", label="합계", tier=TIER_CALC, formula="자른 성분 셋을 더한 값 (표에서 합계 = 재료비+노무비+경비 가 서게)", source=money, code="B09_Estimation_UnitPrice_View.py:267", ), ] ) # ============================================================================= # ④ 관급·사급 자재대 # ============================================================================= def _material_columns(*, excluded: bool) -> list[ColumnProvenance]: """자재대 열 일곱. 「안 갈린 것」 표만 통째로 `excluded` 로 선다.""" if excluded: why = ( "⚠ **관급·사급이 안 갈린 줄** — 어느 합계에도 넣지 않는다. 못 세운 것이 아니라 " "**세면 안 되는** 자리다. 관급자재대에도 도급 재료비에도 넣으면 이중계상이 된다" ) return [ ColumnProvenance( key=key, label=label, tier=TIER_EXCLUDED, source=why, code="B09_Estimation_BillOfQuantities_Rows.py:398", ) for key, label in ( ("name", "자재"), ("spec", "규격"), ("unit", "단위"), ("total_amount", "수량"), ("unit_price_krw", "단가"), ("amount_krw", "금액"), ("note", "비고"), ) ] return [ _label("name", "자재"), _label("spec", "규격"), _label("unit", "단위"), ColumnProvenance( key="total_amount", label="수량", tier=TIER_SURVEY, formula="B08 이 낸 자재 수량 × 할증률", source="할증 사유는 그 줄의 사유에 적힘. 할증률이 아직 없는 자재는 " "**할증 전 값**으로 서고 그 사실이 표 밑에 뜬다", code="B09_Estimation_MaterialSheet.py:47 MaterialSheetRow", ), ColumnProvenance( key="unit_price_krw", label="단가", tier=TIER_STANDARD, formula="단가판에서 찾은 값", source="⚠ **관급과 사급은 원천이 다르다** — 관급은 나라장터, 사급은 물가지·견적. " "관급을 「사급 단가 없음」으로 적으면 안 된다", rule="못 찾으면 **0 으로 때우지 않고 비운다** — 사유가 그 줄에 적힌다", code="B09_Estimation_MaterialSheet.py:117 build_material_sheet", ), ColumnProvenance( key="amount_krw", label="금액", tier=TIER_FINAL, formula="수량 × 단가", source="⚠ **사급만 도급 재료비로 든다.** 관급은 총원가 **밖** 별도 표기라 " "여기 합계가 원가계산서 재료비와 같지 않다", code="B09_Estimation_MaterialSheet.py:117 build_material_sheet", ), _note_column("B09_Estimation_BillOfQuantities.py:BillRow.note"), ] # ============================================================================= # ⑤ 중기목록표 · 기초자료 목록표 # ============================================================================= def machine_sheet() -> dict[str, Any]: hourly = "시간당 사용료 — 「각종 중기경비계산서」에 셈 과정이 그대로 펼쳐진다" return sheet_provenance( [ _label("code", "코드번호"), _label("name", "명 칭"), _label("spec", "규 격"), _label("unit", "단위"), ColumnProvenance( key="total_krw", label="합 계", tier=TIER_CALC, formula="노무비 + 재료비 + 경비", source=hourly, code="B09_Estimation_Lists.py:105 machine_list", ), ColumnProvenance( key="labor_krw", label="노 무 비", tier=TIER_CALC, formula="조종원 노임 ÷ 8시간 × 16/12 × 25/20 (약 1.667배)", source="공표 노임은 기본급여액뿐이라 제수당·상여금·퇴직급여충당금을 따로 " "계상함(건협 임금적용요령 4-나 · 기재부 집행기준 제76조의3). " "⚠ 계수 자체의 예규 원문은 아직 못 봐 실무 관행을 따름", code="B09_Estimation_MachineCost.py:74 OPERATOR_ALLOWANCE_FACTOR", ), ColumnProvenance( key="material_krw", label="재 료 비", tier=TIER_CALC, formula="주연료(L/hr) × 유가 + 잡재료(주연료의 %)", source="유가는 전국 또는 고른 시도의 공시가 — 기초자료 탭에서 고른다. " "잡재료는 연료 소요량에 포함되어 있어 따로 세지 않는다", rule="같은 기종이라도 **조합 사용이면 잡재료가 16% 로 줄어** 재료비가 달라진다 " "— 그래서 층이 따로 선다(건설품셈 제8장 [주]⑤)", code="B09_Estimation_MachineExpenseSheet.py:81 machine_expense_sheets", ), ColumnProvenance( key="expense_krw", label="경 비", tier=TIER_CALC, formula="취득가격 × 손료계수(상각비 + 정비비 + 관리비, 10⁻⁷)", source="취득가격·내용시간·연간표준가동시간·계수 셋은 모두 품셈 표 값", code="B09_Estimation_MachineExpenseSheet.py:81 machine_expense_sheets", ), _note_column("B09_Estimation_Lists.py:105 machine_list"), ] ) def catalog_sheet() -> dict[str, Any]: """기초자료 탭의 목록표 셋(노무비·재료비·경비)이 같이 쓰는 사전.""" return sheet_provenance( [ _label("code", "코드번호"), _label("name", "명 칭"), _label("spec", "규 격"), _label("unit", "단위"), ColumnProvenance( key="unit_price_krw", label="단 가", tier=TIER_STANDARD, formula="단가판 값을 그대로 옮김 (여기서 셈하지 않음)", source="어느 판·어느 기준일인지는 산출기초 ① 에 지문까지 남음. " "⚠ 경비목록표의 값은 **기계 취득가격(천원)** 이고 시간당 사용료가 아니다", rule="자재단가대비표에서 **원천 다섯 중 하나를 골라** 적용 단가가 선다 — " "값 안에 고름이 숨은 자리(PLAN 8-36 ㉯)", code="B09_Estimation_Lists.py:50 catalog_list", ), _note_column("B09_Estimation_Lists.py:50 catalog_list"), ] ) # ============================================================================= # 응답에 싣기 # ============================================================================= def estimation_provenance() -> dict[str, Any] | None: """B09 응답에 실을 사전 — **개발환경이 아니면 `None`.** 시트를 늘릴 때는 여기 한 줄만 더한다. 화면은 시트 이름으로 찾아 쓴다. ⚠ **일부러 안 붙인 둘** (2026-09-12 두 창 합의) · **설계서 구성표** — 프로젝트가 낳은 값이 아니라 「무슨 문서를 낼 것인가」 목록이라 원천도 식도 없다. 없는 것을 지어 붙이면 「분류가 있다」는 거짓만 남는다(조사표 ㉴). · **산출 조건(좌측 패널)** — 표가 아니라 입력 칸이고, 칸 밑 근거 한 줄이 이미 같은 일을 한다. 두 벌이 되면 어긋난다. """ return provenance_payload( { "cost_sheet": cost_sheet(), "boq": boq_sheet(), "unit_price_list": unit_price_list_sheet(), "unit_price_detail": unit_price_detail_sheet(), "material": sheet_provenance(_material_columns(excluded=False)), "material_unknown": sheet_provenance(_material_columns(excluded=True)), "machine": machine_sheet(), "catalog": catalog_sheet(), "material_comparison": material_comparison_sheet(), "base_reference_labor": base_reference_labor_sheet(), "base_reference_fuel": base_reference_fuel_sheet(), "price_basis": price_basis_sheet(), **basis_sheet_tables(), } )