"""Z01 기초단가 — 기계 한 표: 기종 한 줄 + 시간당 단가(계산값) · 산출근거(2026-09-15 브레인 계약). 기계단가는 취득가 613 · 손료계수 387 · 운전경비(연료·잡재료·조종원 — 건설품셈 8-4) · 노임 · 유가가 계산으로 물림. 고치는 칸 = 취득가 · 손료계수(이 표) — 노임·유가는 그 표에서 고치고 여기는 **덮개 얹은 값**을 끌어 씀 계산 칸 = 손료 · 연료 · 조종원 · 시간당 단가 — editable 에서 빼고 locked 에 까닭 ⚠ 셈은 B09 함수를 그대로 부름(`load_machine_catalog` · `load_operating_records` · `hourly_machine_cost`) — 식을 두 벌로 안 둠. `hourly_cost_of`(B09) 와 같은 값인지 `test_z01_base_prices.py` 가 대조. 반만 선 값(연료·조종원 빠짐)은 시간당 단가를 안 냄. """ from __future__ import annotations from dataclasses import dataclass, replace from decimal import Decimal from functools import lru_cache from typing import Any from B09_Estimation.B09_Estimation_MachineCost import ( OPERATOR_WAGE_FORMULA, hourly_machine_cost, load_machine_catalog, ) from B09_Estimation.B09_Estimation_MachineOperating import FUEL_VARIABLES, load_operating_records from Z01_MasterData.Z01_MasterData_Tables import RESOURCES #: B09 셈이 여는 원본 셋 — 바뀌면 캐시를 새로 읽음 _INPUT_FILES = ("mach_base_2026.json", "pum_const_2026.json", "labor_const_2026-01-01.json") EDITABLE = ["price_thousand_krw", "loss_coefficient_per_hour"] COMPUTED = ("loss_krw_per_hour", "fuel_krw_per_hour", "operator_krw_per_hour", "hourly_krw") #: 조종원 식 글자 — B09 식 문자열(좌→우 차례)을 사람이 읽게만 바꿈 _WAGE_TEXT = OPERATOR_WAGE_FORMULA.replace("*", " × ").replace("/", " ÷ ") _FORMULA = { "loss_krw_per_hour": "손료 = 취득가(천원) × 1,000 × 시간당 손료계수 (건설품셈 8-1-6)", "fuel_krw_per_hour": "연료 = 주연료(ℓ/hr) × (1 + 잡재료%) × 전국평균 유가 (건설품셈 8-4 · 8-1-7)", "operator_krw_per_hour": ( f"조종원 = 일 노임 × 인원 × {_WAGE_TEXT}" " · 원 미만 절사 (건설품셈 8-1-2 · 건협 임금적용요령 4-나)" ), "hourly_krw": "시간당 사용료 = 손료 + 연료 + 조종원", } _LOCKED_COMPUTED = "계산값 — 밑값(취득가·손료계수는 이 표 · 노임·유가는 그 표)을 고치면 따라 바뀜" _LOCKED_OPERATING = "건설품셈 8-4 운전경비표(로직)에서 옴 — 기초단가가 아님" _LOCKED = { "machine_code": "자료 열쇠 — 바꾸면 덮개·다른 표가 이 줄을 못 찾음", "machine_name": "공표 원문 칸 — 원본 갱신으로만 바뀜", "specification": "공표 원문 칸 — 원본 갱신으로만 바뀜", "economic_life_hours": "공표 원문 칸 — 원본 갱신으로만 바뀜", "annual_standard_hours": "공표 원문 칸 — 원본 갱신으로만 바뀜", "fuel_kind": _LOCKED_OPERATING, "fuel_liters_per_hour": _LOCKED_OPERATING, "misc_material_percent": _LOCKED_OPERATING, "operator_occupation_code": _LOCKED_OPERATING, "operator_person_days": _LOCKED_OPERATING, "fuel_price_krw_per_l": "유가 표에서 고침 — 여기는 덮개 얹은 전국평균을 끌어 씀", "operator_daily_wage_krw": "노임 표에서 고침 — 여기는 덮개 얹은 건설 노임을 끌어 씀", "hourly_note": "계산이 왜 안 섰는지 — 밑값이 비면 섬", "effective_date": "원본 판 기준일 — 원본 갱신으로만 바뀜", "operating_effective_date": "운전경비 원단위가 나온 건설품셈 판 기준일", "fuel_price_date": "끌어 쓴 유가 판 기준일 — 유가 표 판", "operator_wage_date": "끌어 쓴 노임 판 기준일 — 노임 표 판", **dict.fromkeys(COMPUTED, _LOCKED_COMPUTED), } def _num(value: Decimal | int | None) -> int | float | None: if value is None: return None value = Decimal(value) return int(value) if value == value.to_integral_value() else float(value) def _fmt(value: Decimal | int | float) -> str: text = f"{Decimal(str(value)):,.4f}".rstrip("0").rstrip(".") return text @dataclass(frozen=True) class Inputs: """기계 셈이 다른 표에서 끌어 오는 값 — 덮개를 얹은 뒤 · 판 기준일과 함께.""" wages: dict[str, Decimal | None] wage_date: str | None oil: dict[str, Decimal] oil_dates: dict[str, str | None] operating_date: str | None @lru_cache(maxsize=2) def _cached(_mtimes: tuple[int, ...]): return load_machine_catalog(), {r.machine_code: r for r in load_operating_records().records} def _inputs(): folder = RESOURCES / "data_cost_input_value" return _cached(tuple((folder / name).stat().st_mtime_ns for name in _INPUT_FILES)) def base_rows(source_name: str, effective_date: str | None) -> list[dict[str, Any]]: """기종 한 줄 — 원본 칸(카탈로그가 되살린 이름·규격 · 손료계수 포함)과 운전경비 원단위.""" catalog, records = _inputs() rows = [] for code, m in catalog.machines.items(): record = records.get(code) rows.append( { "@id": code, "@source": source_name, "machine_code": code, "effective_date": effective_date, "machine_name": m.name, "specification": m.specification, "price_thousand_krw": _num(m.price_thousand_krw), "loss_coefficient_per_hour": _num(m.loss_coefficient_per_hour), "economic_life_hours": m.economic_life_hours, "annual_standard_hours": m.annual_standard_hours, "fuel_kind": record.fuel_kind if record else None, "fuel_liters_per_hour": _num(record.fuel_liters_per_hour) if record else None, "misc_material_percent": _num(record.misc_material_percent) if record else None, "operator_occupation_code": record.operator_occupation_code if record else None, "operator_person_days": _num(record.operator_person_days) if record else None, } ) return rows def compute(row: dict[str, Any], inputs: Inputs) -> dict[str, Any]: """덮개 얹은 줄 → 시간당 단가 칸을 붙인 새 줄. B09 `hourly_cost_of` 와 같은 길.""" catalog, records = _inputs() code = row["@id"] coef = row["loss_coefficient_per_hour"] record = records.get(code) out = { **row, "operating_effective_date": inputs.operating_date if record is not None else None, "fuel_price_krw_per_l": None, "fuel_price_date": None, "operator_daily_wage_krw": None, "operator_wage_date": None, **dict.fromkeys(COMPUTED), } if coef is None: out["hourly_note"] = "손료계수 없음 — 시간당 단가 안 섬" return out spec = replace( catalog.machines[code], price_thousand_krw=Decimal(str(row["price_thousand_krw"])), loss_coefficient_per_hour=Decimal(str(coef)), ) liters = fuel_price = wage = None if record is not None: liters = record.fuel_liters_per_hour if liters is not None: variable = FUEL_VARIABLES.get(record.fuel_kind, "") fuel_price = inputs.oil.get(variable) out["fuel_price_date"] = inputs.oil_dates.get(variable) if record.misc_material_percent is not None: liters = liters * (Decimal(1) + record.misc_material_percent / Decimal(100)) wage = inputs.wages.get(record.operator_occupation_code) out["operator_daily_wage_krw"] = _num(wage) out["operator_wage_date"] = inputs.wage_date if wage is not None and record.operator_person_days is not None: wage = wage * record.operator_person_days # 인원 칸이 비면 B09 처럼 일 노임 그대로 cost = hourly_machine_cost( spec, fuel_liters_per_hour=liters if fuel_price is not None else None, fuel_price_per_liter=fuel_price, operator_daily_wage=wage, ) money = cost.money out["fuel_price_krw_per_l"] = _num(fuel_price) out["loss_krw_per_hour"] = _num(money.expense) formula = { "loss_krw_per_hour": ( f"{_fmt(money.expense)} = {_fmt(spec.price_thousand_krw)}천원 × 1,000 × {spec.loss_coefficient_per_hour}" ) } if fuel_price is not None: out["fuel_krw_per_hour"] = _num(money.material) misc = record.misc_material_percent if record else None formula["fuel_krw_per_hour"] = ( f"{_fmt(money.material)} = {_fmt(record.fuel_liters_per_hour)}ℓ" + (f" × (1 + {_fmt(misc)}%)" if misc is not None else "") + f" × {_fmt(fuel_price)}원/ℓ" ) if wage is not None: out["operator_krw_per_hour"] = _num(money.labor) persons = record.operator_person_days formula["operator_krw_per_hour"] = ( f"{_fmt(money.labor)} = {_fmt(out['operator_daily_wage_krw'])}원" + (f" × {_fmt(persons)}인" if persons is not None else "") + f" × {_WAGE_TEXT} (원 미만 절사)" ) if cost.gaps: out["hourly_note"] = " · ".join(cost.gaps) else: out["hourly_krw"] = _num(money.total) out["hourly_note"] = None formula["hourly_krw"] = ( f"{_fmt(money.total)} = {_fmt(money.expense)} + {_fmt(money.material)} + {_fmt(money.labor)}" ) out["@formula"] = formula return out def spec(columns: list[str]) -> dict[str, Any]: return { "editable": list(EDITABLE), "locked": { c: _LOCKED.get(c, "공표 원문 칸 — 원본 갱신으로만 바뀜") for c in columns if c not in EDITABLE }, "formula": dict(_FORMULA), }