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:
@@ -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="㎥",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 돌쌓기"
|
||||
Reference in New Issue
Block a user