- 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
141 lines
5.2 KiB
Python
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) == {}
|