feat(b09): 원가계산서 기준 입력 10·11·12 — 일반관리비 주/전문 · 절사 · 부가세 방식
- 10 일반관리비: (주)공사/전문공사 = 요율 구간표만 갈림 · 밑수는 형식 칸(일반 = 순공사원가 + 관리품목자재대) - 11 절사: 총공사비/공급가액 N원 미만 — 고른 때만 이윤 자동보정(총공사비·공급가액 비례 부가세면 ÷1.1 반올림, 실무 봉화 373→339·영월 695→632, 산림조합은 차액 그대로) · 잔차 맞춤 - 12 부가세: 공급가액·재료비·없음·산림조합 형식·산림조합-면세품 (칸 이름은 기성 선택지를 받을 수 있게 문자열) - 기본값은 종전 계산과 같음 · 자동보정 줄은 「… 절사 자동보정」으로 적음 - 원가계산서 탭: 10·11·12 고르개 · 관리품목 자재대 · 면세품 금액 칸 - 시험: 기본값 불변 · 밑수·표 갈림 · 실무 절사 식 · 끝자리 · 골든셋 초록 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
This commit is contained in:
@@ -19,6 +19,14 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass, field, replace
|
||||
from decimal import ROUND_CEILING, ROUND_FLOOR, Decimal
|
||||
|
||||
from B09_Estimation.B09_Estimation_Engine_Cost_Options import (
|
||||
CUT_BASES,
|
||||
VAT_MODES,
|
||||
cut_gap,
|
||||
overhead_base,
|
||||
profit_cut,
|
||||
vat_base,
|
||||
)
|
||||
from B09_Estimation.B09_Estimation_Rates import (
|
||||
RateDataset,
|
||||
flat_rate,
|
||||
@@ -87,6 +95,8 @@ class CostInput:
|
||||
|
||||
#: 이윤 수동 조정액 — 설계자 명시 입력일 때만. 프로그램이 스스로 채우지 않는다.
|
||||
profit_adjustment_krw: Decimal = _ZERO
|
||||
#: 조정액 줄 산식 칸 — 비면 「설계자 명시 입력」. 절사 자동보정이 제 이름을 적는 자리.
|
||||
profit_adjustment_label: str = ""
|
||||
|
||||
#: 폐기물처리비 — 요율이 아니라 **실비**(수량 × 처리단가).
|
||||
#: ⭐ 2026-09-14 판정 — **비목은 경비**(예정가격작성기준 제19조③18호)라 순공사원가에 들고
|
||||
@@ -97,6 +107,21 @@ class CostInput:
|
||||
#: (거창 원가계산서 `총공사비 = 도급액 + 관급자재대 + 폐기물처리비` 모양). 기본 꺼짐.
|
||||
waste_separate_order: bool = False
|
||||
|
||||
#: ── 기준 입력 10·11·12 (규칙은 `Engine_Cost_Options`) — 기본값은 종전 계산과 같음 ──
|
||||
#: 원가계산 형식 — 일반관리비 밑수가 형식마다 다름(지금은 `general` 만).
|
||||
form: str = "general"
|
||||
#: 10. 일반관리비 — 요율 표 이름((주)공사 / 전문공사).
|
||||
overhead_class: str = "civil_landscape_industrial"
|
||||
#: 일반 형식 일반관리비 밑수에 더하는 관리품목 자재대.
|
||||
overhead_managed_material_krw: Decimal = _ZERO
|
||||
#: 12. 부가세 방식(`VAT_MODES`) · 산림조합-면세품이면 면세품 금액.
|
||||
vat_mode: str = "supply"
|
||||
tax_exempt_material_krw: Decimal = _ZERO
|
||||
#: 11. 절사 — `grand_total`(총공사비에서 조정) · `supply`(공급가액) · 빈 값은 안 자름.
|
||||
#: ⚠ 고른 때만 이윤을 자동보정한다 — 기본은 수동 이윤보정만(2026-09-08 합의).
|
||||
cut_basis: str = ""
|
||||
cut_unit_krw: int = 0
|
||||
|
||||
#: 환경보전비 공종 (`rate_environment.all_work_types` 의 값).
|
||||
#: TODO(미결 PLAN 9-6): 임도가 「도로 0.9 %」인지 「기타 토목 0.8 %」인지 미확정.
|
||||
#: 잠정 = 도로(0.9 %). 요율 데이터가 `pending` 을 달고 있어 결과 줄에 경고가 붙는다.
|
||||
@@ -215,14 +240,16 @@ def _emitter(result: CostResult):
|
||||
_SCALE_MAX_PASSES = 5
|
||||
|
||||
|
||||
def _scale_signature(dataset: RateDataset, amount: Decimal) -> tuple:
|
||||
def _scale_signature(
|
||||
dataset: RateDataset, amount: Decimal, overhead_class: str = "civil_landscape_industrial"
|
||||
) -> tuple:
|
||||
"""이 금액이 어느 구간들에 떨어지는가 — 구간이 바뀌었는지 판정하는 지문.
|
||||
|
||||
규모(추정가격)로 갈리는 요율만 모은다. 지문이 같으면 더 돌 필요가 없다.
|
||||
"""
|
||||
parts: list[str] = []
|
||||
for variable, bracket_field, key in (
|
||||
("rate_overhead", "estimated_price_bracket", "civil_landscape_industrial"),
|
||||
("rate_overhead", "estimated_price_bracket", overhead_class),
|
||||
("rate_profit", "estimated_price_bracket", "brackets"),
|
||||
("rate_goyong", "estimated_amount_bracket", "brackets"),
|
||||
("rate_subcontract_payment_guarantee", "estimated_price_bracket", "brackets"),
|
||||
@@ -280,7 +307,7 @@ def calculate_cost(data: CostInput) -> CostResult:
|
||||
tried_amounts: list[Decimal] = []
|
||||
|
||||
for _ in range(_SCALE_MAX_PASSES):
|
||||
signature = _scale_signature(dataset, scale)
|
||||
signature = _scale_signature(dataset, scale, data.overhead_class)
|
||||
if signature in seen_signatures:
|
||||
# 구간이 진동한다 — 보수적으로 **높은 쪽**을 잡고 그 사실을 남긴다.
|
||||
highest = max([*tried_amounts, scale])
|
||||
@@ -292,7 +319,7 @@ def calculate_cost(data: CostInput) -> CostResult:
|
||||
|
||||
trial = _calculate_with_scale(data, dataset, scale, [])
|
||||
estimated_price = trial.totals["total_cost"]
|
||||
if _scale_signature(dataset, estimated_price) == signature:
|
||||
if _scale_signature(dataset, estimated_price, data.overhead_class) == signature:
|
||||
return trial
|
||||
scale = estimated_price
|
||||
|
||||
@@ -301,11 +328,50 @@ def calculate_cost(data: CostInput) -> CostResult:
|
||||
)
|
||||
|
||||
|
||||
#: 절사 뒤 잔차 맞춤 반복 상한 — 실무 수식도 잔차 칸 한 번뿐(표본 넷 모두 0).
|
||||
_CUT_MAX_PASSES = 3
|
||||
|
||||
|
||||
def _calculate_with_scale(
|
||||
data: CostInput,
|
||||
dataset: RateDataset,
|
||||
scale: Decimal,
|
||||
notes: list[str],
|
||||
) -> CostResult:
|
||||
"""규모 기준액을 못 박고 계산 — 11. 절사를 **고른 때만** 이윤을 보정해 다시 셈."""
|
||||
result = _calculate_once(data, dataset, scale, notes)
|
||||
if not data.cut_basis or data.cut_unit_krw <= 0:
|
||||
return result
|
||||
key = "grand_total" if data.cut_basis == "grand_total" else "total_cost"
|
||||
automatic = _ZERO
|
||||
for _ in range(_CUT_MAX_PASSES):
|
||||
gap = cut_gap(result.totals[key], data.cut_unit_krw)
|
||||
if gap == 0:
|
||||
break
|
||||
# 첫 차례는 실무 식(총공사비·공급가액 비례 부가세면 ÷1.1), 그 뒤는 잔차 그대로.
|
||||
automatic += profit_cut(gap, data.cut_basis, data.vat_mode) if automatic == 0 else gap
|
||||
adjusted = replace(
|
||||
data,
|
||||
profit_adjustment_krw=data.profit_adjustment_krw + automatic,
|
||||
cut_basis="",
|
||||
profit_adjustment_label=(
|
||||
f"{CUT_BASES.get(data.cut_basis, data.cut_basis)} {data.cut_unit_krw:,}원 미만"
|
||||
" 절사 자동보정" + (" + 설계자 입력" if data.profit_adjustment_krw else "")
|
||||
),
|
||||
)
|
||||
result = _calculate_once(adjusted, dataset, scale, notes)
|
||||
result.notes.append(
|
||||
f"{CUT_BASES.get(data.cut_basis, data.cut_basis)} {data.cut_unit_krw:,}원 미만 절사 —"
|
||||
f" 이윤에서 {automatic:,}원 자동보정(설계자가 고른 절사 기준)"
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def _calculate_once(
|
||||
data: CostInput,
|
||||
dataset: RateDataset,
|
||||
scale: Decimal,
|
||||
notes: list[str],
|
||||
) -> CostResult:
|
||||
"""규모 기준액을 못 박고 한 번 계산한다."""
|
||||
result = CostResult(rate_version=dataset.version_stamp, notes=list(notes))
|
||||
@@ -378,17 +444,20 @@ def _calculate_with_scale(
|
||||
amount=net_construction_cost,
|
||||
)
|
||||
|
||||
# 10. (주)공사/전문공사 = 요율 구간표만 갈림 · 밑수는 형식이 정함
|
||||
# (일반 = 순공사원가 + 관리품목자재대).
|
||||
overhead_row = select_bracket(
|
||||
dataset.variable("rate_overhead")["civil_landscape_industrial"],
|
||||
dataset.variable("rate_overhead")[data.overhead_class],
|
||||
amount_field="estimated_price_bracket",
|
||||
amount=ctx.scale_reference,
|
||||
label="일반관리비",
|
||||
)
|
||||
managed = data.overhead_managed_material_krw
|
||||
overhead = emit(
|
||||
key="general_overhead",
|
||||
name="일반관리비",
|
||||
base_label="순공사원가",
|
||||
base=net_construction_cost,
|
||||
base_label="순공사원가+관리품목자재대" if managed else "순공사원가",
|
||||
base=overhead_base(data.form, net_construction_cost, managed),
|
||||
percent=rate_percent(overhead_row, label="일반관리비"),
|
||||
)
|
||||
|
||||
@@ -402,12 +471,19 @@ def _calculate_with_scale(
|
||||
base=total_cost,
|
||||
amount=total_cost,
|
||||
)
|
||||
# 12. 부가세 방식 — 밑수만 갈림(공급가액·재료비·없음·산림조합 둘). 율은 요율 데이터.
|
||||
vat = emit(
|
||||
key="vat",
|
||||
name="부가가치세",
|
||||
base_label="총원가",
|
||||
base=total_cost,
|
||||
percent=flat_rate(dataset, "rate_vat"),
|
||||
base_label="총원가" if data.vat_mode == "supply" else VAT_MODES[data.vat_mode],
|
||||
base=vat_base(
|
||||
data.vat_mode,
|
||||
total_cost,
|
||||
material_cost,
|
||||
data.direct_expense_krw,
|
||||
data.tax_exempt_material_krw,
|
||||
),
|
||||
percent=_ZERO if data.vat_mode == "none" else flat_rate(dataset, "rate_vat"),
|
||||
)
|
||||
contract_amount = total_cost + vat
|
||||
emit(
|
||||
@@ -477,7 +553,7 @@ def _profit_lines(
|
||||
emit(
|
||||
key="profit_adjustment",
|
||||
name="이윤 조정액",
|
||||
base_label="설계자 명시 입력",
|
||||
base_label=data.profit_adjustment_label or "설계자 명시 입력",
|
||||
base=_ZERO,
|
||||
amount=-data.profit_adjustment_krw,
|
||||
note="도급공사비 끝수 맞춤 — 법정 항목 아님 (★법대로 8-10)",
|
||||
|
||||
Reference in New Issue
Block a user