Files
Aislo/B09_Estimation/B09_Estimation_Provenance.py
T
eomsangdonandClaude Opus 5 aadb4f6756 feat(B09): 근거 사전 네 장 추가·칸 단위 최종 등급·사전 파일 셋으로 분리
- 자재단가대비표: 적용 칸을 calc+rule(기.가·유.물·견적), 값 없는 원천 칸은 blocked
- 환율및기초자료: 공표·공시값 standard 와 사용자가 고른 유가 범위 input 을 가름
- 단가산출근거·산출기초: 모으는 표라 식을 비우고 source·등급만 채움
- 설계서 구성표·산출 조건 패널은 일부러 뺌(프로젝트 값이 아니거나 근거 한 줄과 중복)
- 원가계산서 금액은 총원가·도급금액·총계 세 줄에만 칸 단위 final 을 심음
- 700줄을 넘어 Provenance / _Sources / _Common 셋으로 가름(공통 조각을 한쪽에 두면
  불러들이기가 고리를 이룸)

사전 16장 92열, 등급 여덟이 모두 쓰임.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GrXDD23Dvt2sR7q3X6oekp
2026-09-12 16:15:03 +09:00

458 lines
21 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.
"""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(),
}
)