"""M02 일위대가 불러오기 → 단가산출(PLAN 718-1 · 729-1 · 요구서 내역19 1장 7 · 3장 6) — 산근(단산) 블록마다 매칭 → 비교 → 기초 · SK · 고름 대기. 블록마다 그 블록을 부르는 첫 엑셀 줄(「단산N참조」)의 맞춤셈(`M02_Hopyo_Fit` 캐시 · 없으면 그 블록만 셈) → `M02_Hopyo_SkPatch.plan`: 기초 = SK 바탕 = 1위 기초 · 입력값 · 덮개 없음 · SK = 바탕 기초 + 덮개 · 더한줄 · 뺀줄(엑셀 품 그대로) · 고름 대기 = SK 안 만듦(바탕 없는 SK 로 몰래 채우지 않음) · 후보 다섯(성격 다른 단위 = `계수` 칸 뜻) → 사용자가 고른 기초 `바탕 {번: {로직?, 입력값?, 환산?, 잇기?}}` 로 다시 부르면 그 기초에 견줘 SK(이미 있으면 그 키에 덮음) · 잇기 `[{id, 노임|기계|자재: 키} | {id, 뺌}]` = 못옮김 → SK 더한줄 · 남은 못옮김 = 응답 `만듦[].못옮김` · 본뜬 그대로(그 SK 를 쓰는 일위대가 만들기 = 남은 줄 409 · `M02_Hopyo_SkLink`) · 그대로 SK 의 못옮김 = 응답 `남은못옮김 [{번, 이름, 키, 못옮김}]` · 잇기만 보냈는데 1위 기초가 고름 대기면 그 SK 바탕(로직 · 입력값 · 환산)으로 다시 셈(729-5). SK = 이름 블록 이름(없으면 줄 품명) · 단위 = 줄 단위(없으면 블록 · 「식」) · 구분 「실무자료」 · 상세구분 = 파일 · 본뜬 = {층 「실무자료」, 키 「HP-###### 단산N」, 파일, 원본, 확인, 갈래, 엑셀품, 못옮김} · 같은 본뜬(키 · 원본)이 이 층에 있으면 그대로(겹침 없음) · 만드는 층 = 부르는 층. 별칭 제안 = 기초 · SK 만든 블록의 엑셀 줄 → 이 층 별칭 장부 짝 = 바탕 기초까지만(차이 SK 는 이 층 문서 · 요구서 3장 6) · 장부에 이미 짝 · 갈래가 있는 줄은 그대로 · 승인 길 = M01 별칭 장부(716). """ from __future__ import annotations import json from pathlib import Path from typing import Any from common_util.common_util_time import utc_now_text from M01_MasterData import M01_MasterData_Store as m01 from M02_MasterTemplete import M02_Hopyo_Fit as fit from M02_MasterTemplete import M02_Hopyo_Ledger as ledger from M02_MasterTemplete import M02_Hopyo_SkPatch as skpatch 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 store from M02_MasterTemplete import M02_Sangeun_Doc as sd ORIGIN = "실무자료" KIND = "sangeun" def mark(key: str, no: int) -> str: return f"{key} 단산{no}" def made( folder: str | Path, key: str | None = None, source: refs.Here | None = None ) -> dict[str, str]: """이 층 실무자료 단가산출 `{본뜬 키: SK 키}`(`key` = 그 HP 것만 · `source` = 파일 층으로 거름).""" out = {} for path in sorted((Path(folder) / KIND).glob("SK-*.json")): try: got = json.loads(path.read_bytes()).get("본뜬") except (OSError, ValueError, AttributeError): continue if not isinstance(got, dict) or got.get("층") != ORIGIN: continue if key is not None and not str(got.get("키") or "").startswith(f"{key} "): continue if source is None or not got.get("원본") or tuple(got["원본"]) == tuple(source): out[str(got.get("키"))] = path.stem return out def calls(hp: dict[str, Any]) -> tuple[dict[int, dict], dict[int, dict]]: """(블록 {번: 블록}, 부르는 첫 줄 {번: 줄 + `_호표`}).""" blocks = {int(b["번"]): b for b in hp.get("단산") or [] if b.get("번") is not None} first: dict[int, dict] = {} for h in hp.get("호표") or []: for line in h.get("줄") or []: ref = line.get("참조") or {} no = int(ref.get("번") or 0) if ref.get("종류") == "단산" else 0 if no in blocks and no not in first: first[no] = {**line, "_호표": h.get("번")} return blocks, first def _bad(text: str, status: int = 400) -> store.StoreError: return store.StoreError(status, text) def _picks(raw: Any) -> dict[int, dict[str, Any]]: """사용자 고름 `{번: {로직?, 입력값?, 환산?, 잇기?}}` 재검사 — 돈 로직만 · 입력 이름 = 그 로직 입력 · 환산 = SK 꼴(`M02_Sangeun_Doc`) · 잇기 = `[{id, …}]`(id 짝 · 키는 `M02_Hopyo_SkPatch` 가 봄).""" if not raw: return {} if not isinstance(raw, dict) or len(raw) > 500: raise _bad("바탕은 {블록 번: {로직, 입력값}}") logics = ledger._known()[0] out = {} for no, one in raw.items(): code = str((one or {}).get("로직") or "") if isinstance(one, dict) else "?" if code and code not in logics: raise _bad(f"단산{no} · 돈 로직이 아님 「{code}」") if not code and not (one.get("환산") or one.get("잇기")): raise _bad(f"단산{no} · 로직 · 환산 · 잇기 중 하나") pick: dict[str, Any] = {} given = one.get("입력값") if code else None if code: pick["로직"] = code if given is not None: bad = set(given) - ledger.input_names(code) if isinstance(given, dict) else {1} if bad: raise _bad(f"단산{no} · 모르는 입력 {sorted(map(str, bad))}") pick["입력값"] = given if one.get("환산"): try: pick["환산"] = sd._swap_how(one["환산"]) except sd.DocError as e: raise _bad(f"단산{no} · {e}") from e links = one.get("잇기") if links: if not isinstance(links, list) or not all( isinstance(x, dict) and x.get("id") for x in links ): raise _bad(f"단산{no} · 잇기는 [{{id, 노임|기계|자재: 키}}]") pick["잇기"] = links out[int(no)] = pick return out def _doc(line: dict, block: dict, no: int, hp: dict, plan: dict, sure: dict) -> dict[str, Any]: name = str(block.get("이름") or line.get("이름") or "").strip() or f"단산{no}" unit = str(line.get("단위") or block.get("단위") or "").strip() or "식" keep = {k: plan[k] for k in ("갈래", "엑셀품", "못옮김")} keep |= {"놓음": plan["놓음"]} if plan.get("놓음") else {} return { "종류": "단가산출", "이름": name, "단위": unit, "구분": ORIGIN, "상세구분": str(hp.get("파일") or ""), "본뜬": {"층": ORIGIN, "키": mark(hp["키"], no), "파일": hp.get("파일"), **sure, **keep}, "바탕": plan["바탕"], "입력값": plan["입력값"], **({"환산": plan["환산"]} if plan.get("환산") else {}), "열린입력": [], "고름": {}, **{k: plan[k] for k in ("자재표시", "덮개", "더한줄", "뺀줄")}, } def _old_base(folder: Path, key: str) -> dict[str, Any]: """이미 있는 SK 의 바탕 고름 `{로직, 입력값, 환산?}` · `본뜬` — 읽기 실패 = 빈 것.""" try: doc = json.loads((Path(folder) / KIND / f"{key}.json").read_bytes()) except (OSError, ValueError): return {} base = {"로직": (doc.get("바탕") or {}).get("로직"), "입력값": doc.get("입력값") or {}} return { **base, **({"환산": doc["환산"]} if doc.get("환산") else {}), "본뜬": doc.get("본뜬") or {}, } def _alias(line: dict, plan: dict) -> dict[str, Any]: origin = {"품명": line.get("이름"), "규격": line.get("규격"), "단위": line.get("단위")} clauses = (line.get("근거") or {}).get("출처") or [] pair = {"로직": plan["바탕"]["로직"], "입력값": plan["입력값"], "배": 1, "몫": None} return { "원래": {k: str(v or "") for k, v in origin.items()}, "갈래": "짝", "짝": [pair], "엑셀조항": clauses if isinstance(clauses, list) else [], } class Maker: """한 번 만들기 — 파일 · 맞춤셈 · 이 층 SK 를 한 번 읽음(`source` = 파일 층 · 쓰기 = `store.root()`).""" def __init__(self, source: Path, key: str, who: Any = None) -> None: self.source, self.key, self.who = Path(source), key, who self.hp = hopyo_store.read(source, key) self.blocks, self.first = calls(self.hp) got = fit.result(source, key) or {} self.cache, self.done = got.get("블록") or {}, bool(got.get("끝")) self.here = refs.where(source) self.whole = m01.loaded()[1] self.sure = {"원본": list(self.here), "확인": {"누가": who, "때": utc_now_text()}} def plan(self, no: int, pick: dict | None = None) -> dict[str, Any]: line, block = self.first[no], self.blocks[no] entry = self.cache.get(fit.unit_key(line)) if entry is None and not (pick or {}).get("로직"): entry = fit.compute(block, line) try: return skpatch.plan(entry, block, line, self.whole, pick) except skpatch.LinkError as e: raise _bad(f"단산{no} · {e}") from e def write(self, no: int, plan: dict, have: str | None) -> str: doc = _doc(self.first[no], self.blocks[no], no, self.hp, plan, self.sure) if have: raw = (store.root() / KIND / f"{have}.json").read_bytes() store.write(KIND, have, store.version_of(raw), {**doc, "키": have}) return have return refs.create(KIND, doc)["이름"] def build(folder: Path, key: str, who: Any = None, picks: Any = None) -> dict[str, Any]: """[단가산출 만들기] — 산근 블록마다 기초 · SK · 고름 대기 → `{블록, 만듦 [{번, 이름, 키, 갈래, 못옮김, 환산?}], 그대로, 남은못옮김 [{번, 이름, 키, 못옮김}], 기초, SK, 고름대기 [{번, 이름, 단위, 까닭, 후보}], 별칭}` · `picks` = 고름.""" chosen = _picks(picks) maker = Maker(folder, key, who) bad = sorted(set(chosen) - set(maker.first)) if bad: raise _bad(f"없는 블록 · 부르는 줄 없음 「단산{bad[0]}」", 404) if not maker.done and set(maker.first) - set(chosen): fit.ensure(folder, key) raise _bad("맞춤 셈 중 — 끝나면 다시 누름", 409) have = made(folder, key, maker.here) out: dict[str, Any] = {"블록": len(maker.first), "만듦": [], "그대로": 0, "고름대기": []} out["남은못옮김"] = [] out.update({skpatch.BASE: 0, skpatch.SK: 0}) books, alias = ledger.books(folder), {} for no in sorted(maker.first): old = have.get(mark(key, no)) line = maker.first[no] name = maker.blocks[no].get("이름") or line.get("이름") if old and no not in chosen: out["그대로"] += 1 lost = _old_base(folder, old).get("본뜬", {}).get("못옮김") if lost: out["남은못옮김"].append({"번": no, "이름": name, "키": old, "못옮김": lost}) continue plan = maker.plan(no, chosen.get(no)) if plan["갈래"] == skpatch.WAIT and old and not chosen[no].get("로직"): base = _old_base(folder, old) # 고른 기초로 만든 SK 의 잇기 = 그 바탕 그대로 if base.get("로직"): pick = {k: v for k, v in base.items() if k != "본뜬"} plan = maker.plan(no, {**pick, **chosen[no]}) if plan["갈래"] == skpatch.WAIT: head = {"번": no, "이름": maker.blocks[no].get("이름") or line.get("이름")} out["고름대기"].append({**head, "단위": line.get("단위"), **plan}) continue got = maker.write(no, plan, old) out[plan["갈래"]] += 1 made_ = {"번": no, "이름": name, "키": got, "갈래": plan["갈래"], "못옮김": plan["못옮김"]} out["만듦"].append({**made_, **({"환산": plan["환산"]} if plan.get("환산") else {})}) found = ledger.recall(line, folder, books) if no in chosen or found is None or found.get("규격다름"): alias[ledger.key(line)] = _alias(line, plan) out["별칭"] = ledger.write(folder, alias, who, maker.hp.get("파일") or None) return out