Files
Aislo/B09_Estimation/B09_Estimation_Calc_Total.py
T
eomsangdonandClaude Opus 5.5 8013fd901b 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
2026-10-09 15:12:40 +09:00

162 lines
8.7 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 조달수수료율
식 · 끝수 = 엑셀 총괄 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,
}