Files
Aislo/B08_Quantity/B08_Quantity_Engine_StructurePriceLink.py
T
eomsangdonandClaude Opus 5 3ce997fa69 feat(b09): 표 화면 넷 — 중기(목록표·중기사용료) · 자재단가대비표 · 집계표 넷 · 목록표 일곱
- 서버: 내역 응답에 집계표 넷(resources)·목록표(lists) — 내역이 쓴 자원을 처음 쓰인 차례로 되모음
  (묶음 줄·구조물도 호표 줄도 구성까지 풀어 셈) · 재료비 집계표에 자재대(사급·관급) 줄 이어 붙임
- 중기시간금액집계표: 단가 열 없이 합계·노무·재료·경비 — 성분마다 반올림한 뒤 합(골든셋 131/144, 합계 한 번 반올림 107/144)
- 재료·노무·경비 집계표: 반올림(수량 × 단가) 골든셋 279/279 — 시험 한 벌 더함
- 중기사용료 호표: 잡품 줄은 수량 칸 율(%) · 단가 칸 밑수(주연료비) · 금액 — 실무 표 그대로. 제잡비·공구손료 줄도 밑수를 단가 칸에
- 자재단가대비표: 슬롯 1~6 단가·페이지 · 채택 칸 굵게 · 최소단가 표시(서버 min_slot)
- 목록표 일곱(일위대가·단가산출근거·중기·재료비·노무비·경비·일식견적): 색인만 — 서버 번호·단가 그대로, 일위대가·산근·중기 줄은 누르면 들어감
- 옛 중기 탭 줄을 새 탭으로 바꿔 끼움(중기경비계산서는 옛 표를 아래에 이어 보임)
- 검증: ORCA — 중기 목록 9 · 굴착기 0.7 호표 합계 109,704 · 잡품 22 % × 21,418.1 = 4,711.9 · 노무 집계 벌목부 9,686,529 · 중기 집계 성분 합 · 시험 1638 통과

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
2026-09-14 01:56:02 +09:00

176 lines
7.7 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원 절사해 더한 뒤 성분마다 1원 절사) ·
`total` 은 그 합 — 명세 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"],
# 단위당 구성(단가표 코드, 수량) — B09 집계표가 자원까지 풂. 수동 단가 줄은 못 풂.
"parts": [
(str(row["ref_code"]), Decimal(str(row["quantity"])))
for row in table["rows"]
if row.get("ref_code") and "total" in row and not row.get("manual")
],
"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