Files
Aislo/resources/tester/test_b09_cost_options.py
T
eomsangdonandClaude Opus 5 fb4eb59cdb 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
2026-09-14 02:43:14 +09:00

125 lines
4.9 KiB
Python

"""원가계산서 기준 입력 10·11·12 — 일반관리비 주/전문 · 절사 · 부가세 방식 (PLAN 6장 새 절).
⚠ 금액을 박지 않는다 — 구조(어느 밑수·어느 표·끝자리)만 잰다. 실무 값은 식 한 조각만 씀:
봉화 차액 373 → 이윤 339 · 영월 695 → 632 (÷1.1 반올림) · 영덕(산림조합-면세품) 996 → 996.
"""
from __future__ import annotations
import sys
from dataclasses import replace
from decimal import Decimal
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from B09_Estimation.B09_Estimation_Engine_Cost import CostInput, calculate_cost # noqa: E402
from B09_Estimation.B09_Estimation_Engine_Cost_Options import ( # noqa: E402
cut_gap,
profit_cut,
)
BASE = CostInput(
direct_material_krw=Decimal(123_456_789),
direct_labor_krw=Decimal(234_567_891),
direct_expense_krw=Decimal(45_678_912),
estimated_price_krw=Decimal(1_000_000_000), # 10억 — 주공사 8.0 / 전문 6.5 가 갈리는 자리
)
def test_기본값은_종전과_같다() -> None:
explicit = replace(
BASE, overhead_class="civil_landscape_industrial", vat_mode="supply", cut_basis=""
)
assert calculate_cost(explicit).totals == calculate_cost(BASE).totals
def test_전문공사는_요율표만_바뀐다() -> None:
main, special = (
calculate_cost(BASE),
calculate_cost(replace(BASE, overhead_class="specialty_electric_communication_fire_other")),
)
assert (
main.line("general_overhead").base_amount_krw
== special.line("general_overhead").base_amount_krw
)
assert (
main.line("general_overhead").rate_percent != special.line("general_overhead").rate_percent
)
def test_관리품목자재대는_일반관리비_밑수에_든다() -> None:
plain = calculate_cost(BASE)
managed = calculate_cost(replace(BASE, overhead_managed_material_krw=Decimal(10_000_000)))
gain = (
managed.line("general_overhead").base_amount_krw
- plain.line("general_overhead").base_amount_krw
)
assert gain == Decimal(10_000_000)
assert "관리품목자재대" in managed.line("general_overhead").base_label
@pytest.mark.parametrize(
("mode", "expected"),
[
("material", lambda r, d: r.amount("material_cost")),
("none", lambda r, d: Decimal(0)),
("forest_coop", lambda r, d: r.amount("material_cost") + d.direct_expense_krw),
(
"forest_coop_exempt",
lambda r, d: (
r.amount("material_cost") - d.tax_exempt_material_krw + d.direct_expense_krw
),
),
],
)
def test_부가세_방식은_밑수만_바꾼다(mode, expected) -> None:
data = replace(BASE, vat_mode=mode, tax_exempt_material_krw=Decimal(3_327_500))
result = calculate_cost(data)
assert result.line("vat").base_amount_krw == expected(result, data)
if mode == "none":
assert result.amount("vat") == 0
def test_실무_절사_식() -> None:
assert profit_cut(Decimal(373), "grand_total", "supply") == 339 # 봉화
assert profit_cut(Decimal(695), "grand_total", "supply") == 632 # 영월
assert profit_cut(Decimal(996), "grand_total", "forest_coop_exempt") == 996 # 영덕
assert cut_gap(Decimal(1_270_728_373), 1000) == 373
@pytest.mark.parametrize("vat_mode", ["supply", "forest_coop"])
def test_총공사비_천원_절사는_이윤에서_보정(vat_mode) -> None:
plain = calculate_cost(replace(BASE, vat_mode=vat_mode))
cut = calculate_cost(
replace(BASE, vat_mode=vat_mode, cut_basis="grand_total", cut_unit_krw=1000)
)
assert cut.amount("grand_total") % 1000 == 0
assert 0 <= plain.amount("grand_total") - cut.amount("grand_total") < 1000
assert cut.amount("profit") < plain.amount("profit")
assert cut.has("profit_adjustment") and any("자동보정" in note for note in cut.notes)
# 이윤 말고는 안 움직임 — 순공사원가·일반관리비 그대로.
for key in ("net_construction_cost", "general_overhead"):
assert cut.amount(key) == plain.amount(key)
def test_공급가액_절사는_차액_그대로() -> None:
plain = calculate_cost(BASE)
cut = calculate_cost(replace(BASE, cut_basis="supply", cut_unit_krw=10_000))
assert cut.amount("total_cost") % 10_000 == 0
assert plain.amount("profit") - cut.amount("profit") == plain.amount("total_cost") % 10_000
def test_절사를_안_고르면_이윤을_안_건드린다() -> None:
assert not calculate_cost(replace(BASE, cut_unit_krw=1000)).has("profit_adjustment")
def test_자동보정_줄은_제_이름을_적는다() -> None:
cut = calculate_cost(replace(BASE, cut_basis="grand_total", cut_unit_krw=1000))
assert "절사 자동보정" in cut.line("profit_adjustment").base_label
manual = calculate_cost(replace(BASE, profit_adjustment_krw=Decimal(100)))
assert manual.line("profit_adjustment").base_label == "설계자 명시 입력"