Files
Aislo/resources/tester/test_b09_transport.py
T
eomsangdonandClaude Opus 5 1400b7b761 feat(b09): 기계 수송비 회수(대수 × 왕복) 설계 입력 칸 — 원문에 공식이 없어 사유와 함께 엶
산림품셈 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
2026-09-14 20:13:25 +09:00

147 lines
6.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""기계 수송비 — 기계경비의 셋째 몫 (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