- 자재단가대비표: 적용 칸을 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
458 lines
21 KiB
Python
458 lines
21 KiB
Python
"""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(),
|
||
}
|
||
)
|