Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
182 lines
8.3 KiB
Python
182 lines
8.3 KiB
Python
"""준비공 — 임목폐기물 처리 한 줄 (톤) · 2026-09-14 판정 「폐기물처리비」.
|
||
|
||
⭐ 수량 산식 — 한국건설기술연구원 「건설현장 임목폐기물의 재활용 및 자원화 기술개발」(2012) 3.2
|
||
= 환경친화적인 도로건설 지침 부록1 문단 493~508 「3) 임목폐기물」(산출근거: 조경설계요람)
|
||
지상부 WA = k × 3.14 × (B/2)² × h × (1+p) × W1 × N (kg · 지침 식이 π 를 3.14 로 적음)
|
||
뿌리부 — 지침이 두 방법을 둔다. 고르는 칸.
|
||
① 분배비(기본) WR = WA × 15/85 (KICT 일반 수목류 — 2026-09-14 판정 유지)
|
||
② 뿌리분 체적 WR = 0.3927 × D³ × 1,300 × N (지침 「V = πD²(D/2 + RH2/3) = 0.3927D³」
|
||
· 「UW1 뿌리부분의 단위중량 1,300kg/m3」). ⚠ D 는 지침이 「뿌리분의 직경(DBH, m)」으로
|
||
적었으나 2026-09-14 판정으로 **뿌리분 직경**(흉고직경 아님) — 따로 받고 기본값 없음.
|
||
k 수간 형상계수 0.5 · p 지엽 보합률(임목 0.3) — **산식 상수**라 칸이 없다.
|
||
B 평균 흉고직경 · h 평균 수고 · W1 수목 단위체적중량 · N 본수 —
|
||
**현장 조사값**이라 칸으로 받는다.
|
||
N = 표본지 1,000㎡ 실측 본수 × 대상 면적 ÷ 1,000 (실정보고 예 「134/1000 × 7,450」 모양).
|
||
대상 면적 = 벌목·지장목제거 면적(같은 나무를 벤 자리).
|
||
⚠ 실무 실정보고(임목폐기물처리, 한 현장)는 뿌리 14:86 · 무게 0.65톤/㎥ ·
|
||
처리비를 공급가액 뒤(승률 밖)에 두었다 — 판정은 법 문언·공인 자료 쪽(PLAN 10장).
|
||
발주처가 요구하면 칸으로 열 자리.
|
||
|
||
⚠ 금액은 **처리단가가 없어** 못 선다(KCRA 중간처리단가 미확보) — 수동 단가를 넣으면
|
||
금액이 서되 「미확정」으로 센다(빨간 테두리). 비목은 **경비**라 내역 줄이 아니고
|
||
원가계산서 폐기물처리비 줄로 간다(예정가격작성기준 제19조③18호).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import math
|
||
from typing import Any
|
||
|
||
from B08_Quantity.B08_Quantity_Engine_Preparation_Status import STATUS_PENDING, STATUS_READY
|
||
|
||
TREE_WASTE_ITEM = "임목폐기물 처리"
|
||
STEM_FORM_FACTOR = 0.5 # k
|
||
FOLIAGE_ALLOWANCE = 0.3 # p — 임목(고립목은 1.0)
|
||
PI_AS_WRITTEN = 3.14 # 지침 부록1 식 원문 · 실정보고도 3.14
|
||
ROOT_SHARE, ABOVE_SHARE = 15.0, 85.0 # 일반 수목류 분배비
|
||
ROOT_BALL_FACTOR = 0.3927 # V = 0.3927 D³ (지침 부록1)
|
||
ROOT_BALL_UNIT_WEIGHT = 1300.0 # ㎏/㎥ (지침 부록1 UW1)
|
||
ROOT_METHOD_RATIO, ROOT_METHOD_BALL = "ratio", "root_ball"
|
||
|
||
#: 칸 이름 · 화면 이름 · 단위 — 모두 채워야 수량이 선다.
|
||
TREE_WASTE_INPUTS: tuple[tuple[str, str], ...] = (
|
||
("stems_per_1000m2", "표본지 1,000㎡당 본수"),
|
||
("dbh_cm", "평균 흉고직경(㎝)"),
|
||
("height_m", "평균 수고(m)"),
|
||
("unit_weight_kg_m3", "단위체적중량(㎏/㎥)"),
|
||
)
|
||
TREE_WASTE_BASIS = (
|
||
"환경친화적인 도로건설 지침 부록1 · 한국건설기술연구원(2012) 3.2 — k 0.5 · p 0.3(임목) · π 3.14"
|
||
" · N = 표본지 1,000㎡ 본수 × 벌목·지장목제거 면적"
|
||
)
|
||
ROOT_RATIO_BASIS = "뿌리 WR = WA × 15/85(분배비 · 일반 수목류)"
|
||
ROOT_BALL_BASIS = "뿌리 WR = 0.3927 × D³ × 1,300㎏/㎥ × N(뿌리분 체적 · 지침 부록1)"
|
||
ROOT_BALL_LABEL = "뿌리분 직경 D(m, 흉고직경 아님 · 굴취 흙덩이 직경)"
|
||
#: 배출자 신고 경계 — 화면이 알릴 뿐 값을 바꾸지 않는다.
|
||
REPORT_THRESHOLD_TON = 5.0
|
||
NOT_SEPARATE_NOTE = (
|
||
"임목폐기물은 분리발주 대상 아님(환경부 질의회신 — 임목폐기물처리 실정보고 p4 ·"
|
||
" 건설폐기물 업무처리지침: 건설폐기물 아닌 사업장 일반폐기물)"
|
||
)
|
||
EXPENSE_NOTE = (
|
||
"비목은 경비(예정가격작성기준 제19조③18호) — 내역 줄이 아니라 원가계산서 폐기물처리비로 감"
|
||
)
|
||
|
||
|
||
def _positive(value: Any) -> float | None:
|
||
try:
|
||
number = float(value)
|
||
except (TypeError, ValueError):
|
||
return None
|
||
return number if number > 0 else None
|
||
|
||
|
||
def _missing(inputs: dict[str, Any], root_method: str) -> list[str]:
|
||
"""비어 있는 칸 이름 — 뿌리분 체적법이면 뿌리분 직경도 든다."""
|
||
missing = [label for key, label in TREE_WASTE_INPUTS if _positive(inputs.get(key)) is None]
|
||
if root_method == ROOT_METHOD_BALL and _positive(inputs.get("root_ball_diameter_m")) is None:
|
||
missing.append(ROOT_BALL_LABEL)
|
||
return missing
|
||
|
||
|
||
def tree_waste_tons(
|
||
area_m2: float, inputs: dict[str, Any], root_method: str = ROOT_METHOD_RATIO
|
||
) -> tuple[float, float] | None:
|
||
"""(지상부 톤, 뿌리부 톤). 칸이 하나라도 비거나 면적이 0 이면 `None`."""
|
||
if area_m2 <= 0 or _missing(inputs, root_method):
|
||
return None
|
||
stems = float(inputs["stems_per_1000m2"]) * area_m2 / 1000.0
|
||
diameter = float(inputs["dbh_cm"]) / 100.0
|
||
above_kg = (
|
||
STEM_FORM_FACTOR
|
||
* PI_AS_WRITTEN
|
||
* (diameter / 2.0) ** 2
|
||
* float(inputs["height_m"])
|
||
* (1.0 + FOLIAGE_ALLOWANCE)
|
||
* float(inputs["unit_weight_kg_m3"])
|
||
* stems
|
||
)
|
||
if root_method == ROOT_METHOD_BALL:
|
||
ball = float(inputs["root_ball_diameter_m"])
|
||
root_kg = ROOT_BALL_FACTOR * ball**3 * ROOT_BALL_UNIT_WEIGHT * stems
|
||
else:
|
||
root_kg = above_kg * ROOT_SHARE / ABOVE_SHARE
|
||
return above_kg / 1000.0, root_kg / 1000.0
|
||
|
||
|
||
def tree_waste_rows(
|
||
slope_totals: dict[str, float] | None,
|
||
inputs: dict[str, Any] | None,
|
||
unit_price_krw_per_ton: Any = None,
|
||
separate_order: Any = False,
|
||
root_method: Any = None,
|
||
) -> list[dict[str, Any]]:
|
||
"""임목폐기물 처리 한 줄 — 값이 없어도 줄은 선다(무엇을 넣으면 풀리는지 적음)."""
|
||
slope = slope_totals or {}
|
||
area = float(slope.get("tree_removal_fill", 0.0)) + float(slope.get("tree_removal_cut", 0.0))
|
||
inputs = inputs or {}
|
||
method = ROOT_METHOD_BALL if root_method == ROOT_METHOD_BALL else ROOT_METHOD_RATIO
|
||
root_basis = ROOT_BALL_BASIS if method == ROOT_METHOD_BALL else ROOT_RATIO_BASIS
|
||
base = {
|
||
"group": "준비공",
|
||
"item": TREE_WASTE_ITEM,
|
||
"unit": "톤",
|
||
"work_item_code": None,
|
||
# 경비 — 내역에 세우지 않는다(인계가 이 칸을 본다).
|
||
"bill": False,
|
||
"bill_reason": EXPENSE_NOTE,
|
||
"separate_order": bool(separate_order),
|
||
}
|
||
tons = tree_waste_tons(area, inputs, method)
|
||
if tons is None:
|
||
missing = _missing(inputs, method)
|
||
why = (
|
||
f"현장 조사값이 비어 있음 — {' · '.join(missing)}"
|
||
if missing
|
||
else "벌목·지장목제거 면적이 0 ㎡ — 사면표가 아직 안 섬"
|
||
)
|
||
return [
|
||
{
|
||
**base,
|
||
"amount": None,
|
||
"status": STATUS_PENDING,
|
||
"reason": f"{why}. 산식: {TREE_WASTE_BASIS} · {root_basis}",
|
||
"reference_amount": area,
|
||
}
|
||
]
|
||
above, root = tons
|
||
total = above + root
|
||
notes = [
|
||
f"지상부 WA {above:,.2f}톤 + 뿌리부 WR {root:,.2f}톤",
|
||
TREE_WASTE_BASIS,
|
||
root_basis,
|
||
EXPENSE_NOTE,
|
||
NOT_SEPARATE_NOTE,
|
||
]
|
||
if total >= REPORT_THRESHOLD_TON:
|
||
notes.append("5톤 이상 — 배출자 신고 대상 사업장(폐기물관리법 시행령 제2조8호)")
|
||
price = _positive(unit_price_krw_per_ton)
|
||
row = {
|
||
**base,
|
||
"amount": total,
|
||
"status": STATUS_READY,
|
||
"reference_amount": area,
|
||
"above_ground_ton": above,
|
||
"root_ton": root,
|
||
"unit_price_krw_per_ton": price,
|
||
"amount_krw": math.floor(total * price) if price else None,
|
||
# 수동 단가로 선 금액 — 화면 「미확정 N건」·빨간 테두리.
|
||
"unconfirmed": 1 if price else 0,
|
||
}
|
||
if price:
|
||
notes.append(
|
||
f"처리비 {total:,.2f}톤 × {price:,.0f}원 = {math.floor(total * price):,}원"
|
||
" (⚠ 수동 단가 — 미확정 · KCRA 중간처리단가 미확보)"
|
||
)
|
||
else:
|
||
notes.append(
|
||
"처리단가 없음(KCRA 중간처리단가 미확보) — 산출 조건에 수동으로 넣으면 금액이 섬"
|
||
)
|
||
row["reason"] = " · ".join(notes)
|
||
return [row]
|