"""B09 제비율 요율표 화면 자료 — STmate 「원가계산기준」(wM_KAN_RATE) 첫 탭 차례 (PLAN 12장). ⚠ 요율 데이터(`rates_<연도>.json`)를 **읽어 보이기만** 한다 — 묶음 제목·산식 표기는 DFM `TWM_KAN_RATE` 첫 탭(☞ 공사원가계산 제잡비율) 차례를 따르고, 값·구간은 우리 데이터 그대로. ⚠ 수정은 아직 없음 — 엔진이 프로젝트 덮어쓰기 요율을 받는 칸이 서야 열 수 있음. """ 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_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) def _pivot(rows: list[dict[str, Any]], row_keys: tuple[str, ...], col_key: str, safety=False): """세로 줄을 (구간…) × 공종 표로 편다 — 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) -> list[dict[str, Any]]: """묶음 목록 `{title, formula, columns, rows}` — DFM 첫 탭 위→아래 차례.""" v = dataset.variables sections: list[dict[str, Any]] = [] overhead = v["rate_overhead"]["civil_landscape_industrial"] profit = { row["estimated_price_bracket"]: row["rate_percent"] for row in v["rate_profit"]["brackets"] } sections.append( { "title": "일반관리비 · 이윤", "formula": "일반관리비 (재+노+경) × 율 · 이윤 (노+경+일) × 율", "columns": ["공사규모(추정가격)", "일반관리비(%)", "이윤(%)"], "rows": [ [ amount_label(row["estimated_price_bracket"]), row["rate_percent"], profit.get(row["estimated_price_bracket"], ""), ] for row in overhead ], } ) 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(c) for c in columns)], "rows": [ [ amount_label(size), duration_label(days), *(cells[c]["rate_percent"] if c in cells else "" for c in columns), ] for (size, days), cells in grid.items() ], } ) sections.append( { "title": "고용보험료", "formula": "(노무비) × 율", "columns": ["등급", "추정금액", "요율(%)"], "rows": [ [row["grade"], amount_label(row["estimated_amount_bracket"]), row["rate_percent"]] 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(c, safety=True)}(%)" for c in columns), "기초액(원)"], "rows": [ [ amount_label(size), *(cells[c]["rate_percent"] if c in cells else "" for c in columns), next( ( cells[c].get("base_amount_krw") for c in columns if cells.get(c, {}).get("base_amount_krw") ), "", ), ] for (size,), cells in grid.items() ], } ) sections.append( { "title": "환 경 보 전 비", "formula": "(재료비+직접노무비+산출경비) × 율", "columns": ["구 분", "요율(%)"], "rows": [ [_type(row["work_type"]), row["rate_percent"]] 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"]), row["rate_percent"]] 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"]), row["rate_percent"]] for row in equipment["general_construction"] ), *( ["전문건설 — " + row["work_type"], row["rate_percent"]] for row in equipment["specialty_construction"] ), ], } ) pension = next( ( row["rate_percent"] for row in v["rate_pension"]["annual_rates"] if row["year"] == int(str(dataset.version_stamp.get("effective_date", "0"))[:4] or 0) ), "", ) sections.append( { "title": "퇴직부금비 · 법 정 부 담 금 · 보험료", "formula": "", "columns": ["항목", "산식", "요율(%)"], "rows": [ [ "퇴직공제부금비", "(직접노무비) × 율", v["rate_retirement_mutual_aid"]["rate_percent"], ], ["석면분담금", "(노무비) × 율", v["rate_asbestos_contribution"]["rate_percent"]], [ "임금채권부담금", "(노무비) × 율", v["rate_wage_claim_contribution"]["rate_percent"], ], ["산재보험료", "(노무비) × 율", v["rate_sanjae"]["rate_percent"]], ["건강보험료", "(직접노무비) × 율", v["rate_health"]["rate_percent"]], ["장기요양보험료", "(건강보험료) × 율", v["rate_care"]["rate_percent"]], ["연금보험료", "(직접노무비) × 율", pension], ["부가가치세", "(공급가액) × 율", v["rate_vat"]["rate_percent"]], ], } ) return sections