- 영월 꼴(STmate 엑셀 내역 줄 = 호표 코드만) = 일위대가표 같은 코드 호표 번 채움 → 서고 사본 21 · 이음 34 - 이은 줄 차가 비목마다 1 원/단위 안 = 「끝수」(STmate 하위 호표 반올림) · 끝 알림 한 줄 「엑셀 합계와 차 n 원 = 하위 호표 반올림 끝수 m 줄」(브레인 정함 = 끝수 줄도 이음) - 요율 못 읽은 공사(.estx · .STC) = 총공사비 칸 안 보임 - 시험 test_707_7 11(영월 끝수 · 일위대가 시트 없음 까닭) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
176 lines
7.7 KiB
Python
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
|