"""준비공 — 임목폐기물 처리 한 줄 (톤) · 2026-09-14 판정 「폐기물처리비」. ⭐ 수량 산식 — 한국건설기술연구원 「건설현장 임목폐기물의 재활용 및 자원화 기술개발」(2012) 3.2 지상부 WA = k × π × (B/2)² × h × (1+p) × W1 × N (kg) 뿌리부 WR = WA × 뿌리 분배비 ÷ 지상 분배비 = WA × 15/85 (일반 수목류, 같은 보고서 표) k 수간 형상계수 0.5 · p 지엽 보합률(임목 0.3) — **산식 상수**라 칸이 없다. B 평균 흉고직경 · h 평균 수고 · W1 수목 단위체적중량 · N 본수 — **현장 조사값**이라 칸으로 받는다. N = 표본지 1,000㎡ 실측 본수 × 대상 면적 ÷ 1,000 (실정보고 예 「134/1000 × 7,450」 모양). 대상 면적 = 벌목·지장목제거 면적(같은 나무를 벤 자리). ⚠ 금액은 **처리단가가 없어** 못 선다(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) ROOT_SHARE, ABOVE_SHARE = 15.0, 85.0 # 일반 수목류 분배비 #: 칸 이름 · 화면 이름 · 단위 — 모두 채워야 수량이 선다. TREE_WASTE_INPUTS: tuple[tuple[str, str], ...] = ( ("stems_per_1000m2", "표본지 1,000㎡당 본수"), ("dbh_cm", "평균 흉고직경(㎝)"), ("height_m", "평균 수고(m)"), ("unit_weight_kg_m3", "단위체적중량(㎏/㎥)"), ) TREE_WASTE_BASIS = ( "한국건설기술연구원(2012) 3.2 임목 발생량 — k 0.5 · p 0.3(임목)" " · 뿌리 15 : 지상 85(일반 수목류)" " · N = 표본지 1,000㎡ 본수 × 벌목·지장목제거 면적" ) #: 톤 경계 — 화면이 알릴 뿐 값을 바꾸지 않는다. REPORT_THRESHOLD_TON = 5.0 SEPARATE_ORDER_THRESHOLD_TON = 100.0 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 tree_waste_tons(area_m2: float, inputs: dict[str, Any]) -> tuple[float, float] | None: """(지상부 톤, 뿌리부 톤). 칸이 하나라도 비거나 면적이 0 이면 `None`.""" values = {key: _positive(inputs.get(key)) for key, _ in TREE_WASTE_INPUTS} if area_m2 <= 0 or any(value is None for value in values.values()): return None stems = values["stems_per_1000m2"] * area_m2 / 1000.0 diameter = values["dbh_cm"] / 100.0 above_kg = ( STEM_FORM_FACTOR * math.pi * (diameter / 2.0) ** 2 * values["height_m"] * (1.0 + FOLIAGE_ALLOWANCE) * values["unit_weight_kg_m3"] * stems ) return above_kg / 1000.0, above_kg * ROOT_SHARE / ABOVE_SHARE / 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, ) -> 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 {} 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) if tons is None: missing = [label for key, label in TREE_WASTE_INPUTS if _positive(inputs.get(key)) is None] why = ( f"현장 조사값이 비어 있음 — {' · '.join(missing)}" if missing else "벌목·지장목제거 면적이 0 ㎡ — 사면표가 아직 안 섬" ) return [ { **base, "amount": None, "status": STATUS_PENDING, "reason": f"{why}. 산식: {TREE_WASTE_BASIS}", "reference_amount": area, } ] above, root = tons total = above + root notes = [ f"지상부 WA {above:,.2f}톤 + 뿌리부 WR {root:,.2f}톤(WA × 15/85)", TREE_WASTE_BASIS, EXPENSE_NOTE, ] if total >= SEPARATE_ORDER_THRESHOLD_TON: notes.append( "⚠ 100톤 이상 — 건설폐기물이면 분리발주 대상(업무처리지침). 다만 임목폐기물은" " 건설폐기물이 아닌 사업장 일반폐기물로 분류됨(같은 지침) — 산출 조건에서 고를 것" ) elif 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]