Files
Aislo/B09_Estimation/B09_Estimation_Bill_Hopyo.py
T
eomsangdonandClaude Opus 5.5 c93554d331 fix(B09 불러오기): 코드로만 가리킨 내역 줄도 일위대가에 이음 · 끝수 줄 꼬리표 · 요율 없음 총공사비 숨김 (707-7)
- 영월 꼴(STmate 엑셀 내역 줄 = 호표 코드만) = 일위대가표 같은 코드 호표 번 채움 → 서고 사본 21 · 이음 34
- 이은 줄 차가 비목마다 1 원/단위 안 = 「끝수」(STmate 하위 호표 반올림) · 끝 알림 한 줄 「엑셀 합계와 차 n 원 = 하위 호표 반올림 끝수 m 줄」(브레인 정함 = 끝수 줄도 이음)
- 요율 못 읽은 공사(.estx · .STC) = 총공사비 칸 안 보임
- 시험 test_707_7 11(영월 끝수 · 일위대가 시트 없음 까닭)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:21:15 +09:00

176 lines
7.7 KiB
Python

"""B09 경쟁사 공사 통째 불러오기(PLAN 707-7) — 엑셀 일위대가 · 단가산출 → 이 공사 서고 사본
(층 쓰기 0).
705-3 [모두 고정으로 들여오기] 그대로(`M02_Hopyo_Build.Builder` · 하위 호표 먼저 · 고정 줄 ·
빠짐 0) —
층 대신 임시 폴더(`store.at` · 층 = 「project」)에서 만들고 호표마다 서고 사본으로 굳힘:
폴더 `precombo@project@-@{표지}-{번}` · 원본 참조 `{층 project, 주인 표지, 키 SC-{번}}` ·
참조사본 = 그 호표 + 닿는 하위 호표(주인 = 표지 → 다른 파일과 안 섞임) ·
단가 묶음 = 서고 사본과 한 길.
읽기 = `M02_Hopyo_Excel.read_book`(엑셀만 — .estx · .STC 구성 줄은 못 읽음) ·
원본이 없으니 [업데이트] 대상 아님.
"""
from __future__ import annotations
import json
import tempfile
from pathlib import Path
from typing import Any
from B09_Estimation import B09_Estimation_Bill_Library as library
from M02_MasterTemplete import M02_Hopyo_Build as hb
from M02_MasterTemplete import M02_Hopyo_Excel as hopyo_excel
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_MasterTemplete_Store as mstore
from M02_MasterTemplete.M02_Item_Snapshot import MANIFEST_NAME, now_text
from M02_MasterTemplete.M02_Item_Source import (
LABOR_FILE,
PRICE_FILE,
logic_rows,
price_bundle,
read_json,
)
LAYER = "project"
SUM_KEYS = ("품셈", "고정", "호표참조", "빠짐", "이음", "이음후보")
def read(name: str, data: bytes) -> dict[str, Any]:
"""엑셀 일위대가 시트 → 읽은 결과 — 시트 없음 · DRM = `hopyo_excel.ExcelError`."""
return hopyo_excel.read_book(name, data)
_PARTS = {"labor": "노무비", "material": "재료비", "expense": "경비"}
def numbered(bill: dict[str, Any], book: dict[str, Any]) -> dict[str, Any]:
"""번 없이 코드로만 가리킨 대가 줄(STmate 엑셀 영월 꼴) →
일위대가표 같은 코드 호표 번을 채운 사본."""
by_code = {h.get("코드"): int(h["번"]) for h in book.get("호표") or [] if h.get("코드")}
def fill(line: dict[str, Any]) -> dict[str, Any]:
hopyo = line.get("호표") or {}
no = by_code.get(hopyo.get("코드"))
if hopyo.get("종류") != "대가" or "번" in hopyo or no is None:
return line
return {**line, "호표": {**hopyo, "번": no}}
return {**bill, "줄": [fill(line) for line in bill["줄"]]}
def agree(bill: dict[str, Any], book: dict[str, Any]) -> tuple[set[int], list[dict[str, Any]]]:
"""내역 줄 대가 번 가운데 내역 단가 = 일위대가표 호표 금액(비목마다 1 원 안)인 번만 이음 ·
다른 줄 = 내역 단가 그대로(경쟁사 금액 재현) →
(이을 번, 다른 줄 [{행, 이름, 번, 내역, 일위대가}])."""
units = {int(h["번"]): h.get("금액") or {} for h in book.get("호표") or []}
good: set[int] = set()
bad: list[dict[str, Any]] = []
for line in bill["줄"]:
hopyo = line.get("호표") or {}
no = int(hopyo["번"]) if hopyo.get("종류") == "대가" and "번" in hopyo else None
if no is None or no not in units:
continue
price = line.get("단가") or {}
if all(abs((price.get(p) or 0) - (units[no].get(k) or 0)) < 1 for p, k in _PARTS.items()):
good.add(no)
continue
each = sum(price.get(p) or 0 for p in _PARTS)
sheet = sum(units[no].get(k) or 0 for k in _PARTS.values())
bad.append(
{"행": line["행"], "이름": line["이름"], "번": no, "내역": each, "일위대가": sheet}
)
return good - {x["번"] for x in bad}, bad
def _key(no: int) -> str:
return f"SC-{no:06d}"
def _owned(doc: dict[str, Any], tag: str) -> dict[str, Any]:
"""하위 호표 참조 주인 = 표지(임시 층 `project` 주인 없음 → 파일마다 가름)."""
lines = [
{**one, "참조": {**one["참조"], "주인": tag}}
if isinstance(one, dict) and (one.get("참조") or {}).get("층") == LAYER
else one
for one in doc.get("담은로직") or []
]
return {**doc, "담은로직": lines}
def build(parsed: dict[str, Any], name: str, tag: str, who: Any) -> dict[str, Any]:
"""호표 모두 → `{문서 {번: 사전조합}, 실패,
수 {호표, 줄, 품셈, 고정, 호표참조, 빠짐, 이음, 이음후보}}`."""
units = {int(h["번"]): h for h in parsed.get("호표") or []}
docs: dict[int, dict[str, Any]] = {}
sums = dict.fromkeys(SUM_KEYS, 0)
sums |= {"호표": len(units), "줄": sum(len(h.get("줄") or []) for h in units.values())}
failed = []
with tempfile.TemporaryDirectory() as scratch, mstore.at(scratch):
made_dir = Path(scratch) / "precombo"
made_dir.mkdir()
key = hopyo_store.save(scratch, name, parsed, who)["키"]
for no in hb._order(units):
try:
made = hb.Builder(Path(scratch), Path(scratch), key, no, who).build({}, [], False)
except mstore.StoreError as e:
failed.append({"번": no, "까닭": str(e.detail)})
continue
doc = {**made["문서"], "종류": refs.TITLE["precombo"], "키": _key(no)}
# 다음 호표의 `confirmed` 가 하위로 찾음(본뜬 확인 · 같은 원본 층)
(made_dir / f"{_key(no)}.json").write_text(
json.dumps(doc, ensure_ascii=False, default=float), "utf-8"
)
docs[no] = _owned(json.loads(json.dumps(doc, default=float)), tag)
for k in SUM_KEYS:
sums[k] += made["줄수"]["호표" if k == "호표참조" else k]
return {"문서": docs, "수": sums, "실패": failed}
def _ref(tag: str, no: int) -> dict[str, Any]:
return {"층": LAYER, "주인": tag, "키": _key(no)}
def _copies(docs: dict[int, dict[str, Any]], tag: str, no: int) -> dict[str, dict[str, Any]]:
"""그 호표 + 닿는 하위 호표 `{ref_id: 문서}`(굳힌 알약까지)."""
out: dict[str, dict[str, Any]] = {}
todo = [no]
while todo:
at = todo.pop()
rid = refs.ref_id(_ref(tag, at))
if rid in out or at not in docs:
continue
out[rid] = refs.with_pills(docs[at])
todo += [
int(one["참조"]["키"][3:])
for one in docs[at].get("담은로직") or []
if (one.get("참조") or {}).get("층") == LAYER
]
return out
def folder_of(tag: str, no: int) -> str:
return library.folder_name("precombo", LAYER, None, f"{tag}-{no}")
def copy(root: str | Path, built: dict[str, Any], tag: str, no: int, who: Any) -> str | None:
"""호표 하나 → 서고 사본 폴더 이름(있으면 그대로) · 못 만든 호표 = None."""
docs = built["문서"]
if no not in docs:
return None
name = folder_of(tag, no)
if isinstance(read_json(library.library_dir(root) / name / MANIFEST_NAME), dict):
return name
ref, doc = _ref(tag, no), docs[no]
copies = _copies(docs, tag, no)
line = {"참조": ref, "수량": {"손": 1}}
top = {"이름": doc.get("이름"), "단위": doc.get("단위"), "담은로직": [line]}
pill = refs.pill(ref, lambda r: copies.get(refs.ref_id(r)))
labor = {**logic_rows(top), "알약": [pill], "참조사본": copies}
files = {LABOR_FILE: labor, PRICE_FILE: price_bundle(labor, Path(mstore.FOLDER))}
origin = {"참조": ref, "name": doc.get("이름") or ref["키"], "단위": doc.get("단위") or ""}
manifest = {"종류": "precombo", "원본": origin, "가져온날": now_text(), "가져온이": who}
library._write(root, name, files, manifest, {})
return name