Files
Aislo/B09_Estimation/B09_Estimation_Transport.py
T

192 lines
8.4 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.
"""B09 원가계산 — **기계 수송비** (산림품셈 10-4 중기운반 · 건설품셈 8-1-6의 2).
**무엇이 빠져 있었나** — 기계경비는 「기계손료 + 운전경비 + **수송비**」인데(건설품셈 8-1-6의 1),
우리 시간당 사용료 층에는 손료·연료·운전사만 있었다. 수송비를 내는 공종 `FP-10-04` 중기운반은
표가 「미판정」으로 남아 **단가가 아예 안 서고 있었다**(2026-09-09 밤 실측).
**원문이 정하는 것** (지어낸 값이 하나도 없다)
산림품셈 10-4 중기운반 (단위: **회당**)
트레일러운반(20TON) t1=20min · t3=20min · t4=0.42min · t2=운반시간 참조
트럭운반(10.5TON) t1=10min · t3=10min · t4=5min · t2=운반시간 참조
㎝ = t1+t2+t3+t4 · N = 60×0.9/㎝
[주] **자주식 기계는 제외**하며 **인근 기초자치단체와 현장중심점까지의 거리**로 계상한다.
건설품셈 8-1-6의 2 가 — 「가장 가까운 시·도·군·구청 소재지로부터 공사현장까지의 **왕복**
수송비」 · 나 — 자주식 기계의 이동속도표(도로 구분별 km/hr)
**우리가 정한 것 둘 — 원문이 안 정해 화면 근거에 그대로 적는다**
㉠ t2(운반시간)의 속도를 **8-1-6의 2 나 이동속도표**에서 가져온다.
그 표는 「자주식 기계가 자주로 이동할 때」의 표이고, 운반 차량의 주행속도로 쓰라는
말은 원문에 없다. 다만 **같은 장에서 같은 도로 구분으로 준 유일한 속도**다.
㉡ t2 를 **왕복**으로 본다(건설품셈이 「왕복 수송비」라 못 박으므로).
⚠ **거리는 설계 입력이다** — 안 넣으면 **줄이 안 선다**(사토장 운반거리와 같은 자리).
임의 거리를 넣으면 금액이 조용히 서므로 **비면 사유만 남긴다.**
⚠ **회수(몇 대를 몇 번 나르나)는 여기서 안 정한다** — 단가는 「회당」이고, 회수는 수량 쪽
(설계 입력)이다. 품셈이 대수·회수를 정해 주지 않는다.
"""
from __future__ import annotations
from decimal import Decimal
from typing import Any
from B09_Estimation.B09_Estimation_PriceBook import PriceBook, PriceDetail, PriceKind, PriceTitle
_ZERO = Decimal(0)
#: 수송 갈래 — 원문 10-4 가 준 둘. 기종 코드는 카탈로그(건설품셈 8-3) 분류번호.
TRANSPORT_VARIANTS: tuple[dict[str, Any], ...] = (
{
"key": "트레일러20ton",
"label": "트레일러운반(20TON)",
"machine_code": "2702-0020",
"t1_min": Decimal(20),
"t3_min": Decimal(20),
"t4_min": Decimal("0.42"),
"speed_column": "trailer",
},
{
"key": "트럭10.5ton",
"label": "트럭운반(10.5TON)",
"machine_code": "0602-0105",
"t1_min": Decimal(10),
"t3_min": Decimal(10),
"t4_min": Decimal(5),
"speed_column": "dump_truck",
},
)
#: 이동속도(km/hr) — 건설품셈 8-1-6의 2 나 「자주식 건설기계의 이동속도」.
#: ⚠ `None` 은 원문의 「-」다 — **그 도로 구분에 그 차량 값이 없다는 뜻**이라 지어내지 않는다.
ROAD_CLASSES: tuple[dict[str, Any], ...] = (
{"key": "expressway_4", "label": "포장도로(고속4차선)", "dump_truck": 60, "trailer": None},
{"key": "expressway_2", "label": "포장도로(고속2차선)", "dump_truck": 50, "trailer": 50},
{"key": "paved", "label": "포장도로", "dump_truck": 40, "trailer": 40},
{"key": "gravel_good", "label": "사리도로(양호)", "dump_truck": 25, "trailer": 20},
{"key": "gravel_poor", "label": "사리도로(불량)", "dump_truck": 10, "trailer": 10},
)
_ROAD_BY_KEY = {row["key"]: row for row in ROAD_CLASSES}
#: 회전율 — 원문 `N = 60 × 0.9 / ㎝`. 0.9 는 작업효율이고 60 은 시간→분이다.
_CYCLE_EFFICIENCY = Decimal("0.9")
_MINUTES_PER_HOUR = Decimal(60)
WORK_ITEM_CODE = "FP-10-04"
UNIT = "회"
BASIS_TEXT = (
"산림품셈 10-4 중기운반(회당) · 건설품셈 8-1-6의 2 — 가장 가까운 시·도·군·구청 소재지에서"
" 현장까지 왕복. [주] 자주식 기계는 제외."
)
ASSUMPTION_TEXT = (
"⚠ 원문이 안 정해 우리가 정한 둘 — ㉠ 운반시간의 속도는 8-1-6의 2 나 이동속도표에서 가져옴"
"(그 표는 자주식 이동표라 쓰임이 꼭 같지는 않음) · ㉡ 운반시간을 왕복으로 봄."
)
def parse_distance_km(raw: Any) -> Decimal | None:
"""설정 칸의 거리. **비면 `None`**(= 수송비 줄이 안 섬)."""
text = str(raw or "").strip().rstrip("km").rstrip("㎞").strip()
if not text:
return None
try:
value = Decimal(text)
except (ArithmeticError, ValueError):
raise ValueError(f"수송 거리를 숫자로 못 읽었습니다: {raw!r}") from None
if value <= 0:
return None
return value
def road_class(key: str | None) -> dict[str, Any] | None:
"""도로 구분. 못 고르면 `None` — **기본 도로를 우리가 정하지 않는다.**"""
return _ROAD_BY_KEY.get(str(key or ""))
def cycle_minutes(variant: dict[str, Any], distance_km: Decimal, road: dict[str, Any]) -> Decimal:
"""㎝ = t1 + t2 + t3 + t4 (분). t2 는 **왕복** 운반시간이다."""
speed = road.get(variant["speed_column"])
if not speed:
raise ValueError(
f"{road['label']} 에는 {variant['label']} 속도가 원문에 없습니다(「-」)"
" — 지어내지 않습니다"
)
round_trip_hours = distance_km * Decimal(2) / Decimal(str(speed))
t2 = round_trip_hours * _MINUTES_PER_HOUR
return variant["t1_min"] + t2 + variant["t3_min"] + variant["t4_min"]
def hours_per_trip(variant: dict[str, Any], distance_km: Decimal, road: dict[str, Any]) -> Decimal:
"""회당 시간(시간). `N = 60 × 0.9 / ㎝` 의 역수다."""
minutes = cycle_minutes(variant, distance_km, road)
trips_per_hour = _MINUTES_PER_HOUR * _CYCLE_EFFICIENCY / minutes
return Decimal(1) / trips_per_hour
def attach_transport(
book: PriceBook,
names: dict[str, str],
*,
distance_km: Decimal | None,
road_key: str | None,
) -> list[str]:
"""수송비 일위대가(갈래 둘)를 세운다. 돌려주는 것 — 못 세운 사유 목록.
⚠ **거리·도로 구분이 없으면 한 줄도 안 세운다** — 값이 없는데 금액이 서면 안 된다.
"""
notes: list[str] = []
if distance_km is None:
notes.append(
"수송비 — 거리(인근 시·군·구청 소재지 → 현장)가 안 들어와 줄을 안 세웠습니다."
" 산출 조건에서 거리를 넣으면 섭니다."
)
return notes
road = road_class(road_key)
if road is None:
notes.append("수송비 — 도로 구분을 안 골라 줄을 안 세웠습니다(속도가 도로로 갈립니다).")
return notes
base_name = names.get(WORK_ITEM_CODE) or "중기운반"
for variant in TRANSPORT_VARIANTS:
hourly_code = f"X-{variant['machine_code']}"
if hourly_code not in book.titles:
notes.append(f"수송비 {variant['label']} — 그 차량의 시간당 사용료 층이 없습니다.")
continue
try:
hours = hours_per_trip(variant, distance_km, road)
except ValueError as error:
notes.append(f"수송비 {variant['label']}{error}")
continue
title_code = f"B-{WORK_ITEM_CODE}#{variant['key']}"
if title_code in book.titles:
continue
book.add_title(
PriceTitle(
code=title_code,
kind=PriceKind.UNIT_PRICE,
name=f"{base_name} ({variant['label']})",
spec=f"{road['label']} · 편도 {distance_km:g}㎞",
unit=UNIT,
)
)
minutes = cycle_minutes(variant, distance_km, road)
book.add_detail(
PriceDetail(
title_code,
hourly_code,
hours,
note=(
f"㎝ = {variant['t1_min']:g} + 왕복 {distance_km * 2:g}㎞"
f{road[variant['speed_column']]}㎞/hr + {variant['t3_min']:g}"
f" + {variant['t4_min']:g} = {minutes:.2f}분 · N = 60×0.9/㎝"
f" ⇒ 회당 {hours:.4f}시간 · {BASIS_TEXT} · {ASSUMPTION_TEXT}"
),
)
)
return notes