"""M02 일위대가 불러오기 → 단가산출(PLAN 718-1 · 729-1 · 778-5 D-3 · 요구 621 · 623) — 산근(단산) 블록마다 줄 판정 모음(`M02_Hopyo_SkUnit`) → 사용자가 확인한 블록만 단위 SK. 모두 줄로만(바탕 기초 안 씀 · 맞춤셈 1위 기초 = 근거 꼬리표만) · 자동 없음 — `확인 [{번, 고름?: {차례: 고름}}]` (블록마다 [확인] · [옮기기])에 든 블록만 만듦 · 확인 안 한 블록 = 판정만(안 만듦) · 줄 다 이어지지 않은 확인 블록 = `못만듦` · 하위 단산 줄 = 그 블록 SK 참조(확인 목록에 있으면 먼저 만듦 · 없으면 못만듦). SK = 이름 블록 이름(없으면 줄 품명) · 단위 = 줄 단위(없으면 블록 · 「식」) · 구분 「실무자료」 · 상세구분 = 파일 · 바탕 없음 · 본뜬 = {층 「실무자료」, 키 「HP-###### 단산N」, 파일, 원본, 확인, 갈래 「단위」, 근거 {로직, 이름}, 못옮김 []} · 같은 본뜬(키 · 원본)이 이 층에 있으면 그 키에 덮음 · 만드는 층 = 부르는 층. 같은 내용(733-1 · `M02_Library_Same` 지문) SK 가 이 층에 있으면 새로 안 만들고 그것을 씀 — 이름 = 먼저 만든 것 · 뒤 출처 = 그 SK `본뜬.또 [그 블록 본뜬 + 이름]`(`made` · `origin_of` 가 읽음) · 다른 출처가 쓰는 SK 는 내용을 안 바꿈(바뀌면 떼고 다시 찾음) · 응답 `같은것씀 [{번, 키}]` · 위 층 같은 것 = `위층같음 [{번, 층, 주인, 키}]`(알림만). 일위대가 만들기(`M02_Hopyo_SkLink`)가 부르는 `Maker.plan(번)`(확인 없음) = 고름 대기 「확인 전」 — 못 이음. """ 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_SkUnit as skunit from M02_MasterTemplete import M02_Hopyo_Units as units from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store from M02_MasterTemplete import M02_Labor_Ref as refs from M02_MasterTemplete import M02_Library_Same as same from M02_MasterTemplete import M02_MasterTemplete_Store as store ORIGIN = "실무자료" KIND = "sangeun" WAIT_WHY = "확인 전 — [단가산출]에서 블록 [확인] 뒤 [옮기기]" 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 for one in [got, *(got.get("또") or [])]: # 또 = 같은 내용으로 이 SK 를 쓴 뒤 출처(733-1) mark_ = str((one or {}).get("키") or "") if isinstance(one, dict) else "" if not mark_ or (key is not None and not mark_.startswith(f"{key} ")): continue if source is None or not one.get("원본") or tuple(one["원본"]) == tuple(source): out[mark_] = path.stem return out def _same_mark(a: dict, b: dict) -> bool: """같은 출처 — 본뜬 키 같음 · 원본(파일 층)이 둘 다 있으면 같음.""" if a.get("키") != b.get("키"): return False return not a.get("원본") or not b.get("원본") or tuple(a["원본"]) == tuple(b["원본"]) def origin_of(doc: dict, mark_: str, source: refs.Here | None = None) -> dict[str, Any]: """그 블록 출처의 본뜬(갈래 · 못옮김 · 놓음 …) — 또 가운데 `mark_` · 없으면 제 본뜬.""" own = doc.get("본뜬") or {} want = {"키": mark_, "원본": list(source) if source else None} also = [x for x in own.get("또") or [] if isinstance(x, dict)] return next((x for x in also if _same_mark(x, want)), own) 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 _sure(raw: Any) -> dict[int, dict[int, Any]]: """확인 목록 `[{번, 고름?: {차례: 고름}}]` → `{번: {차례: 고름}}`(고름 꼴은 `M02_Hopyo_SkUnit` 이 봄).""" if not raw: return {} if not isinstance(raw, list) or len(raw) > 500: raise _bad("확인은 [{번, 고름?: {차례: 고름}}]") out: dict[int, dict[int, Any]] = {} for one in raw: try: picks = one.get("고름") or {} out[int(one["번"])] = {int(k): v for k, v in picks.items()} except (TypeError, ValueError, KeyError, AttributeError) as e: raise _bad("확인은 [{번, 고름?: {차례: 고름}}]") from e 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 ("갈래", "엑셀품", "못옮김", "근거") if k in plan} 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 ("자재표시", "덮개", "더한줄", "뺀줄")}, } 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()}} self.same, self.reused, self.above = same.Same(), [], [] self.keys = made(source, key, self.here) # {본뜬 키: SK} — 만든 것 더해 감 self.judged: dict[int, dict] = {} self._units: units.Units | None = None def units(self) -> units.Units: if self._units is None: books = ledger.books(self.source) self._units = units.Units(books, ledger.doc_id(self.source, self.key)) return self._units def basis(self, no: int) -> dict[str, Any] | None: """근거 꼬리표 = 맞춤셈 1위 기초(참고).""" entry = self.cache.get(fit.unit_key(self.first[no])) or {} top = (entry.get("차례") or [None])[0] if not top: return None name = (self.whole.logic(top) or {}).get("이름") return {"로직": top, "이름": name, "판정": (entry.get("판정") or {}).get("상태")} def ref_of(self, sub: dict[str, Any]) -> dict[str, Any] | None: """하위 줄 → 참조 칸 — 단산 = 이 HP 그 블록 SK · 일위 = 확인된 그 호표 SC · 없으면 None.""" from M02_MasterTemplete import M02_Hopyo_Build as hopyo_build # Build → SkLink → 여기 if sub.get("종류") == "단산": got = self.keys.get(mark(self.key, int(sub["번"]))) else: done = hopyo_build.confirmed(store.root(), self.here) got = done.get(hopyo_build.mark(self.key, int(sub["번"]))) layer, owner = refs.here() return {"층": layer, "주인": owner, "키": got} if got else None def judge(self, no: int, picks: dict | None = None) -> dict[str, Any]: line, block = self.first[no], self.blocks[no] unit = str(line.get("단위") or block.get("단위") or "").strip() try: got = skunit.judge(block, self.units(), self.ref_of, picks, unit) except skunit.PickError as e: raise _bad(f"단산{no} · {e}") from e self.judged[no] = got return got def plan(self, no: int, picks: dict | None = None) -> dict[str, Any]: """확인한 블록 → 단위 SK 꼴 · 확인 없음(`picks` None) · 고를 줄 남음 = 고름 대기.""" if picks is None: return {"갈래": skpatch.WAIT, "까닭": WAIT_WHY, "후보": []} got = self.judge(no, picks) if got["고를줄"]: return {"갈래": skpatch.WAIT, "까닭": got["판정"], "후보": []} return skunit.plan(got, self.basis(no)) def make(self, no: int, sure: dict[int, dict], out: dict, path: tuple = ()) -> None: """확인한 블록 하나 — 확인 목록의 하위 단산 먼저 · 못 만들면 `못만듦`.""" mark_ = mark(self.key, no) if no in path or any(x["번"] == no for x in out["만듦"] + out["못만듦"]): return for one in self.blocks[no].get("항목") or []: sub = one.get("하위") or {} m = int(sub.get("번") or 0) if sub.get("종류") == "단산" and m in sure and mark(self.key, m) not in self.keys: self.make(m, sure, out, (*path, no)) plan = self.plan(no, sure[no]) got = self.judged[no] lost = [r for r in got["줄"] if r["상태"] == units.LINKED and r["차례"] not in got["담음"]] name = self.blocks[no].get("이름") or self.first[no].get("이름") if plan["갈래"] == skpatch.WAIT or lost: why = plan.get("까닭") or f"하위 「{lost[0]['이름']}」 — 그 블록 먼저 확인" out["못만듦"].append({"번": no, "이름": name, "까닭": why}) return key = self.write(no, plan, self.keys.get(mark_)) self.keys[mark_] = key out["만듦"].append({"번": no, "이름": name, "키": key, "갈래": skunit.UNIT, "못옮김": []}) def write(self, no: int, plan: dict, have: str | None) -> str: """그 블록 SK 를 씀 → 키 — 다른 출처(또)가 쓰는 SK 의 내용은 안 바꿈(바뀌면 떼고 다시 찾음).""" doc = _doc(self.first[no], self.blocks[no], no, self.hp, plan, self.sure) root, mine = store.root(), doc["본뜬"] self.above += [{"번": no, **r} for r in self.same.find_above(root, KIND, doc)] if have: raw, old, own, rest = self._read(have, mine) owner = _same_mark(own, mine) if self.same.finger(KIND, old) == self.same.finger(KIND, doc): # 내용 같음 = 출처만 self._put( have, raw, old, *((mine, rest) if owner else (own, [*rest, self._entry(doc)])) ) return have if owner and not rest: # 혼자 쓰던 SK — 제자리 덮음 store.write(KIND, have, store.version_of(raw), {**doc, "키": have}) self.same.add(root, KIND, have, doc) return have head = {k: v for k, v in rest[0].items() if k != "이름"} if owner else own self._put(have, raw, old, head, rest[1:] if owner else rest) # 떼어 냄 found = self.same.find_same(root, KIND, doc) if found: raw, old, own, rest = self._read(found[0], mine) self._put(found[0], raw, old, own, [*rest, self._entry(doc)]) self.reused.append({"번": no, "키": found[0]}) return found[0] key = refs.create(KIND, doc)["이름"] self.same.add(root, KIND, key, doc) return key @staticmethod def _entry(doc: dict) -> dict[str, Any]: return {**doc["본뜬"], "이름": doc["이름"]} @staticmethod def _read(key: str, mine: dict) -> tuple[bytes, dict, dict, list]: """(바이트, 문서, 제 본뜬(또 뺌), 또 가운데 `mine` 뺀 것).""" raw = (store.root() / KIND / f"{key}.json").read_bytes() doc = json.loads(raw) got = doc.get("본뜬") or {} own = {k: v for k, v in got.items() if k != "또"} rest = [x for x in got.get("또") or [] if isinstance(x, dict) and not _same_mark(x, mine)] return raw, doc, own, rest @staticmethod def _put(key: str, raw: bytes, doc: dict, own: dict, also: list) -> None: body = {**doc, "본뜬": {**own, "또": also} if also else own} if body != doc: store.write(KIND, key, store.version_of(raw), body) def build(folder: Path, key: str, who: Any = None, picks: Any = None) -> dict[str, Any]: """[단가산출] — 블록마다 줄 판정 · 확인한 블록만 단위 SK → `{블록, 판정 [{번, 이름, 단위, 판정, 고를줄, 우리금액, 엑셀금액, 근거, 만든, 줄}], 만듦 [{번, 이름, 키, 갈래, 못옮김}], 못만듦 [{번, 이름, 까닭}], 그대로, 같은것씀, 위층같음}` · `picks` = 확인 목록.""" sure = _sure(picks) maker = Maker(folder, key, who) bad = sorted(set(sure) - set(maker.first)) if bad: raise _bad(f"없는 블록 · 부르는 줄 없음 「단산{bad[0]}」", 404) if not maker.done: fit.ensure(folder, key) raise _bad("맞춤 셈 중 — 끝나면 다시 누름", 409) out: dict[str, Any] = {"블록": len(maker.first), "만듦": [], "못만듦": []} for no in sorted(sure): maker.make(no, sure, out) rows = [] for no in sorted(maker.first): line, block = maker.first[no], maker.blocks[no] got = maker.judged.get(no) or maker.judge(no) head = {"번": no, "이름": block.get("이름") or line.get("이름"), "단위": line.get("단위")} keep = ("판정", "고를줄", "우리금액", "엑셀금액", "줄") head |= {k: got[k] for k in keep} | {"근거": maker.basis(no)} rows.append({**head, "만든": maker.keys.get(mark(key, no))}) out["판정"] = rows out["그대로"] = sum(1 for r in rows if r["만든"] and r["번"] not in sure) out["같은것씀"], out["위층같음"] = maker.reused, maker.above return out