diff --git a/B08_Quantity/B08_Quantity_Haul.py b/B08_Quantity/B08_Quantity_Haul.py index 5a628cb43..baf49493b 100644 --- a/B08_Quantity/B08_Quantity_Haul.py +++ b/B08_Quantity/B08_Quantity_Haul.py @@ -131,16 +131,18 @@ def with_haul_routes(data: dict[str, Any], groups: list[dict[str, Any]]) -> dict def route_bill_key(route: dict[str, Any], index: int = 0) -> str: """거리이정표 행 → B09 운반공 줄 키(253-1) — 자동 상자 = 옛 키 · 사용자 상자 = `haul:r-{id}` · - 한 상자 둘째 행부터 `haul:r-{id}.{n}`(경로 id 에 「.」 없음 — 안 겹침).""" + 한 상자 둘째 행부터 `.{n}`(261-4 자동 상자도 · 경로 id · 묶음 키에 「.」 없음 — 안 겹침).""" + tail = f".{index}" if index else "" if route.get("haul"): - return bill_key(str(route["haul"])) - return f"haul:r-{route.get('id')}" + (f".{index}" if index else "") + return bill_key(str(route["haul"])) + tail + return f"haul:r-{route.get('id')}" + tail def haul_rows(data: dict[str, Any], table: list[dict[str, Any]]) -> list[dict[str, Any]]: - """거리이정표 표 행 하나 = 운반공 줄 하나(253-1) — [{key, name, ref, user, L1, L2}] · + """거리이정표 표 행 하나 = 운반공 줄 하나(253-1 · 261-4 단가연결 무관) — + [{key, name, ref, user, haul, L1, L2}] · name = 행 항목명 그대로(없으면 거래처 · 그것도 없으면 빈칸) · L1 = 포장 계(2차 + 1차 + 포장) · L2 = 비포장(`compute_milestone` 결과) · - user = 사용자 상자(자동 상자는 총괄자재 묶음 줄이 이미 있음 · 둘째 행은 안 씀).""" + user = 사용자 상자 · haul = 자동 상자 묶음 키(첫 행은 총괄자재 묶음 줄 이름 · 거리).""" routes = {r.get("id"): r for r in data.get("routes") or []} out, seen = [], set() for row in table: @@ -149,13 +151,13 @@ def haul_rows(data: dict[str, Any], table: list[dict[str, Any]]) -> list[dict[st if key in seen: continue seen.add(key) - item = str(row.get("item") or "").strip() out.append( { "key": key, - "name": f"{item}운반" if item else "운반", + "name": str(row.get("item") or "").strip() or str(route.get("name") or "").strip(), "ref": route.get("ref"), "user": not route.get("haul"), + "haul": route.get("haul"), "L1": row.get("paved"), "L2": row.get("unpaved"), } diff --git a/B09_Estimation/B09_Estimation_Bill_Items.py b/B09_Estimation/B09_Estimation_Bill_Items.py index 72ad89487..9282a4e92 100644 --- a/B09_Estimation/B09_Estimation_Bill_Items.py +++ b/B09_Estimation/B09_Estimation_Bill_Items.py @@ -111,48 +111,45 @@ def haul_items( routes: list[dict[str, Any]] | None = None, ) -> list[dict[str, Any]]: """운반공 줄 — B08 운반 묶음(251-1 · 장비 둘 + 총괄자재 묶음 · 옛 키 haul:1~6 그대로) + - 거리이정표 사용자 상자 행(253-1 · `B08_Quantity_Haul.haul_rows` · 수량 빈칸) · + 거리이정표 행(253-1 · `B08_Quantity_Haul.haul_rows` · 행 하나 = 줄 하나 · 둘째 행부터 수량 빈칸) · + 이름 = 거리이정표 항목명 그대로(261-4 · 「운반」 안 붙임 · 비면 거래처 · 그것도 없으면 묶음 이름 · 「운반」) · 연결 = 거리이정표 단가연결(251-5 · `refs{줄 키: ref}` · 설계내역서 연결 덮개가 이김) · `dist{L1, L2}` = 그 행 거리(253-3 「운반 단가」 모달 · 없으면 None).""" - dists = {r["key"]: {"L1": r.get("L1"), "L2": r.get("L2")} for r in routes or []} + rows = routes or [] + by_key = {r["key"]: r for r in rows} + extra: dict[str, list[dict[str, Any]]] = {} # 자동 상자 둘째 행부터 — 묶음 줄 바로 뒤 + for r in rows: + if r.get("haul") and r["key"] != bill_key(str(r["haul"])): + extra.setdefault(str(r["haul"]), []).append(r) + + def line(key: str, name: str, unit: str, qty: Any, note: str) -> dict[str, Any]: + ref = (refs or {}).get(key) + route = by_key.get(key) or {} + return { + "key": key, + "section": "3", + "name": name, + "spec": "", + "unit": unit, + "qty": qty, + "note": note, + "ref": ref, + "dist": {"L1": route["L1"], "L2": route["L2"]} if route else None, + "why": WHY_LOGIC if (ref or {}).get("tab") == "logic" else WHY_HAUL, + } + out = [] for g in haul_groups(materials): - qty = g["qty"] - ref = (refs or {}).get(bill_key(g["key"])) - logic = (ref or {}).get("tab") == "logic" + key = bill_key(g["key"]) + name = (by_key.get(key) or {}).get("name") or g["name"] out.append( - { - "key": bill_key(g["key"]), - "section": "3", - "name": f"{g['name']}운반", - "spec": "", - "unit": g["unit"], - "qty": qty, - "note": "B08 총괄자재" if qty is not None else "", - "ref": ref, - "dist": dists.get(bill_key(g["key"])), - "why": WHY_LOGIC if logic else WHY_HAUL, - } - ) - for r in routes or []: - if not r.get("user"): - continue - ref = (refs or {}).get(r["key"]) - logic = (ref or {}).get("tab") == "logic" - out.append( - { - "key": r["key"], - "section": "3", - "name": r["name"], - "spec": "", - "unit": "", - "qty": None, - "note": "B08 거리이정표", - "ref": ref, - "dist": dists.get(r["key"]), - "why": WHY_LOGIC if logic else WHY_HAUL, - } + line(key, name, g["unit"], g["qty"], "B08 총괄자재" if g["qty"] is not None else "") ) + for r in extra.get(g["key"], []): + out.append(line(r["key"], r.get("name") or g["name"], "", None, "B08 거리이정표")) + for r in rows: + if r.get("user"): + out.append(line(r["key"], r.get("name") or "운반", "", None, "B08 거리이정표")) return out diff --git a/resources/tester/test_145_3_b09_bill.py b/resources/tester/test_145_3_b09_bill.py index ec6ef4aad..1113830dd 100644 --- a/resources/tester/test_145_3_b09_bill.py +++ b/resources/tester/test_145_3_b09_bill.py @@ -190,8 +190,8 @@ def test_항목_짜기_B08_수량(): "key": "pipe:revet|돌쌓기(찰)|2.0", } haul = {v["name"]: v["qty"] for k, v in items.items() if k.startswith("haul:")} - assert haul == {"중기운반": None, "기계운반": None, "모래운반": 16, "자갈운반": None, - "시멘트운반": None, "철근운반": 0.75} # fmt: skip + assert haul == {"중기": None, "기계": None, "모래": 16, "자갈": None, + "시멘트": None, "철근": 0.75} # fmt: skip 261-4 묶음 이름 그대로 def test_거울_TS_가_같은_값(): diff --git a/resources/tester/test_251_1_haul_groups.py b/resources/tester/test_251_1_haul_groups.py index 3251d7a59..e7fc2efd1 100644 --- a/resources/tester/test_251_1_haul_groups.py +++ b/resources/tester/test_251_1_haul_groups.py @@ -44,8 +44,8 @@ def test_묶음_장비_옛_여섯_늘_수량_없는_새_묶음_없음(): def test_B09_운반_줄_옛_키_그대로_새_묶음_키(): items = {i["key"]: i for i in haul_items(_mats(ROWS))} assert [bill_key(k) for k in ("equip_heavy", "sand", "rebar")] == ["haul:1", "haul:3", "haul:6"] - assert items["haul:3"]["name"] == "모래운반" and items["haul:3"]["qty"] == 16 - assert items["haul:remicon"]["name"] == "레미콘운반" and items["haul:remicon"]["unit"] == "㎥" + assert items["haul:3"]["name"] == "모래" and items["haul:3"]["qty"] == 16 + assert items["haul:remicon"]["name"] == "레미콘" and items["haul:remicon"]["unit"] == "㎥" assert all(i["section"] == "3" for i in items.values()) assert {"haul:1", "haul:2", "haul:3", "haul:4", "haul:5", "haul:6"} <= set(items) diff --git a/resources/tester/test_253_1_haul_rows.py b/resources/tester/test_253_1_haul_rows.py index 514dcf8b4..574e91208 100644 --- a/resources/tester/test_253_1_haul_rows.py +++ b/resources/tester/test_253_1_haul_rows.py @@ -37,7 +37,7 @@ def test_rows_one_per_table_row(): assert list(got) == ["haul:3", "haul:r-u1", "haul:r-u1.1"] assert (got["haul:3"]["L1"], got["haul:3"]["L2"], got["haul:3"]["user"]) == (25.1, 0, False) assert (got["haul:r-u1"]["name"], got["haul:r-u1"]["L1"], got["haul:r-u1"]["L2"]) == ( - "혼합골재운반", 10, 0.8) # fmt: skip + "혼합골재", 10, 0.8) # fmt: skip 261-4 항목명 그대로 assert got["haul:r-u1.1"]["L2"] == 2.8 # 중점 = 시점 → 중점 2 km 비포장 더함 assert got["haul:r-u1"]["ref"] == LOGIC @@ -47,14 +47,14 @@ def test_bill_items_user_rows(): extra = haul_user(rows) items = {i["key"]: i for i in haul_items(None, extra["haul_refs"], rows)} assert ( - "haul:3" in items and items["haul:3"]["name"] == "모래운반" - ) # 자동 = 총괄자재 묶음 그대로 + "haul:3" in items and items["haul:3"]["name"] == "모래" + ) # 자동 = 상자 항목명(261-4) user = items["haul:r-u1"] assert (user["section"], user["name"], user["qty"], user["ref"]) == ( "3", - "혼합골재운반", + "혼합골재", None, LOGIC, ) - assert items["haul:r-u1.1"]["name"] == "깬돌운반" and items["haul:r-u1.1"]["ref"] == LOGIC + assert items["haul:r-u1.1"]["name"] == "깬돌" and items["haul:r-u1.1"]["ref"] == LOGIC assert extra["haul_dist"]["haul:r-u1"] == {"L1": 10, "L2": 0.8} diff --git a/resources/tester/test_261_4_haul_names.py b/resources/tester/test_261_4_haul_names.py new file mode 100644 index 000000000..57163c791 --- /dev/null +++ b/resources/tester/test_261_4_haul_names.py @@ -0,0 +1,65 @@ +"""261-4 운반공 줄 이름 = 거리이정표 항목명 그대로(「운반」 안 붙임) · 행마다 개별 줄 · 단가연결 무관.""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + +from B08_Quantity.B08_Quantity_Haul import haul_rows # noqa: E402 +from B08_Quantity.B08_Quantity_Milestone_Calc import compute_milestone # noqa: E402 +from B09_Estimation.B09_Estimation_Bill_Items import haul_items # noqa: E402 +from B09_Estimation.B09_Estimation_Router import haul_user # noqa: E402 + +LOGIC = {"tab": "logic", "code": "GF000134", "name": "골재운반", "given": {}} +DATA = { + "site": {"mid_station_m": 2000, "mid_road": "비포장"}, + "base": {"enabled": False, "legs": []}, + "routes": [ + # 자동 상자 — 항목명 고침 · 둘째 행(같은 이름) + {"id": "h-sand", "name": "골재상", "haul": "sand", "via_base": False, + "legs": [{"road": "포장", "km": 25.1}], + "rows": [{"item": "강모래", "dest": "start"}, {"item": "강모래", "dest": "mid"}]}, + # 자동 상자 — 항목명 빈칸 · 거래처 없음 = 묶음 이름 + {"id": "h-gravel", "name": "", "haul": "gravel", "via_base": False, + "legs": [], "rows": [{"item": "", "dest": "start"}]}, + # 사용자 상자 — 「운반」 이 든 항목명 · 같은 이름 두 행 · 빈 항목명(거래처) · 연결 없음 + {"id": "u1", "name": "석산", "via_base": False, "legs": [{"road": "포장", "km": 10}], + "rows": [{"item": "전석운반", "dest": "start"}, {"item": "전석운반", "dest": "start"}, + {"item": " ", "dest": "start"}]}, + # 사용자 상자 — 항목명 · 거래처 모두 빈칸 · 연결 있음 + {"id": "u2", "name": "", "ref": LOGIC, "via_base": False, "legs": [], + "rows": [{"item": "", "dest": "start"}]}, + ], +} # fmt: skip + + +def _items() -> dict[str, dict]: + rows = haul_rows(DATA, compute_milestone(DATA, None)) + return {i["key"]: i for i in haul_items(None, haul_user(rows)["haul_refs"], rows)} + + +def test_자동_상자_이름_항목명(): + items = _items() + assert items["haul:3"]["name"] == "강모래" + assert items["haul:4"]["name"] == "자갈" # 항목 · 거래처 빈칸 = 묶음 이름 + assert items["haul:1"]["name"] == "중기" # 상자 없음 = 묶음 이름 + + +def test_자동_상자_둘째_행_개별_줄_묶음_바로_뒤(): + items = _items() + keys = list(items) + assert keys.index("haul:3.1") == keys.index("haul:3") + 1 + extra = items["haul:3.1"] + assert (extra["name"], extra["qty"], extra["dist"]["L2"]) == ("강모래", None, 2) + + +def test_사용자_상자_이름_항목명_같은_이름_두_줄(): + items = _items() + assert items["haul:r-u1"]["name"] == items["haul:r-u1.1"]["name"] == "전석운반" + assert items["haul:r-u1.2"]["name"] == "석산" # 빈 항목명 = 거래처 + assert items["haul:r-u2"]["name"] == "운반" and items["haul:r-u2"]["ref"] == LOGIC + assert items["haul:r-u1"]["ref"] is None # 연결 없어도 줄 + + +def test_운반운반_없음(): + assert not any("운반운반" in i["name"] for i in _items().values())