- Rates.rate_table: M01 조달청 제비율 압축 표(구간 위 끝 · 토목) → payload rate_table - Calc_Total 짝(py · ts): pick_rates 자동 고름(직접공사비 × 공기 · 추정가격 · 안전 대상액 + 기초액 · 연도 · 환경 공종) · 사용자 값 앞섬 - 총괄 탭: 추정가격 · 공사기간 · 환경보전비 공종 칸 · 요율 출처 칸(자동 근거 풍선 · 누르면 자동으로) - Store: environment_kind 칸 - 시험: test_166_1 새 · test_145_4 node 입력을 stdin 으로(명령 줄 길이) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QND7Co43B24zK2P937rdxt
171 lines
6.6 KiB
Python
171 lines
6.6 KiB
Python
"""B09 원가 요율(PLAN 145-4 · 166-1) — M01 `요율_조달청제비율.json` 을 읽는 유일한 곳.
|
||
|
||
rate_candidates 줄마다 후보 글 한 줄(출처 풍선)
|
||
rate_table 짝 셈(`B09_Estimation_Calc_Total`)이 자동으로 고르는 압축 표 — 총괄 줄 키마다
|
||
{rt, pct} 단일 · {price, pcts} 추정가격 구간 · {direct, days, grid} 직접공사비 × 공기 ·
|
||
{target, pcts, bases} 안전 대상액 구간 · {years} 연도 · {kinds} 환경 공종
|
||
구간 = 위 끝 목록(그 값 미만 · 마지막 = 끝없음) · 공종 = 토목(임도) 고정
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import math
|
||
from functools import lru_cache
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
FILE = Path(__file__).resolve().parents[1] / "resources" / "master_data" / "요율_조달청제비율.json"
|
||
|
||
|
||
def _percents(value: Any) -> list[float]:
|
||
"""값 안 모든 rate_percent — 구간 · 연도 · 공종 갈래 통째로."""
|
||
if isinstance(value, dict):
|
||
own = value.get("rate_percent")
|
||
found = [float(own)] if isinstance(own, (int, float)) else []
|
||
return found + [
|
||
p for key, v in value.items() if key != "rate_percent" for p in _percents(v)
|
||
]
|
||
if isinstance(value, list):
|
||
return [p for item in value for p in _percents(item)]
|
||
return []
|
||
|
||
|
||
@lru_cache(maxsize=1)
|
||
def _rows() -> tuple[dict[str, Any], ...]:
|
||
try:
|
||
return tuple(json.loads(FILE.read_text(encoding="utf-8")).get("줄") or [])
|
||
except (OSError, ValueError):
|
||
return ()
|
||
|
||
|
||
@lru_cache(maxsize=1)
|
||
def rate_candidates() -> dict[str, str]:
|
||
"""{RT 키: 「M01 RT… 3.56%」 · 「… 1.01~1.57%(구간 · 고르는 값 사용자)」} — 파일 없으면 {}."""
|
||
rows = _rows()
|
||
out: dict[str, str] = {}
|
||
for row in rows:
|
||
percents = sorted(set(_percents(row.get("값"))))
|
||
if not percents:
|
||
text = "구간 식(고르는 값 사용자)"
|
||
elif len(percents) == 1:
|
||
text = f"{percents[0]:g}%"
|
||
else:
|
||
text = f"{percents[0]:g}~{percents[-1]:g}%(구간 · 고르는 값 사용자)"
|
||
out[str(row.get("키"))] = f"M01 {row.get('키')} {text}"
|
||
return out
|
||
|
||
|
||
_UNIT = {"million": 10**6, "billion": 10**9, "days": 1}
|
||
|
||
|
||
def _upper(label: Any) -> float | None:
|
||
"""구간 이름 → 위 끝(그 값 미만) — 「gte_」 = 끝없음(inf) · 일수는 끝 포함이라 +1 ·
|
||
수 없는 이름(below_official_threshold) = None. `billion` = 십억(M01 basis 글)."""
|
||
words = str(label).split("_")
|
||
if words[0] == "gte":
|
||
return math.inf
|
||
for i in range(len(words) - 1, 0, -1):
|
||
if words[i] in _UNIT and words[i - 1].isdigit():
|
||
return int(words[i - 1]) * _UNIT[words[i]] + (words[i] == "days")
|
||
return None
|
||
|
||
|
||
def _steps(rows: list[dict[str, Any]], field: str) -> tuple[list[float], list[dict[str, Any]]]:
|
||
"""구간 줄 → (위 끝 목록 · 끝없음 뺌, 줄 목록) — 위 끝 차례."""
|
||
keyed = sorted(
|
||
((u, r) for r in rows if (u := _upper(r.get(field))) is not None), key=lambda p: p[0]
|
||
)
|
||
return [u for u, _ in keyed[:-1]], [r for _, r in keyed]
|
||
|
||
|
||
def _grid(rows: list[dict[str, Any]]) -> dict[str, Any]:
|
||
"""간접노무비 · 기타경비 — 직접공사비 × 공기 × 토목."""
|
||
civil = [r for r in rows if r.get("work_type") == "civil"]
|
||
d_up = sorted({_upper(r["direct_cost_bracket"]) for r in civil})
|
||
t_up = sorted({_upper(r["duration_bracket"]) for r in civil})
|
||
grid: list[list[float | None]] = [[None] * len(t_up) for _ in d_up]
|
||
for r in civil:
|
||
i = d_up.index(_upper(r["direct_cost_bracket"]))
|
||
grid[i][t_up.index(_upper(r["duration_bracket"]))] = r["rate_percent"]
|
||
return {"direct": d_up[:-1], "days": t_up[:-1], "grid": grid, "note": "토목"}
|
||
|
||
|
||
def _priced(rows: list[dict[str, Any]], field: str, note: str = "") -> dict[str, Any]:
|
||
bounds, picked = _steps(rows, field)
|
||
return {"price": bounds, "pcts": [r["rate_percent"] for r in picked], "note": note}
|
||
|
||
|
||
@lru_cache(maxsize=1)
|
||
def rate_table() -> dict[str, dict[str, Any]]:
|
||
"""총괄 줄 키 → 압축 표(머리 글) — 파일 · 줄이 없으면 그 줄 없음(자동 없음 = 빈칸)."""
|
||
v = {str(r.get("키")): r.get("값") or {} for r in _rows()}
|
||
out: dict[str, dict[str, Any]] = {}
|
||
|
||
def put(key: str, rt: str, build: Any) -> None:
|
||
if rt in v:
|
||
try:
|
||
out[key] = {"rt": rt, **build(v[rt])}
|
||
except (KeyError, TypeError, ValueError, IndexError):
|
||
pass # M01 꼴이 바뀌면 그 줄만 자동 없음
|
||
|
||
for key, rt in (("accident", "RT000001"), ("health", "RT000003"), ("care", "RT000005")):
|
||
put(key, rt, lambda x: {"pct": x["rate_percent"]})
|
||
put("vat", "RT000019", lambda x: {"pct": x["rate_percent"]})
|
||
put(
|
||
"machine",
|
||
"RT000015",
|
||
lambda x: {
|
||
"pct": next(
|
||
r["rate_percent"]
|
||
for r in x["general_construction"]
|
||
if r["work_type"] == "civil_general"
|
||
),
|
||
"note": "종합공사 토목 일반",
|
||
},
|
||
)
|
||
put("employment", "RT000002", lambda x: _priced(x["brackets"], "estimated_amount_bracket"))
|
||
put(
|
||
"overhead",
|
||
"RT000017",
|
||
lambda x: _priced(
|
||
x["civil_landscape_industrial"], "estimated_price_bracket", "종합(토목) 표"
|
||
),
|
||
)
|
||
put("profit", "RT000018", lambda x: _priced(x["brackets"], "estimated_price_bracket"))
|
||
put("indirect_labor", "RT000008", lambda x: _grid(x["brackets"]))
|
||
put("other", "RT000009", lambda x: _grid(x["brackets"]))
|
||
|
||
def safety(x: dict[str, Any]) -> dict[str, Any]:
|
||
# ponytail: 관리자 선임 대상(추정 1000억 이상) 구간은 안 고름 — 임도 규모 밖
|
||
civil = [
|
||
r
|
||
for r in x["brackets"]
|
||
if r["work_type"] == "civil" and "at_or_above" not in r["target_amount_bracket"]
|
||
]
|
||
bounds, picked = _steps(civil, "target_amount_bracket")
|
||
return {
|
||
"target": bounds,
|
||
"pcts": [r["rate_percent"] for r in picked],
|
||
"bases": [r.get("base_amount_krw", 0) for r in picked],
|
||
"note": "토목",
|
||
}
|
||
|
||
put("safety", "RT000006", safety)
|
||
put(
|
||
"pension",
|
||
"RT000004",
|
||
lambda x: {"years": {str(r["year"]): r["rate_percent"] for r in x["annual_rates"]}},
|
||
)
|
||
put(
|
||
"environment",
|
||
"RT000010",
|
||
lambda x: {
|
||
"kinds": {
|
||
"road" if r["work_type"] == "road" else "other": r["rate_percent"]
|
||
for r in x["forest_road_candidates"]
|
||
}
|
||
},
|
||
)
|
||
return out
|