Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
91 lines
4.2 KiB
Python
91 lines
4.2 KiB
Python
"""B09 원가계산 — **풀베기 묘목찾기 = 조림목 본수 칸** (2026-09-15 브레인 차례 ② 판정 ㉠ · 지금 칸을 만듦).
|
||
|
||
산림 6-2-2 줄베기 · 6-2-3 모두베기 [주]④⑤ 「묘목찾기와 줄베기(모두베기) 품을 **합하여** 적용」 — 묘목찾기는
|
||
「낫 | 인/100본 | 0.07 | 보통인부」 라 조림목 본수(본/ha · [주]② 둘레베기 참고 · 설계 입력)가 있어야 ha당이 섬.
|
||
⇒ 본수가 들면 갈래마다 보통인부 0.07 × 본수 ÷ 100 · 비면 「일부만」 으로 막고 사유(반만 선 틀린 값보다 안 선 값).
|
||
⚠ 제안값 없음 — 부록 예시 2,700본/ha 는 같은 부록이 품도 표와 다르게 적은 자료(묘목찾기 0.10 ↔ 표 0.07).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from decimal import Decimal, InvalidOperation
|
||
from typing import Any
|
||
|
||
#: 공종 → 표 번호(`_JudgedTable` 도구 줄 표 · 인/100본 줄이 있는 둘).
|
||
SEEDLING_TABLES = {"FP-06-02-02": "F0155", "FP-06-02-03": "F0156"}
|
||
NAME = "묘목찾기"
|
||
MISSING = (
|
||
"조림목 본수(본/ha) 미입력 — 원문 [주]④⑤ 가 묘목찾기({amount}인/100본)와 합하여 적용하라 함"
|
||
" · 「산출 조건」 에서 넣으면 섬"
|
||
)
|
||
|
||
|
||
def parse_seedlings_per_ha(text: str | None) -> Decimal | None:
|
||
"""조림목 본수 칸 — 빈 칸은 `None` · 0 이하·수가 아닌 값은 `ValueError`(조용히 접지 않음)."""
|
||
cleaned = re.sub(r"[\s,]", "", str(text or ""))
|
||
if not cleaned:
|
||
return None
|
||
try:
|
||
value = Decimal(cleaned)
|
||
except InvalidOperation as exc:
|
||
raise ValueError(f"조림목 본수는 수로 넣어야 합니다: {text}") from exc
|
||
if value <= 0:
|
||
raise ValueError(f"조림목 본수는 0 보다 커야 합니다: {text}")
|
||
return value
|
||
|
||
|
||
def _per_hundred(node: dict[str, Any], table_id: str) -> tuple[Decimal, str] | None:
|
||
"""(인/100본 값, 인력 이름) — 표에서 읽음. 칸이 달라지면 `None`."""
|
||
from B09_Estimation.B09_Estimation_ResourceAxis import parse_amount
|
||
|
||
table = next((t for t in node.get("tables") or [] if t.get("pum_table_id") == table_id), {})
|
||
for row in table.get("raw_row") or []:
|
||
cells = ["".join(str(c).split()) for c in row]
|
||
if "인/100본" in cells:
|
||
unit = cells.index("인/100본")
|
||
amount = parse_amount(cells[unit + 1]) if len(cells) > unit + 2 else None
|
||
if amount is not None and cells[unit + 2]:
|
||
return amount, cells[unit + 2]
|
||
return None
|
||
|
||
|
||
def attach_seedling_finding(
|
||
build: Any, nodes: dict[str, dict[str, Any]], seedlings_per_ha: Decimal | None
|
||
) -> None:
|
||
"""본수가 들면 갈래 제목마다 묘목찾기 인력을 붙이고, 없으면 일부만 + 사유."""
|
||
from B09_Estimation.B09_Estimation_PriceBook import PriceDetail, PriceKind
|
||
|
||
book = build.book
|
||
for code, table_id in SEEDLING_TABLES.items():
|
||
found = _per_hundred(nodes.get(code) or {}, table_id)
|
||
titles = [t for t in book.titles if t.startswith(f"B-{code}#")]
|
||
labels = [
|
||
label for label in build.unattached.get(code) or [] if "".join(label.split()) != NAME
|
||
]
|
||
labor = next(
|
||
(
|
||
c
|
||
for c, t in book.titles.items()
|
||
if found and t.kind is PriceKind.LABOR and "#" not in c and t.name == found[1]
|
||
),
|
||
None,
|
||
)
|
||
if found is None or labor is None or not titles:
|
||
build.component_gaps[code] = "묘목찾기 줄 칸이 달라져 못 읽음"
|
||
build.partial_ratio.setdefault(code, Decimal(0))
|
||
continue
|
||
amount, name = found
|
||
if seedlings_per_ha is None:
|
||
reason = MISSING.format(amount=amount)
|
||
build.component_gaps[code] = reason
|
||
build.partial_ratio.setdefault(code, Decimal(0))
|
||
labels.append(reason)
|
||
else:
|
||
note = f"묘목찾기 {amount}인/100본 × 조림목 {seedlings_per_ha}본/ha ÷ 100 — [주]④⑤ 합하여 적용"
|
||
for title in titles:
|
||
book.add_detail(
|
||
PriceDetail(title, labor, amount * seedlings_per_ha / 100, note=note)
|
||
)
|
||
build.unattached[code] = labels
|