Files
Aislo/B08_Quantity/B08_Quantity_Engine_Handoff_Trench.py
T
eomsangdonandClaude Opus 5 072f560a59 fix(B08): 옹벽 묶음 감사 둘 — 구조물 이름 코드값 노출 · 터파기 빠진 줄이 안 보이던 것
- 레지스트리 이름이 없으면 내역 줄 이름에 코드값이 그대로 떴음(`retaining_wall`).
  _Wording.type_label 이 이미 대비표를 들고 있으므로 그것을 씀 — 이제 「옹벽」으로 섬.
- 관측 원단위로 가는 구조물은 표에 터파기 줄이 없으면 그 구조물만 조용히 빠졌음.
  옹벽이 터파기 0 줄인데 사유도 안 났음(돌쌓기는 세 줄이 섬). 값을 지어내지 않고
  「터파기가 안 선 구조물」 줄을 in_bill False · blocked_kind unit_data_missing 로
  세워 빠졌다는 사실이 보이게 함.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 13:06:06 +09:00

340 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""구조물 터파기·되메우기·잔토 인계 줄 (2026-09-08).
⚠⚠ **빠뜨렸던 자리다.** 구조물 전개는 이 셋을 `destination: earthwork` 로 내는데,
토공집계표는 **토적표만** 읽어 만들어져 그 성분을 아무도 받지 않았다. 그래서 두께 식·
기초 몫을 아무리 맞춰도 **내역서에 한 줄도 안 나갔다**(2026-09-08 B09 매김에서 드러남).
실무 내역에는 서는 줄이다 — 울진 대흥 1공구 토적집계 D12~D14:
`구조물터파기 토사 1,248 / 암 30 ㎥` · `되메우기 739 ㎥`
⚠ **품셈 9-13 은 18구분이다**(토질 3 × 육상/용수 × 심도 3). 세 축이 다 서면 자식 코드로
내려간다 — 토질은 측점 설계값(`design.ground_type`), 심도는 구조물 제원(직고 + 기초 깊이),
용수는 산출 조건 칸(기본 「육상」은 **통상값**이고 사용자 확정이 아니다). 하나라도 없으면
**지어내지 않고** 상위 코드로 세운 뒤 사유를 붙인다.
⚠ **잔토는 내역 줄로 세우지 않는다** — 사토로 실어 내는 몫이라 유토곡선(B06)이 세야 겹치지
않는다. **그 통로는 2026-09-09 에 생겼다**(`Engine_HaulInputs` → 유토곡선 사토 가산 →
「사토 운반」 줄). 여기서는 값과 사유만 넘기고 `in_bill=False` 로 둔다 — 빼 버리면 빠진 줄을
아무도 못 찾는다.
"""
from __future__ import annotations
from typing import Any
from B08_Quantity.B08_Quantity_Engine_Handoff_Mapping import (
BLOCKED_INPUT_MISSING,
BLOCKED_UNIT_DATA_MISSING,
ORIGIN_EARTHWORK,
ORIGIN_STRUCTURE,
WorkItemMapping,
)
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import GROUND_TYPE_LABEL
from common_util.common_util_excavation import (
WALL_BLINDING_DEPTH_M,
WALL_FOUNDATION_DEPTH_M,
)
#: 품셈 9-13 심도 갈래 — 원문 표기(`0~1m · 1~2m · 2~3m`)를 그대로 쓴다.
DEPTH_BANDS: tuple[tuple[float, str], ...] = ((1.0, "0~1m"), (2.0, "1~2m"), (3.0, "2~3m"))
DEPTH_OVER = "3m 초과"
TRENCH_GROUP = "구조물터파기"
RUBBLE_GROUP = "기초잡석"
BACKFILL_GROUP = "되메우기"
SPOIL_GROUP = "잔토처리"
#: ⚠ 남은 축은 **용수 하나**다(2026-09-08). 토질은 측점 설계값(`design.ground_type`)에서
#: 끌어오고, 심도는 구조물 제원(직고 + 기초 깊이)에서 나온다. 용수는 저장에 칸이 없어
#: **입력 칸이 서야 하는 자리**다 — 모른다고 「육상」으로 눅이지 않는다.
WATER_BLOCKED_REASON = (
"용수 유무가 저장에 없어 품셈 9-13 의 18구분 중 육상·용수 어느 쪽인지 못 고름"
" — 모른다고 육상으로 눅이지 않음"
)
GROUND_BLOCKED_REASON = "토질을 못 가름"
#: 품셈 9-13 의 18구분 — **토질 3 × 육상/용수 × 심도 3**. 코드 차례가 원문 그대로다
#: (육상토사 0~1·1~2·2~3 → 용수토사 셋 → 육상 암절취 셋 → … → 용수 발파암 셋).
GROUND_ORDER = ("soil", "ripping_rock", "blasting_rock")
WATER_ORDER = ("육상", "용수")
DEPTH_ORDER = ("0~1m", "1~2m", "2~3m")
#: ⚠ 3m 를 넘는 칸이 **원문에 없다** — 지어내지 않고 상위 코드로 두고 사유를 낸다.
DEPTH_OVER_REASON = "심도 3m 를 넘는 칸이 품셈 9-13 원문에 없음 — 상위 코드로 둠"
#: ⚠ **사용자 확정이 아니라 통상값**이다(2026-09-09 확정 3차 ④). 화면에도 그 사실이 뜨고
#: 사용자가 「용수」로 뒤집으면 코드가 한 칸 옮겨 간다.
WATER_DEFAULT_NOTE = "용수 유무 — 「육상」은 통상값이고 사용자 확정이 아님(확정 3차 ④)"
def trench_child_code(parent: str | None, ground: str | None, water: str, band: str) -> str | None:
"""품셈 9-13 의 18구분 중 한 칸. 축이 하나라도 없으면 `None`(상위 코드로 둔다)."""
if not parent or ground not in GROUND_ORDER or water not in WATER_ORDER:
return None
if band not in DEPTH_ORDER:
return None
index = (
GROUND_ORDER.index(ground) * len(WATER_ORDER) * len(DEPTH_ORDER)
+ WATER_ORDER.index(water) * len(DEPTH_ORDER)
+ DEPTH_ORDER.index(band)
)
return f"{parent}-{index + 1:02d}"
SPOIL_REASON = (
"사토로 실어 내는 몫이라 유토곡선(B06)이 세야 겹치지 않음 — 값을 버리지 않고 사유와"
" 함께 넘김(유토곡선이 사토에 얹어 「사토 운반」 줄로 섬)"
)
#: ⚠ **값 옆에 두는 사실** — 코드 주석에만 두면 화면에서 안 보인다(오늘 여러 번 나온 자리).
#: 되메우기·잔토는 **제자리 기하 부피**로 셈한다. 엄밀히는 되메움에 **쓰이는 흙**이
#: 다짐부피 ÷ C 라 조금 더 들고 그만큼 잔토가 줄어야 한다. **실무 정본도 그냥 빼므로**
#: (터파기 1.55 되메우기 0.30 = 잔토 1.25) 값은 그대로 두고 **사실만 적어 둔다** —
#: 「잔토가 조금 많다」가 나중에 올라오면 이 문장이 답이다.
STATE_NOTE = (
"제자리 기하 부피(자연상태) — ⚠ 되메움에 쓰이는 흙은 다짐부피 ÷ C 라 조금 더 들지만"
" 실무 정본도 그냥 빼므로 그대로 둠"
)
def _depth_band(depth_m: float) -> str:
for limit, label in DEPTH_BANDS:
if depth_m <= limit:
return label
return DEPTH_OVER
def _foundation_depth(options: dict[str, Any]) -> float:
"""기초 깊이 — 「기초유」 0.5 · 「기초버림」 0.1 · 안 고르면 0(비탈분만)."""
value = str(options.get("foundation") or "").strip()
if value == "기초유":
return WALL_FOUNDATION_DEPTH_M
if value == "기초버림":
return WALL_BLINDING_DEPTH_M
return 0.0
def _row(**fields: Any) -> dict[str, Any]:
"""줄 한 벌 — 계약이 요구하는 칸을 **모두** 채운다(빌더마다 같은 모양이라야 한다)."""
row: dict[str, Any] = {
"work_item_code": None,
"name": "",
"spec": "",
"unit": "㎥",
"quantity": 0.0,
"quantity_gross": None,
"application_ratio_pct": None,
"application_ratio_breakdown": None,
"quantity_breakdown": None,
"ground_class": None,
"haul_distance_m": None,
"haul_equipment": None,
"station_from": None,
"station_to": None,
"excavation_method": None,
"spec_detail": "",
"composite_parts": None,
"structure_kind": None,
"blocked_kind": None,
"blocked_reason": "",
"variant_axis": None,
"variant_value": None,
"secondary_axes": None,
"spec_class": None,
"spec_class_basis": "",
"composite_not_ready": None,
"in_bill": True,
"in_bill_reason": "",
"origin": ORIGIN_EARTHWORK,
}
row.update(fields)
return row
def rubble_base_rows(
unit_quantity_table: dict[str, Any], mapping: WorkItemMapping
) -> tuple[list[dict[str, Any]], list[str]]:
"""기초잡석(품셈 12-25) — 구조물 전개가 낸 성분을 **한 줄로** 올린다.
⚠ **묶음으로 서는 구조물은 건너뛴다** — 옹벽 묶음에 이미 `FP-12-25` 조각이 있어
여기서 또 세우면 같은 잡석을 두 번 센다(타설 줄에서 이미 겪은 자리).
"""
total = 0.0
bases: list[str] = []
for structure in unit_quantity_table.get("structures") or []:
if mapping.composite_for(str(structure.get("type_id") or "")):
continue
for component in structure.get("components") or []:
if str(component.get("name") or "") != RUBBLE_GROUP:
continue
amount = float(component.get("amount") or 0.0)
if amount <= 0:
continue
total += amount
basis = str(component.get("basis") or "")
if basis and basis not in bases:
bases.append(basis)
if total <= 0:
return [], []
entry = mapping.for_earthwork(RUBBLE_GROUP, None)
code = (entry or {}).get("work_item_code")
return [
_row(
work_item_code=code,
name=RUBBLE_GROUP,
spec="구조물",
spec_detail=bases[0] if bases else "구조물 전개 합",
quantity=round(total, 3),
origin=ORIGIN_STRUCTURE,
in_bill=code is not None,
in_bill_reason="" if code else "품셈 공종을 아직 못 이었습니다",
)
], ([] if code else [RUBBLE_GROUP])
def structure_earthwork_rows(
unit_quantity_table: dict[str, Any],
mapping: WorkItemMapping,
water: str | None = None,
) -> tuple[list[dict[str, Any]], list[str]]:
"""구조물이 낸 터파기·되메우기·잔토를 **공종 축**으로 올린다.
`water` 는 「육상」·「용수」. 안 주면 「안 정함」이라 상위 코드로 두고 사유를 낸다.
"""
# ⚠ 키는 **(심도, 토질)** 뿐이다 — 근거 문구까지 키에 넣으면 같은 「암절취 · 2~3m」이
# 측점 문구가 다르다는 이유로 **두 줄로 갈린다**(2026-09-08 실화면에서 그랬다).
trench: dict[tuple[str, str | None], float] = {}
bases: dict[tuple[str, str | None], list[str]] = {}
backfill = 0.0
spoil = 0.0
# ⚠ 터파기 성분이 아예 없는 구조물을 적어 둔다 — **줄이 안 서는 것보다 나쁜 것이
# 「빠진 줄조차 안 보이는 것」**이다(2026-09-09 감사: 옹벽이 터파기 0 줄이었고
# 사유도 안 났다). 관측 원단위로 가는 종류는 표에 터파기 줄이 없으면 이렇게 된다.
without_trench: list[str] = []
for structure in unit_quantity_table.get("structures") or []:
options = structure.get("options") or {}
height = float(structure.get("height_m") or options.get("height_m") or 0.0)
band = _depth_band(height + _foundation_depth(options))
ground = structure.get("ground_type") or None
ground_basis = str(structure.get("ground_type_basis") or "")
for component in structure.get("components") or []:
if str(component.get("destination") or "") != "earthwork":
continue
name = str(component.get("name") or "")
amount = float(component.get("amount") or 0.0)
if amount <= 0:
continue
if name == "터파기":
key = (band, ground)
trench[key] = trench.get(key, 0.0) + amount
reasons = bases.setdefault(key, [])
if ground_basis and ground_basis not in reasons:
reasons.append(ground_basis)
elif name == "되메우기":
backfill += amount
elif name == "잔토처리":
spoil += amount
if not any(
str(component.get("name") or "") == "터파기"
and float(component.get("amount") or 0.0) > 0
for component in structure.get("components") or []
):
without_trench.append(
str(structure.get("name") or structure.get("type_id") or "이름 없는 구조물")
)
rows: list[dict[str, Any]] = []
unmatched: list[str] = []
if trench:
entry = mapping.for_earthwork(TRENCH_GROUP, None)
code = (entry or {}).get("work_item_code")
if code is None:
unmatched.append(TRENCH_GROUP)
order = [band for _, band in DEPTH_BANDS] + [DEPTH_OVER]
for (band, ground), amount in sorted(
trench.items(), key=lambda item: (order.index(item[0][0]), str(item[0][1] or ""))
):
if not amount:
continue
ground_basis = " · ".join(bases.get((band, ground)) or [])
label = GROUND_TYPE_LABEL.get(str(ground), str(ground)) if ground else None
child = trench_child_code(code, ground, str(water or ""), band)
if not ground:
reason = f"{GROUND_BLOCKED_REASON}{ground_basis}"
elif not water:
reason = WATER_BLOCKED_REASON
elif child is None:
reason = DEPTH_OVER_REASON
else:
reason = ""
detail = " · ".join(
part
for part in (
"구조물 전개 합",
ground_basis,
WATER_DEFAULT_NOTE if water == "육상" else "",
)
if part
)
spec = " · ".join(part for part in (label or "", water or "", f"심도 {band}") if part)
rows.append(
_row(
work_item_code=child or code,
name=TRENCH_GROUP,
spec=spec,
ground_class=label,
spec_detail=detail,
quantity=amount,
blocked_kind=BLOCKED_INPUT_MISSING if reason else None,
blocked_reason=reason,
in_bill=not reason,
in_bill_reason=reason,
)
)
if backfill > 0:
entry = mapping.for_earthwork(BACKFILL_GROUP, None)
code = (entry or {}).get("work_item_code")
if code is None:
unmatched.append(BACKFILL_GROUP)
rows.append(
_row(
work_item_code=code,
name=BACKFILL_GROUP,
spec="구조물",
spec_detail=f"구조물 전개 합 · {STATE_NOTE}",
quantity=backfill,
in_bill=code is not None,
in_bill_reason="" if code else "품셈 공종을 아직 못 이었습니다",
)
)
if spoil > 0:
rows.append(
_row(
name=SPOIL_GROUP,
spec="구조물",
spec_detail=f"구조물 전개 합 (터파기 − 되메우기) · {STATE_NOTE}",
quantity=spoil,
in_bill=False,
in_bill_reason=SPOIL_REASON,
)
)
if without_trench:
# 값을 지어내지 않고 **빠졌다는 사실만** 낸다 — 관측 원단위(옹벽·집수정 등)는 표에
# 터파기 줄이 있는 것도 있고 없는 것도 있어, 없으면 그 구조물만 조용히 빠진다.
rows.append(
_row(
name=TRENCH_GROUP,
spec="구조물",
spec_detail=(
"⚠ 터파기가 안 선 구조물: "
+ " · ".join(sorted(set(without_trench)))
+ " — 관측 원단위 표에 터파기 줄이 없음(전개식으로 가는 종류는 높이·두께에서"
" 나오지만 관측표는 표에 있는 줄만 냄). **값을 지어내지 않음** —"
" 실무 내역서에서 그 구조물의 터파기를 어떻게 잡는지 확인할 것"
),
quantity=0.0,
in_bill=False,
in_bill_reason="터파기 물량을 못 세움 — 관측 원단위에 그 줄이 없음",
blocked_kind=BLOCKED_UNIT_DATA_MISSING,
blocked_reason="관측 원단위 표에 터파기 줄이 없음",
)
)
return rows, unmatched