기계경비 = 기계손료 + 운전경비 + 수송비인데 수송비가 없었음. 수송비를 내는 공종 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>
117 lines
5.0 KiB
Python
117 lines
5.0 KiB
Python
"""기계 수송비 — 기계경비의 셋째 몫 (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"]
|