- 셈 짝(Calc_Total py ↔ ts) — STmate 일반형식 식 · 자리(지식DB 12 · 35 분석) · 경비 묶음 안 · 나.소계에 듦 퇴직공제부금비 = 직접노무비 × 율 · 임금채권 · 석면 = (직노 + 간노) × 율 · 하도급 = 순공사비 × 율 · 공사이행보증 = 금액 직접 입력(performance_fee) · 자동 요율 없음 · 요율 빈칸 = 해당 없음(0) - 요율 출처 「불러옴」 = rates_imported(707-2 불러오기가 실음 · 요율 고치면 사용자) - 707-2 불러오기 — 「셈 없음」 알림 뺌 · 공사이행보증 줄 금액 = 손입력 칸 - 총괄 · 원가계산서 탭 · 인쇄 미리보기 = 같은 줄 짜기 - 낡은 시험 고침 — 현행 비목 기대값 빈칸 → 0 · 퇴직공제 요율 칸 고침 가능 · 거창 총공사비 = 엑셀 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
272 lines
14 KiB
Python
272 lines
14 KiB
Python
"""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.
|
||
현행 비목 다섯(PLAN 708-2 · 엑셀 총괄에 없음 · 경쟁사 둘 다 셈) = STmate 일반형식 식 · 자리
|
||
(지식DB `STmate 분석/20_분석/12_원가계산_4형식과_RATE표_복원.md` 일반 · 수공 경비 차례 ·
|
||
`35_형식과_단계의_공통과_차이.md` 밑수 표) — 경비 묶음 안 · 나.소계(순공사원가)에 듦 ·
|
||
퇴직공제부금비 = 직접노무비 × 율 · 임금채권 · 석면 = (직접노무비 + 간접노무비) × 율 ·
|
||
하도급대금지급보증수수료 = 순공사비 × 율 · 공사이행보증수수료 = 금액 손입력(`performance_fee` ·
|
||
STmate 식 = 공종 · 기간 표라 뒤로) · 자동 요율 없음 · 요율 빈칸 = 해당 없음(0 · 빈칸 아님).
|
||
요율 출처 = 사용자 값 중 `rates_imported` 키 = 「import」(707-2 불러옴).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import Any
|
||
|
||
from B09_Estimation.B09_Estimation_Calc_Base import num, rounddown, roundup, total
|
||
from common_util.common_util_time import kst_today
|
||
|
||
#: 제비율 줄 (키, 이름, M01 요율 후보 키, 산식 글) — 엑셀 총괄 49~58 차례 +
|
||
#: 현행 비목(STmate 경비 차례) · 나.소계에 듦
|
||
RATE_LINES: tuple[tuple[str, str, str, str], ...] = (
|
||
("indirect_labor", "간접노무비", "RT000008", "직접노무비 × 율"),
|
||
("accident", "산재보험료", "RT000001", "(직접노무비 + 간접노무비) × 율"),
|
||
("employment", "고용보험료", "RT000002", "(직접노무비 + 간접노무비) × 율"),
|
||
("health", "건강보험료", "RT000003", "직접노무비 × 율"),
|
||
("care", "노인장기요양보험료", "RT000005", "건강보험료 × 율"),
|
||
("pension", "연금보험료", "RT000004", "직접노무비 × 율"),
|
||
("retirement", "퇴직공제부금비", "RT000011", "직접노무비 × 율"),
|
||
("safety", "산업안전보건관리비", "RT000006", "A · B 중 작은 쪽 + 기초액"),
|
||
("other", "기타경비", "RT000009", "(직접노무비 + 간접노무비 + 재료비) × 율"),
|
||
("performance", "공사이행보증수수료", "RT000016", "금액 직접 입력"),
|
||
("subcontract", "하도급대금지급보증수수료", "RT000014", "순공사비 × 율"),
|
||
("environment", "환경보전비", "RT000010", "순공사비 × 율"),
|
||
("machine", "건설기계대여금지급보증", "RT000015", "순공사비 × 율"),
|
||
("asbestos", "석면피해구제분담금", "RT000013", "(직접노무비 + 간접노무비) × 율"),
|
||
("wage_claim", "임금채권보장분담금", "RT000012", "(직접노무비 + 간접노무비) × 율"),
|
||
)
|
||
#: 현행 비목 — 요율 빈칸 = 해당 없음(0) · 공사이행보증 = 금액 손입력(율 칸 없음)
|
||
CURRENT_ONLY = ("retirement", "subcontract", "asbestos", "wage_claim", "performance")
|
||
#: 소계 아래 줄 (키, 이름, 후보 키, 산식 글)
|
||
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 {}
|
||
imported = est.get("rates_imported") if isinstance(est.get("rates_imported"), list) 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 kst_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": ("import" if key in imported else "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"])
|
||
line["retirement"] = _pct(labor, rate["retirement"])
|
||
line["subcontract"] = _pct(direct, rate["subcontract"])
|
||
line["asbestos"] = _pct(total_labor, rate["asbestos"])
|
||
line["wage_claim"] = _pct(total_labor, rate["wage_claim"])
|
||
for key in CURRENT_ONLY: # 요율 빈칸 = 해당 없음
|
||
if rate[key] is None:
|
||
line[key] = 0.0
|
||
line["performance"] = hand("performance_fee")
|
||
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 RATE_LINES[1:]},
|
||
"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,
|
||
}
|