Files
Aislo/resources/tester/test_b09_contract.py
T
eomsangdonandClaude Opus 5 ff6672a13a feat(b09): 계약 단계 2벌 — 기초단가 적용(단가표 복사본 다시 조립) · 계약 일위대가/산출근거 W 호표 · 0% = 0 원
- 기초단가 적용: 자재·노임·중기 취득가 채택 단가에 적용률 — 복사본만, 설계 단가표 불변
- 조립값과 다른 줄(할증·수동 단가)·단가표 밖 코드는 성분 곱셈 + 비고에 까닭
- 계약 호표: 설계 본표와 같은 꼴(detail_of)로 W-B·W-D 따로 — 기초단가 적용과 함께만
- 0% 는 0 원(브레인 판정) · 공내역 생성은 그 줄 이름만 가름
- 시험 11건 · 전체 1699 통과(골든셋 포함)

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

212 lines
8.4 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). 여기서 재는 것:
① 적용률 100% 면 설계와 같음 · 설계 줄(입력)은 안 바뀜
② 노·재·경 따로 곱해짐 · 절사는 설계 내역 규칙(성분 단가 원 미만 · 줄 성분마다)
③ 적용 제외 줄은 설계 단가 · W 코드 안 붙음
④ 0% = 0 원(브레인 판정) · 「공내역 생성」 켜면 그 줄에 공내역 이름
⑤ 동일코드 개별생성 켜면 줄마다 다른 W 코드 · 끄면 같은 코드 한 벌
⑥ 묶음 줄 합 · 틀린 적용률 거름
⑦ 2벌 — 기초단가 적용은 단가표 **복사본**만(설계 단가표 불변) · 못 푸는 줄은 까닭 ·
계약 호표(W-B·W-D)는 기초단가 적용과 함께만 섬
"""
from __future__ import annotations
import copy
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_Contract import clean_settings, contract_bill # noqa: E402
from B09_Estimation.B09_Estimation_PriceBook import ( # noqa: E402
PriceBook,
PriceDetail,
PriceKind,
PriceTitle,
)
from B09_Estimation.B09_Estimation_UnitPrice import UnitPriceBuild # noqa: E402
ROWS = [
{"item_no": "1", "is_group": True, "in_bill": True},
{
"item_no": "1.1",
"is_group": False,
"in_bill": True,
"quantity": "12.5",
"price_code": "B-FP-09-03-02",
"unit_material_krw": "1001",
"unit_labor_krw": "2003",
"unit_expense_krw": "3005",
},
{
"item_no": "1.2",
"is_group": False,
"in_bill": True,
"quantity": "3",
"price_code": "B-FP-09-03-02",
"unit_material_krw": "1001",
"unit_labor_krw": "2003",
"unit_expense_krw": "3005",
},
{
"item_no": "1.3",
"is_group": False,
"in_bill": True,
"quantity": "2",
"price_code": "M-관급자재",
"unit_material_krw": "50000",
"unit_labor_krw": "0",
"unit_expense_krw": "0",
},
]
def _rows(settings: dict) -> dict[str, dict]:
return {row["item_no"]: row for row in contract_bill(ROWS, settings)["rows"]}
def test_100퍼센트면_설계와_같고_입력은_안_바뀐다() -> None:
before = copy.deepcopy(ROWS)
result = contract_bill(ROWS, clean_settings({})[0])
assert ROWS == before
assert result["totals"]["design"] == result["totals"]["contract"]
def test_노재경을_따로_곱하고_성분_단가를_원_미만_절사() -> None:
row = _rows({"labor_pct": "80", "material_pct": "90", "expense_pct": "70"})["1.1"]
assert (
row["contract_unit_material_krw"],
row["contract_unit_labor_krw"],
row["contract_unit_expense_krw"],
) == ("900", "1602", "2103") # 900.9→900 · 1602.4→1602 · 2103.5→2103
assert row["contract_material_krw"] == "11250" # 12.5 × 900
assert row["contract_code"] == "W-B-FP-09-03-02"
def test_적용_제외_줄은_설계_단가_W코드_없음() -> None:
row = _rows({"material_pct": "50", "excluded": ["1.3"]})["1.3"]
assert row["contract_unit_material_krw"] == "50000" and row["contract_excluded"] is True
assert not row["contract_code"].startswith("W-")
def test_0퍼센트_공내역_옵션() -> None:
zero = {"labor_pct": "0", "material_pct": "0", "expense_pct": "0"}
empty = _rows({**zero, "zero_makes_empty": True})["1.1"]
assert empty["contract_amount_krw"] == "0" and "공내역" in empty["contract_note"]
plain = _rows(zero)["1.1"]
assert plain["contract_amount_krw"] == "0" and plain["contract_note"] == "" # 그냥 0 원 줄
def test_동일코드_개별생성() -> None:
shared = _rows({"material_pct": "90"})
assert shared["1.1"]["contract_code"] == shared["1.2"]["contract_code"]
separate = _rows({"material_pct": "90", "separate_same_code": True})
assert separate["1.1"]["contract_code"] != separate["1.2"]["contract_code"]
def test_묶음_줄_합과_합계_비율() -> None:
result = contract_bill(ROWS, {"material_pct": "90", "labor_pct": "90", "expense_pct": "90"})
group = result["rows"][0]
children = [r for r in result["rows"] if not r.get("is_group")]
assert Decimal(group["contract_amount_krw"]) == sum(
Decimal(r["contract_amount_krw"]) for r in children
)
assert result["totals"]["ratio_pct"]["labor"] is not None
def test_틀린_적용률은_거른다() -> None:
cleaned, errors = clean_settings({"labor_pct": "-3", "material_pct": "abc", "expense_pct": ""})
assert len(errors) == 2 and cleaned["expense_pct"] == "100"
# ── 2벌 — 「기초단가에 적용」 · 「적용율 적용된 일위대가/산출근거 생성」 ──────────────
def _build():
"""자재 1,001 · 노임 2,003 · 산출근거 D-T(노임 0.5) · 일위대가 B-T(자재 2 + D-T 1)."""
book = PriceBook()
for code, kind, price in (
("M-A", PriceKind.MATERIAL, "1001"),
("L-B", PriceKind.LABOR, "2003"),
):
slots = [None] * 6
slots[5] = Decimal(price)
book.add_title(PriceTitle(code=code, kind=kind, name=code, slots=slots))
book.add_title(PriceTitle(code="D-T", kind=PriceKind.PRICE_BASIS, name="산출근거"))
book.add_title(PriceTitle(code="B-T", kind=PriceKind.UNIT_PRICE, name="일위대가"))
book.add_detail(PriceDetail("D-T", "L-B", Decimal("0.5")))
book.add_detail(PriceDetail("B-T", "M-A", Decimal(2)))
book.add_detail(PriceDetail("B-T", "D-T", Decimal(1)))
return UnitPriceBuild(book=book)
def _line(item_no: str, code: str, material: str, labor: str) -> dict:
return {
"item_no": item_no,
"is_group": False,
"in_bill": True,
"quantity": "3",
"price_code": code,
"unit_material_krw": material,
"unit_labor_krw": labor,
"unit_expense_krw": "0",
}
BASE_ROWS = [
_line("2.1", "B-T", "2002", "1001"), # 설계 단가 = 단가표 조립값
_line("2.2", "B-T", "2500", "1001"), # 수동·할증으로 조립값과 다름
_line("2.3", "Z-없음", "100", "0"), # 단가표 밖
]
BASE_ON = {
"material_pct": "90",
"labor_pct": "80",
"apply_to_base_prices": True,
"generate_unit_prices": True,
}
def test_기초단가_적용은_복사본만_고치고_다시_조립() -> None:
build = _build()
before = (build.book.resolve("B-T"), copy.deepcopy(build.book.titles["M-A"].slots))
result = contract_bill(BASE_ROWS, BASE_ON, build=build)
assert (build.book.resolve("B-T"), build.book.titles["M-A"].slots) == before # 설계 단가표 불변
rows = {row["item_no"]: row for row in result["rows"]}
# 자재 900.9 × 2 = 1801.8 → 1801 · 노임 1602.4 × 0.5 = 801.2 → D 801 → B 801
# (성분에 곱했으면 노무 1001 × 0.8 = 800.8 → 800 — 층을 다시 조립해야 801)
assert (rows["2.1"]["contract_unit_material_krw"], rows["2.1"]["contract_unit_labor_krw"]) == (
"1801",
"801",
)
assert rows["2.1"]["contract_note"].startswith("기초단가 적용")
def test_기초단가로_못_푸는_줄은_성분에_곱하고_까닭을_적는다() -> None:
rows = {r["item_no"]: r for r in contract_bill(BASE_ROWS, BASE_ON, build=_build())["rows"]}
assert (
rows["2.2"]["contract_unit_material_krw"] == "2250"
and "조립값과 다름" in (rows["2.2"]["contract_note"])
)
assert "단가표 밖" in rows["2.3"]["contract_note"]
def test_계약_호표는_W코드로_따로_서고_산출근거도_따라_선다() -> None:
sheets = contract_bill(BASE_ROWS, BASE_ON, build=_build())["unit_price_sheets"]
by_code = {sheet["code"]: sheet for sheet in sheets}
assert set(by_code) == {"W-B-T", "W-D-T"}
assert (by_code["W-B-T"]["design_code"], by_code["W-B-T"]["total"]) == ("B-T", "2602")
assert by_code["W-D-T"]["labor"] == "801"
def test_호표_생성은_기초단가_적용이_함께_켜져야_선다() -> None:
only_sheets = {**BASE_ON, "apply_to_base_prices": False}
result = contract_bill(BASE_ROWS, only_sheets, build=_build())
assert result["unit_price_sheets"] == []
assert "generate_unit_prices" in result["options_not_used"]
off = contract_bill(BASE_ROWS, {**BASE_ON, "generate_unit_prices": False}, build=_build())
assert off["unit_price_sheets"] == []