기계경비 = 기계손료 + 운전경비 + 수송비인데 수송비가 없었음. 수송비를 내는 공종 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>
192 lines
8.4 KiB
Python
192 lines
8.4 KiB
Python
"""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
|