Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
205 lines
8.4 KiB
Python
205 lines
8.4 KiB
Python
"""사토를 실어 내는 줄 (2026-09-08).
|
|
|
|
⚠⚠ **유토곡선이 사토를 내는데 아무도 실어 내지 않고 있었다.** 운반 줄은 띠(`bands`)와
|
|
이동(`transfers`)에서만 만들어지고 사토는 잔량(`residuals`)으로 남아 어느 쪽에도 없었다 —
|
|
그래서 구조물 잔토 126.63㎥ 를 사토에 얹어도 **덤프가 하나도 안 늘었고**, 채집석 공제도
|
|
사토를 줄이는 값이라 **끝까지 금액에 안 나타났다**.
|
|
|
|
⚠ 겨누는 것 다섯
|
|
① 사토가 있으면 줄이 선다
|
|
② 거리를 안 정했으면 **막고 사유** — 임의 거리는 그대로 금액이 된다
|
|
③ 거리를 정하면 덤프 코드가 붙고 금액이 선다
|
|
④ ⚠ 자연방토는 실어 내지 않으므로 **뺀다**
|
|
⑤ 사토가 없으면 줄도 없다
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from B08_Quantity.B08_Quantity_Engine_Handoff import build_handoff, load_mapping # noqa: E402
|
|
from B08_Quantity.B08_Quantity_Router_Earthwork import _spoil_of # noqa: E402
|
|
|
|
|
|
def 줄(**spoil: object) -> dict | None:
|
|
table = {"rows": [], "spoil": spoil}
|
|
rows = build_handoff(haul_table=table)["work_items"]
|
|
found = [row for row in rows if row["name"] == "사토 운반"]
|
|
return found[0] if found else None
|
|
|
|
|
|
def test_사토가_있으면_줄이_선다() -> None:
|
|
row = 줄(volume_m3=126.63, distance_m=None)
|
|
assert row is not None and row["quantity"] == 126.63
|
|
|
|
|
|
def test_거리를_안_정했으면_막고_사유() -> None:
|
|
"""⚠ 품셈이 정하는 값이 아니다 — 임의 거리를 넣으면 그대로 금액이 된다."""
|
|
row = 줄(volume_m3=126.63, distance_m=None)
|
|
assert row["blocked_kind"] == "input_missing"
|
|
assert row["in_bill"] is False
|
|
# ⚠ 문구가 **바뀐 자리** — 2026-09-09 사용자 확정으로 사토장이 **이미 있는 측점 위에만**
|
|
# 놓이게 되어 거리가 저절로 나온다. 그래서 「거리가 없다」가 아니라 「사토장을 안 놓았다」다.
|
|
assert "사토장을 아직 안 놓았고" in row["blocked_reason"]
|
|
|
|
|
|
def test_거리를_정하면_금액이_선다() -> None:
|
|
row = 줄(volume_m3=126.63, distance_m=1200.0)
|
|
assert row["work_item_code"] == load_mapping().for_haul("dump_truck")["work_item_code"]
|
|
assert row["haul_distance_m"] == 1200.0
|
|
assert row["in_bill"] is True
|
|
assert row["blocked_kind"] is None
|
|
|
|
|
|
def test_자연방토는_뺀다() -> None:
|
|
"""⚠ 자연방토는 실어 내지 않는 몫이다 — 함께 세면 운반이 부푼다."""
|
|
got = _spoil_of({"haul_plan": {"spoil_m3": 200.0, "natural_spoil_m3": 50.0}}, {})
|
|
assert got["volume_m3"] == 150.0
|
|
assert got["distance_m"] is None
|
|
assert "자연방토" in got["note"]
|
|
|
|
|
|
def test_공제와_가산이_끝난_값을_그대로_쓴다() -> None:
|
|
"""⚠ 여기서 또 빼거나 더하면 두 번 셈이다 — 근거 문구에만 적는다."""
|
|
got = _spoil_of(
|
|
{
|
|
"haul_plan": {
|
|
"spoil_m3": 126.63,
|
|
"natural_spoil_m3": 0.0,
|
|
"structure_spoil_added_m3": 126.63,
|
|
"collected_stone_deducted_m3": 0.0,
|
|
}
|
|
},
|
|
{"spoil_site_distance_m": 800.0},
|
|
)
|
|
assert got["volume_m3"] == 126.63
|
|
assert got["distance_m"] == 800.0
|
|
assert "구조물 잔토" in got["note"]
|
|
|
|
|
|
def test_사토가_없으면_줄도_없다() -> None:
|
|
assert 줄(volume_m3=0.0, distance_m=500.0) is None
|
|
|
|
|
|
def 줄들(**spoil: object) -> list[dict]:
|
|
rows = build_handoff(haul_table={"rows": [], "spoil": spoil})["work_items"]
|
|
return [row for row in rows if row["name"] == "사토 운반"]
|
|
|
|
|
|
def test_갈래마다_한_줄로_선다() -> None:
|
|
"""⚠ 덤프 단가가 토사·암으로 갈린다 — 합쳐 세우면 한쪽 단가로 다 물린다."""
|
|
rows = 줄들(
|
|
volume_m3=100.0,
|
|
distance_m=800.0,
|
|
by_ground_m3={"ea_m3": 40.0, "rr_m3": 60.0},
|
|
ground_unknown_m3=0.0,
|
|
)
|
|
assert {(row["ground_class"], row["quantity"]) for row in rows} == {
|
|
("리핑암", 60.0),
|
|
("토사", 40.0),
|
|
}
|
|
assert all(row["in_bill"] for row in rows)
|
|
|
|
|
|
def test_갈래를_못_붙인_몫은_따로_서고_막힌다() -> None:
|
|
"""⚠ 토사로 눅이면 임의 단가가 된다 — 거리가 있어도 이 줄은 막는다."""
|
|
rows = 줄들(
|
|
volume_m3=100.0,
|
|
distance_m=800.0,
|
|
by_ground_m3={"ea_m3": 70.0},
|
|
ground_unknown_m3=30.0,
|
|
)
|
|
unknown = next(row for row in rows if row["ground_class"] is None)
|
|
assert unknown["quantity"] == 30.0
|
|
assert unknown["in_bill"] is False
|
|
assert "지반 갈래를 못 붙인" in unknown["blocked_reason"]
|
|
|
|
|
|
def test_갈래가_안_오면_종전처럼_한_줄() -> None:
|
|
"""옛 저장분(갈래 없는 유토곡선 결과)에서도 줄이 사라지지 않는다."""
|
|
rows = 줄들(volume_m3=61.88, distance_m=None)
|
|
assert len(rows) == 1 and rows[0]["quantity"] == 61.88
|
|
|
|
|
|
def test_사토장_거리가_오면_그것이_이긴다() -> None:
|
|
"""⚠ 정확한 값이 이긴다 — 설정의 대체 거리보다 **사토장 측점까지 누가거리**가 앞선다."""
|
|
rows = 줄들(
|
|
volume_m3=100.0,
|
|
distance_m=800.0,
|
|
by_ground_m3={"ea_m3": 40.0, "rr_m3": 60.0},
|
|
distance_by_ground_m={"ea_m3": 250.0, "rr_m3": 410.0},
|
|
ground_unknown_m3=0.0,
|
|
)
|
|
got = {row["ground_class"]: row for row in rows}
|
|
assert got["토사"]["haul_distance_m"] == 250.0
|
|
assert got["리핑암"]["haul_distance_m"] == 410.0
|
|
assert all(row["in_bill"] for row in rows)
|
|
assert "사토장 측점까지" in got["토사"]["spec_detail"]
|
|
|
|
|
|
def test_사토장_거리가_없는_갈래는_대체값으로_서고_그_사실이_적힌다() -> None:
|
|
rows = 줄들(
|
|
volume_m3=100.0,
|
|
distance_m=800.0,
|
|
by_ground_m3={"ea_m3": 40.0, "rr_m3": 60.0},
|
|
distance_by_ground_m={"ea_m3": 250.0},
|
|
ground_unknown_m3=0.0,
|
|
)
|
|
got = {row["ground_class"]: row for row in rows}
|
|
assert got["리핑암"]["haul_distance_m"] == 800.0
|
|
assert "대체 거리" in got["리핑암"]["spec_detail"]
|
|
|
|
|
|
def test_사토도_자연상태로_선다() -> None:
|
|
"""⚠ 내역 수량은 자연상태 — 되돌린 값이 오면 그 값으로 선다(config 5-4-3)."""
|
|
rows = 줄들(
|
|
volume_m3=61.88,
|
|
distance_m=800.0,
|
|
by_ground_m3={"rr_m3": 61.88},
|
|
natural_m3_by_ground={"rr_m3": 53.809},
|
|
ground_unknown_m3=0.0,
|
|
)
|
|
assert rows[0]["quantity"] == 53.809
|
|
assert "자연상태" in rows[0]["spec_detail"]
|
|
|
|
|
|
def test_사토에도_적재_짝_줄이_서고_단가가_선다() -> None:
|
|
"""사토도 싣는 일이 따로(10-12 「1. 적재」) — 내역에 서는 운반 줄에만 붙고, 검산엔 안 셈."""
|
|
from B09_Estimation.B09_Estimation_BillOfQuantities import build_bill
|
|
from B09_Estimation.B09_Estimation_MachineProductivity_Dump import dump_haul_distances
|
|
from B09_Estimation.B09_Estimation_UnitPrice import cached_build
|
|
|
|
payload = build_handoff(
|
|
haul_table={
|
|
"rows": [],
|
|
"spoil": {
|
|
"volume_m3": 100.0,
|
|
"distance_m": 800.0,
|
|
"by_ground_m3": {"ea_m3": 70.0},
|
|
"ground_unknown_m3": 30.0,
|
|
},
|
|
}
|
|
)
|
|
rows = payload["work_items"]
|
|
loads = [row for row in rows if row["name"] == "덤프 적재"]
|
|
assert [(row["variant_value"], row["quantity"]) for row in loads] == [("토사", 70.0)]
|
|
assert loads[0]["haul_distance_m"] is None and loads[0]["haul_equipment"] == "dump_loading"
|
|
# 갈래 못 붙인 몫은 막힌 줄이라 짝 줄이 없음
|
|
assert len([row for row in rows if row["name"] == "사토 운반"]) == 2
|
|
|
|
bill = build_bill(payload, build=cached_build(dump_haul_m=dump_haul_distances(payload)))
|
|
got = {row.name: row for row in bill.rows if row.name in ("사토 운반", "덤프 적재")}
|
|
assert got["사토 운반"].price_code.startswith("B-FP-10-12-01#L800")
|
|
assert got["덤프 적재"].price_code == "B-FP-10-12-01#적재"
|
|
assert got["사토 운반"].amount_krw > 0 and got["덤프 적재"].amount_krw > 0
|
|
|
|
|
|
def test_되돌린_값이_없으면_다짐_그대로_두고_사유() -> None:
|
|
rows = 줄들(volume_m3=61.88, distance_m=800.0, by_ground_m3={"rr_m3": 61.88})
|
|
assert rows[0]["quantity"] == 61.88
|
|
assert "다짐상태 그대로" in rows[0]["spec_detail"]
|