Files
Aislo/B08_Quantity/B08_Quantity_Engine_StructurePriceLink.py
T
eomsangdonandClaude Opus 5 41e0b5b7ea fix(b09): 구조물도 호표 내역 단가를 호표 계금으로 — 화면과 1원 갈림 없앰
- 내역 단가 = 구조물도 일위대가 표 계금(성분 칸 0.1원 절사 합의 1원 절사), 금액 = 성분 소계 × 연장
- 명세 7장 「호표 안 성분 소계는 절사」 — 안 자른 값을 쓰면 192,429 ↔ 192,430 으로 갈렸음

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

170 lines
7.3 KiB
Python

"""구조물도 일위대가 → B09 내역 **받는 문** (PLAN 6장 첫 일감 ② · 2026-09-13 브레인 판정).
⭐ 종전엔 구조물도가 세운 일위대가(`B-AX-ST-*`)가 인계로 **한 번도 안 나갔음** — 돌쌓기는
`FP-13-04-05` ㎡ 단가만 가고 모르터·기초잡석은 따로거나 빠졌음. 사슬의 진짜 빈칸.
한 벌로 두는 것 — 인계(B08)와 내역(B09)이 **같은 목록·같은 키**를 봄
양식 일위대가로 셀 장 → `priced_sheets` (장마다 코드 · 제원 키 · 구성원 · 품은 줄)
인계 줄 → `template_row` (코드 AX-ST · 갈래 = 제원 키 · 수량 = 연장 m)
내역 금액 → `price_sheets` (B09 단가표로 B08 일위대가 엔진을 돌림 — 코드 한 벌)
⚠ 호표 코드 `B-AX-ST-<8hex>#<키>` — 키는 **사람이 읽는 제목이 아니라** 양식 제원 칸 값을 칸 이름
차례로 이은 것(연장 L 뺌) + `normalize_variant_key`. 제목 글자가 바뀌어도 코드는 안 바뀜.
⚠ 양식 일위대가가 품은 원단위 줄(돌쌓기·모르터·기초잡석)은 **따로 인계하지 않음** — `covered`.
⚠ 금액 정본은 B09 — 여기 `price_sheets` 는 B09 가 넘긴 단가표로만 셈. 수동 단가도 입력으로 씀.
"""
from __future__ import annotations
from collections.abc import Callable, Iterable
from typing import Any
#: 인계 줄 갈래 축 이름 — 「구조물도 장 제원」.
VARIANT_AXIS = "structure_sheet"
def _key_value(value: Any) -> str:
if value is None:
return ""
if isinstance(value, (int, float)) and not isinstance(value, bool):
return format(float(value), ".6g")
return str(value).strip()
def sheet_variant_key(template: dict[str, Any], sheet: dict[str, Any]) -> str:
"""장의 **제원 키** — 양식 칸 이름 차례로 `이름=값` 을 `|` 로 이음(연장 칸 뺌: m당)."""
values = (sheet.get("formula_sheet") or {}).get("vars") or {}
specs = template.get("vars") or {}
return "|".join(
f"{name}={_key_value(values.get(name))}"
for name in sorted(specs)
if specs[name].get("source") != "length_m"
)
def structure_price_code(code: str, key: str) -> str:
"""호표 코드 `B-AX-ST-<8hex>#<정규화 키>` — 인계·내역이 이것 하나로 만남."""
from B09_Estimation.B09_Estimation_UnitPrice import normalize_variant_key
return f"B-{code}#{normalize_variant_key(key)}"
def priced_sheets(
sheets: Iterable[dict[str, Any]],
templates: dict[str, dict[str, Any]],
row_edits: dict[str, Any] | None = None,
) -> list[dict[str, Any]]:
"""양식 일위대가로 셀 장 — 양식(고친 줄 조합 얹음)에 일위대가 줄과 코드가 있는 장만."""
from B08_Quantity.B08_Quantity_Engine_StructureUnitPrice import with_rows
found = []
for sheet in sheets:
type_id = str(sheet.get("type_id") or "")
base = templates.get(type_id)
if not base or not base.get("code") or not sheet.get("formula_sheet"):
continue
template = with_rows(base, (row_edits or {}).get(type_id))
rows = template["unit_price"]["rows"]
if not rows:
continue
names = {row.get("seq"): str(row.get("name") or "") for row in template.get("rows") or []}
found.append(
{
"type_id": type_id,
"code": str(template["code"]),
"name": str(template.get("name") or type_id),
"title": str(sheet.get("title") or ""),
"key": sheet_variant_key(template, sheet),
"structure_ids": [
str(member["structure_id"])
for member in sheet.get("members") or []
if member.get("structure_id")
],
# 일위대가가 품은 원단위 줄 이름 — 따로 인계하면 두 번 셈.
"covered": sorted(
{names[r["from_row"]] for r in rows if r.get("from_row") in names}
),
"template": template,
"sheet": sheet,
}
)
return found
def by_structure(entries: Iterable[dict[str, Any]]) -> dict[str, dict[str, Any]]:
return {sid: entry for entry in entries for sid in entry["structure_ids"]}
def template_row(row: dict[str, Any], structure: dict[str, Any], entry: dict[str, Any]) -> dict:
"""전개 인계 줄(같은 칸 29개 계약)을 **양식 일위대가 줄**로 바꿔 씀 — 코드·갈래·수량·단위만."""
length = float(structure.get("length_m") or 0.0)
in_bill = length > 0
return {
**row,
"work_item_code": entry["code"],
"name": entry["name"],
"spec": entry["title"],
"spec_detail": entry["title"],
"unit": "m",
"quantity": length,
"composite_parts": None,
"composite_not_ready": None,
"structure_kind": None,
"variant_axis": VARIANT_AXIS,
"variant_value": entry["key"],
"secondary_axes": None,
"spec_class": None,
"spec_class_basis": f"구조물도 양식 일위대가 — 품은 줄: {'·'.join(entry['covered'])}",
# 막힘은 일위대가 표가 정함(B09 가 셈) — 전개 쪽 갈래 사유는 여기 안 실음.
"blocked_kind": None if in_bill else row.get("blocked_kind"),
"blocked_reason": "" if in_bill else row.get("blocked_reason") or "연장이 0 이라 안 세움",
"in_bill": in_bill,
"in_bill_reason": "" if in_bill else "연장이 0 이라 내역에 안 세움",
}
def price_sheets(
entries: Iterable[dict[str, Any]],
book: Any,
find_variant: Callable[[str, str], str | None],
library: Iterable[dict[str, Any]],
manual_all: dict[str, Any] | None = None,
) -> dict[str, dict[str, Any]]:
"""B09 내역이 쓸 호표 금액 `{B-AX-ST-…#키: {money, total, blocked, unconfirmed, reasons}}`.
⚠ `money` 는 **호표 표의 성분 소계**(칸마다 0.1원 절사한 합) · `total` 은 계금(1원 절사) —
명세 7장 「호표 안 성분 소계는 절사」. 안 자른 값을 쓰면 구조물도 화면(192,429)과
내역 단가(192,430)가 1원 갈림(2026-09-13 검증 프로젝트 실측).
"""
from decimal import Decimal
from B08_Quantity.B08_Quantity_Engine_StructureUnitPrice import unit_price_money
from B09_Estimation.B09_Estimation_PriceBook import Money3
prices: dict[str, dict[str, Any]] = {}
library = list(library)
for entry in entries:
assembled = unit_price_money(
entry["template"],
entry["sheet"],
book,
find_variant,
library,
(manual_all or {}).get(entry["type_id"]) or {},
)
if assembled is None:
continue
table, _exact = assembled
prices[structure_price_code(entry["code"], entry["key"])] = {
"money": Money3(*(Decimal(str(table[k])) for k in ("material", "labor", "expense"))),
"total": Decimal(str(table["total"])),
"blocked": table["blocked"],
"unconfirmed": table["unconfirmed"],
"reasons": [
f"{row['name']}: {row['reason']}"
for row in table["rows"]
if row["reason"] and not row["skipped"] and "total" not in row
],
}
return prices