feat(b09): 연결고리 표를 읽는 한 자리 — 산림·건설 어느 품을 쓸지 표가 정함(고시 총칙 7 · 표 policy both_precedence) · B09 인계 입구에서 둘 다 있는 공종은 표가 고른 열쇠·목차 코드로 · 미판정은 안 고르고 막음(link_undecided · 사유 그대로 · 금액에 안 듦) · 열쇠 빈 미판정 행도 내역서 요약에 목록 · 지금 인계 코드 57 판정 안 달라짐 = 금액 불변 · Z01 목록에 줄 구실(총칙) 표시 칸(코덱스 크로스체크 ①② · 브레인)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
This commit is contained in:
@@ -47,6 +47,7 @@ _ZERO = Decimal(0)
|
||||
|
||||
# ⬇ 인계 읽기는 `_Input` 으로 옮겼다(2026-09-15 700줄 분리) — 쓰던 이름이 그대로 살게 다시 내보낸다.
|
||||
from B09_Estimation.B09_Estimation_BillOfQuantities_Input import ( # noqa: E402,F401
|
||||
BLOCKED_LINK_UNDECIDED,
|
||||
BLOCKED_UNCONFIRMED,
|
||||
SUPPLY_OWNER,
|
||||
SUPPLY_UNKNOWN,
|
||||
@@ -56,6 +57,7 @@ from B09_Estimation.B09_Estimation_BillOfQuantities_Input import ( # noqa: E402
|
||||
_decimal,
|
||||
parse_handoff,
|
||||
)
|
||||
from common_util.common_util_work_item_link import undecided_links # noqa: E402
|
||||
|
||||
#: 총 절취량으로 셀 공종 — 9-3 토사깎기 · 9-4 암절취 · 9-5 발파암(인계 대응표 「흙깎기」 셋).
|
||||
#: ⚠ 판에 묶인 절 번호임(명세 17장) — 판이 바뀌면 대응표와 함께 고칠 것.
|
||||
@@ -75,6 +77,7 @@ _BLOCKED_LABELS = {
|
||||
"input_missing": "입력이 필요합니다",
|
||||
"unit_data_missing": "원단위가 없습니다(우리가 만들 것)",
|
||||
"formula_missing": "전개식이 없습니다(우리가 만들 것)",
|
||||
BLOCKED_LINK_UNDECIDED: "연결고리 미판정 — 금액에 안 들어감",
|
||||
}
|
||||
|
||||
|
||||
@@ -353,6 +356,18 @@ def build_bill(
|
||||
}
|
||||
)
|
||||
continue
|
||||
if item.blocked_kind == BLOCKED_LINK_UNDECIDED:
|
||||
# 산림·건설 어느 품을 쓸지 연결고리 표가 미판정 — 근거 없이 고르지 않음(브레인 · 코덱스 ②).
|
||||
result.missing.append(
|
||||
{
|
||||
"name": item.display_name,
|
||||
"unit": item.unit,
|
||||
"quantity": str(item.quantity),
|
||||
"reason": f"{_BLOCKED_LABELS[BLOCKED_LINK_UNDECIDED]} — {item.blocked_reason}",
|
||||
"blocked_kind": BLOCKED_LINK_UNDECIDED,
|
||||
}
|
||||
)
|
||||
continue
|
||||
if (item.composite_parts or item.composite_not_ready) and not item.work_item_code:
|
||||
# 묶음 줄 — 품셈에 그 공종이 없어 **조각을 합쳐** 한 줄로 세운다
|
||||
# (옹벽 = 타설 + 거푸집 + 철근 + 잡석). 「코드 없음」으로 세면 안 된다.
|
||||
@@ -577,6 +592,8 @@ def bill_summary(result: BillResult) -> dict[str, Any]:
|
||||
"direct_labor_krw": str(result.direct_labor_krw),
|
||||
"direct_expense_krw": str(result.direct_expense_krw),
|
||||
"notes": result.notes,
|
||||
# 연결고리 표 미판정 행 — 열쇠가 비어 줄에 못 닿는 것도 드러냄(조용히 안 버림)
|
||||
"link_undecided": undecided_links(),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,11 @@ from dataclasses import dataclass
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from common_util.common_util_work_item_key import work_item_key
|
||||
from common_util.common_util_work_item_key import work_item_code_of, work_item_key
|
||||
from common_util.common_util_work_item_link import choose
|
||||
|
||||
#: 연결고리 표 미판정 — 산림·건설 어느 품을 쓸지 근거가 없어 **안 고른** 줄(금액에 안 듦).
|
||||
BLOCKED_LINK_UNDECIDED = "link_undecided"
|
||||
|
||||
_ZERO = Decimal(0)
|
||||
|
||||
@@ -121,6 +125,23 @@ def _decimal(value: Any, default: Decimal | None = _ZERO) -> Decimal | None:
|
||||
return Decimal(str(value))
|
||||
|
||||
|
||||
def _linked(row: dict[str, Any]) -> dict[str, Any]:
|
||||
"""열쇠 길목 + **연결고리 표** — 산림·건설 어느 품을 쓸지 표가 정함(고시 총칙 7 · 표 `policy`).
|
||||
|
||||
둘 다 있는 공종이면 표가 적은 우선 품셈 열쇠·목차 코드로 바꿈 · 미판정이면 막음(사유 그대로).
|
||||
⚠ 이미 B08 이 막아 보낸 줄은 그 까닭을 덮지 않음.
|
||||
"""
|
||||
edition = row.get("pum_edition") or None
|
||||
key = row.get("work_item_key") or work_item_key(row.get("work_item_code"), edition)
|
||||
choice = choose(key)
|
||||
out = {"work_item_key": key or "", "work_item_code": row.get("work_item_code")}
|
||||
if choice.blocked and not row.get("blocked_reason"):
|
||||
out.update(blocked_reason=choice.blocked, blocked_kind=BLOCKED_LINK_UNDECIDED)
|
||||
elif choice.key and choice.key != key:
|
||||
out.update(work_item_key=choice.key, work_item_code=work_item_code_of(choice.key, edition))
|
||||
return out
|
||||
|
||||
|
||||
def parse_handoff(payload: dict[str, Any]) -> tuple[list[HandoffWorkItem], list[HandoffMaterial]]:
|
||||
"""인계 응답을 우리 자료형으로 옮긴다. **모르는 칸을 채우지 않는다.**"""
|
||||
if "work_items" not in payload or "materials" not in payload:
|
||||
@@ -128,7 +149,7 @@ def parse_handoff(payload: dict[str, Any]) -> tuple[list[HandoffWorkItem], list[
|
||||
|
||||
work_items = [
|
||||
HandoffWorkItem(
|
||||
work_item_code=row.get("work_item_code"),
|
||||
work_item_code=linked["work_item_code"],
|
||||
name=row.get("name", ""),
|
||||
spec=row.get("spec") or "",
|
||||
unit=row.get("unit") or "",
|
||||
@@ -147,19 +168,18 @@ def parse_handoff(payload: dict[str, Any]) -> tuple[list[HandoffWorkItem], list[
|
||||
composite_parts=tuple(row.get("composite_parts") or ()),
|
||||
composite_not_ready=tuple(row.get("composite_not_ready") or ()),
|
||||
structure_kind=row.get("structure_kind") or "",
|
||||
blocked_reason=row.get("blocked_reason") or "",
|
||||
blocked_kind=row.get("blocked_kind") or "",
|
||||
blocked_reason=row.get("blocked_reason") or linked.get("blocked_reason") or "",
|
||||
blocked_kind=row.get("blocked_kind") or linked.get("blocked_kind") or "",
|
||||
variant_axis=row.get("variant_axis") or "",
|
||||
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 ""),
|
||||
# 옛 인계(열쇠 없음)도 같은 길목으로 채움
|
||||
work_item_key=row.get("work_item_key")
|
||||
or work_item_key(row.get("work_item_code"), row.get("pum_edition") or None)
|
||||
or "",
|
||||
# 옛 인계(열쇠 없음)도 같은 길목으로 채움 · 연결고리 표가 고른 열쇠
|
||||
work_item_key=linked["work_item_key"],
|
||||
)
|
||||
for row in payload["work_items"]
|
||||
for linked in (_linked(row),)
|
||||
]
|
||||
materials = [
|
||||
HandoffMaterial(
|
||||
|
||||
Reference in New Issue
Block a user