Files
Aislo/resources/tester/test_b09_cost_sheet_layout.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

141 lines
5.2 KiB
Python

"""원가계산서 탭(일반형식) — STmate 서식 차례로 우리 엔진 줄을 늘어놓는가 (PLAN 12장).
⚠ 금액을 박지 않는다 — 차례·표기·자리(경비 안/총원가 밖)와 「줄 = 엔진 값」만 잰다.
"""
from __future__ import annotations
import sys
from decimal import Decimal
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from B09_Estimation.B09_Estimation_CostSheet import ( # noqa: E402
clean_settings,
cost_input,
field_options,
sheet_rows,
)
from B09_Estimation.B09_Estimation_Engine_Cost import calculate_cost # noqa: E402
from B09_Estimation.B09_Estimation_Rates import load_rate_dataset # noqa: E402
from B09_Estimation.B09_Estimation_RateTable import amount_label, rate_sections # noqa: E402
DIRECT = {
"material": Decimal(30_000_000),
"labor": Decimal(40_000_000),
"expense": Decimal(10_000_000),
}
def _sheet(waste: Decimal = Decimal(0), separate: bool = False, stored: dict | None = None):
data = cost_input(DIRECT, stored or {}, waste, separate)
return sheet_rows(calculate_cost(data), data), calculate_cost(data)
def test_큰_묶음이_STmate_차례로_선다() -> None:
rows, _ = _sheet()
marks = [
row["mark"]
for row in rows
if row["mark"] in ("나.", "ㄱ.", "ㄴ.", "ㄷ.", "다.", "라.", "마.", "바.")
]
assert marks == ["나.", "ㄱ.", "ㄴ.", "ㄷ.", "다.", "라.", "마.", "바."]
def test_줄_금액은_엔진_값_그대로() -> None:
rows, result = _sheet()
by_key = {row["key"]: row for row in rows}
for key in ("net_construction_cost", "general_overhead", "profit", "total_cost", "grand_total"):
assert Decimal(by_key[key]["amount_krw"]) == result.amount(key)
assert Decimal(by_key["subtotal"]["amount_krw"]) == result.amount(
"net_construction_cost"
) + result.amount("general_overhead")
def test_경비_세목은_2번부터_차례로_번호() -> None:
rows, _ = _sheet()
expense = [row for row in rows if row["level"] == 2 and row["mark"].endswith(")")]
numbers = [
int(row["mark"][:-1])
for row in expense
if row["key"] not in ("direct_labor", "indirect_labor_cost")
]
assert numbers[0] == 1 and numbers[1:] == list(range(2, len(numbers) + 1))
def test_폐기물은_기본_경비_안_분리발주면_도급공사비_뒤() -> None:
rows, _ = _sheet(Decimal(1_000_000))
keys = [row["key"] for row in rows]
assert keys.index("waste_disposal") < keys.index("general_overhead")
rows, _ = _sheet(Decimal(1_000_000), separate=True)
keys = [row["key"] for row in rows]
assert keys.index("waste_disposal") > keys.index("contract_amount")
def test_저장값은_데이터에_있는_공종만_받는다() -> None:
dataset = load_rate_dataset()
cleaned = clean_settings(
{
"work_type_safety": "civil",
"environment_work_type": "moon_base",
"duration_days": "200",
"owner_supplied_material_krw": "-5",
"profit_adjustment_krw": "",
},
dataset,
)
assert cleaned == {
"work_type_safety": "civil",
"duration_days": 200,
"owner_supplied_material_krw": "0",
}
assert {o["value"] for o in field_options(dataset)["work_type_indirect_labor"]} == {
"civil",
"landscape",
"industrial_facilities_civil",
}
def test_저장한_기간이_간접노무비_구간을_바꾼다() -> None:
short = {r["key"]: r for r in _sheet(stored={"duration_days": 100})[0]}
long = {r["key"]: r for r in _sheet(stored={"duration_days": 800})[0]}
assert (
short["indirect_labor_cost"]["rate_percent"] != long["indirect_labor_cost"]["rate_percent"]
)
def test_요율표_구간_표기와_묶음() -> None:
assert amount_label("lt_5_billion") == "50억 미만"
assert amount_label("5_to_30_billion") == "50억~300억 미만"
titles = [section["title"] for section in rate_sections(load_rate_dataset())]
assert titles[0] == "일반관리비 · 이윤" and "산업안전보건관리비" in titles
for section in rate_sections(load_rate_dataset()):
assert all(len(row) == len(section["columns"]) for row in section["rows"]), section["title"]
def test_10_11_12_칸이_저장값을_엔진으로_옮긴다() -> None:
dataset = load_rate_dataset()
stored = clean_settings(
{
"overhead_class": "specialty_electric_communication_fire_other",
"cut_basis": "grand_total",
"cut_unit_krw": "1000",
"vat_mode": "forest_coop",
"tax_exempt_material_krw": "5000",
"cut_unit_krw_bad": "7",
},
dataset,
)
assert stored["cut_unit_krw"] == 1000 and stored["vat_mode"] == "forest_coop"
data = cost_input(DIRECT, stored)
assert (data.overhead_class, data.cut_basis, data.cut_unit_krw) == (
"specialty_electric_communication_fire_other",
"grand_total",
1000,
)
rows = {r["key"]: r for r in sheet_rows(calculate_cost(data), data)}
assert int(rows["grand_total"]["amount_krw"]) % 1000 == 0
assert clean_settings({"cut_unit_krw": "7", "vat_mode": "moon"}, dataset) == {}