Files
Aislo/resources/tester/test_b09_progress.py
T
eomsangdonandClaude Opus 5 a7a1b246b6 feat(b09): 준공 단계 — 계약금액 | 준공금액(기성 마지막 회차 누계를 옮기기만)
- 준공 별도 계산 규칙 미확인(35번 §3) — 규칙을 짓지 않고 기성 누계를 옮김 · 기성 한 장 불변
- 기성 회차가 없으면 준공금액 비움(0 원으로 안 채움)
- 기성 계산 조각(progress_for)을 떼어 준공이 같은 길로 받음
- 기성 간접재료비 확인 대기 닫음 — 예정가격작성기준 제17조·제39조② 근거(브레인 판정)
- 준공 탭 파일 · 사전 키(등록은 서브) · 시험 2건 · 전체 1730 통과

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
2026-09-14 07:01:46 +09:00

238 lines
9.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""기성 단계 — 계약 → 기성내역 · 기성 제잡비 계산서 **구조**가 서는가 (PLAN 12장 · 2026-09-14 배정).
⚠ 값이 맞는지는 못 잰다 — 기성 표본 0건(STmate 27번 §9 · 35번). 여기서 재는 것:
① ⭐ 제잡비 줄 = 금회직접공사비 × 계약잡비율(도급액 ÷ 계약 직접공사비)
— 줄마다 밑수×요율 다시 안 셈
② 계약 · 전회(앞 회차 금회의 합) · 금회 · 누계 네 칸과 각 비율 · 기성(%) · 잔량
③ 부가세 넷 — 직접입력 · 공급가액 · 재료비 · 재료비+산출경비
④ 계약 줄(입력) 불변 · 분리발주 폐기물은 제잡비 밖 · 틀린 칸 거름
"""
from __future__ import annotations
import copy
import sys
from decimal import Decimal
from pathlib import Path
from types import SimpleNamespace
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from B09_Estimation.B09_Estimation_Engine_Cost import CostLine, CostResult # noqa: E402
from B09_Estimation.B09_Estimation_Progress import ( # noqa: E402
clean_settings,
completion_sheet,
progress_sheet,
)
def _contract_row(item_no: str, qty: str, unit: tuple[int, int, int]) -> dict:
total = sum(unit) * int(qty)
return {
"item_no": item_no,
"is_group": False,
"in_bill": True,
"quantity": qty,
"contract_unit_material_krw": str(unit[0]),
"contract_unit_labor_krw": str(unit[1]),
"contract_unit_expense_krw": str(unit[2]),
"contract_amount_krw": str(total),
}
CONTRACT_ROWS = [
{"item_no": "1", "is_group": True, "in_bill": True},
_contract_row("1.1", "100", (1000, 2000, 500)), # 350,000
_contract_row("1.2", "10", (5000, 0, 0)), # 50,000 → 계약 직접공사비 400,000
]
def _cost(separate_waste: bool = False) -> tuple[SimpleNamespace, CostResult]:
result = CostResult()
for key, amount in (
("indirect_labor_cost", 20000),
("industrial_accident_insurance", 7000),
("waste_disposal", 5000),
("general_overhead", 24000),
("profit", 30000),
("vat", 48100),
):
result.lines.append(
CostLine(key, key, "", Decimal(amount), None, Decimal(0), Decimal(amount))
)
data = SimpleNamespace(waste_separate_order=separate_waste, indirect_material_krw=Decimal(0))
return data, result
SETTINGS = {
"rounds": [
{"quantities": {"1.1": "40", "1.2": "2"}, "vat_mode": "supply"},
{"quantities": {"1.1": "30"}, "vat_mode": "material"},
]
}
def _sheet(settings=SETTINGS, round_no=None, separate_waste=True) -> dict:
data, result = _cost(separate_waste)
return progress_sheet(CONTRACT_ROWS, data, result, clean_settings(settings)[0], round_no)
def test_제잡비는_금회직접공사비_곱하기_계약잡비율() -> None:
before = copy.deepcopy(CONTRACT_ROWS)
sheet = _sheet()
assert CONTRACT_ROWS == before # 계약 줄 불변
items = {item["key"]: item for item in sheet["items"]}
labor = items["indirect_labor_cost"]
# 도급액 20,000 ÷ 계약 직접 400,000 = 5% · 1회 150,000 → 7,500 · 2회 105,000 → 5,250
assert (labor["contract_ratio_pct"], labor["previous_krw"], labor["current_krw"]) == (
"5.000",
"7500",
"5250",
)
assert (labor["cumulative_krw"], labor["cumulative_pct"]) == ("12750", "63.750")
# 7,000 × 105,000 ÷ 400,000 = 1,837.5 → 회차마다 원 미만 절사
assert items["industrial_accident_insurance"]["current_krw"] == "1837"
assert "waste_disposal" not in items # 분리발주면 도급 밖
assert sheet["contract_overhead_ratio_pct"] == "20.250" # 81,000 ÷ 400,000
def test_네_칸과_기성율_잔량() -> None:
sheet = _sheet()
row = {r["item_no"]: r for r in sheet["rows"]}["1.1"]
assert (
row["progress_previous_quantity"],
row["progress_current_quantity"],
row["progress_cumulative_quantity"],
) == ("40", "30", "70")
assert (row["progress_cumulative_amount_krw"], row["progress_pct"]) == ("245000", "70.000")
assert row["progress_remaining_quantity"] == "30"
group = sheet["rows"][0]
assert group["progress_cumulative_amount_krw"] == "255000" # 245,000 + 1.2 의 10,000
summary = {s["key"]: s for s in sheet["summary"]}
assert (summary["direct"]["previous_krw"], summary["direct"]["current_krw"]) == (
"150000",
"105000",
)
assert summary["direct"]["cumulative_pct"] == "63.750"
def test_부가세_넷() -> None:
summary = {s["key"]: s for s in _sheet()["summary"]}
# 1회 공급가액 180,375 × 10% = 18,037 · 2회 재료비 30,000 × 10% = 3,000
assert (summary["supply"]["previous_krw"], summary["vat"]["previous_krw"]) == (
"180375",
"18037",
)
assert summary["vat"]["current_krw"] == "3000"
coop = {"rounds": [{"quantities": {"1.1": "40"}, "vat_mode": "forest_coop"}]}
vat = {s["key"]: s for s in _sheet(coop)["summary"]}["vat"]["current_krw"]
assert vat == "6000" # (재료비 40,000 + 산출경비 20,000) × 10%
manual = {
"rounds": [{"quantities": {"1.1": "40"}, "vat_mode": "manual", "vat_manual_krw": "12345"}]
}
assert {s["key"]: s for s in _sheet(manual)["summary"]}["vat"]["current_krw"] == "12345"
def test_회차를_고르면_그_앞이_전회() -> None:
first = _sheet(round_no=1)
row = {r["item_no"]: r for r in first["rows"]}["1.1"]
assert (row["progress_previous_quantity"], row["progress_current_quantity"]) == ("0", "40")
assert first["round"] == 1 and first["round_count"] == 2
def test_계약_수량_넘으면_알림과_분리발주_아니면_폐기물도_제잡비() -> None:
over = {"rounds": [{"quantities": {"1.2": "12"}, "vat_mode": "supply"}]}
row = {r["item_no"]: r for r in _sheet(over)["rows"]}["1.2"]
assert row["progress_remaining_quantity"] == "-2" and "넘음" in row["progress_note"]
items = {item["key"] for item in _sheet(separate_waste=False)["items"]}
assert "waste_disposal" in items
def _items(sheet: dict) -> dict:
return {item["key"]: item for item in sheet["items"]}
def _summary(sheet: dict) -> dict:
return {s["key"]: s for s in sheet["summary"]}
def test_계약잡비율_직접입력이_이기고_사유가_없으면_거른다() -> None:
settings = {
**SETTINGS,
"rate_overrides": {"indirect_labor_cost": {"rate_pct": "10", "reason": "계약서 제잡비율"}},
}
labor = _items(_sheet(settings))["indirect_labor_cost"]
assert (labor["contract_ratio_pct"], labor["default_ratio_pct"]) == ("10.000", "5.000")
assert labor["current_krw"] == "10500" and labor["override"]["reason"] == "계약서 제잡비율"
_, errors = clean_settings({"rate_overrides": {"profit": {"rate_pct": "7", "reason": " "}}})
assert len(errors) == 1
def test_단일율_곱과_벌어진_차이를_알린다() -> None:
# 1.2 × 0.1998 = 999원 → 줄별 49 + 17 + 59 + 74 = 199 · 단일율 20.25% 곱 202 → 3원
sheet = _sheet({"rounds": [{"quantities": {"1.2": "0.1998"}}]})
assert sheet["single_rate_gap_krw"]["current"] == "3"
assert any("단일율 곱과 금회 3원" in note for note in sheet["notes"])
def test_공급가액_절사와_총공사비_절사는_이윤에서() -> None:
first = {"quantities": {"1.1": "40", "1.2": "2"}, "vat_mode": "supply"}
cut = _sheet({"rounds": [{**first, "supply_cut_krw": "100"}]})
# 공급가액 180,375 → 180,300 · 이윤 11,250 75 · 부가세 18,030
assert _items(cut)["profit"]["current_krw"] == "11175"
assert (_summary(cut)["supply"]["current_krw"], _summary(cut)["vat"]["current_krw"]) == (
"180300",
"18030",
)
total = _sheet({"rounds": [{**first, "total_cut_krw": "1000"}]})
# 총공사비 198,412 → 412 ÷ 1.1 = 375 을 이윤에서 → 공급가액 180,000 · 부가세 18,000
assert _summary(total)["total"]["current_krw"] == "198000"
assert _items(total)["profit"]["current_krw"] == "10875"
def test_이윤금액_직접입력과_사정_칸() -> None:
entry = {
"quantities": {"1.1": "40", "1.2": "2"},
"profit_manual_krw": "10000",
"assessed": {"1.1": "130000"},
}
sheet = _sheet({"rounds": [entry]})
assert _items(sheet)["profit"]["current_krw"] == "10000"
assert _summary(sheet)["supply"]["current_krw"] == "179125"
row = {r["item_no"]: r for r in sheet["rows"]}["1.1"]
assert row["progress_assessed_krw"] == "130000" # 칸만 — 금액에 안 섞임
assert row["progress_current_amount_krw"] == "140000"
def test_준공은_마지막_회차_누계를_옮기기만() -> None:
progress = _sheet()
before = copy.deepcopy(progress)
done = completion_sheet(progress)
assert progress == before # 기성 한 장 불변
row = {r["item_no"]: r for r in done["rows"]}["1.1"]
assert (row["contract_amount_krw"], row["completion_amount_krw"]) == ("350000", "245000")
lines = {line["key"]: line for line in done["lines"]}
summary = {s["key"]: s for s in progress["summary"]}
assert lines["total"]["completion_krw"] == summary["total"]["cumulative_krw"]
assert lines["indirect_labor_cost"]["completion_krw"] == "12750" and done["from_round"] == 2
def test_기성_회차가_없으면_준공금액을_비운다() -> None:
done = completion_sheet(_sheet({"rounds": []}))
assert all(line["completion_krw"] is None for line in done["lines"])
assert done["notes"] and done["from_round"] == 0
def test_틀린_칸은_거른다() -> None:
_, errors = clean_settings(
{
"rounds": [
{"quantities": {"1.1": "-1"}, "vat_mode": "supply"},
{"quantities": {}, "vat_mode": "manual", "vat_manual_krw": ""},
{"quantities": {}, "vat_mode": "없는방식"},
]
}
)
assert len(errors) == 3