fix(b08·b09): 내역 표시 흠 묶음 — 운반 줄 장비 키 · 덤프 거리 14자리 · 규격 갈래 두 번(㉰㉵)

- 운반 줄 이름: 「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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
This commit is contained in:
2026-09-15 01:34:52 +09:00
co-authored by Claude Opus 5
parent 490cfe1d08
commit 7dd5dda98b
7 changed files with 88 additions and 14 deletions
@@ -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"))))
),
@@ -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"(?<![\d.]){re.escape(variant)}(?![\d.])", spec): # 숫자 경계: Ø1800 ≠ 800
return spec
return variant if variant.startswith(spec) else f"{spec} {variant}"
def bill_line(unit: Money3, quantity) -> 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]}"
@@ -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="",
)
)
+3 -3
View File
@@ -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
@@ -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
+2 -2
View File
@@ -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
@@ -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 돌쌓기"