feat(B09): 총괄설계내역서 · 공사원가계산서 탭 · 제비율 셈 파이썬 · TS 짝(145-4)

- compute_total(sums, estimate) · computeTotal — 설계내역서 비목 합(bill.sums) + 사용자 요율 · 손입력
- 끝수 엑셀 그대로(줄마다 ROUNDDOWN 0 · 관급 ROUNDUP −3 · 이윤 − 보정 · 부가세 + 조정) · 안전관리비 A/B 작은 쪽
- 요율 빈칸 시작 · 빈칸이면 그 줄 · 소계 빈칸 · 후보 = M01 요율_조달청제비율 글(확정 아님)
- 엑셀에 없는 현행 비목 다섯 = 줄만 · 원가계산서 = 총괄 거울 + 구성비 · 줄 누름 = 총괄 그 줄
- 엑셀 검산(순공사원가 868,874,829 · 총공사비 1,201,879,000) · 거울 시험

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TNMXVCZXMpHpVvqmFkeChL
This commit is contained in:
2026-10-09 15:12:40 +09:00
co-authored by Claude Opus 5.5
parent 2face8aa78
commit 8013fd901b
7 changed files with 809 additions and 2 deletions
+161
View File
@@ -0,0 +1,161 @@
"""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 조달수수료율
식 · 끝수 = 엑셀 총괄 49~67 그대로(줄마다 ROUNDDOWN 0 · 밑수 = 앞 줄 버린 값 · 관급 ROUNDUP −3) ·
산업안전보건관리비 = A(관급 순자재대 ÷ 1.1 포함) · B(관급 빼고 × 1.2) 중 작은 쪽 ·
이윤 = ROUNDDOWN((다 − 재료비) × 율, 0) − 보정 · 부가세 = ROUNDDOWN(공급가액 × 율, 0) + 조정.
빈칸 규칙 = 요율 · 비목 합이 빈칸이면 그 줄 빈칸(0 을 채우지 않음) · 그 줄을 쓰는 소계도 빈칸 ·
손입력(기초액 · 보정 · 조정 · 간접재료비 · 작업설) 빈칸 = 0 · 엑셀에 없는 현행 비목 = 줄만(늘 빈칸).
"""
from __future__ import annotations
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 compute_total(sums: dict[str, Any] | None, estimate: dict[str, Any] | 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 {}
rate = {key: num(rates_in.get(key)) for key, *_ in (*RATE_LINES, *TAIL_LINES)}
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]) # 가. 순공사비계
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
labor_material = total([labor, material])
if labor_material is not None and rate["safety"] is not None:
base = hand("safety_base")
target = labor_material + (supply_net or 0.0) / _SAFETY_VAT_DIVISOR
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,
}