Files
Aislo/B08_Quantity/B08_Quantity_Engine_Preparation_TreeWaste.py
T

182 lines
8.3 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.
"""준비공 — 임목폐기물 처리 한 줄 (톤) · 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 ×× 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 ×× 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]