Files
Aislo/B09_Estimation/B09_Estimation_ResourceAxis_ProcessSum.py
T
eomsangdonandClaude Opus 5 980c5440a0 fix(axis): 표 읽기 — 단끊기 5-16-1 공정 줄 합산 · 「(인/100m당)」 꼴 밑수 22표
단끊기는 공정 네 줄을 토질 갈래마다 더하고 [주]⑤ 작업반장 1인/보통인부 20인을 더함.
원문 합계 줄은 절취를 ㎥당으로 더한 값이라 안 씀(까닭을 적은 공종만 섬). 같은 모양 뭉기기 13-12-1 은 안 더함.
「단위:」 없이 분모만 적은 본문 밑수를 읽어 조림·시비·떼심기 22표가 밑수 없음에서 풀림.

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

121 lines
5.1 KiB
Python

"""B09 원가계산 — **공정 줄을 더해 한 품이 되는 표** 읽기 (자원 축 보조, 2026-09-13).
공정별 | 보통인부(인) | 비고
보통토사 | 경질ㆍ고사점토 및 자갈섞인 점토 | 호박돌 섞인 토사
절 취 | 2.4 | 3.3 | 5.4
수평잡기 및 단정리 | 0.34 | 0.34 | 0.34
합계 | 2.03 | 2.09 | 2.23
행은 공정, 열은 토질 갈래, 자원은 머리 한 칸. 갈래마다 공정 줄을 **더한 것**이 한 품이다.
지금은 「숫자 칸이 자원 열보다 많다」로 표째 버려져 단끊기 5-16-1 이 한 줄도 안 섰다.
⚠ **모양만으로는 「더하는 표」인지 못 가른다** — 뭉기기 13-12-1 은 같은 모양인데 줄마다 단위가
달라(절취 ㎥ · 면고르기 시간/㎡) 더하면 틀린다. 그래서 **사람이 적은 공종만** 더한다.
⚠ 원문 합계 줄은 안 읽는다 — 줄 합과 다르면 **까닭을 적어 둔 공종만** 서고, 아니면 막는다.
"""
from __future__ import annotations
from decimal import Decimal
from typing import Any
from B09_Estimation.B09_Estimation_ResourceAxis import (
AxisResult,
ResourceCatalog,
ResourceRow,
UnmatchedRow,
parse_amount,
)
_TOTAL_LABELS = ("계", "합계", "소계", "총계")
#: 공종 → 근거 · 원문 합계 줄과 다를 때의 까닭 · [주]가 더하라는 직종(코드, 보통인부 몇 인당 1인).
PROCESS_SUM: dict[str, dict[str, Any]] = {
"FP-05-16-01": {
"basis": (
"품셈 5-16-1 표 머리 (인/100m당) · [주]④ 「수평잡기 및 단정리, 잡석 및 뿌리정리,"
" 성토면고르기, 고르기품은 100m당 품」 · [주]③ 절취량 0.15㎥/m 기준"
),
"total_mismatch": (
"원문 합계 줄(2.03·2.09·2.23)은 절취를 ㎥당(2.4÷15=0.16)으로 더한 값이라 줄 합과 다름"
" — 100m당 줄을 더함(STmate 단끊기 산출과 같음)"
),
"per_workers": ("1001", Decimal(20), "[주]⑤ 작업반장 1인/보통인부 20인 가산"),
},
}
def _tight(text: Any) -> str:
return "".join(str(text or "").split())
def match_process_sum_table(
node: dict[str, Any],
table: dict[str, Any],
catalog: ResourceCatalog,
result: AxisResult,
basis_quantity: Decimal | None,
unit: str,
) -> bool:
"""적어 둔 공종의 공정 합산 표를 읽는다. 그 공종이 아니면 `False`."""
from B09_Estimation.B09_Estimation_ResourceAxis_Transposed import transposed_columns
code = str(node.get("work_item_code") or "")
spec = PROCESS_SUM.get(code)
if spec is None:
return False
table_id = str(table.get("pum_table_id", ""))
rows = [[str(c).strip() for c in row] for row in (table.get("raw_row") or [])]
columns = transposed_columns(table, catalog)
def block(cell: str, why: str) -> bool:
result.unmatched.append(UnmatchedRow(code, table_id, cell, why))
result.partial_items[code] = why
return True
labels = [cell for cell in (rows[0] if rows else []) if cell]
if len(columns) != 1 or len(labels) < 2 or basis_quantity in (None, 0):
return block(code, "공정 합산 표 모양이 아닙니다(자원 한 칸 · 갈래 줄 · 밑수)")
sums = [Decimal(0)] * len(labels)
totals: list[Decimal] | None = None
for cells in rows[1:]:
numbers = [value for value in (parse_amount(c) for c in cells[1:]) if value is not None]
if not cells or not cells[0] or not numbers:
continue
if len(numbers) != len(labels):
return block(cells[0], f"값 {len(numbers)} 개가 갈래 {len(labels)} 개와 안 맞습니다")
if _tight(cells[0]) in _TOTAL_LABELS:
totals = numbers
continue
sums = [total + value for total, value in zip(sums, numbers)]
if totals is not None and totals != sums and not spec.get("total_mismatch"):
return block("합계", f"원문 합계 {totals} 가 줄 합 {sums} 와 다릅니다")
entry = columns[0][1]
members = [(entry, Decimal(1))]
if spec.get("per_workers"):
chief_code, workers, _note = spec["per_workers"]
chief = next((e for e in catalog.entries if e.code == chief_code), None)
if chief is None:
return block(chief_code, "[주]가 더하라는 직종이 카탈로그에 없습니다")
members.append((chief, Decimal(1) / workers))
for label, total in zip(labels, sums):
for member, ratio in members:
result.rows.append(
ResourceRow(
work_item_code=code,
pum_table_id=table_id,
pum_form=str(table.get("pum_form", "")),
resource_kind=member.kind,
resource_code=member.code,
resource_name=member.name,
resource_spec=member.spec,
amount=total * ratio / basis_quantity,
amount_unit=unit,
raw_row_index=0,
variant=label,
)
)
return True