From 7dd5dda98b69410e4dbde1ee8d700dbc1e56197f Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 15 Sep 2026 01:34:52 +0900 Subject: [PATCH] =?UTF-8?q?fix(b08=C2=B7b09):=20=EB=82=B4=EC=97=AD=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=20=ED=9D=A0=20=EB=AC=B6=EC=9D=8C=20=E2=80=94?= =?UTF-8?q?=20=EC=9A=B4=EB=B0=98=20=EC=A4=84=20=EC=9E=A5=EB=B9=84=20?= =?UTF-8?q?=ED=82=A4=20=C2=B7=20=EB=8D=A4=ED=94=84=20=EA=B1=B0=EB=A6=AC=20?= =?UTF-8?q?14=EC=9E=90=EB=A6=AC=20=C2=B7=20=EA=B7=9C=EA=B2=A9=20=EA=B0=88?= =?UTF-8?q?=EB=9E=98=20=EB=91=90=20=EB=B2=88(=E3=89=B0=E3=89=B5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 운반 줄 이름: 「dozer 운반」 → 「불도저 운반」(매핑 master_name) · 「dump_truck 운반」 → 「덤프 운반」 · 「free_haul 운반」 → 「무대(종방향유용토)」(토공집계 이름) - 덤프 규격·호표 규격 L: 「L=137.13474461798225m」 → 「L=137.13m」 — 보이기만 둘째 자리 · 호표 열쇠·운반 식은 받은 거리 그대로 - 규격 칸 갈래: 이미 적힌 갈래는 한 번만(`spec_with_variant` · 숫자 경계) — 「Ø800 800」 → 「Ø800」 - 936be972 내역: 흠 줄 6 → 0 · 금액 불변 161,514,889 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq --- .../B08_Quantity_Engine_Handoff_Rows.py | 8 +-- .../B09_Estimation_BillOfQuantities_Rows.py | 14 ++++- ...B09_Estimation_MachineProductivity_Dump.py | 11 +++- resources/tester/test_b08_handoff.py | 6 +- .../tester/test_b08_haul_double_count.py | 2 +- resources/tester/test_b08_haul_state.py | 4 +- resources/tester/test_b09_display_labels.py | 57 +++++++++++++++++++ 7 files changed, 88 insertions(+), 14 deletions(-) create mode 100644 resources/tester/test_b09_display_labels.py diff --git a/B08_Quantity/B08_Quantity_Engine_Handoff_Rows.py b/B08_Quantity/B08_Quantity_Engine_Handoff_Rows.py index d812bd70..0d250f83 100644 --- a/B08_Quantity/B08_Quantity_Engine_Handoff_Rows.py +++ b/B08_Quantity/B08_Quantity_Engine_Handoff_Rows.py @@ -11,6 +11,7 @@ import string from typing import Any from B08_Quantity.B08_Quantity_Engine_BasisUnit import normalize_unit, unit_for_code +from B08_Quantity.B08_Quantity_Engine_EarthworkSummary import HAUL_LABELS from B08_Quantity.B08_Quantity_Engine_Handoff_Mapping import ( BLOCKED_FORMULA_MISSING, BLOCKED_INPUT_MISSING, @@ -243,9 +244,8 @@ def _haul_rows( in_bill = bool(row.get("in_bill", True)) and entry.get("in_bill", True) if code is None and in_bill: unmatched.append(f"운반({equipment})") - # ⚠ 코드가 없으면 **줄에 막힘 표시를 단다**(2026-09-09) — 목록에만 실으면 줄 단위로 - # 보는 쪽이 「멀쩡한 줄」로 읽어 금액이 조용히 빠진다(도자운반·덤프운반이 그랬다). - # ⚠ `in_bill` 이 False 인 무대 줄은 **막힌 것이 아니다** — 품에 포함이라 안 세우는 것. + # ⚠ 코드가 없으면 **줄에 막힘 표시를 단다**(2026-09-09) — 목록에만 실으면 금액이 조용히 + # 빠진다. ⚠ `in_bill` 이 False 인 무대 줄은 **막힌 것이 아니다** — 품에 포함이라 안 세움. no_code = code is None and in_bill # 암 줄 막힘은 운반표가 구성비로 가르며 단 것(㉱) haul_blocked = ( BLOCKED_UNIT_DATA_MISSING if no_code else in_bill and row.get("blocked_kind") or None @@ -276,7 +276,7 @@ def _haul_rows( rows.append( { "work_item_code": code, - "name": f"{equipment} 운반", + "name": entry.get("master_name") or HAUL_LABELS.get(equipment, f"{equipment} 운반"), "spec": " · ".join( dict.fromkeys(filter(None, (row.get("rock_class"), row.get("ground")))) ), diff --git a/B09_Estimation/B09_Estimation_BillOfQuantities_Rows.py b/B09_Estimation/B09_Estimation_BillOfQuantities_Rows.py index eacfa1f7..722d1bfe 100644 --- a/B09_Estimation/B09_Estimation_BillOfQuantities_Rows.py +++ b/B09_Estimation/B09_Estimation_BillOfQuantities_Rows.py @@ -9,6 +9,7 @@ from __future__ import annotations +import re from decimal import Decimal from B09_Estimation.B09_Estimation_BillOfQuantities import ( @@ -25,6 +26,7 @@ from B09_Estimation.B09_Estimation_BillOfQuantities import ( from B09_Estimation.B09_Estimation_MachineProductivity_Dump import ( DUMP_PARENT, LOADING_EQUIPMENT, + distance_label, dump_child_for, dump_title_code, loading_title_code, @@ -40,6 +42,13 @@ from B09_Estimation.B09_Estimation_UnitPrice import ( ) +def spec_with_variant(spec: str, variant: str) -> str: + """규격에 갈래를 한 번만 — 적힌 갈래(Ø800·리핑암)는 안 붙임(㉵) · 규격 글로 시작하면 갈음.""" + if re.search(rf"(? Money3: """내역 줄 금액 — **성분마다** `절사(수량 × 성분 단가)`, 줄 합계는 셋의 합(명세 7장). @@ -362,7 +371,7 @@ def _leaf_row( return row price_code = wanted if not loading: - row.spec = f"{row.spec} L={item.haul_distance_m}m".strip() + row.spec = f"{row.spec} {distance_label(item.haul_distance_m)}".strip() if price_code not in unit_prices.book.titles: # B08 은 **의미**(어느 공종·어느 제원)만 보내고 갈래 키는 우리가 만든다. # 못 맞추면 후보를 보이는 길로 내려간다 — 가까운 갈래를 임의로 고르지 않는다. @@ -374,8 +383,7 @@ def _leaf_row( default = unit_prices.default_variants.get(node.code) if picked is not None: price_code = picked - variant = str(item.variant_value) # 규격 글로 시작하는 갈래는 한 번만(면고르기) - row.spec = variant if variant.startswith(row.spec) else f"{row.spec} {variant}" + row.spec = spec_with_variant(row.spec, str(item.variant_value)) elif default is not None: # 표에 없거나 안 준 암질(풍화암·암) — **원문이 정한 갈래**로만 선다(9-4-1 [주]① 평균). price_code = f"{price_code}#{default[0]}" diff --git a/B09_Estimation/B09_Estimation_MachineProductivity_Dump.py b/B09_Estimation/B09_Estimation_MachineProductivity_Dump.py index 3b73a5b6..361ed6bc 100644 --- a/B09_Estimation/B09_Estimation_MachineProductivity_Dump.py +++ b/B09_Estimation/B09_Estimation_MachineProductivity_Dump.py @@ -170,6 +170,15 @@ def dump_title_code(work_item_code: str, distance_m: Decimal) -> str: return f"B-{work_item_code}#L{distance_m.normalize():f}m" +def distance_label(distance_m: Decimal) -> str: + """보이는 운반거리 「L=137.13m」 — 소수 둘째 자리까지(㉰ 2026-09-14 · 가중평균이 14자리로 떴음). + + ⚠ **보이기만** 줄임 — 호표 열쇠(`dump_title_code`)·운반 식은 받은 거리 그대로 씀. + """ + shown = Decimal(distance_m).quantize(Decimal("0.01")).normalize() + return f"L={shown:f}m" + + def attach_dump_hauls(build: Any, master: dict[str, Any], distances_m: tuple[Decimal, ...]) -> None: """거리마다 덤프 운반 일위대가를 세운다 — X(덤프트럭 15ton) → D → B. @@ -226,7 +235,7 @@ def attach_dump_hauls(build: Any, master: dict[str, Any], distances_m: tuple[Dec kind=PriceKind.UNIT_PRICE, name=f"{names.get(DUMP_PARENT) or '덤프운반'} " f"{names.get(code) or material.label}", - spec=f"L={distance.normalize():f}m", + spec=distance_label(distance), unit="㎥", ) ) diff --git a/resources/tester/test_b08_handoff.py b/resources/tester/test_b08_handoff.py index 201dc864..08970e28 100644 --- a/resources/tester/test_b08_handoff.py +++ b/resources/tester/test_b08_handoff.py @@ -180,8 +180,8 @@ def test_무대는_넘기되_내역에는_안_섬() -> None: ] } handoff = build_handoff(haul_table=haul) - free = 줄(handoff, "free_haul 운반") - dump = 줄(handoff, "dump_truck 운반") + free = 줄(handoff, "무대(종방향유용토)") + dump = 줄(handoff, "덤프 운반") assert free["in_bill"] is False assert free["in_bill_reason"] # 왜 빠지는지 함께 간다 assert free["quantity"] == pytest.approx(800.0) # 값은 그대로 넘어간다 @@ -616,7 +616,7 @@ def test_운반계획이_있으면_네_줄이_실림() -> None: def test_무대가_함께_와야_운반_검산이_걸림() -> None: """무대를 빼고 넘기면 `무대+도자+덤프 = 총 운반토량` 검산이 죽는다.""" handoff = build_handoff(haul_table=운반표()) - free = 줄(handoff, "free_haul 운반") + free = 줄(handoff, "무대(종방향유용토)") assert free["in_bill"] is False assert free["quantity"] == pytest.approx(871.0) assert handoff["excluded_row_count"] == 1 diff --git a/resources/tester/test_b08_haul_double_count.py b/resources/tester/test_b08_haul_double_count.py index abd3e573..1493d0d3 100644 --- a/resources/tester/test_b08_haul_double_count.py +++ b/resources/tester/test_b08_haul_double_count.py @@ -66,7 +66,7 @@ def test_사유가_적힌다() -> None: def test_운반표_쪽은_그대로_선다() -> None: - row = 줄들()["dozer 운반"] + row = 줄들()["불도저 운반"] assert row["in_bill"] is True and row["work_item_code"] == "FP-10-11" assert row["quantity"] == 17.39 diff --git a/resources/tester/test_b08_haul_state.py b/resources/tester/test_b08_haul_state.py index 333542e1..21e9cca4 100644 --- a/resources/tester/test_b08_haul_state.py +++ b/resources/tester/test_b08_haul_state.py @@ -38,7 +38,7 @@ def 표(**extra: object) -> dict: def 줄(**extra: object) -> dict: rows = build_handoff(haul_table=표(**extra))["work_items"] - return next(row for row in rows if row["name"] == "dump_truck 운반") + return next(row for row in rows if row["name"] == "덤프 운반") def test_되돌린_값이_오면_그것으로_선다() -> None: @@ -80,5 +80,5 @@ def test_갈래가_없는_줄은_칸이_비어_있다() -> None: rows = build_handoff(haul_table={"rows": [{"equipment": "dozer", "volume_m3": 10.0}]})[ "work_items" ] - row = next(r for r in rows if r["name"] == "dozer 운반") + row = next(r for r in rows if r["name"] == "불도저 운반") assert row["variant_axis"] is None and row["variant_value"] is None diff --git a/resources/tester/test_b09_display_labels.py b/resources/tester/test_b09_display_labels.py new file mode 100644 index 00000000..732bb424 --- /dev/null +++ b/resources/tester/test_b09_display_labels.py @@ -0,0 +1,57 @@ +"""내역 표시 흠 묶음 — 장비 id · 거리 14자리 · 규격 갈래 두 번 (2026-09-14 브레인 ㉰㉵). + +훑기(936be972 내역)에서 잡은 것: + ㉰ 운반 줄 이름이 장비 키 그대로 「dozer 운반」「dump_truck 운반」「free_haul 운반」 + ㉰ 덤프 규격 칸 「토사 L=137.13474461798225m」 — 가중평균 거리를 그대로 찍음 + ㉵ 배수관 규격 칸 「Ø800 800」 — 이미 적힌 관경을 갈래로 한 번 더 붙임 +⚠ 거리는 **보이기만** 줄인다 — 호표 열쇠·운반 식은 받은 거리 그대로. +""" + +from __future__ import annotations + +from decimal import Decimal + +import B09_Estimation.B09_Estimation_BillOfQuantities # noqa: F401 — 줄 모듈은 조판 모듈이 먼저 섬 +from B08_Quantity.B08_Quantity_Engine_Handoff import build_handoff +from B09_Estimation.B09_Estimation_BillOfQuantities_Rows import spec_with_variant +from B09_Estimation.B09_Estimation_MachineProductivity_Dump import distance_label, dump_title_code + + +def test_운반_줄_이름에_장비_키가_안_뜬다() -> None: + haul = { + "rows": [ + {"equipment": "free_haul", "ground": "토사", "volume_m3": 10.0, "in_bill": False}, + {"equipment": "dozer", "ground": "토사", "volume_m3": 20.0, "average_distance_m": 40.0}, + { + "equipment": "dump_truck", + "ground": "토사", + "volume_m3": 30.0, + "average_distance_m": 137.13474461798225, + }, + ] + } + names = [row["name"] for row in build_handoff(haul_table=haul)["work_items"]] + assert "불도저 운반" in names and "덤프 운반" in names and "무대(종방향유용토)" in names + assert not [ + name for name in names if any(key in name for key in ("dozer", "dump_truck", "free_haul")) + ] + + +def test_거리는_둘째_자리까지_보이고_열쇠는_그대로() -> None: + raw = Decimal("137.13474461798225") + assert distance_label(raw) == "L=137.13m" + assert distance_label(Decimal("100")) == "L=100m" + assert distance_label(Decimal("60.5")) == "L=60.5m" + # 호표 열쇠는 받은 거리 그대로 — 거리마다 갈리는 호표가 뭉치면 안 된다. + assert dump_title_code("FP-10-12-01", raw).endswith("L137.13474461798225m") + + +def test_규격에_적힌_갈래는_한_번만() -> None: + assert spec_with_variant("Ø800", "800") == "Ø800" + assert spec_with_variant("연암 · 리핑암", "리핑암") == "연암 · 리핑암" + # 숫자 경계 — 「Ø1800」 에 800 은 적힌 것이 아님. + assert spec_with_variant("Ø1800", "800") == "Ø1800 800" + # 종전 동작 — 빈 규격 · 규격 글로 시작하는 갈래(면고르기) · 새 갈래. + assert spec_with_variant("", "토사") == "토사" + assert spec_with_variant("성토면", "성토면 인력") == "성토면 인력" + assert spec_with_variant("H=2", "돌쌓기") == "H=2 돌쌓기"