산림품셈 10-4 · 건설품셈 8-1-3 은 회당 단가 산출식만 주고 대수·횟수를 정하지 않음(전수 확인). 지어내지 않고 설계 입력으로 닫음. · parse_trips — 비거나 0 이면 없음. 기본값을 두지 않음(1 회로 안 때움) · transport_amount — 회수 × 회당 단가. 회수가 없으면 None(0 원으로 안 채움) · TRIPS_NOTE — 「원문에 공식 없음 · 설계 입력 · 비우면 금액이 안 섬」 사유 · 도는 자리 넷 — 저장(PUT estimation/factors) · 산출 조건 ② · 산출 조건 화면(회당 N원 × M회) · 비면 transport_notes 에 사유가 붙어 못 채운 자리로 뜸 · 화면에 있던 「회수는 여기서 안 정합니다」 안내는 칸으로 바뀌어 걷음 잴 시험 먼저 빨강 확인한 뒤 고침. 전체 시험 1976 통과 · 28 건너뜀 · 1 xfail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Prk9BCHG1EMAywk9k8wegA
147 lines
6.5 KiB
Python
147 lines
6.5 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,
|
||
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
|