- 새 저장 칸을 만들지 않음. 측점마다 이미 있는 `design.ground_type` (soil·ripping_rock·blasting_rock)이 품셈 9-13 토질 3구분과 그대로 맞물림 - 판정 규칙: 구조물이 **걸친 측점 전부**를 보고 갈래가 하나일 때만 값을 냄. 섞이면 다수결로 고르지 않고 갈래별 측점 수를 사유에 적음(암 단가가 몇 배라 임의 선택이 금액으로 굳음). 걸친 측점이 없으면 가장 가까운 측점 + 그 사실을 근거에 - 터파기 줄이 토질·심도로 갈려 서고, 막힌 사유가 「용수 유무」 하나로 좁혀짐 - tmp/tests 2건 추가(측점값 반영·섞임 판정) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
197 lines
8.0 KiB
Python
197 lines
8.0 KiB
Python
"""구조물 터파기·되메우기·잔토 인계 줄 (2026-09-08).
|
|
|
|
⚠⚠ **빠뜨렸던 자리다.** 구조물 전개는 이 셋을 `destination: earthwork` 로 내는데,
|
|
토공집계표는 **토적표만** 읽어 만들어져 그 성분을 아무도 받지 않았다. 그래서 두께 식·
|
|
기초 몫을 아무리 맞춰도 **내역서에 한 줄도 안 나갔다**(2026-09-08 B09 매김에서 드러남).
|
|
|
|
실무 내역에는 서는 줄이다 — 울진 대흥 1공구 토적집계 D12~D14:
|
|
`구조물터파기 토사 1,248 / 암 30 ㎥` · `되메우기 739 ㎥`
|
|
|
|
⚠ **품셈 9-13 은 18구분이다**(토질 3 × 육상/용수 × 심도 3). 우리는 **심도만** 안다
|
|
(터파기 깊이 = 직고 + 기초 깊이). 토질·용수는 저장 제원에 칸이 없으므로 **지어내지 않고**
|
|
상위 코드로 세운 뒤 사유를 붙인다 — 심도 갈래는 미리 갈라 두어 칸이 생기는 날 바로 붙는다.
|
|
|
|
⚠ **잔토는 내역 줄로 세우지 않는다** — 사토로 실어 내는 몫이라 유토곡선(B06)이 세야 겹치지
|
|
않는다. 다만 **지금 그 통로가 없다**(채집석 공제만 있다). 값을 버리지 않고 `in_bill=False`
|
|
로 넘기며 그 사실을 사유에 적는다 — 빼 버리면 빠진 줄을 아무도 못 찾는다.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from B08_Quantity.B08_Quantity_Engine_Handoff_Mapping import (
|
|
BLOCKED_INPUT_MISSING,
|
|
ORIGIN_EARTHWORK,
|
|
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 = "구조물터파기"
|
|
BACKFILL_GROUP = "되메우기"
|
|
SPOIL_GROUP = "잔토처리"
|
|
|
|
#: ⚠ 남은 축은 **용수 하나**다(2026-09-08). 토질은 측점 설계값(`design.ground_type`)에서
|
|
#: 끌어오고, 심도는 구조물 제원(직고 + 기초 깊이)에서 나온다. 용수는 저장에 칸이 없어
|
|
#: **입력 칸이 서야 하는 자리**다 — 모른다고 「육상」으로 눅이지 않는다.
|
|
WATER_BLOCKED_REASON = (
|
|
"용수 유무가 저장에 없어 품셈 9-13 의 18구분 중 육상·용수 어느 쪽인지 못 고름"
|
|
" — 모른다고 육상으로 눅이지 않음"
|
|
)
|
|
GROUND_BLOCKED_REASON = "토질을 못 가름"
|
|
SPOIL_REASON = (
|
|
"사토로 실어 내는 몫이라 유토곡선(B06)이 세야 겹치지 않음 — ⚠ 지금 그 통로가 없어"
|
|
" 어디에도 안 실림. 값을 버리지 않고 사유와 함께 넘김"
|
|
)
|
|
|
|
|
|
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 structure_earthwork_rows(
|
|
unit_quantity_table: dict[str, Any], mapping: WorkItemMapping
|
|
) -> tuple[list[dict[str, Any]], list[str]]:
|
|
"""구조물이 낸 터파기·되메우기·잔토를 **공종 축**으로 올린다."""
|
|
trench: dict[tuple[str, str | None, str], float] = {}
|
|
backfill = 0.0
|
|
spoil = 0.0
|
|
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, ground_basis)
|
|
trench[key] = trench.get(key, 0.0) + amount
|
|
elif name == "되메우기":
|
|
backfill += amount
|
|
elif name == "잔토처리":
|
|
spoil += amount
|
|
|
|
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, ground_basis), amount in sorted(
|
|
trench.items(), key=lambda item: (order.index(item[0][0]), str(item[0][1] or ""))
|
|
):
|
|
if not amount:
|
|
continue
|
|
label = GROUND_TYPE_LABEL.get(str(ground), str(ground)) if ground else None
|
|
reason = WATER_BLOCKED_REASON if ground else f"{GROUND_BLOCKED_REASON} — {ground_basis}"
|
|
rows.append(
|
|
_row(
|
|
work_item_code=code,
|
|
name=TRENCH_GROUP,
|
|
spec=f"{label} · 심도 {band}" if label else f"심도 {band}",
|
|
ground_class=label,
|
|
spec_detail=f"구조물 전개 합 · {ground_basis}"
|
|
if ground_basis
|
|
else "구조물 전개 합",
|
|
quantity=amount,
|
|
blocked_kind=BLOCKED_INPUT_MISSING,
|
|
blocked_reason=reason,
|
|
in_bill=False,
|
|
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="구조물 전개 합",
|
|
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="구조물 전개 합 (터파기 − 되메우기)",
|
|
quantity=spoil,
|
|
in_bill=False,
|
|
in_bill_reason=SPOIL_REASON,
|
|
)
|
|
)
|
|
return rows, unmatched
|