diff --git a/B08_Quantity/B08_Quantity_Engine_Handoff_Spoil.py b/B08_Quantity/B08_Quantity_Engine_Handoff_Spoil.py index 0a4087d7..189c9790 100644 --- a/B08_Quantity/B08_Quantity_Engine_Handoff_Spoil.py +++ b/B08_Quantity/B08_Quantity_Engine_Handoff_Spoil.py @@ -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,36 +52,77 @@ def spoil_haul_rows( blocked = distance is None or float(distance) <= 0 reason = DISTANCE_MISSING if blocked else "" note = spoil.get("note") or "" - return [ - { - "work_item_code": code, - "name": SPOIL_NAME, - "spec": f"{float(distance):g}m" if not blocked else "", - "unit": "㎥", - "quantity": volume, - "quantity_gross": spoil.get("volume_gross_m3"), - "application_ratio_pct": None, - "application_ratio_breakdown": None, - "quantity_breakdown": None, - "ground_class": None, - "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), - "composite_parts": None, - "structure_kind": None, - "blocked_kind": BLOCKED_INPUT_MISSING if blocked else None, - "blocked_reason": reason, - "variant_axis": None, - "variant_value": None, - "secondary_axes": None, - "spec_class": None, - "spec_class_basis": "", - "composite_not_ready": None, - "in_bill": not blocked and code is not None, - "in_bill_reason": reason, - "origin": ORIGIN_HAUL, - } - ] + # 갈래별로 나눠 세운다 — 덤프 단가가 토사·암으로 갈린다. 갈래가 안 오면 종전처럼 한 줄. + 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": " · ".join( + part for part in (ground or "", "" if blocked else f"{float(distance):g}m") if part + ), + "unit": "㎥", + "quantity": round(volume, 3), + "quantity_gross": None, + "application_ratio_pct": None, + "application_ratio_breakdown": None, + "quantity_breakdown": 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, extra) if part), + "composite_parts": None, + "structure_kind": None, + "blocked_kind": BLOCKED_INPUT_MISSING if blocked else None, + "blocked_reason": reason, + "variant_axis": None, + "variant_value": None, + "secondary_axes": None, + "spec_class": None, + "spec_class_basis": "", + "composite_not_ready": None, + "in_bill": not blocked and code is not None, + "in_bill_reason": reason, + "origin": ORIGIN_HAUL, + } diff --git a/B08_Quantity/B08_Quantity_Router_Earthwork.py b/B08_Quantity/B08_Quantity_Router_Earthwork.py index 9e23aa45..6973b881 100644 --- a/B08_Quantity/B08_Quantity_Router_Earthwork.py +++ b/B08_Quantity/B08_Quantity_Router_Earthwork.py @@ -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), }