· 연료 — `fuel_kind` 를 조립이 안 읽어 휘발유 기계 10종(래머·플레이트 콤팩터·믹서·커터·진동기)이 경유값이었음 → `M-FUEL-휘발유` · 시도 판도 종류대로 · 기계 하나 사용료·중기경비 장도 같은 값 · 진동기 — 건설품셈 8-3 (4611) 두 줄이 한 칸에 뭉쳐 규격·손료계수가 비었음 → 원문값 되살림(전기식 ø45 0.75㎾ 4,935 · 엔진식 ø45 2.6㎾ 5,101 · 판정 없음) · 12-15 「봉상후렉시블(45mm)」 → 엔진식 4611-0350 범위 별칭 — 12-15 는 전기·엔진을 안 적음 · 12-34-1 「3.5HP」 = 2.6㎾ · 운전경비표에 엔진식만(브레인 승인) → 구체콘크리트 갈래가 진동기 1/5.4 hr/㎥ 와 함께 섬 · 661 뒤처리 — 중기경비 장의 `#암석` 이 조합 16%·비암석 계수로 보이던 것을 보정 상각·정비·계와 본 잡품률로 · 마스터 재생성 — 12-15 갈래 키만(구체콘크리트 더함) 휘발유 기계 전수 대조: 재료비 +13~113원/hr · 조립에 선 것은 플레이트 콤팩터(+20원/hr → 9-14-02 +5원)와 진동기 · 나머지 제목 그대로 · 프로젝트 내역 그대로 · 잴 시험 일곱 빨강→초록 · 끄기 다섯 빨강 · 전체 시험 1695 + 330 통과 (시험 파일은 앞선 autopush 가 먼저 쓸어 올린 빨강본 — 이 커밋이 초록으로 맞춤) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
170 lines
7.8 KiB
Python
170 lines
7.8 KiB
Python
"""B09 원가계산 — **암석 작업 기계손료 보정** (건설품셈 8-1-7 1 · 2026-09-14 브레인 661 ①②).
|
||
|
||
원문: 「다음 건설기계가 암석굴착, 암석적재, 암석운반 등의 가혹한 작업에 사용되는 경우에는
|
||
손료(관리비 제외)를 다음과 같이 보정 가산한다」 — 불도저(19톤 이상 제외) 25 · 굴착기(무한궤도)
|
||
및 로더(무한궤도) 20 · 덤프트럭 25 (%) · [주]① 전용덤프트럭(18톤 이상)과 불도저(19톤 이상)는
|
||
보정하지 않는다(타이어·습지 불도저는 보정). 율은 `mach_base` 의 `mach_rock_adj`(원문 파싱) 한 벌.
|
||
|
||
암석 손료계수 = (상각 + 정비) × (1 + 가산) + 관리 — 1e-7 정수 아래 버림
|
||
실무 봉화 2024 「(암석)」 줄 셋이 그대로 역산됨(굴착기 1.0 0.2405 · 덤프 2.5 0.3533 · 덤프 15 0.2679)
|
||
|
||
거는 자리 암 공종(자기·부모 이름이나 갈래가 연암·보통암·경암·발파암·파쇄암·암절취·암석)의
|
||
대상 기계 줄만 — 기계 호표를 「암석」 한 벌 더 세워 부름(봉화와 같은 모양)
|
||
안 거는 것 브레이커 조합 본체(`#조합`) — 봉화 「굴삭기 0.7 브레이커조합」 손료가 비암석 23,128 + 브레이커
|
||
풍화암·호박돌 섞인 토사 — 원문 표 「암석작업(연암·보통암·경암)」 밖
|
||
전석섞인토사 10% — 혼입율(0.5㎥ 이상 전석 30% 이상) 입력이 없어 판정 못 함(② · 칸 안 만듦)
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from dataclasses import replace
|
||
from decimal import ROUND_FLOOR, Decimal
|
||
from functools import lru_cache
|
||
from typing import Any
|
||
|
||
ROCK_SUFFIX = "#암석"
|
||
_ROCK_WORDS = re.compile(r"연암|보통암|경암|발파암|파쇄암|암절취|암석")
|
||
_E7 = Decimal("1e-7")
|
||
NOT_CORRECTED = "8-1-7 [주]① {what} 은 암석 손료보정 안 함"
|
||
|
||
|
||
def _tight(text: Any) -> str:
|
||
return re.sub(r"\s", "", str(text or ""))
|
||
|
||
|
||
@lru_cache(maxsize=1)
|
||
def _sources() -> tuple[dict[str, dict[str, Any]], dict[str, int]]:
|
||
"""(기계 코드 → 손료 성분 레코드, 규칙 이름 → 암석 가산 %)."""
|
||
from B09_Estimation.B09_Estimation_MachineCost import _read_json
|
||
|
||
variables = _read_json("mach_base_2026.json")["variables"]
|
||
records = {r["machine_code"]: r for r in variables["mach_loss_coef"]["records"]}
|
||
rules = {r["machine_group"]: int(r["rock_work"]) for r in variables["mach_rock_adj"]["rules"]}
|
||
return records, rules
|
||
|
||
|
||
def rock_rate(code: str) -> tuple[int | None, str]:
|
||
"""(가산 %, 안 거는 까닭) — 표에 없는 기종은 `(None, "")`."""
|
||
from B09_Estimation.B09_Estimation_MachineCost import load_machine_catalog
|
||
|
||
machine = load_machine_catalog().machines.get(code)
|
||
if machine is None:
|
||
return None, ""
|
||
_, rules = _sources()
|
||
name = _tight(machine.name)
|
||
size = re.match(r"\d+(?:\.\d+)?", machine.specification.replace(",", ""))
|
||
tons = Decimal(size.group()) if size else Decimal(0)
|
||
if name in ("불도저(타이어)", "습지불도저"):
|
||
return rules["bulldozer_under_19_ton"], ""
|
||
if name == "불도저(무한궤도)":
|
||
if tons >= 19:
|
||
return None, NOT_CORRECTED.format(what="불도저 19톤 이상")
|
||
return rules["bulldozer_under_19_ton"], ""
|
||
if name in ("굴착기(무한궤도)", "로더(무한궤도)"):
|
||
return rules["crawler_excavator_or_loader"], ""
|
||
if name == "덤프트럭":
|
||
if tons >= 18:
|
||
return None, NOT_CORRECTED.format(what="덤프트럭 18톤 이상")
|
||
return rules["dump_truck"], ""
|
||
return None, ""
|
||
|
||
|
||
def rock_parts(code: str) -> tuple[Decimal, Decimal, Decimal, int] | None:
|
||
"""(보정 상각, 보정 정비, 관리, 계) — 1e-7 단위 · 계는 정수 아래 버림(봉화 3533.75 → 3533)."""
|
||
rate, _ = rock_rate(code)
|
||
record = _sources()[0].get(code)
|
||
if rate is None or record is None:
|
||
return None
|
||
depreciation, maintenance, management = (
|
||
Decimal(str(record[f"{key}_coefficient_1e_minus_7"]))
|
||
for key in ("depreciation", "maintenance", "management")
|
||
)
|
||
factor = 1 + Decimal(rate) / 100
|
||
raised = (depreciation * factor, maintenance * factor, management)
|
||
return (*raised, int(sum(raised).quantize(Decimal(1), rounding=ROUND_FLOOR)))
|
||
|
||
|
||
def rock_coefficient(code: str) -> Decimal | None:
|
||
"""암석 손료계수(원당) = (상각 + 정비) × (1 + 가산) + 관리 — `rock_parts` 의 계 × 1e-7."""
|
||
parts = rock_parts(code)
|
||
return None if parts is None else parts[3] * _E7
|
||
|
||
|
||
def _rock_hourly(book: Any, code: str) -> str | None:
|
||
"""`X-<코드>#암석` — 손료만 보정 계수로 바꾼 호표(연료·조종원·잡품은 본 호표 그대로)."""
|
||
from B09_Estimation.B09_Estimation_MachineCost import load_machine_catalog
|
||
from B09_Estimation.B09_Estimation_PriceBook import PriceKind, PriceTitle
|
||
from B09_Estimation.B09_Estimation_UnitPrice import _slots
|
||
|
||
hourly, rock = f"X-{code}", f"X-{code}{ROCK_SUFFIX}"
|
||
if rock in book.titles:
|
||
return rock
|
||
coefficient = rock_coefficient(code)
|
||
if coefficient is None or hourly not in book.titles:
|
||
return None
|
||
machine = load_machine_catalog().machines[code]
|
||
rate, _ = rock_rate(code)
|
||
base, rock_base = f"S-{code}", f"S-{code}{ROCK_SUFFIX}"
|
||
plain = book.titles[base]
|
||
book.add_title(
|
||
replace(
|
||
plain, code=rock_base, slots=_slots(machine.price_thousand_krw * 1000 * coefficient)
|
||
)
|
||
)
|
||
title = book.titles[hourly]
|
||
book.add_title(
|
||
PriceTitle(
|
||
code=rock,
|
||
kind=PriceKind.MACHINE_HOURLY,
|
||
name=title.name,
|
||
spec=f"{title.spec} · 암석".strip(" ·"),
|
||
unit=title.unit,
|
||
)
|
||
)
|
||
note = f"암석 작업 손료보정 — (상각 + 정비) × {100 + rate}% + 관리 = {coefficient} (건설품셈 8-1-7 1)"
|
||
for detail in book.details.get(hourly, []):
|
||
ref = {base: rock_base, hourly: rock}.get(detail.ref_code, detail.ref_code)
|
||
book.add_detail(
|
||
replace(
|
||
detail,
|
||
parent_code=rock,
|
||
ref_code=ref,
|
||
note=note if detail.ref_code == base else detail.note,
|
||
)
|
||
)
|
||
return rock
|
||
|
||
|
||
def attach_rock_loss(build: Any, master: dict[str, Any]) -> int:
|
||
"""암 공종의 대상 기계 줄을 암석 호표로 바꿔 닮 — 바꾼 줄 수. 조합 16% 바꿔 달기 **뒤**에 부름."""
|
||
nodes = {str(n.get("work_item_code")): n for n in master.get("work_items", [])}
|
||
book = build.book
|
||
changed = 0
|
||
for title_code in [code for code in book.titles if code.startswith("B-")]:
|
||
work_item, _, variant = title_code[2:].partition("#")
|
||
node = nodes.get(work_item) or {}
|
||
parent = nodes.get(str(node.get("parent_code"))) or {}
|
||
title = book.titles[title_code]
|
||
text = " ".join(map(str, (node.get("name"), parent.get("name"), title.name, variant)))
|
||
if not _ROCK_WORDS.search(text):
|
||
continue
|
||
own = book.details.get(title_code) or []
|
||
owners = [title_code, *(d.ref_code for d in own if d.ref_code.startswith("D-"))]
|
||
for owner in owners:
|
||
details = book.details.get(owner) or []
|
||
for index, detail in enumerate(details):
|
||
ref = detail.ref_code
|
||
if not ref.startswith("X-") or "#" in ref:
|
||
continue
|
||
rate, why = rock_rate(ref[2:])
|
||
if rate is None:
|
||
if why and why not in detail.note:
|
||
details[index] = replace(detail, note=f"{detail.note} · {why}".strip(" ·"))
|
||
continue
|
||
rock = _rock_hourly(book, ref[2:])
|
||
if rock:
|
||
details[index] = replace(detail, ref_code=rock)
|
||
changed += 1
|
||
return changed
|