Files
Aislo/B08_Quantity/B08_Quantity_Engine_Preparation_TreeWaste.py
T
eomsangdonandClaude Opus 5 7d856f37a3 feat(b08,b09): 폐기물처리비 — 경비 비목(일반관리비·이윤 밑수) · 임목폐기물 톤 · 분리발주 칸
- 원가계산서: 폐기물처리비를 경비 줄로(예정가격작성기준 제19조③18호) · 법정경비 뒤라 그 밑수엔 안 섞임
- 분리발주 칸(기본 아님) — 켜면 총원가 밖 · 총공사비에만 더함
- 준비공 「임목폐기물 처리」 톤: WA = 0.5·π·(B/2)²·h·1.3·W1·N · WR = WA × 15/85 (한국건설기술연구원 2012)
- 조사값 넷(1,000㎡당 본수·흉고직경·수고·단위체적중량) 칸 · 기본값 없음 · 5톤·100톤 경계 알림
- 수동 처리단가 빨간 테두리 + 「미확정 N건」 · 내역엔 안 서고 제외 사유 「경비」
- 시험: 승률 구조(경비·일반관리비·이윤 밑수 증가 · 법정경비 불변 · 분리발주) · 실정보고 부피 대조

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
2026-09-14 00:55:41 +09:00

151 lines
6.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
지상부 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]