"""기계 수송비 — 기계경비의 셋째 몫 (2026-09-09 밤). 기계경비 = 기계손료 + 운전경비 + **수송비**(건설품셈 8-1-6의 1)인데 수송비가 통째로 빠져 있었다. 수송비를 내는 공종 `FP-10-04` 중기운반은 표가 「미판정」이라 **단가가 안 서고** 있었다. ⚠ 겨누는 것 여섯 ① **거리가 없으면 한 줄도 안 선다** — 임의 거리로 금액이 조용히 서면 안 됨 ② 도로 구분도 마찬가지 — 속도가 도로로 갈림 ③ 사이클이 원문 그대로 — 트레일러 t1=20·t3=20·t4=0.42 / 트럭 t1=10·t3=10·t4=5 ④ 운반시간은 **왕복** · 회전율 N = 60×0.9/㎝ ⑤ 원문이 「-」로 둔 칸(트레일러 · 고속4차선)은 **지어내지 않고 사유로 냄** ⑥ 그 차량들의 운전경비가 실제로 읽혀 있음 — 앞서 표가 페이지에서 잘려 통째로 버려졌음 """ from __future__ import annotations import sys from decimal import Decimal from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) from B09_Estimation.B09_Estimation_MachineOperating import ( # noqa: E402 expand_codes, load_operating_records, ) from B09_Estimation.B09_Estimation_Transport import ( # noqa: E402 TRANSPORT_VARIANTS, TRIPS_NOTE, cycle_minutes, hours_per_trip, parse_distance_km, parse_trips, road_class, transport_amount, ) from B09_Estimation.B09_Estimation_UnitPrice import build_unit_prices # noqa: E402 _TRAILER = next(v for v in TRANSPORT_VARIANTS if v["key"] == "트레일러20ton") _TRUCK = next(v for v in TRANSPORT_VARIANTS if v["key"] == "트럭10.5ton") def test_거리가_없으면_한_줄도_안_선다() -> None: build = build_unit_prices() assert not [code for code in build.book.titles if code.startswith("B-FP-10-04")] assert any("거리" in note for note in build.transport_notes) def test_도로를_안_고르면_안_선다() -> None: build = build_unit_prices(transport_distance_km=Decimal(12)) assert not [code for code in build.book.titles if code.startswith("B-FP-10-04")] assert any("도로" in note for note in build.transport_notes) def test_사이클이_원문_그대로다() -> None: """③④ 트레일러 12㎞ 사리도로(양호) — 20 + (24÷20)×60 + 20 + 0.42 = 112.42분.""" road = road_class("gravel_good") assert road is not None minutes = cycle_minutes(_TRAILER, Decimal(12), road) assert minutes == Decimal(20) + Decimal("72") + Decimal(20) + Decimal("0.42") # N = 60×0.9/㎝ 의 역수가 회당 시간이다. hours = hours_per_trip(_TRAILER, Decimal(12), road) assert hours == minutes / (Decimal(60) * Decimal("0.9")) def test_트럭은_트럭_사이클을_쓴다() -> None: road = road_class("gravel_good") assert road is not None minutes = cycle_minutes(_TRUCK, Decimal(12), road) # 트럭은 사리도로(양호) 25㎞/hr — 10 + (24÷25)×60 + 10 + 5 assert minutes == Decimal(10) + Decimal("57.6") + Decimal(10) + Decimal(5) def test_원문이_비운_칸은_지어내지_않는다() -> None: """⑤ 트레일러는 고속4차선 값이 원문에 「-」다.""" road = road_class("expressway_4") assert road is not None and road["trailer"] is None with pytest.raises(ValueError) as caught: cycle_minutes(_TRAILER, Decimal(12), road) assert "원문에 없습니다" in str(caught.value) def test_거리_칸은_비우면_없음이다() -> None: assert parse_distance_km("") is None assert parse_distance_km(" ") is None assert parse_distance_km("0") is None assert parse_distance_km("12.5") == Decimal("12.5") with pytest.raises(ValueError): parse_distance_km("멀다") def test_거리를_넣으면_회당_단가가_선다() -> None: build = build_unit_prices(transport_distance_km=Decimal(12), transport_road="gravel_good") assert build.transport_notes == [] for variant in TRANSPORT_VARIANTS: code = f"B-FP-10-04#{variant['key']}" title = build.book.titles[code] assert title.unit == "회" money = build.book.resolve(code) assert money.total > 0 # 손료(경비)·연료(재료)·운전사(노무) 셋이 다 들어야 수송비다. assert money.material > 0 and money.labor > 0 and money.expense > 0 def test_수송_차량_운전경비가_읽힌다() -> None: """⑥ 표가 페이지에서 잘려 앞자리를 못 이어받아 39 기종이 통째로 버려지고 있었다.""" codes = {record.machine_code for record in load_operating_records().records} assert "2702-0020" in codes # 트럭 트랙터 및 평판트레일러 20ton assert "0602-0105" in codes # 덤프트럭 10.5ton def test_앞자리_이어받기는_이어받을_것이_있을_때만() -> None: """⚠ 넓히면 없는 코드를 지어낸다 — 이어받을 앞자리가 없으면 그대로 버린다.""" assert expand_codes(["0080", "0100"]) == [] assert expand_codes(["0080", "0100"], "2101") == ["2101-0080", "2101-0100"] assert expand_codes(["2101-0010", "0015"]) == ["2101-0010", "2101-0015"] # ── 회수(대수 × 왕복) — 품셈이 안 정하는 자리 (2026-09-14 브레인 판정) ── def test_회수는_설계_입력_칸이고_비우면_없음이다() -> None: """품셈 원문에 **회수 공식이 없음** — 산림품셈 10-4·건설품셈 8-1-3 은 회당 단가 산출식뿐. ⇒ 대수·왕복은 **설계자가 넣는 값**이다. 비면 `None` 이고 금액이 안 선다(지어내지 않음). """ assert parse_trips("") is None and parse_trips(None) is None assert parse_trips("0") is None # 0 회는 「안 나른다」 — 줄이 안 섬 assert parse_trips("3") == Decimal(3) assert parse_trips(" 2.5 ") == Decimal("2.5") # 반 회(편도 한 번)도 설계자가 넣을 수 있음 with pytest.raises(ValueError): parse_trips("두 번") def test_회수를_넣으면_회당_단가에_곱해진다() -> None: """회수 × 회당 단가 = 수송비. **곱하기 하나뿐** — 품셈이 안 준 규칙을 끼워 넣지 않는다.""" assert transport_amount(Decimal(12_345), None) is None assert transport_amount(Decimal(12_345), Decimal(3)) == Decimal(37_035) def test_회수_사유가_늘_붙는다() -> None: """화면이 「왜 설계자가 넣나」를 읽을 수 있어야 함 — 근거 문구에 원문 없음이 적혀 있음.""" assert "회수" in TRIPS_NOTE and "설계" in TRIPS_NOTE