"""M02 단가산출(SK) 문서 꼴 · 줄 정체 · 고친 줄(PLAN 715-1a · `M02_Sangeun_Doc`) — 엔진 안 부름.""" from decimal import Decimal import pytest from M02_MasterTemplete import M02_Sangeun_Doc as sd ROW = { "키": "GX000001", "결과단위": "원/㎡", "입력": [{"이름": "높이"}, {"이름": "돌", "고르기": ["깬잡석", "견치돌"]}], "호표": [ { "종류": "인력", "요소": "LB000033", "이름": "석공", "수량": "높이 * 0.1", "비목": "노무비", }, {"종류": "재료", "요소": {"검색어": "모래"}, "이름": "모래", "수량": "1", "비목": "재료비"}, {"종류": "인력", "요소": "LB000033", "이름": "석공", "수량": "0.2", "비목": "노무비"}, {"종류": "재료", "요소": {"검색어": "자갈"}, "이름": "자갈", "수량": "2", "비목": "재료비"}, { "종류": "로직", "요소": "로직(GC000276, 기계='5401-0017')", "이름": "굴착기", "수량": "0.5", }, ], "덧줄": [{"이름": "공구손료", "식": "(석공 * LB000033) * 3 / 100", "비목": "경비"}], } def _doc(**extra): return { "키": "SK-000001", "이름": "돌쌓기 내 값", "단위": "㎡", "바탕": {"로직": "GX000001"}, **extra, } def test_line_ids_kind_key_n(): assert sd.line_ids(ROW) == [ "labor:LB000033#0", "material:모래#0", "labor:LB000033#1", "material:자갈#0", "logic:GC000276#0", "pct:공구손료#0", ] def test_clean_shape_and_refusals(): got = sd.clean(_doc(입력값={"높이": 3, "모름": 1, "돌": ""}, 원값=1), ROW) assert got["입력값"] == {"높이": 3} and got["종류"] == "단가산출" and got["뺀줄"] == [] for bad, why in ( ({"키": "SC-000001"}, "SK-"), ({"이름": " "}, "이름"), ({"단위": ""}, "단위"), ({"바탕": {"참조": {"키": "SC-000001"}}}, "SK-"), ({"덮개": {"labor:LB000033#0": {"qty": "식"}}}, "수가 아님"), ({"덮개": {"labor:LB000033#0": {"qty": 1, "배율": 2}}}, "하나"), ({"덮개": {"labor:LB#x": {"qty": 1}}}, "kind:키#n"), ({"뺀줄": ["material:모래#0", "material:모래#0"]}, "두 번"), ({"열린입력": ["높이"], "입력값": {"높이": 3}}, "열린입력"), ({"더한줄": [{"종류": "인력", "요소": "LB000002"}]}, "수량"), ({"더한줄": [{"종류": "식", "수량": 1}]}, "종류"), ): with pytest.raises(sd.DocError, match=why): sd.clean(_doc(**bad), ROW) # 원값 · 바탕판 = 서버 몫 — 브라우저 값 버림 got = sd.clean(_doc(덮개={"labor:LB000033#0": {"qty": 0.5, "원값": 9}}), ROW) assert got["덮개"] == {"labor:LB000033#0": {"qty": 0.5}} def test_open_inputs_only_ratio(): with pytest.raises(sd.DocError, match="배율만"): sd.clean(_doc(열린입력=["돌"], 덮개={"labor:LB000033#0": {"qty": 1}}), ROW) got = sd.clean(_doc(열린입력=["돌"], 덮개={"labor:LB000033#0": {"배율": 1.1}}), ROW) assert got["덮개"]["labor:LB000033#0"] == {"배율": 1.1} def test_patched_cover_qty_price_ratio(): doc = sd.clean( _doc( 덮개={ "labor:LB000033#1": {"qty": 0.25, "labor": 200000}, "labor:LB000033#0": {"배율": 1.5}, "logic:GC000276#0": {"expense": 1000}, "pct:공구손료#0": {"qty": 5}, } ), ROW, ) got = sd.patched(ROW, doc) rows = got["줄"]["호표"] assert rows[0]["수량"] == "(높이 * 0.1) * 1.5" assert rows[2]["수량"] == "0.25" and rows[2]["단가"] == Decimal(200000) assert rows[4]["값묶음"] == {"경비": Decimal(1000)} assert got["줄"]["덧줄"][0]["식"] == "(석공 * LB000033) * 5 / 100" assert got["짝잃음"] == [] and ROW["호표"][2]["수량"] == "0.2" # 바탕은 안 바뀜 def test_drop_keeps_pick_on_its_line(): """뺀줄 뒤 고름 제자리 — 자갈(바탕 3째) 고름이 뺀줄 뒤 새 2째로 옮겨 감 · 모래 줄에 안 붙음.""" doc = sd.clean(_doc(뺀줄=["material:모래#0"], 고름={"material:자갈#0": "MT000010"}), ROW) got = sd.patched(ROW, doc) names = [x["이름"] for x in got["줄"]["호표"]] assert names == ["석공", "석공", "자갈", "굴착기"] assert got["고름"] == {"2": "MT000010"} assert got["정체"][:4] == [ "labor:LB000033#0", "labor:LB000033#1", "material:자갈#0", "logic:GC000276#0", ] def test_added_lines_before_extras(): doc = sd.clean( _doc( 더한줄=[ {"종류": "인력", "요소": "LB000002", "이름": "보통인부", "단위": "인", "수량": 0.1}, { "종류": "로직", "로직": "GC000001", "입력값": {"규격": "가", "두께": 2}, "수량": 1, }, {"참조": {"층": "system", "주인": None, "키": "SK-000002"}, "수량": 2}, ] ), ROW, ) got = sd.patched(ROW, doc) added = got["줄"]["호표"][5:] assert added[0]["비목"] == "노무비" and added[0]["수량"] == "0.1" assert added[1]["요소"] == "로직(GC000001, 규격='가', 두께=2)" assert added[2]["참조"]["키"] == "SK-000002" and added[2]["값묶음"] is None assert got["정체"][5:] == ["add:#0", "add:#1", "add:#2", "pct:공구손료#0"] def test_lost_covers_kept_and_listed(): """짝 잃은 덮개 = 문서에 남김 + 목록(지우지 않음) · 비목 다른 단가 · 재료 아닌 고름.""" doc = sd.clean( _doc( 덮개={"labor:LB999999#0": {"qty": 1}, "labor:LB000033#0": {"material": 5}}, 고름={"labor:LB000033#1": "MT000010"}, 뺀줄=["material:없음#0"], ), ROW, ) got = sd.patched(ROW, doc) why = {x["cid"]: x["까닭"] for x in got["짝잃음"]} assert why["labor:LB999999#0"].startswith("덮개") and "비목 다름" in why["labor:LB000033#0"] assert why["labor:LB000033#1"].startswith("고름") and why["material:없음#0"].startswith("뺀줄") assert "labor:LB999999#0" in doc["덮개"] def test_chain_moves_base_picks(): """바탕 SK 사슬 — 바탕 쪽 차례 고름이 아래 SK 뺀줄 뒤에도 그 줄에.""" first = sd.patched(ROW, sd.clean(_doc(고름={"material:자갈#0": "MT000010"}), ROW)) child = sd.clean({**_doc(뺀줄=["labor:LB000033#0"]), "바탕": None}, first["줄"]) got = sd.patched(first["줄"], child, first["고름"]) assert got["고름"] == {"2": "MT000010"} and got["줄"]["호표"][2]["이름"] == "자갈" def test_empty_base(): row = sd.empty_row({"키": "SK-000003", "이름": "자체", "단위": "식"}) doc = sd.clean( {"키": "SK-000003", "이름": "자체", "단위": "식", "바탕": None, "더한줄": [{"종류": "재료", "요소": "MT000010", "수량": 3, "단가": 1200}]}, row, ) # fmt: skip got = sd.patched(row, doc) assert got["줄"]["호표"][0]["단가"] == Decimal(1200) and got["줄"]["결과단위"] == "원/식"