- 서버: /estimation/cost-sheet(GET·PUT) · /estimation/rate-table — 새 라우터 파일
- 직접비 = 설계내역서 합계 · 기준 입력 = estimation.cost_sheet · 폐기물처리비 = 임목폐기물 톤 × 수동 단가
- 서식 줄: 나.순공사원가 … 바.총공사비 (DFM TWM_KANJUB 차례·표기) · 경비 세목 번호 차례
- 기준 입력판: 형식(일반만) · 2.공사의종류 · 3.공사기간 · 6.관/사급 · 7.산업안전 · 8.환경보전 · 9.건설기계 — 선택지는 요율 데이터에서
- 요율표: 첫 탭 묶음 10개(보기 전용 · 수정은 잠김)
- 탭 계약 파일은 서브 314c0040 과 같은 내용(blob 동일) · 사전은 새 벌 b4
- 시험: 서식 차례·엔진 값 일치·폐기물 자리·저장값 거름·구간 표기
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
236 lines
9.0 KiB
Python
236 lines
9.0 KiB
Python
"""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
|