· 버리던 세 자리: 규격이 이름 앞인 장비 칸(「0.8㎥ 굴착기 | h」 가 머리글로 걸러짐) · 값·이름 둘 다 못 읽은 줄(「디젤엔진(15HP) | 11.19kW」 · 「연료비 | ps × 0.253ℓ × 6h」) · 수량 빈 연료 줄(「연료비 | 경유 | L」) · 연료·휘발유·경유·엔진·기관·양수기·발전기 이름이거나 시간·대·kW·HP 단위인 줄만 — 머리 줄·설명 줄은 그대로 · 사유 「기계·연료 줄이 안 붙음」 · 파쇄 모듈은 제가 쓰는 파쇄기 줄의 사유를 걷음 · ⚠ 처음 승인받은 「_names_a_machine 을 모든 공종으로」 는 재어 보니 0 을 막아 정정 보고 뒤 이 자리로 새로 사유가 붙은 줄 8 · 사라진 줄 0 · 새로 막힌 공종 5(12-26 물푸기 · 14-1 · 14-2 근주이식 · 10-8-3 케이블크레인 운전 · 10-7-4 모노레일) · 제목 금액 변화 0 · 프로젝트 내역 그대로 · 끄기 둘 빨강 · 전체 시험 1712 + 374 통과 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
122 lines
5.4 KiB
Python
122 lines
5.4 KiB
Python
"""B09 원가계산 — **이동식 임목 파쇄** 8-11 (2026-09-15 브레인 판정 · ㉰ 둘째).
|
||
|
||
원문 L4651 표: 이동식 임목 파쇄기 93.25KW · Q = 3.5 ㎥/hr · 비고 「잡재료 : 주연료비의 16% · 소모품비(파쇄기날)
|
||
0.00125개/hr」 · [주]① 1일 8시간 ② 1일 기계운전자 1인·보통인부 2인 ③ 장비 운반비 별도 ④ 우드그랩 별도
|
||
⑤ 연료 「10.8 + 16.3ℓ / 2 = 13.5ℓ (디젤)」.
|
||
|
||
⚠ 건설품셈 운전경비표(8-4 L3458) 7205-0125 93.25㎾ 줄은 연료·잡품 칸이 「-」 → 운전경비 레코드가 없어
|
||
기계 층이 안 서고 일위대가도 조용히 없었음. 8-11 [주]⑤·비고가 **유일한 값**(부딪히는 원문 없음)이라 그대로 씀.
|
||
기계운전자는 기계 층 조종원(건설기계운전사 · 8-4 잠정 규칙과 같은 직종)으로 한 번만 셈.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from decimal import Decimal
|
||
from typing import Any
|
||
|
||
CODE = "FP-08-11"
|
||
MACHINE = "7205-0125"
|
||
BLADE = "AR-M-249d0a01"
|
||
TABLE = "F0237"
|
||
#: [주]⑤ — 표 칸이 아니라 [주] 에만 있어 마스터 원문 줄에 안 실림. 원문 그대로 한 곳.
|
||
FUEL_LITERS_PER_HOUR = Decimal("13.5")
|
||
#: [주]① 1일 8시간 · [주]② 1일 보통인부 2인(기계운전자 1인은 기계 층 조종원).
|
||
HOURS_PER_DAY = Decimal(8)
|
||
LABORERS_PER_DAY = Decimal(2)
|
||
LABORER_CODE = "1002" # 보통인부
|
||
BLADE_MISSING = "파쇄기날 — 규격·단가 없음 · 「자재 단가」 탭에서 넣으면 0.00125개/hr ÷ Q 로 붙음"
|
||
|
||
_RE_Q = re.compile(r"Q\s*=\s*(\d+(?:\.\d+)?)")
|
||
_RE_MISC = re.compile(r"주연료비의\s*(\d+(?:\.\d+)?)\s*%")
|
||
_RE_BLADE = re.compile(r"(\d+(?:\.\d+)?)\s*개\s*/\s*hr")
|
||
|
||
|
||
def _table_values(node: dict[str, Any]) -> tuple[Decimal, Decimal, Decimal] | None:
|
||
"""(Q ㎥/hr, 잡재료 %, 파쇄기날 개/hr) — 표 한 줄에서. 칸이 달라지면 `None`."""
|
||
table = next((t for t in node.get("tables") or [] if t.get("pum_table_id") == TABLE), {})
|
||
text = " ".join(str(c) for row in table.get("raw_row") or [] for c in row)
|
||
found = [pattern.search(text) for pattern in (_RE_Q, _RE_MISC, _RE_BLADE)]
|
||
if not all(found):
|
||
return None
|
||
return tuple(Decimal(m.group(1)) for m in found) # type: ignore[return-value]
|
||
|
||
|
||
def operating_record(node: dict[str, Any] | None = None):
|
||
"""7205-0125 운전경비 — 8-11 [주]⑤ 연료 · 비고 잡재료 · [주]② 기계운전자 1인."""
|
||
from B09_Estimation.B09_Estimation_MachineOperating import (
|
||
_CATALOG_SUBPATH,
|
||
OperatingRecord,
|
||
_operator_code,
|
||
_read_json,
|
||
)
|
||
from B09_Estimation.B09_Estimation_ResourceAxis import load_work_item_master
|
||
|
||
if node is None:
|
||
node = next(n for n in load_work_item_master()["work_items"] if n["work_item_code"] == CODE)
|
||
values = _table_values(node)
|
||
if values is None:
|
||
return None
|
||
aliases = _read_json(*_CATALOG_SUBPATH, "labor_const_2026-01-01.json")["variables"]["aliases"]
|
||
return OperatingRecord(
|
||
machine_code=MACHINE,
|
||
machine_name="이동식 임목파쇄기",
|
||
specification="93.25",
|
||
fuel_liters_per_hour=FUEL_LITERS_PER_HOUR,
|
||
fuel_kind="경유",
|
||
misc_material_percent=values[1],
|
||
operator_person_days=Decimal(1),
|
||
operator_occupation_code=_operator_code("이동식 임목파쇄기", aliases),
|
||
)
|
||
|
||
|
||
def attach_wood_chipping(build: Any, nodes_by_code: dict[str, dict[str, Any]]) -> None:
|
||
"""`B-FP-08-11` ㎥당 — 파쇄기 1/Q(D) · 보통인부 2인 ÷ 8시간 ÷ Q · 파쇄기날(단가가 들면)."""
|
||
from B09_Estimation.B09_Estimation_PriceBook import PriceDetail, PriceKind, PriceTitle
|
||
|
||
node = nodes_by_code.get(CODE)
|
||
values = _table_values(node or {})
|
||
book = build.book
|
||
uses = build.material_uses.setdefault(BLADE, [])
|
||
if CODE not in uses:
|
||
uses.append(CODE)
|
||
if node is None or values is None or f"X-{MACHINE}" not in book.titles:
|
||
build.component_gaps[CODE] = "8-11 표 칸이 달라졌거나 파쇄기 기계 층이 안 섬"
|
||
return
|
||
q, _misc, blade_per_hour = values
|
||
title = f"B-{CODE}"
|
||
book.add_title(
|
||
PriceTitle(code=title, kind=PriceKind.UNIT_PRICE, name=str(node.get("name")), unit="㎥")
|
||
)
|
||
book.add_output_detail(
|
||
title, f"X-{MACHINE}", Decimal(1) / q, f"Q = {q} ㎥/hr (산림품셈 8-11)", output=q
|
||
)
|
||
book.add_detail(
|
||
PriceDetail(
|
||
title,
|
||
LABORER_CODE,
|
||
LABORERS_PER_DAY / HOURS_PER_DAY / q,
|
||
note=f"[주]② 1일 보통인부 {LABORERS_PER_DAY}인 ÷ [주]① {HOURS_PER_DAY}시간 ÷ Q {q}",
|
||
)
|
||
)
|
||
# 표의 파쇄기 줄은 이 모듈이 씀 — 자원 축이 「기계·연료 줄이 안 붙음」 으로 올린 것을 걷음.
|
||
labels = [
|
||
label
|
||
for label in build.unattached.get(CODE, [])
|
||
if "파쇄기날" not in label and "".join(label.split()) != "이동식임목파쇄기"
|
||
]
|
||
if "파쇄기" in (build.component_gaps.get(CODE) or ""):
|
||
build.component_gaps.pop(CODE)
|
||
build.partial_ratio.pop(CODE, None)
|
||
if BLADE in book.titles:
|
||
book.add_detail(
|
||
PriceDetail(
|
||
title, BLADE, blade_per_hour / q, note=f"파쇄기날 {blade_per_hour}개/hr ÷ Q"
|
||
)
|
||
)
|
||
else:
|
||
labels.append(BLADE_MISSING)
|
||
build.unattached[CODE] = labels
|
||
if CODE in build.skipped:
|
||
build.skipped.remove(CODE)
|