feat(B08): 사토 운반을 지반 갈래별로 나눠 세움 — 못 붙인 몫은 따로 막음

- 유토곡선 잔량이 갈래별 물량(ea/rr/br)을 들고 오므로 갈래마다 한 줄로 세움
  (덤프 단가가 토사·암으로 갈려 합쳐 세우면 한쪽 단가로 다 물림)
- 갈래를 못 붙인 몫(ground_unknown_m3)은 따로 한 줄로 세우고 막음 — 토사로 눅이면
  임의 단가가 됨
- 갈래가 안 오는 옛 저장분에서는 종전처럼 한 줄
- tmp/tests 3건 추가

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-08 23:19:06 +09:00
co-authored by Claude Opus 5
parent 8eed1f7b4a
commit 2731e3bf63
2 changed files with 98 additions and 36 deletions
@@ -9,8 +9,9 @@
⚠ **거리는 품셈이 정하지 않는다.** 사토장까지 거리는 설계 입력(`spoil_site_distance_m`)이고,
안 정했으면 **막고 사유를 낸다** — 임의 거리를 넣으면 그대로 금액이 된다.
⚠ **지반 갈래를 지어내지 않는다.** 유토곡선이 사토 잔량에 `ea/rr/br` 안분을 안 한다
(실어 내는 흙이 어느 지반에서 나왔는지 모름). 갈래 없이 한 줄로 세우고 그 사실을 적는다.
⚠ **지반 갈래는 유토곡선이 준 것만 쓴다.** 잔량이 갈래별 물량(`ea/rr/br`)을 들고 오면
**갈래마다 한 줄**로 세운다 — 덤프 단가가 토사·암으로 갈리기 때문이다. 갈래를 못 붙인
몫(`ground_unknown_m3`)은 **따로 한 줄**로 세우고 막는다. 토사로 눅이면 임의 단가가 된다.
"""
from __future__ import annotations
@@ -29,7 +30,12 @@ DISTANCE_MISSING = (
"사토장까지 운반거리가 저장에 없어 값이 서지 않음 — 품셈이 정하는 값이 아니라 설계 입력임"
"(임의 거리를 넣으면 그대로 금액이 됨)"
)
GROUND_UNKNOWN = "⚠ 사토의 지반 갈래는 유토곡선이 안분하지 않아 갈래 없이 한 줄로 섬"
GROUND_UNKNOWN = (
"지반 갈래를 못 붙인 몫 — 구조물 잔토 가운데 걸친 측점의 지반이 섞여 못 가른 것."
" ⚠ 토사로 눅이면 덤프 단가가 임의로 정해짐"
)
#: 잔량이 들고 오는 갈래 키 ↔ 우리 갈래 이름(흙깎기·운반이 쓰는 그 낱말).
GROUND_KEYS = {"ea_m3": "토사", "rr_m3": "리핑암", "br_m3": "발파암"}
def spoil_haul_rows(
@@ -46,24 +52,66 @@ def spoil_haul_rows(
blocked = distance is None or float(distance) <= 0
reason = DISTANCE_MISSING if blocked else ""
note = spoil.get("note") or ""
return [
{
# 갈래별로 나눠 세운다 — 덤프 단가가 토사·암으로 갈린다. 갈래가 안 오면 종전처럼 한 줄.
by_ground = {
GROUND_KEYS[key]: float(value)
for key, value in (spoil.get("by_ground_m3") or {}).items()
if key in GROUND_KEYS and float(value or 0.0) > 0
}
unknown = float(spoil.get("ground_unknown_m3") or 0.0)
if by_ground or unknown > 0:
rows: list[dict[str, Any]] = []
for label, amount in sorted(by_ground.items()):
rows.append(
_spoil_row(code, amount, distance, blocked, reason, note, ground=label, extra="")
)
if unknown > 0:
rows.append(
_spoil_row(
code,
unknown,
distance,
True,
GROUND_UNKNOWN,
note,
ground=None,
extra=GROUND_UNKNOWN,
)
)
return rows
return [_spoil_row(code, volume, distance, blocked, reason, note, None, GROUND_UNKNOWN)]
def _spoil_row(
code: str | None,
volume: float,
distance: Any,
blocked: bool,
reason: str,
note: str,
ground: str | None,
extra: str,
) -> dict[str, Any]:
"""사토 운반 줄 하나 — 갈래마다 같은 모양으로 낸다."""
return {
"work_item_code": code,
"name": SPOIL_NAME,
"spec": f"{float(distance):g}m" if not blocked else "",
"spec": " · ".join(
part for part in (ground or "", "" if blocked else f"{float(distance):g}m") if part
),
"unit": "",
"quantity": volume,
"quantity_gross": spoil.get("volume_gross_m3"),
"quantity": round(volume, 3),
"quantity_gross": None,
"application_ratio_pct": None,
"application_ratio_breakdown": None,
"quantity_breakdown": None,
"ground_class": None,
"ground_class": ground,
"haul_distance_m": None if blocked else float(distance),
"haul_equipment": SPOIL_EQUIPMENT,
"station_from": None,
"station_to": None,
"excavation_method": None,
"spec_detail": " · ".join(part for part in (note, GROUND_UNKNOWN) if part),
"spec_detail": " · ".join(part for part in (note, extra) if part),
"composite_parts": None,
"structure_kind": None,
"blocked_kind": BLOCKED_INPUT_MISSING if blocked else None,
@@ -78,4 +126,3 @@ def spoil_haul_rows(
"in_bill_reason": reason,
"origin": ORIGIN_HAUL,
}
]
@@ -166,6 +166,19 @@ def _spoil_of(plan: dict[str, Any] | None, settings: dict[str, Any]) -> dict[str
total = float(source.get("spoil_m3") or 0.0)
natural = float(source.get("natural_spoil_m3") or 0.0)
volume = max(total - natural, 0.0)
# 지반 갈래 — 사토 잔량이 갈래별 물량을 들고 온다(2026-09-08 랩탑 메인). 갈래를 못 붙인
# 몫은 `ground_unknown_m3` 로 따로 온다. **여기서 안분하지 않는다** — 근거 없는 몫을
# 토사로 눅이면 덤프 단가가 임의로 정해진다.
grounds: dict[str, float] = {}
unknown = 0.0
for residual in source.get("residuals") or []:
if str(residual.get("kind") or "") != "spoil":
continue
for key in ("ea_m3", "rr_m3", "br_m3"):
value = float(residual.get(key) or 0.0)
if value > 0:
grounds[key] = grounds.get(key, 0.0) + value
unknown += float(residual.get("ground_unknown_m3") or 0.0)
note_parts = [f"사토 {total:,.2f}"]
if natural > 0:
note_parts.append(f"자연방토 {natural:,.2f}㎥ 뺀 값")
@@ -179,6 +192,8 @@ def _spoil_of(plan: dict[str, Any] | None, settings: dict[str, Any]) -> dict[str
"volume_m3": round(volume, 3),
"distance_m": settings.get("spoil_site_distance_m"),
"note": " · ".join(note_parts),
"by_ground_m3": {key: round(value, 3) for key, value in grounds.items()},
"ground_unknown_m3": round(unknown, 3),
}