Files
Aislo/resources/tester/test_b09_progress.py
T
eomsangdonandClaude Opus 5 1edde503fe feat(b09): 기성 단계 1벌 — 금회직접공사비 × 계약잡비율 · 계약/전회/금회/누계와 각 율 · 부가세 넷
- 계약내역·계약 원가계산서(같은 엔진에 계약 직접비)를 읽기만 해 파생 — 계약 불변
- 제잡비 줄마다 계약잡비율 = 도급액 ÷ 계약 직접공사비 (구조로 읽음, 확인 대기)
- 회차 목록 저장 · 기성(%) · 잔량 · 계약 수량 넘으면 알림
- 부가세: 직접입력 / 공급가액 / 재료비 / 재료비+산출경비 (설계 vat_base 같이 씀)
- 원가계산서 조립·계약본 받기 조각을 떼어 기성이 같은 길로 받음
- 기성 탭 파일 · 사전 키(등록은 서브) · 시험 6건 · 전체 1717 통과

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

159 lines
6.3 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 clean_settings, progress_sheet # noqa: E402
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 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