Files
Aislo/B09_Estimation/B09_Estimation_Calc_Total.py
T
eomsangdonandClaude Opus 5.5 58283a578f feat(b09): 총괄 요율 자동 고름 · 사용자 값 앞섬 · 고르는 값 칸(166-1)
- 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
2026-10-10 00:06:34 +09:00

257 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""B09 총괄설계내역서 · 공사원가계산서 셈(PLAN 145-4) — 화면 없음 · 저장 안 함 · 파일 · M01 안 읽음.
⚠ 짝 = `B09_Estimation_Calc_Total.ts`(같은 식 · 같은 차례 · 같은 끝수) — 한쪽을 고치면 다른 쪽도 ·
거울 시험 `resources/tester/test_145_4_b09_total_calc.py`.
조작 중은 화면 짝이 바로 셈 · [저장] 때 서버가 이 파일로 정본을 다시 셈.
입력
sums = 설계내역서 비목 합(145-3 `sheets.bill.sums`)
material 직접재료비 · labor 직접노무비 · expense 산출경비(관급 제외 순공사비계) ·
supply_net 관급 순자재대(조달수수료 전)
estimate = 사용자 값(`user.estimate`) — rates {줄 키: %} · safety_base 기초액 ·
profit_adjust 이윤 보정 · vat_adjust 부가세 조정 · indirect_material 간접재료비 ·
scrap 작업설 · procurement_fee_pct 조달수수료율
table = M01 요율 압축 표(`B09_Estimation_Rates.rate_table` · payload `rate_table`) · year = 연금 해
요율 = 사용자 값(`rates`)이 앞섬 · 없으면 표에서 자동(166-1 `pick_rates` — 직접공사비 = 가.순공사비계 ·
공기 · 추정가격 = 사용자 칸 · 안전 대상액 = 재료비 + 직노 + 관급 ÷ 1.1 · 공종 = 토목 · 환경 = 그 밖 기본) ·
안전 기초액 = 사용자 값 ?? 자동 구간 기초액
식 · 끝수 = 엑셀 총괄 49~67 그대로(줄마다 ROUNDDOWN 0 · 밑수 = 앞 줄 버린 값 · 관급 ROUNDUP −3) ·
산업안전보건관리비 = A(관급 순자재대 ÷ 1.1 포함) · B(관급 빼고 × 1.2) 중 작은 쪽 ·
이윤 = ROUNDDOWN((다 − 재료비) × 율, 0) − 보정 · 부가세 = ROUNDDOWN(공급가액 × 율, 0) + 조정.
빈칸 규칙 = 요율(사용자 · 자동 둘 다 없음) · 비목 합이 빈칸이면 그 줄 빈칸(0 을 채우지 않음) · 그 줄을 쓰는 소계도 빈칸 ·
손입력(기초액 · 보정 · 조정 · 간접재료비 · 작업설) 빈칸 = 0 · 엑셀에 없는 현행 비목 = 줄만(늘 빈칸).
"""
from __future__ import annotations
from datetime import date
from typing import Any
from B09_Estimation.B09_Estimation_Calc_Base import num, rounddown, roundup, total
#: 제비율 줄 (키, 이름, M01 요율 후보 키, 산식 글) — 엑셀 총괄 49~58 차례 · 나.소계에 듦
RATE_LINES: tuple[tuple[str, str, str, str], ...] = (
("indirect_labor", "간접노무비", "RT000008", "직접노무비 × 율"),
("accident", "산재보험료", "RT000001", "(직접노무비 + 간접노무비) × 율"),
("employment", "고용보험료", "RT000002", "(직접노무비 + 간접노무비) × 율"),
("health", "건강보험료", "RT000003", "직접노무비 × 율"),
("care", "노인장기요양보험료", "RT000005", "건강보험료 × 율"),
("pension", "연금보험료", "RT000004", "직접노무비 × 율"),
("safety", "산업안전보건관리비", "RT000006", "A · B 중 작은 쪽 + 기초액"),
("other", "기타경비", "RT000009", "(직접노무비 + 간접노무비 + 재료비) × 율"),
("environment", "환경보전비", "RT000010", "순공사비 × 율"),
("machine", "건설기계대여금지급보증", "RT000015", "순공사비 × 율"),
)
#: 엑셀에 없는 현행 비목 — 줄만 두고 빈칸(밑수 · 고르는 값 미정 · 나.소계에 안 듦)
CURRENT_ONLY: tuple[tuple[str, str, str], ...] = (
("retirement", "퇴직공제부금비", "RT000011"),
("wage_claim", "임금채권보장분담금", "RT000012"),
("asbestos", "석면피해구제분담금", "RT000013"),
("subcontract", "하도급대금지급보증수수료", "RT000014"),
("performance", "공사이행보증수수료", "RT000016"),
)
#: 소계 아래 줄 (키, 이름, 후보 키, 산식 글)
TAIL_LINES: tuple[tuple[str, str, str, str], ...] = (
("overhead", "일반관리비", "RT000017", "나.소계 × 율"),
("profit", "이윤", "RT000018", "(다.소계 − 재료비) × 율 − 보정"),
("vat", "부가가치세", "RT000019", "공급가액 × 율 + 조정"),
)
_SAFETY_VAT_DIVISOR = 1.1 # 관급 순자재대가 부가세 포함 — 1.1 로 나눠 대상액에
_SAFETY_B = 1.2 # B안 = (관급 빼고) × 1.2
def _pct(base: float | None, rate: float | None) -> float | None:
"""ROUNDDOWN(밑수 × 율 ÷ 100, 0) — 둘 중 빈칸이면 빈칸."""
return None if base is None or rate is None else rounddown(base * rate / 100)
def procurement_fee(net: float | None, pct: float | None) -> float | None:
"""조달수수료 = ROUNDDOWN(관급 순자재대 × 율 ÷ 100, 0)(엑셀 설계내역서 253 줄)."""
return _pct(net, pct)
def _step(bounds: list[float], value: float) -> int:
"""구간 번호 — 위 끝(그 값 미만) 목록에서 value 가 넘은 개수."""
return sum(1 for b in bounds if value >= b)
def _band(bounds: list[float], i: int, days: bool = False) -> str:
def fmt(v: float) -> str:
return f"{v:g}일" if days else f"{v / 1e8:g}억"
if i == 0:
return f"{fmt(bounds[0])} 미만"
if i == len(bounds):
return f"{fmt(bounds[-1])} 이상"
return f"{fmt(bounds[i - 1])} 이상 {fmt(bounds[i])} 미만"
def _pick(spec: dict[str, Any], ctx: dict[str, Any]) -> tuple[float | None, float | None, str]:
"""표 한 줄 → (자동 율, 기초액, 까닭 글) — 고르는 값이 빈칸이면 율 None."""
if "pct" in spec:
return spec["pct"], None, "단일 요율"
if "price" in spec:
if ctx["price"] is None:
return None, None, "추정가격 빈칸"
i = _step(spec["price"], ctx["price"])
return spec["pcts"][i], None, f"추정가격 {_band(spec['price'], i)}"
if "grid" in spec:
if ctx["direct"] is None or ctx["days"] is None:
return None, None, "직접공사비 · 공기 빈칸"
i, j = _step(spec["direct"], ctx["direct"]), _step(spec["days"], ctx["days"])
why = f"직접공사비 {_band(spec['direct'], i)} · 공기 {_band(spec['days'], j, True)}"
return spec["grid"][i][j], None, why
if "target" in spec:
if ctx["target"] is None:
return None, None, "대상액 빈칸"
i = _step(spec["target"], ctx["target"])
return spec["pcts"][i], spec["bases"][i], f"대상액 {_band(spec['target'], i)}"
if "years" in spec:
pct = spec["years"].get(str(ctx["year"]))
return pct, None, f"{ctx['year']}년" if pct is not None else f"{ctx['year']}년 요율 없음"
if "kinds" in spec:
kind = "road" if ctx["env"] == "road" else "other"
return spec["kinds"].get(kind), None, "도로" if kind == "road" else "그 밖의 토목"
return None, None, ""
def pick_rates(table: dict[str, Any] | None, ctx: dict[str, Any]) -> dict[str, dict[str, Any]]:
"""{줄 키: {rate, base, why}} — 표 없으면 {}(자동 없음)."""
out: dict[str, dict[str, Any]] = {}
for key, spec in (table if isinstance(table, dict) else {}).items():
rate, base, why = _pick(spec, ctx)
note = spec.get("note") or ""
out[key] = {"rate": rate, "base": base, "why": f"{why} · {note}" if note else why}
return out
def compute_total(
sums: dict[str, Any] | None,
estimate: dict[str, Any] | None,
table: dict[str, Any] | None = None,
year: int | None = None,
) -> dict[str, Any]:
"""총괄 제비율 줄 · 소계 · 관급 · 원가계산서 줄 · 구성비."""
sums = sums if isinstance(sums, dict) else {}
est = estimate if isinstance(estimate, dict) else {}
rates_in = est.get("rates") if isinstance(est.get("rates"), dict) else {}
def hand(key: str) -> float:
return num(est.get(key)) or 0.0
material, labor = num(sums.get("material")), num(sums.get("labor"))
expense, supply_net = num(sums.get("expense")), num(sums.get("supply_net"))
direct = total([material, labor, expense]) # 가. 순공사비계
labor_material = total([labor, material])
target = (
None
if labor_material is None
else labor_material + (supply_net or 0.0) / _SAFETY_VAT_DIVISOR
)
auto = pick_rates(
table,
{
"direct": direct,
"target": target,
"price": num(est.get("estimated_price")),
"days": num(est.get("duration_days")),
"year": year or date.today().year,
"env": est.get("environment_kind"),
},
)
rates: dict[str, dict[str, Any]] = {}
for key, *_ in (*RATE_LINES, *TAIL_LINES):
mine, got = num(rates_in.get(key)), auto.get(key) or {}
rates[key] = {
"rate": got.get("rate") if mine is None else mine,
"from": "user"
if mine is not None
else ("auto" if got.get("rate") is not None else None),
"auto": got.get("rate"),
"why": got.get("why") or "",
}
rate = {key: r["rate"] for key, r in rates.items()}
base_in = num(est.get("safety_base"))
safety_base = base_in if base_in is not None else (auto.get("safety") or {}).get("base") or 0.0
rates["safety"]["base"] = safety_base
line: dict[str, float | None] = {}
line["indirect_labor"] = _pct(labor, rate["indirect_labor"])
total_labor = total([labor, line["indirect_labor"]])
line["accident"] = _pct(total_labor, rate["accident"])
line["employment"] = _pct(total_labor, rate["employment"])
line["health"] = _pct(labor, rate["health"])
line["care"] = _pct(line["health"], rate["care"])
line["pension"] = _pct(labor, rate["pension"])
# 산업안전보건관리비 — A(관급 순자재대 ÷ 1.1 포함) · B(× 1.2) 중 작은 쪽(관급 없으면 A)
safety_a = safety_b = None
if labor_material is not None and target is not None and rate["safety"] is not None:
base = safety_base
safety_a = rounddown(target * rate["safety"] / 100 + base)
safety_b = rounddown((labor_material * rate["safety"] / 100 + base) * _SAFETY_B)
line["safety"] = None if safety_a is None else min(safety_a, safety_b)
line["other"] = _pct(total([labor, line["indirect_labor"], material]), rate["other"])
line["environment"] = _pct(direct, rate["environment"])
line["machine"] = _pct(direct, rate["machine"])
net_cost = total([direct, *(line[key] for key, *_ in RATE_LINES)]) # 나. 순공사원가
line["overhead"] = _pct(net_cost, rate["overhead"])
da = total([net_cost, line["overhead"]]) # 다
profit_raw = _pct(None if da is None or material is None else da - material, rate["profit"])
line["profit"] = None if profit_raw is None else profit_raw - hand("profit_adjust")
supply_price = total([da, line["profit"]]) # 라. 공급가액
vat = _pct(supply_price, rate["vat"])
line["vat"] = None if vat is None else vat + hand("vat_adjust")
contract = total([supply_price, line["vat"]]) # 마. 도급공사비
# 관급자재대 = ROUNDUP(순자재대 + 조달수수료, −3) · 관급 0 이면 0
fee = (
0.0 if supply_net == 0 else procurement_fee(supply_net, num(est.get("procurement_fee_pct")))
)
supply_raw = total([supply_net, fee])
line["supply"] = None if supply_raw is None else roundup(supply_raw, -3)
grand = total([contract, line["supply"]]) # 바. 총공사비
indirect_material, scrap = hand("indirect_material"), hand("scrap")
cost = {
"direct_material": None if material is None else material - indirect_material + scrap,
"indirect_material": indirect_material,
"scrap": scrap,
"material": material,
"direct_labor": labor,
"indirect_labor": line["indirect_labor"],
"labor": total_labor,
"expense": expense,
**{key: line[key] for key in ("accident", "employment", "health", "care", "pension")},
**{key: line[key] for key in ("safety", "other", "environment", "machine")},
"expense_sum": total(
[expense, *(line[key] for key, *_ in RATE_LINES if key != "indirect_labor")]
),
"overhead": line["overhead"],
"profit": line["profit"],
"supply_price": supply_price,
"vat": line["vat"],
"contract": contract,
"supply": line["supply"],
"grand": grand,
}
share = {
key: (None if value is None or not grand else value / grand * 100)
for key, value in cost.items()
}
return {
"sums": {"material": material, "labor": labor, "expense": expense, "direct": direct},
"lines": line,
"subtotals": {
"net_cost": net_cost,
"da": da,
"supply_price": supply_price,
"contract": contract,
"grand": grand,
},
"safety": {"a": safety_a, "b": safety_b},
"profit_raw": profit_raw,
"supply": {"net": supply_net, "fee": fee, "raw": supply_raw},
"cost": cost,
"share": share,
"rates": rates,
}