feat(axis): 품셈 판 pum_edition 칸 — 자원·공종 쪽, 판이 다르면 값을 안 씀

공종 마스터·자원 축 줄·B08 인계 줄마다 공종 코드가 본 판을 실음(명세 17장 규칙 3).
판이 다른 인계 줄은 내역서에서 「확인 대기」로 빠지고, 코드에 박힌 절 번호(합산 단계·표 형태 판정·가드 목록)는
마스터 판과 다르면 마스터 빌드·단가 조립을 멈춤(규칙 4). 판 기준은 common_util_pum_edition 한 곳.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
This commit is contained in:
2026-09-13 22:41:47 +09:00
co-authored by Claude Opus 5
parent d1958d067b
commit 66d57bb630
11 changed files with 145 additions and 5 deletions
@@ -34,6 +34,7 @@ from B09_Estimation.B09_Estimation_Guards import (
check_material_surcharge_once,
)
from B09_Estimation.B09_Estimation_ResourceAxis import load_work_item_master
from common_util.common_util_pum_edition import edition_waiting_reason
from B09_Estimation.B09_Estimation_QuantityDigits import round_quantity
from B09_Estimation.B09_Estimation_Rounding import OutputPlace, round_at
from B09_Estimation.B09_Estimation_UnitPrice import (
@@ -115,6 +116,8 @@ class HandoffWorkItem:
#: **근거는 산출근거 칸에 그대로 적는다** — 우리가 다시 지어내지 않는다.
spec_class: str = ""
spec_class_basis: str = ""
#: 이 줄의 공종 코드가 본 품셈 판 — 마스터 판과 다르면 값을 안 씀(명세 17장).
pum_edition: str = ""
@property
def display_name(self) -> str:
@@ -295,6 +298,7 @@ def parse_handoff(payload: dict[str, Any]) -> tuple[list[HandoffWorkItem], list[
variant_value=str(row.get("variant_value") or ""),
spec_class=row.get("spec_class") or "",
spec_class_basis=row.get("spec_class_basis") or "",
pum_edition=str(row.get("pum_edition") or ""),
)
for row in payload["work_items"]
]
@@ -381,7 +385,8 @@ def build_bill(
"""
work_items, materials = parse_handoff(payload)
unit_prices = build or cached_build()
index = _master_index(master or load_work_item_master())
master = master or load_work_item_master()
index = _master_index(master)
result = BillResult()
# ── 1) 쓰인 공종의 조상만 남긴 가지치기 나무 ────────────────────────────────
@@ -414,6 +419,19 @@ def build_bill(
# 구조물도 호표(PLAN 6장 ②) — 공종 마스터에 없는 자체 확장 코드라 나무에 안 끼움.
templated.append(item)
continue
# 품셈 판이 다른 줄 — 절 번호가 같아도 같은 공종이라는 보장이 없어 값을 안 씀(명세 17장 규칙 4).
waiting = edition_waiting_reason(item.pum_edition, str(master.get("effective_date", "")))
if waiting:
result.missing.append(
{
"name": item.display_name,
"unit": item.unit,
"quantity": str(item.quantity),
"reason": waiting,
"blocked_kind": "edition_waiting",
}
)
continue
if (item.composite_parts or item.composite_not_ready) and not item.work_item_code:
# 묶음 줄 — 품셈에 그 공종이 없어 **조각을 합쳐** 한 줄로 세운다
# (옹벽 = 타설 + 거푸집 + 철근 + 잡석). 「코드 없음」으로 세면 안 된다.
@@ -240,6 +240,8 @@ class ResourceRow:
#: ⚠ **이 값을 안 보면 단가가 조용히 틀린다** — 인력 몫 원단위를 전량에 곱하게 된다
#: (2026-09-08 실측: 측구터파기 39,575.6원/㎥ 이 인력 10 % 몫만이었다).
group_ratio_pct: Decimal | None = None
#: 이 줄을 읽어 낸 품셈 판 — `pum_table_id`·`work_item_code` 가 판에 묶임(명세 17장).
pum_edition: str = ""
def as_dict(self) -> dict[str, Any]:
return {
@@ -258,6 +260,7 @@ class ResourceRow:
None if self.alternative_amount is None else str(self.alternative_amount)
),
"group_ratio_pct": None if self.group_ratio_pct is None else str(self.group_ratio_pct),
"pum_edition": self.pum_edition,
}
@@ -666,6 +669,8 @@ def build_resource_axis(master: dict[str, Any], catalog: ResourceCatalog) -> Axi
for node in master.get("work_items", []):
for table in node.get("tables", []):
match_table(node, table, catalog, result)
for row in result.rows:
row.pum_edition = edition
return result
@@ -583,6 +583,12 @@ def build_unit_prices(
)
master = load_work_item_master()
# 합산 단계·표 읽기 선언·가드 목록이 절 번호로 박혀 판에 묶임 — 판이 다르면 조립 안 함(명세 17장).
from common_util.common_util_pum_edition import require_code_edition
require_code_edition(
str(master.get("effective_date", "")), "B09 단가 조립 — 코드에 박힌 절 번호"
)
if factor_choices is None:
factor_choices = chosen_values(scan_range_factors(master))
# 「…와 동일」 참조로 이어 온 계수 — **사용자가 고른 값이 있으면 그것이 이긴다.**