"""B09 제비율 요율표 화면 자료 — STmate 「원가계산기준」(wM_KAN_RATE) 첫 탭 차례 (PLAN 12장). ⚠ 묶음 제목·산식 표기는 DFM `TWM_KAN_RATE` 첫 탭(☞ 공사원가계산 제잡비율) 차례, 값·구간은 우리 데이터. ⚠ 요율 칸마다 **주소**(`RateOverride.address_of`)와 **기본값(마스터)·지금 값(프로젝트)** 을 함께 실음 — 화면이 「기본값 ↔ 고친 값」을 갈라 보이고 [수 정]에서 그 칸만 고쳐 사유와 함께 돌려줌. """ from __future__ import annotations from decimal import Decimal from typing import Any from B09_Estimation.B09_Estimation_CostSheet import SAFETY_LABELS, WORK_TYPE_LABELS from B09_Estimation.B09_Estimation_RateOverride import address_of from B09_Estimation.B09_Estimation_Rates import RateDataset, _bracket_bounds, _duration_bounds _EOK = Decimal(100_000_000) def _won(amount: Decimal) -> str: """원 → 「50억」·「5천만」 표기.""" if amount >= _EOK: return f"{amount / _EOK:g}억" return f"{amount / 10_000_000:g}천만" def amount_label(label: str) -> str: """구간 키 → 사람 표기. 모르는 키는 그대로(지어내지 않음).""" bounds = _bracket_bounds(label) if bounds is None: return label low, high = bounds if low == 0: return f"{_won(high)} 미만" if high == Decimal("Infinity"): return f"{_won(low)} 이상" + (" " + label.split("_", 3)[-1] if label.count("_") > 2 else "") return f"{_won(low)}~{_won(high)} 미만" def duration_label(label: str) -> str: bounds = _duration_bounds(label) if bounds is None: return label low, high = bounds if low == 0: return f"{high}일 이하" if high >= 10**9: return f"{low}일 이상" return f"{low}~{high}일" def _type(value: str, safety: bool = False) -> str: return {**WORK_TYPE_LABELS, **(SAFETY_LABELS if safety else {})}.get(value, value) class _Cells: """요율 칸 만들기 — 지금 판(덮어쓴 복사본)과 마스터 판을 같은 주소로 맞대 봄.""" def __init__(self, current: RateDataset, master: RateDataset) -> None: self.current, self.master = current.variables, master.variables def cell(self, variable: str, table: str | None, row: dict[str, Any] | None) -> dict[str, Any]: address = address_of(variable, table, row) value = self._rate(self.current, address) default = self._rate(self.master, address) return { "value": value, "default": default, "overridden": value != default, "address": address, } @staticmethod def _rate(variables: dict[str, Any], address: dict[str, Any]) -> Any: variable = variables[address["variable"]] if not address["table"]: return variable.get("rate_percent") for row in variable[address["table"]]: if all(str(row.get(k)) == str(v) for k, v in address["match"].items()): return row.get("rate_percent") return None def _pivot(rows: list[dict[str, Any]], row_keys: tuple[str, ...], col_key: str): """세로 줄을 (구간…) × 공종 표로 편다 — DFM 표 모양(행 구간 · 열 공종).""" columns = list(dict.fromkeys(str(row[col_key]) for row in rows)) grid: dict[tuple[str, ...], dict[str, Any]] = {} for row in rows: grid.setdefault(tuple(str(row[k]) for k in row_keys), {})[str(row[col_key])] = row return columns, grid def rate_sections(dataset: RateDataset, master: RateDataset | None = None) -> list[dict[str, Any]]: """묶음 목록 `{title, formula, columns, rows}` — DFM 첫 탭 위→아래 차례. `rows` 의 요율 칸은 `{value, default, overridden, address}` · 이름 칸은 글자. """ v = dataset.variables c = _Cells(dataset, master or dataset) sections: list[dict[str, Any]] = [] profit = {row["estimated_price_bracket"]: row for row in v["rate_profit"]["brackets"]} sections.append( { "title": "일반관리비 · 이윤", "formula": "일반관리비 (재+노+경) × 율 · 이윤 (노+경+일) × 율", "columns": ["공사규모(추정가격)", "일반관리비(%)", "이윤(%)"], "rows": [ [ amount_label(row["estimated_price_bracket"]), c.cell("rate_overhead", "civil_landscape_industrial", row), c.cell("rate_profit", "brackets", profit[row["estimated_price_bracket"]]) if row["estimated_price_bracket"] in profit else "", ] for row in v["rate_overhead"]["civil_landscape_industrial"] ], } ) for variable, title, formula in ( ("rate_indirect_labor", "간접노무비", "(직접노무비) × 율(%)"), ("rate_other_expense", "기타경비", "(재료비+노무비) × 율(%)"), ): columns, grid = _pivot( v[variable]["brackets"], ("direct_cost_bracket", "duration_bracket"), "work_type" ) sections.append( { "title": title, "formula": formula, "columns": ["공사규모(직접공사비)", "공사기간", *(_type(col) for col in columns)], "rows": [ [ amount_label(size), duration_label(days), *( c.cell(variable, "brackets", cells[col]) if col in cells else "" for col in columns ), ] for (size, days), cells in grid.items() ], } ) sections.append( { "title": "고용보험료", "formula": "(노무비) × 율", "columns": ["등급", "추정금액", "요율(%)"], "rows": [ [ str(row["grade"]), amount_label(row["estimated_amount_bracket"]), c.cell("rate_goyong", "brackets", row), ] for row in v["rate_goyong"]["brackets"] ], } ) columns, grid = _pivot( v["rate_safety_pct"]["brackets"], ("target_amount_bracket",), "work_type" ) sections.append( { "title": "산업안전보건관리비", "formula": "(재료비+직접노무비+관급자재) × 율 + 기초액", "columns": [ "대상액", *(f"{_type(col, safety=True)}(%)" for col in columns), "기초액(원)", ], "rows": [ [ amount_label(size), *( c.cell("rate_safety_pct", "brackets", cells[col]) if col in cells else "" for col in columns ), str( next( ( cells[col].get("base_amount_krw") for col in columns if cells.get(col, {}).get("base_amount_krw") ), "", ) ), ] for (size,), cells in grid.items() ], } ) sections.append( { "title": "환 경 보 전 비", "formula": "(재료비+직접노무비+산출경비) × 율", "columns": ["구 분", "요율(%)"], "rows": [ [_type(row["work_type"]), c.cell("rate_environment", "all_work_types", row)] for row in v["rate_environment"]["all_work_types"] ], } ) sections.append( { "title": "공사이행보증수수료", "formula": "[추가금액 + (직접공사비-기준금액) × 율] × 공기(년)", "columns": ["공사규모(직접공사비)", "수수료 산식"], "rows": [ [ amount_label(row["direct_cost_bracket"]), str(row.get("formula", "")) .replace("direct_cost", "직공비") .replace("duration_years", "공기(년)"), ] for row in v["rate_performance_guarantee_fee"]["brackets"] ], } ) sections.append( { "title": "건설하도급대금지급보증서발급수수료", "formula": "(직접공사비) × 율", "columns": ["공사규모", "요율(%)"], "rows": [ [ amount_label(row["estimated_price_bracket"]), c.cell("rate_subcontract_payment_guarantee", "brackets", row), ] for row in v["rate_subcontract_payment_guarantee"]["brackets"] ], } ) equipment = v["rate_equipment_payment_guarantee"] sections.append( { "title": "건설기계대여대금 지급보증서 발급금액", "formula": "(직접공사비) × 율", "columns": ["구 분", "요율(%)"], "rows": [ *( [ _type(row["work_type"]), c.cell("rate_equipment_payment_guarantee", "general_construction", row), ] for row in equipment["general_construction"] ), *( [ "전문건설 — " + row["work_type"], c.cell("rate_equipment_payment_guarantee", "specialty_construction", row), ] for row in equipment["specialty_construction"] ), ], } ) year = int(str(dataset.effective_date)[:4] or 0) pension_row = next((r for r in v["rate_pension"]["annual_rates"] if r["year"] == year), None) sections.append( { "title": "퇴직부금비 · 법 정 부 담 금 · 보험료", "formula": "", "columns": ["항목", "산식", "요율(%)"], "rows": [ [ "퇴직공제부금비", "(직접노무비) × 율", c.cell("rate_retirement_mutual_aid", None, None), ], ["석면분담금", "(노무비) × 율", c.cell("rate_asbestos_contribution", None, None)], [ "임금채권부담금", "(노무비) × 율", c.cell("rate_wage_claim_contribution", None, None), ], ["산재보험료", "(노무비) × 율", c.cell("rate_sanjae", None, None)], ["건강보험료", "(직접노무비) × 율", c.cell("rate_health", None, None)], ["장기요양보험료", "(건강보험료) × 율", c.cell("rate_care", None, None)], [ "연금보험료", "(직접노무비) × 율", c.cell("rate_pension", "annual_rates", pension_row) if pension_row else "", ], ["부가가치세", "(공급가액) × 율", c.cell("rate_vat", None, None)], ], } ) return sections