feat(b08): 토취(반입토) 줄 — 유토곡선이 낸 성토 부족분을 수량만 세우고 금액은 사유(브레인 ①)
- B06 운반계획의 borrow_m3·구간(residuals)을 운반표에 싣고 토공집계에 「토취(반입토)」 줄 · 인계는 코드 없이 수량만 · 막힘 사유 「토취장 거리·재료 미정」(집계 비고 그대로) · 다짐상태 그대로(반입 재료를 몰라 되돌리지 않음) - 종전엔 어느 탭·인계에도 안 옮겨 수량이 통째로 빠졌음 - 936be972: 토공집계 7,259.84㎥(246.54~529.33m 3,494.69 · 720~1,078.01m 3,765.15) · 내역 「금액을 못 세운 줄」 · 본체 금액 변화 0 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
This commit is contained in:
@@ -119,6 +119,17 @@ class SummaryInput:
|
||||
subgrade_compaction_enabled: bool = False
|
||||
# 면고르기 면적 덮어쓰기 `{fill, cut}`(㎡) — 비우면 파종 면적(판정 Ⓐ).
|
||||
face_dressing_area_m2: dict[str, float | None] = field(default_factory=dict)
|
||||
# 토취(반입토) — 유토곡선이 낸 성토 부족분(다짐상태 ㎥)과 구간(2026-09-14 브레인 ①).
|
||||
borrow_m3: float = 0.0
|
||||
borrow_sites: list[dict[str, Any]] = field(default_factory=list)
|
||||
|
||||
|
||||
#: 토취(반입토) 줄 — 수량만 서고 금액은 사유(「줄은 서고 금액은 안 섬」).
|
||||
BORROW_NAME = "토취(반입토)"
|
||||
BORROW_REASON = (
|
||||
"유토곡선이 성토 부족분으로 낸 수량(다짐상태) — 토취장 거리·재료 미정이라 금액이 서지 않음"
|
||||
" · 반입 재료를 몰라 자연상태로 되돌리지 않음"
|
||||
)
|
||||
|
||||
|
||||
#: 노체다짐 줄 — ⭐ 2026-09-13 판정 「별도 줄 · 칸으로 켜고 끔 · 기본 꺼짐」.
|
||||
@@ -191,6 +202,16 @@ def build_rows(source: SummaryInput) -> list[SummaryRow]:
|
||||
)
|
||||
)
|
||||
|
||||
if source.borrow_m3 > 0:
|
||||
sites = " · ".join(
|
||||
f"{float(s['from_m']):g}~{float(s['to_m']):g}m {float(s['volume_m3']):,.2f}㎥"
|
||||
for s in source.borrow_sites
|
||||
)
|
||||
note = f"{BORROW_REASON} · 구간 {sites}" if sites else BORROW_REASON
|
||||
rows.append(
|
||||
SummaryRow(group=BORROW_NAME, amount=source.borrow_m3, note=note, in_bill=False)
|
||||
)
|
||||
|
||||
# ── 운반 — 수단별. 무대는 집계에 오르되 내역 줄이 아니다 ────────
|
||||
rows.extend(_haul_rows(source))
|
||||
|
||||
|
||||
@@ -146,10 +146,16 @@ BLOCKED_UNCONFIRMED = "unconfirmed"
|
||||
#: ⚠ `item` 칸이 **지반 갈래**인 공종 — 그 밖의 공종에서 `item` 은 **작업 갈래**다
|
||||
#: (지장목제거의 「뿌리뽑기·잡관목제거」). 갈래로 읽으면 「시공법 미지정」이라는 **틀린 사유**가
|
||||
#: 붙는다(2026-09-09 실측). 정의처는 `EarthworkSummary` 이고 여기서 그대로 가져다 쓴다.
|
||||
from B08_Quantity.B08_Quantity_Engine_EarthworkSummary import ( # noqa: E402
|
||||
BORROW_NAME,
|
||||
)
|
||||
from B08_Quantity.B08_Quantity_Engine_EarthworkSummary import ( # noqa: E402
|
||||
GROUND_SPLIT_GROUPS as GROUND_SPLIT_GROUPS,
|
||||
)
|
||||
|
||||
#: 코드 없이 **수량만** 서는 집계 줄 — 막힘 사유는 집계 비고 그대로(토취 · 2026-09-14 브레인 ①).
|
||||
SUMMARY_ONLY_GROUPS = frozenset({BORROW_NAME})
|
||||
|
||||
SLOPE_GROUPS = frozenset({"성토면다짐", "초류종자살포", "지장목제거", "층따기", "면고르기"})
|
||||
|
||||
#: 집계 합계 줄 — 내역 줄이 아니라 검산용이다.
|
||||
|
||||
@@ -28,6 +28,7 @@ from B08_Quantity.B08_Quantity_Engine_Handoff_Mapping import (
|
||||
ORIGIN_STRUCTURE,
|
||||
SLOPE_GROUPS,
|
||||
SUBTOTAL_GROUPS,
|
||||
SUMMARY_ONLY_GROUPS,
|
||||
WorkItemMapping,
|
||||
composite_quantities,
|
||||
masonry_class,
|
||||
@@ -164,7 +165,6 @@ def _earthwork_rows(
|
||||
template_ready = all(inputs.get(name) for name in needs)
|
||||
if template and template_ready:
|
||||
variant_value = template.format(**inputs)
|
||||
# 부모(갈래 고르기형)를 가리키는 매핑 — 설정값으로 잎 코드를 고름(초류종자살포 5-24 · 09-14 ㉮).
|
||||
leaf_from = str((entry or {}).get("leaf_from") or "")
|
||||
leaf_codes = (entry or {}).get("leaf_codes") or {}
|
||||
leaf = leaf_codes.get(inputs.get(leaf_from)) if leaf_from else None
|
||||
@@ -174,7 +174,9 @@ def _earthwork_rows(
|
||||
if code and missing and (entry or {}).get("variant_missing_reason"):
|
||||
blocked_kind = blocked_kind or BLOCKED_INPUT_MISSING
|
||||
blocked_reason = blocked_reason or str(entry["variant_missing_reason"])
|
||||
if code is None and not is_subtotal:
|
||||
if group in SUMMARY_ONLY_GROUPS: # 토취 — 코드 없이 수량만 · 사유는 집계 비고(브레인 ①)
|
||||
blocked_kind, blocked_reason = BLOCKED_INPUT_MISSING, str(row.get("note") or "")
|
||||
elif code is None and not is_subtotal:
|
||||
label = f"{group}({ground})" if ground else group
|
||||
unmatched.append(f"{label} — {method_note}" if method_note else label)
|
||||
if blocked_kind is None:
|
||||
|
||||
@@ -266,6 +266,22 @@ def summary_input_rows(table: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
]
|
||||
|
||||
|
||||
def borrow_of(plan: dict[str, Any] | None) -> dict[str, Any] | None:
|
||||
"""토취(반입토) — 유토곡선 잔량의 부족분과 구간(2026-09-14 브레인 ①). 없으면 `None`.
|
||||
|
||||
⚠ **다짐상태** 그대로 — 반입 재료를 몰라 자연상태로 되돌릴 계수가 없음(지어내지 않음).
|
||||
"""
|
||||
volume = float((plan or {}).get("borrow_m3") or 0.0)
|
||||
if volume <= 0:
|
||||
return None
|
||||
sites = [
|
||||
{"from_m": r.get("from_m"), "to_m": r.get("to_m"), "volume_m3": float(r["volume_m3"])}
|
||||
for r in (plan or {}).get("residuals") or []
|
||||
if r.get("kind") == "borrow" and float(r.get("volume_m3") or 0.0) > 0
|
||||
]
|
||||
return {"volume_m3": volume, "volume_basis": "compacted", "sites": sites}
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class HaulCheck:
|
||||
"""검산 — 무대를 안 내면 이 대조가 죽는다(PLAN 8-7 ㉡)."""
|
||||
|
||||
@@ -43,7 +43,11 @@ from B08_Quantity.B08_Quantity_Engine_EarthworkSummary import build_table as bui
|
||||
from B08_Quantity.B08_Quantity_Engine_EarthworkTable import StationArea, build_table
|
||||
from B08_Quantity.B08_Quantity_Engine_Handoff import load_mapping
|
||||
from B08_Quantity.B08_Quantity_Engine_HaulSummary import build_table as build_haul_table
|
||||
from B08_Quantity.B08_Quantity_Engine_HaulSummary import check_against_plan, summary_input_rows
|
||||
from B08_Quantity.B08_Quantity_Engine_HaulSummary import (
|
||||
borrow_of,
|
||||
check_against_plan,
|
||||
summary_input_rows,
|
||||
)
|
||||
from B08_Quantity.B08_Quantity_Engine_RockSplit import apply_rock_split
|
||||
from B08_Quantity.B08_Quantity_Router_Earthwork_HaulPlan import HAUL_PLAN_KEYS
|
||||
from B08_Quantity.B08_Quantity_Router_Earthwork_HaulPlan import (
|
||||
@@ -130,6 +134,7 @@ async def get_earthwork_table(project_id: UUID, route_id: int) -> JSONResponse:
|
||||
# 여기서 그 값을 운반표에 실어 인계가 「사토 운반」 한 줄을 세우게 한다.
|
||||
# ⚠ 거리는 품셈이 정하지 않는다 — 설계 입력(`spoil_site_distance_m`)이고 없으면 막힌다.
|
||||
haul["spoil"] = _spoil_of(plan, settings, _spoil_sites(designs))
|
||||
haul["borrow"] = borrow_of(plan) # 토취(반입토) — 수량만 · 금액은 사유(브레인 ①)
|
||||
# 배수관 연장 — B06 이 측점 `design.pipe_length_m` 에 남긴 값. **여기서 짓지 않는다.**
|
||||
# 인계가 관 줄을 세울 때 쓴다. 단면을 두 번 읽지 않으려고 이 응답에 실어 보낸다.
|
||||
table["pipe_lengths"] = [
|
||||
@@ -162,6 +167,8 @@ async def get_earthwork_table(project_id: UUID, route_id: int) -> JSONResponse:
|
||||
earthwork_totals=table.get("totals") or {},
|
||||
slope_totals=slope.get("totals") or {},
|
||||
haul_rows=summary_input_rows(haul),
|
||||
borrow_m3=(haul["borrow"] or {}).get("volume_m3") or 0.0,
|
||||
borrow_sites=(haul["borrow"] or {}).get("sites") or [],
|
||||
rock_classes=rock_classes(settings),
|
||||
rock_ratios_pct=settings.get("rock_ratios_pct") or {},
|
||||
application_ratios={
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
"""토취(반입토) 줄 — 2026-09-14 브레인 ① 승인(수량만 · 금액은 사유).
|
||||
|
||||
B06 유토곡선이 성토 부족분을 토취로 냄(936be972 `haul_plan.borrow_m3` 7,259.84㎥ · 두 구간)인데 B08 이 어느 탭·인계에도
|
||||
안 옮겨 수량이 통째로 빠졌음. ⇒ 운반표에 싣고 · 토공집계에 줄 · 인계에 막힌 줄(「토취장 거리·재료 미정」).
|
||||
⚠ 수량은 유토곡선이 쌓은 **다짐상태** 그대로 — 반입 재료를 몰라 자연상태로 되돌릴 계수가 없음(지어내지 않음).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B08_Quantity.B08_Quantity_Engine_EarthworkSummary import ( # noqa: E402
|
||||
BORROW_NAME,
|
||||
SummaryInput,
|
||||
build_rows,
|
||||
)
|
||||
from B08_Quantity.B08_Quantity_Engine_Handoff import build_handoff # noqa: E402
|
||||
from B08_Quantity.B08_Quantity_Engine_HaulSummary import borrow_of # noqa: E402
|
||||
|
||||
PLAN = {
|
||||
"borrow_m3": 7259.84,
|
||||
"residuals": [
|
||||
{"kind": "borrow", "volume_m3": 3494.69, "from_m": 246.54, "to_m": 529.33},
|
||||
{"kind": "borrow", "volume_m3": 3765.15, "from_m": 720, "to_m": 1078.01},
|
||||
{"kind": "spoil", "volume_m3": 4.57, "from_m": 205, "to_m": 205},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_운반계획의_토취를_구간과_함께_싣는다() -> None:
|
||||
borrow = borrow_of(PLAN)
|
||||
assert borrow["volume_m3"] == pytest.approx(7259.84) and borrow["volume_basis"] == "compacted"
|
||||
assert [s["volume_m3"] for s in borrow["sites"]] == [3494.69, 3765.15]
|
||||
assert borrow_of({"borrow_m3": 0}) is None and borrow_of(None) is None
|
||||
|
||||
|
||||
def test_토공집계에_수량만_선다() -> None:
|
||||
rows = [r for r in build_rows(SummaryInput(borrow_m3=7259.84)) if r.group == BORROW_NAME]
|
||||
assert len(rows) == 1 and rows[0].amount == pytest.approx(7259.84)
|
||||
assert rows[0].in_bill is False and "토취장 거리·재료 미정" in rows[0].note
|
||||
|
||||
|
||||
def test_인계는_막힌_줄_하나_금액_없음() -> None:
|
||||
from B08_Quantity.B08_Quantity_Engine_EarthworkSummary import build_table
|
||||
|
||||
borrow = borrow_of(PLAN)
|
||||
summary = build_table(SummaryInput(borrow_m3=borrow["volume_m3"], borrow_sites=borrow["sites"]))
|
||||
handoff = build_handoff(summary_table=summary, haul_table={"rows": [], "borrow": borrow})
|
||||
rows = [r for r in handoff["work_items"] if r["name"] == BORROW_NAME]
|
||||
assert len(rows) == 1, rows # 한 줄만
|
||||
row = rows[0]
|
||||
assert row["quantity"] == pytest.approx(7259.84) and row["in_bill"] is False
|
||||
assert (
|
||||
row["blocked_kind"] == "input_missing" and "토취장 거리·재료 미정" in row["blocked_reason"]
|
||||
)
|
||||
assert "246.54~529.33m 3,494.69㎥" in row["blocked_reason"]
|
||||
assert not any(BORROW_NAME in str(item) for item in handoff["unmatched_work_items"])
|
||||
Reference in New Issue
Block a user