"""연결고리 표 소비 — 산림·건설 어느 품을 쓸지 **표가 정함** (2026-09-17 브레인 · 코덱스 크로스체크 ②). 고시 총칙 7 「유사 공종은 본 품셈 우선」 이 규칙으로만 있고 아무도 안 읽던 자리. 못박는 것: ① 둘 다 있는 공종은 표의 `both_precedence` 쪽 — 표를 바꾸면 고르는 쪽도 바뀜(코드에 안 박힘) ② 미판정은 안 고르고 막음 · 사유 그대로 · 금액에 안 듦 ③ 지금 인계 코드는 한 줄도 판정이 안 달라짐 = **금액 불변**(이행 전 금액은 `test_work_item_key_gate` 가 못박음) """ from __future__ import annotations import json import re from pathlib import Path import pytest from B09_Estimation.B09_Estimation_BillOfQuantities import bill_summary, build_bill from B09_Estimation.B09_Estimation_BillOfQuantities_Input import ( BLOCKED_LINK_UNDECIDED, parse_handoff, ) from B09_Estimation.B09_Estimation_UnitPrice import build_unit_prices from common_util import common_util_work_item_link as link from common_util.common_util_work_item_key import MASTER_DIR, work_item_code_of, work_item_key MAPPING = MASTER_DIR.parent / "data_work_item_mapping" / "work_item_mapping_2026-01-01.json" def _table( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, links: list, precedence="forest" ) -> None: folder = tmp_path / "data_work_item_link" folder.mkdir(parents=True) doc = { "policy": {"both_precedence": precedence, "unconfirmed_action": "do_not_link"}, "links": links, } (folder / "work_item_link_2026-01-01.json").write_text( json.dumps(doc, ensure_ascii=False), encoding="utf-8" ) monkeypatch.setattr(link, "FOLDER", folder) def test_지금_인계_코드는_판정이_안_달라짐_금액_불변() -> None: codes = set( re.findall(r'"((?:FP|AX-WK|AX-ST)-[0-9A-Za-z-]+)"', MAPPING.read_text(encoding="utf-8")) ) changed = [] for code in sorted(codes): key = work_item_key(code, "2026-01-01") choice = link.choose(key) if choice.key != key or choice.blocked: changed.append((code, key, choice)) assert changed == [] # 달라지면 그동안 잘못 고르고 있었다는 뜻 — 브레인에 먼저 알릴 것 def test_둘_다_있으면_표가_적은_쪽() -> None: """실제 표 — 산림 9-7-1 무근콘크리트 깨기 ↔ 건설 8-2-13 대형브레이커(E 0.35 ↔ 0.45).""" assert link.choose("CW-00330").key == "FW-00260" assert link.choose("FW-00260").key == "FW-00260" assert link.choose("CW-00844").key == "CW-00844" # 건설에만(모르타르 배합) — 그대로 assert link.choose("FW-00249").key == "FW-00249" # 표에 없음 — 제 품셈 def test_우선_품셈은_코드가_아니라_표가_정함( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: rows = [{"forest_key": "FW-00260", "const_key": "CW-00330", "branch": "both", "status": "확정"}] _table(tmp_path, monkeypatch, rows, precedence="construction") assert link.choose("FW-00260").key == "CW-00330" _table(tmp_path / "b", monkeypatch, rows, precedence="") assert link.choose("FW-00260").key is None and link.choose("FW-00260").blocked def test_미판정은_안_고르고_막음_금액에_안_듦( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: payload = { "work_items": [ {"work_item_code": "FP-09-03-02", "name": "토사깍기", "unit": "㎥", "quantity": 100}, {"work_item_code": "FP-09-12-01", "name": "측구터파기", "unit": "㎥", "quantity": 7}, ], "materials": [], } build = build_unit_prices() before = build_bill(json.loads(json.dumps(payload)), build=build) ditch = work_item_key("FP-09-12-01", "2026-01-01") rows = [ { "forest_key": ditch, "const_key": "", "branch": "both", "status": "미판정", "evidence": "시험 — 측구 터파기 건설 대응 미확인", } ] _table(tmp_path, monkeypatch, rows) items, _ = parse_handoff(json.loads(json.dumps(payload))) assert ( items[1].blocked_kind == BLOCKED_LINK_UNDECIDED and "시험 — 측구" in items[1].blocked_reason ) after = build_bill(json.loads(json.dumps(payload)), build=build) blocked = [m for m in after.missing if m.get("blocked_kind") == BLOCKED_LINK_UNDECIDED] assert [m["name"] for m in blocked] == ["측구터파기"] ditch_amount = sum( r.amount_krw or 0 for r in before.rows if not r.is_group and r.name == "측구터파기" ) assert ditch_amount > 0 and after.body_total_krw == before.body_total_krw - ditch_amount assert bill_summary(after)["link_undecided"][0]["evidence"].startswith("시험") def test_건설_열쇠로_온_줄은_표가_고른_산림_열쇠와_목차_코드로() -> None: const_code = work_item_code_of("CW-00330") assert const_code and const_code.startswith("CP-") items, _ = parse_handoff( { "work_items": [ { "work_item_code": const_code, "name": "대형브레이커", "unit": "㎥", "quantity": 1, "pum_edition": "2026-01-01", } ], "materials": [], } ) assert (items[0].work_item_key, items[0].work_item_code) == ( "FW-00260", work_item_code_of("FW-00260"), ) assert items[0].work_item_code.startswith("FP-") def test_미판정_행은_열쇠가_비어도_목록으로_드러남() -> None: undecided = link.undecided_links() assert undecided and all(row["evidence"] for row in undecided)