feat(B09): 기계 수송비를 세움 — 기계경비의 셋째 몫이 통째로 빠져 있었음

기계경비 = 기계손료 + 운전경비 + 수송비인데 수송비가 없었음. 수송비를 내는 공종
FP-10-04 중기운반은 표가 미판정이라 단가가 아예 안 섰음.

- 산림품셈 10-4 사이클 그대로 — 트레일러(20TON) t1=20·t3=20·t4=0.42 /
  트럭(10.5TON) t1=10·t3=10·t4=5 · ㎝=t1+t2+t3+t4 · N=60×0.9/㎝, 단위는 회당.
- 거리·도로 구분은 설계 입력 — 비면 줄이 안 서고 사유만 남음(임의 거리 금지).
- 원문이 「-」로 둔 칸(트레일러·고속4차선)은 지어내지 않고 사유로 냄.
- ⚠ 우리가 정한 둘을 화면 근거에 적음 — 속도를 8-1-6의 2 나 이동속도표에서 가져온 것,
  운반시간을 왕복으로 본 것.
- 곁다리: 운전경비 표가 페이지에서 잘려 앞자리를 못 이어받아 39 기종이 통째로
  버려지고 있었음(그 안에 수송 차량 2702 가 있었음). 이어받되 카탈로그에 있는
  코드일 때만 받게 함 — 운전경비 줄 92 → 158.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-09 22:23:06 +09:00
co-authored by Claude Opus 5
parent 08c0ec3134
commit fdc30ecf17
6 changed files with 473 additions and 6 deletions
+116
View File
@@ -0,0 +1,116 @@
"""기계 수송비 — 기계경비의 셋째 몫 (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,
cycle_minutes,
hours_per_trip,
parse_distance_km,
road_class,
)
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"]