"""160-3 B08 자재수량 → 총괄 — 맞춤 없는 사본 자재 = 총괄 끝 줄 · 맞춤은 규격 · 단위까지 · 기계 주연료 · 할증(ℓ) 뺌 · 참조 줄(승격 · 사전조합) 안 재료 셈 · 고른 품목 = MT 코드 · 거울. """ from __future__ import annotations import asyncio import json from decimal import Decimal from B08_Quantity import B08_Quantity_Router_Material as router from B08_Quantity.B08_Quantity_Material_Calc import clean_overrides, compute, matched from M02_MasterTemplete import M02_Item_Copy as copies from M02_MasterTemplete import M02_Item_Prices as item_prices from M02_MasterTemplete import M02_Labor_Doc as labor from M02_MasterTemplete.M02_Labor_Ref import ref_id from resources.tester.test_121_b08_material_sheets import _run_ts REVET = "pipe:revet|돌쌓기(찰)|2.0" SUMMARY = { "columns": [{"id": REVET, "group": ["흙막이", "찰쌓기", "H=2.0"], "label": "m"}], "total": {REVET: 10}, "snapshot": { "materials": [ {"key": "MT1", "name": "레미콘", "spec": "25-18-80", "unit": "m3"}, {"key": "MT2", "name": "이형철근", "spec": "D13", "unit": "ton"}, # 단위 다름 {"key": "부직포|㎡", "name": "토목용부직포", "spec": "", "unit": "㎡"}, ], "units": {REVET: {"MT1": 0.31, "MT2": 0.002, "부직포|㎡": 2.871}}, }, } def test_맞춤은_이름_규격_단위(): assert matched("레미콘", "25-18-80", "㎥") == "remicon_25_18_80" assert matched("레미콘", "25-18-80", "M3") == "remicon_25_18_80" # m3 = ㎥ assert matched("이형철근", "D13", "ton") is None # kg ↔ ton 은 맞춤 아님 assert matched("호안블럭", "", "㎥") is None # 규격 빈 고정 열도 단위는 봄 def test_맞춤_없는_자재는_총괄_끝_줄_그대로(): sheets = compute(SUMMARY, clean_overrides({"surcharge": {"부직포|㎡": 10}})) assert sheets["quantity"]["unmatched"] == ["MT2", "부직포|㎡"] rows = sheets["material"]["rows"] tail = {row["key"]: row for row in rows[-2:]} steel, cloth = tail["MT2"], tail["부직포|㎡"] assert (steel["group"], steel["spec"], steel["unit"], steel["qty"]) == ( "이형철근", "D13", "ton", 0.02, ) assert steel["surcharge"] is None and steel["amount"] == 0.02 # 할증 빈칸 · 끝수 없음 assert cloth["qty"] == 28.71 and cloth["amount"] == 31.581 # 손 할증 10 % total = {row["key"]: row for row in rows} assert total["remicon_25_18_80"]["qty"] == 3.1 # 맞춤 = 고정 줄로 def test_거울_TS와_파이썬이_같다(): over = clean_overrides({"surcharge": {"부직포|㎡": 10}}) assert _run_ts(SUMMARY, over) == compute(SUMMARY, over) def test_알림에_맞춤_없는_이름(tmp_path, monkeypatch): async def summary(_project_id): return {**SUMMARY, "notes": []} monkeypatch.setattr(router, "structure_summary", summary) monkeypatch.setattr(router, "snapshot_units", lambda root, s: SUMMARY["snapshot"]) got = asyncio.run(router._sheets("p", tmp_path, clean_overrides({}))) assert got["notes"] == ["2공구 열 맞춤 없음 2종 — 이형철근 D13 ton · 토목용부직포 ㎡"] SC = {"층": "project", "주인": None, "키": "SC-000001"} SP = {"층": "project", "주인": None, "키": "SP-000001"} def test_참조_줄_재료_연료_뺌_고른_품목(tmp_path, monkeypatch): """사전조합(로직 L1 손 2 · 알약 배 10 + 승격) · 승격(L2 · 배 4) 안 재료 · 연료(ℓ) 뺌.""" fake = { "줄": [ {"수량": 2, "배": 1, "참조": SC}, {"수량": 3, "배": 1, "참조": SP}, { "수량": 4, "배": 2, "재료": [ {"이름": "주연료 할증", "단위": "ℓ", "수량": 5}, {"이름": "페인트", "단위": "ℓ", "수량": 1}, {"이름": "레미콘", "단위": "㎥", "수량": 0.5, "품목": {"키": "MT9"}}, ], }, ], "자재": [], } price = {"묶음": {"줄": [{"키": "MT9", "이름": "레디믹스트 레미콘", "규격": "25-18-80"}]}} frozen = { ref_id(SC): { "담은로직": [{"로직": "L1", "수량": {"손": 2}}, {"참조": SP}], "알약": [{"로직": ["L1"], "배": 10}], }, ref_id(SP): {"담은로직": [{"로직": "L2"}], "배": 4}, } mats = { "L1": [{"이름": "모래", "단위": "㎥", "수량": Decimal("5")}], "L2": [{"이름": "돌", "단위": "㎥", "수량": 8}, {"이름": "경유", "단위": "ℓ", "수량": 3}], } (tmp_path / "단가.json").write_text(json.dumps(price, ensure_ascii=False), "utf-8") (tmp_path / "일위대가.json").write_text( json.dumps({"참조사본": frozen}, ensure_ascii=False), "utf-8" ) monkeypatch.setattr(copies, "calc", lambda root, folder: fake) monkeypatch.setattr(copies, "folder_of", lambda root, folder: tmp_path) monkeypatch.setattr(item_prices, "load_prices", lambda bundle: "whole") monkeypatch.setattr(labor, "run_line", lambda whole, code, *rest: {"재료": mats[code]}) meta: dict = {} got = router._folder_units("r", "f", meta) # 사전조합 1 단위 = 모래 5 ÷ 10 × 2 + 돌 2 · 승격 1 단위 = 돌 8 ÷ 4 · 로직 줄 = × 4 ÷ 2 assert got == {"모래|㎥": 2.0, "돌|㎥": 4.0 + 6.0, "페인트|ℓ": 2.0, "MT9": 1.0} assert meta["MT9"] == { "key": "MT9", "name": "레디믹스트 레미콘", "spec": "25-18-80", "unit": "㎥", } assert matched("레디믹스트 레미콘", "25-18-80", "㎥") == "remicon_25_18_80"