"""프로젝트 요율 덮어쓰기 — 프로젝트 단위 · 사유 저장 · 기본값↔고친 값 (PLAN 6장 새 절). ⚠ 겨누는 것 ① 마스터 요율판은 **안 바뀜**(덮어쓰기는 복사본에만) ② 사유 없는 줄·틀린 주소·같은 칸 두 번은 거름 ③ 덮어쓴 요율이 계산에 실제로 걸림 · 안 쓴 프로젝트는 값이 그대로 ④ 요율표 칸이 기본값·지금 값·주소를 함께 싣고, 그 주소를 그대로 돌려주면 저장됨 """ from __future__ import annotations import sys from dataclasses import replace from decimal import Decimal from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) from B09_Estimation.B09_Estimation_Engine_Cost import CostInput, calculate_cost # noqa: E402 from B09_Estimation.B09_Estimation_RateOverride import ( # noqa: E402 apply_overrides, clean_overrides, ) from B09_Estimation.B09_Estimation_Rates import RateLookupError, load_rate_dataset # noqa: E402 from B09_Estimation.B09_Estimation_RateTable import rate_sections # noqa: E402 BASE = CostInput( direct_material_krw=Decimal(123_456_789), direct_labor_krw=Decimal(234_567_891), direct_expense_krw=Decimal(45_678_912), estimated_price_krw=Decimal(300_000_000), # 50억 미만 구간 ) OVERHEAD = { "variable": "rate_overhead", "table": "civil_landscape_industrial", "match": {"estimated_price_bracket": "lt_5_billion"}, "rate_percent": "7.5", "reason": "발주처 지침(시험)", } def test_마스터는_안_바뀐다() -> None: master = load_rate_dataset() before = master.variables["rate_overhead"]["civil_landscape_industrial"][0]["rate_percent"] changed = apply_overrides(master, [OVERHEAD]) assert ( changed.variables["rate_overhead"]["civil_landscape_industrial"][0]["rate_percent"] == 7.5 ) assert ( master.variables["rate_overhead"]["civil_landscape_industrial"][0]["rate_percent"] == before ) assert ( load_rate_dataset().variables["rate_overhead"]["civil_landscape_industrial"][0][ "rate_percent" ] == before ) def test_덮어쓴_요율이_계산에_걸리고_안_쓰면_그대로() -> None: plain = calculate_cost(BASE) again = calculate_cost(replace(BASE, rate_overrides=())) assert plain.totals == again.totals changed = calculate_cost(replace(BASE, rate_overrides=(OVERHEAD,))) assert changed.line("general_overhead").rate_percent == Decimal("7.5") assert changed.amount("general_overhead") < plain.amount("general_overhead") assert any("요율 덮어쓰기 1건" in note and "발주처 지침" in note for note in changed.notes) def test_한_값짜리_요율도_덮어쓴다() -> None: sanjae = { "variable": "rate_sanjae", "table": None, "match": {}, "rate_percent": "3.7", "reason": "고시 개정", } result = calculate_cost(replace(BASE, rate_overrides=(sanjae,))) assert result.line("industrial_accident_insurance").rate_percent == Decimal("3.7") def test_사유_주소_중복을_거른다() -> None: master = load_rate_dataset() kept, errors = clean_overrides( [ {**OVERHEAD, "reason": " "}, {**OVERHEAD, "match": {"estimated_price_bracket": "no_such"}}, {**OVERHEAD, "rate_percent": "-1"}, OVERHEAD, OVERHEAD, ], master, ) assert len(kept) == 1 and kept[0]["rate_percent"] == "7.5" assert len(errors) == 4 assert any("사유" in e for e in errors) and any("두 번" in e for e in errors) def test_주소가_안_맞으면_계산이_멈춘다() -> None: stale = {**OVERHEAD, "match": {"estimated_price_bracket": "gone"}} with pytest.raises(RateLookupError): calculate_cost(replace(BASE, rate_overrides=(stale,))) def test_요율표_칸이_기본값과_주소를_싣고_그대로_돌려주면_저장된다() -> None: master = load_rate_dataset() current = apply_overrides(master, [OVERHEAD]) sections = rate_sections(current, master) cells = [cell for s in sections for row in s["rows"] for cell in row if isinstance(cell, dict)] assert cells and all({"value", "default", "overridden", "address"} <= set(c) for c in cells) changed = [c for c in cells if c["overridden"]] assert len(changed) == 1 and (changed[0]["value"], changed[0]["default"]) == (7.5, 8.0) # 화면이 받은 주소 그대로 새 값·사유를 붙여 돌려주면 모두 저장 가능해야 함. raw = [{**c["address"], "rate_percent": "1", "reason": "왕복"} for c in cells] kept, errors = clean_overrides(raw, master) assert not errors and len(kept) == len(cells)