① 구조물 터파기·되메우기는 토공집계로만 — 토공·사토 공제·참고로 가는 성분을 구조물 줄·묶음 조각·타설·기초잡석이 또 집으면 오류. ② 시멘트·모래·자갈이 자재총괄에 뜨면 오류. ③ 원단위표가 할증을 붙였거나, 자재 합계가 순수량 × (1+율) 과 다르거나, 할증 포함 재료량을 준 공종에 자재가 일위대가 재료비로 붙으면 오류. 판정은 B08 인계(double_count_violations), 멈춤은 B09 build_bill 한 자리. 배정 프로젝트 실측 어긴 자리 0건. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
95 lines
5.2 KiB
Python
95 lines
5.2 KiB
Python
"""B08 → B09 인계 — **이중계상 경계를 갈 곳(`destination`) 칸으로 판정** (명세 6장 · 2026-09-13).
|
|
|
|
주석 경고뿐이던 규칙 셋을 수로 걸리는 검사로 세운다. 원문은 옛 계획서 8-7절(㉠·㉢)과 명세 6장.
|
|
|
|
① 구조물 터파기·되메우기는 **토공집계로만** — 갈 곳이 `earthwork` 인 성분을 다른 공종 줄이
|
|
또 집으면 같은 물량이 두 공종 코드에 붙음. 터파기·되메우기가 `earthwork` 밖으로 가도 오류
|
|
② 배합 성분(시멘트·모래·자갈)은 **B09 일위대가만 쪼갬** — 자재총괄에 뜨면 오류(㉢)
|
|
③ 자재 할증은 **자재총괄 한 곳** — 원단위표가 이미 할증을 붙였으면 오류(㉠).
|
|
자재총괄 합계의 할증 한 번·일위대가 재료비의 할증 전은 B09 가드가 금액 자리에서 봄
|
|
|
|
⚠ 새 얼개를 만들지 않는다 — 성분마다 붙은 `destination` 과, 인계 줄이 성분을
|
|
**이름으로 집는 자리**(구조물 줄 `billing_component` · 묶음 조각 `from_components` ·
|
|
콘크리트 타설 · 기초잡석)만 대조한다.
|
|
⚠ 여기는 목록만 낸다. 멈추는 것은 B09 `build_bill`(`DoubleCountError`) — 가드가 모인 한 자리.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from B08_Quantity.B08_Quantity_Engine_Handoff_Mapping import WorkItemMapping
|
|
from B08_Quantity.B08_Quantity_Engine_Handoff_Rows import PLACING_TARGET_NAMES
|
|
from B08_Quantity.B08_Quantity_Engine_Handoff_Trench import RUBBLE_GROUP
|
|
from B08_Quantity.B08_Quantity_Engine_MaterialSummary import verify_single_surcharge
|
|
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import MIX_COMPONENTS
|
|
|
|
#: 토공집계로만 가야 하는 성분 — 전개·양식·관측 원단위가 같은 이름을 쓴다.
|
|
EARTHWORK_ONLY = ("터파기", "되메우기")
|
|
#: 공종 줄이 값으로 매기면 안 되는 갈 곳 — 이미 다른 자리가 세거나(토공·사토 공제) 참고값이다.
|
|
NOT_BILLED_BY_WORK_ITEM = {
|
|
"earthwork": "토공집계",
|
|
"haul_deduction": "유토곡선 사토 공제",
|
|
"reference": "참고 줄",
|
|
}
|
|
|
|
|
|
def _pickers(structure: dict[str, Any], mapping: WorkItemMapping) -> list[tuple[str, set[str]]]:
|
|
"""이 구조물의 성분을 **이름으로 집는** 공종 줄 — (자리, 이름들). 집는 조건은 각 빌더와 같다."""
|
|
type_id = str(structure.get("type_id") or "")
|
|
entry = mapping.for_structure(type_id) or {}
|
|
composite = mapping.composite_for(type_id)
|
|
found: list[tuple[str, set[str]]] = []
|
|
if entry.get("billing_component"):
|
|
found.append(("구조물 줄", {str(entry["billing_component"])}))
|
|
if composite and not entry.get("work_item_code"):
|
|
for part in composite.get("parts") or []:
|
|
if isinstance(part, dict):
|
|
label = f"묶음 조각 「{part.get('name') or part.get('code')}」"
|
|
found.append((label, {str(name) for name in part.get("from_components") or []}))
|
|
if not composite:
|
|
# 묶음 구조물은 타설·기초잡석 줄이 건너뛴다(조각이 이미 셈) — 빌더와 같은 조건.
|
|
found.append(("콘크리트 타설 줄", set(PLACING_TARGET_NAMES)))
|
|
found.append(("기초잡석 줄", {RUBBLE_GROUP}))
|
|
return found
|
|
|
|
|
|
def verify_double_count_boundaries(
|
|
unit_quantity_table: dict[str, Any] | None,
|
|
material_table: dict[str, Any] | None,
|
|
mapping: WorkItemMapping,
|
|
) -> list[str]:
|
|
"""어긴 자리를 사람 말로 — 비면 경계가 지켜진 것."""
|
|
found: list[str] = []
|
|
for structure in (unit_quantity_table or {}).get("structures") or []:
|
|
label = str(structure.get("name") or structure.get("type_id") or "구조물")
|
|
destinations: dict[str, set[str]] = {}
|
|
for component in structure.get("components") or []:
|
|
if float(component.get("amount") or 0.0) <= 0:
|
|
continue
|
|
name = str(component.get("name") or "").strip()
|
|
destination = str(component.get("destination") or "")
|
|
destinations.setdefault(name, set()).add(destination)
|
|
if name in EARTHWORK_ONLY and destination != "earthwork":
|
|
found.append(
|
|
f"① {label} 「{name}」 갈 곳이 {destination or '(없음)'} — "
|
|
"구조물 터파기·되메우기는 토공집계(earthwork)로만 감"
|
|
)
|
|
for where, names in _pickers(structure, mapping):
|
|
for name in sorted(names):
|
|
for destination in sorted(
|
|
destinations.get(name, set()) & set(NOT_BILLED_BY_WORK_ITEM)
|
|
):
|
|
found.append(
|
|
f"① {label} 「{name}」은 {NOT_BILLED_BY_WORK_ITEM[destination]}"
|
|
f"({destination})로 가는데 {where}이 또 셈 — 같은 물량이 두 자리에 붙음"
|
|
)
|
|
for row in (material_table or {}).get("rows") or []:
|
|
name = str(row.get("name") or "").strip()
|
|
if name in MIX_COMPONENTS:
|
|
found.append(
|
|
f"② 자재총괄에 배합 성분 「{name}」 — 시멘트·모래·자갈은 B09 일위대가만 쪼갬(㉢)"
|
|
)
|
|
found.extend(f"③ {warning} (㉠)" for warning in verify_single_surcharge(unit_quantity_table))
|
|
return found
|