"""M02 엑셀 불러오기 만들기(PLAN 785-5 · 요구 642 · 설계 642-A 2-7 ⑥) — 확인한 것 → 우리 SC · SK · 장부. 옮기기 몸 = `{호표 {번: {차례: [고른 것]}}, 블록 {번: [고른 것]}}` · 고른 것 = 785-3 꼴 `{갈래 기초|단가산출|일위대가|인력|재료|유가|기계|율|고정, 키, 입력값, 몫, 배?, 비목?, 기준?, 별도계상?, 층?, 주인?, 항목?}` · 서버가 다시 셈(브라우저 금액 · 품 안 받음 · 배 = `Row.factor` · 사용자 배 우선 · 입력값 = 엔진이 쓴 칸 모두 `Judge.inputs`) · 「입력 필요」 · 배 못 정함 · 없는 키 = 그 줄(블록)만 `막힘`. SC(`M02_Hopyo_Build.Builder` 가 줄 차례 · 확인 안 한 줄 = 모두 고정 길 그대로) — 한 엑셀 줄 = 고른 것마다 우리 줄(같은 별칭 = 엑셀 품명 · 줄 `본뜬 {호표, 차례, 행}`) · 수량 {손 = 엑셀 수량 × 몫 × 배} · 기초 = 로직 줄(같은 로직 둘째 = 한 줄 SC 사본) · 재료 · 유가 = 자재 목록(별도계상) · 인력 · 기계 · 유가 이음 · 별도계상 줄(`고정` + 별도계상 · 786-1) = 단가 0 고정 줄(합 밖 · 메모 「별도계상 · 품목은 프로젝트에서」) · 율 = 고정 줄 `비율 {기준, 비목}` · 고정 = 엑셀 단가 고정 줄 · 단가산출 · 일위대가 = 참조 (`단산N` = 같은 옮기기 블록 SK · `제N호표` = 같은 HP 호표 SC — 하위 먼저 · 확인 안 된 하위 = 모두 고정). SK(블록) = 바탕 없음 + 더한줄 — 기초 `{종류 로직, 입력값, 수량 = 몫}` · 참조 · 단위 줄(`항목`)에 이은 인력 · 기계 · 재료 · 유가 · 율 = `SkUnit.adds` · 고정 = 엑셀 금액 · 쓰기 = `Sangeun.Maker.write`. 같은 내용 재사용(`Kids.save` · `Sangeun.Maker.write`) · 장부 = 고른 것 꼴 짝(`Ledger.check_pair`) · 엑셀 대조 = `Compare.compare`(참고 · 비목마다 1 원). `Lines` = 확인한 줄 몸(옛 대상 꼴 `put` 은 옛 길 몫 · 785-7 지움). """ from __future__ import annotations import re from decimal import Decimal from pathlib import Path from typing import Any from M02_MasterTemplete import M02_Hopyo_Build as hbuild from M02_MasterTemplete import M02_Hopyo_Fixed as hfix from M02_MasterTemplete import M02_Hopyo_Judge as judge from M02_MasterTemplete import M02_Hopyo_Kids as hkids from M02_MasterTemplete import M02_Hopyo_Ledger as ledger from M02_MasterTemplete import M02_Hopyo_Material as hmat from M02_MasterTemplete import M02_Hopyo_Sangeun as sangeun from M02_MasterTemplete import M02_Hopyo_SkUnit as skunit from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store from M02_MasterTemplete import M02_Hopyo_Units as units from M02_MasterTemplete import M02_Labor_Doc as labor from M02_MasterTemplete import M02_Labor_Ref as refs from M02_MasterTemplete import M02_Library_Same as lsame from M02_MasterTemplete import M02_MasterTemplete_Store as store from M02_MasterTemplete import M02_Sangeun_Doc as sd from M02_MasterTemplete.M02_Hopyo_Compare import compare _SUB = {"단가산출": re.compile(r"^단산(\d+)$"), "일위대가": re.compile(r"^제(\d+)호표$")} LINK = {"인력": "노임", "기계": "기계", "유가": "유가"} OLD_PICK = {"인력": "노임", "기계": "기계", "재료": "자재", "유가": "자재"} NEED = "입력 필요" APART_MEMO = "별도계상 · 품목은 프로젝트에서" COVER = "이미 옮긴 것이 있음 — 덮으려면 [덮어 옮기기](덮음)" class Stuck(Exception): """그 줄(블록)만 못 옮김 — `{까닭, 칸?}`.""" def __init__(self, why: str, cells: list[str] | None = None) -> None: super().__init__(why) self.why, self.cells = why, cells or [] def _num(value: Decimal) -> int | float: return hbuild._num(value) # ── 확인한 줄 몸(SC) ───────────────────────────────────────────────────── class Lines: """한 호표 줄 모음 — `Builder.build` 가 줄 차례대로 부름 · 확인 안 한 줄은 Builder 가 고정 길로.""" def __init__(self, b: Any, rows: list[dict], where: dict, stamp: str, linker: Any) -> None: self.b, self.rows, self.where, self.stamp, self.linker = b, rows, where, stamp, linker self.lines: list[dict] = [] self.mats: dict[str, dict] = {} self.missing: list[dict] = [] self.used: set[str] = set() self.mat_lines: dict[str, dict] = {} self.nation: dict[tuple, dict] = {} self.taken: set[str] = set() self.kids: set[int] = set() self.left: list[dict] = [] def put(self, line: dict, one: dict, alias: str | None, qty: Decimal | None = None, mark: dict | None = None) -> None: # fmt: skip """대상 하나 → 줄 · 자재 — 옛 꼴 `{로직|자재|호표|전국|이음, 입력값, 배, 몫}` · `qty` = 새 길 수량 (엑셀 × 몫 × 배) · `mark` = 새 길 줄 `본뜬`.""" keep = len(self.lines) if "이음" in one: others = [x for x in self.rows if x is not line] fx = hfix.line_of(line, others, self.where, self.taken, self.linker, one["이음"]) self.lines.append(fx) else: qty = ( qty if qty is not None else Decimal(str(line.get("수량") or 0)) * Decimal(str(one["배"])) ) self._put(line, one, alias, qty, mark is not None) for row in self.lines[keep:]: row.update({"본뜬": mark} if mark else {}) def _put(self, line: dict, one: dict, alias: str | None, qty: Decimal, new: bool) -> None: b = self.b memo = hbuild._memo(line, one) if "전국" in one: at = (one["이름"], one["규격"], one["단위"]) have = self.nation.get(at) hand = qty + Decimal(str(have["수량"]["손"])) if have else qty head = {"이름": at[0], "규격": at[1], "단위": at[2], "수량": {"손": _num(hand)}} self.nation[at] = {**head, **(hmat.marks(line, at[0]) if not have else { k: have[k] for k in labor.MARKS if k in have })} # fmt: skip self.missing.append(hbuild._missing(line, hbuild.NATIONAL)) return if "자재" in one: code = one["자재"] have = self.mats.get(code) hand = qty + Decimal(str(have["수량"]["손"])) if have else qty name = b.mat_names.get(code, "") marks = ( have and {k: have[k] for k in labor.MARKS if k in have} or hmat.marks(line, name) ) if one.get("별도계상") is not None: # 세 칸 값(빈 칸 = 기본 「별도계상」) marks = {**marks, "별도계상": "별도계상" if one["별도계상"] else "직접계산"} self.mats[code] = {"코드": code, "수량": {"손": _num(hand)}, **marks} if not have: self.mat_lines[code] = line return if "호표" in one or "참조" in one: if "호표" in one: self.kids.add(one["호표"]) key = b.done.get(hbuild.mark(b.key, one["호표"]), "SC-미리") ref = {"층": refs.here()[0], "주인": refs.here()[1], "키": key} else: ref = one["참조"] self.lines.append({"참조": ref, "별칭": alias, "메모": memo, "수량": {"손": _num(qty)}}) return row = {"로직": one["로직"], "입력값": one["입력값"], "메모": memo} if not new: kept, why = sangeun.kept(b, line, one, qty, alias, memo) if why: self.left.append(sangeun.left(line, why)) if kept is not None: self.lines.append(kept) return if one["로직"] not in self.used: self.used.add(one["로직"]) self.lines.append({**row, "별칭": alias, "수량": {"손": _num(qty)}}) return at = int(line.get("차례", 0)) hopyo, file = b.hopyo, b.doc.get("파일") or "" wrap = { # 같은 로직 둘째 — 한 줄 사전조합으로 감쌈 "이름": f"{hopyo['이름']} — {line.get('이름')}", "단위": line.get("단위") or None, "구분": hbuild.ORIGIN, "상세구분": file, "담은로직": [{**row, "별칭": alias, "수량": {"손": 1}}], "자재": [], "본뜬": b._origin(f"{hbuild.mark(b.key, b.no, at)} {one['로직']}", self.stamp), } ref = {"키": "SC-미리"} if b.dry else hkids.save(wrap, b.same)[0] # 미리 보기 = 쓰기 없음 self.lines.append({"참조": ref, "별칭": alias, "메모": memo, "수량": {"손": _num(qty)}}) def apart(self, line: dict, alias: str | None, mark: dict) -> None: """별도계상 줄(786-1) = 합 밖 고정 줄 — 단가 0(셈에 안 듦) · 품명 · 규격 · 단위 · 수량 그대로 · 품목은 프로젝트에 복사할 때 정함(메모 표지).""" zero = {**line, "단가": {}, "금액": {}} self.put(zero, {"이음": dict.fromkeys(hfix.fixed.LINKS)}, alias, None, mark) self.lines[-1]["메모"] = f"{APART_MEMO} · {self.lines[-1].get('메모') or ''}".strip(" ·") def picks(self, at: int, line: dict, picks: list[dict]) -> None: """고른 것(검사 · 다시 셈 끝난 것) → 그 엑셀 줄의 우리 줄 · 자재.""" alias = str(line.get("이름") or "")[: labor.ALIAS_MAX].strip() or None mark = {"호표": self.b.no, "차례": at, "행": line.get("행")} for p in picks: kind, code = p["갈래"], p.get("키") one = {"배": p["배"], "몫": p.get("몫")} qty = p["수량"] if kind == "기초": self.put(line, {**one, "로직": code, "입력값": p["입력값"]}, alias, qty, mark) elif kind in ("재료", "유가") and not p.get("이음"): self.put( line, {**one, "자재": code, "별도계상": p.get("별도계상")}, alias, qty, mark ) elif kind in ("단가산출", "일위대가"): self.put(line, {**one, "참조": p["참조"]}, alias, qty, mark) elif kind == "율": self.put(line, {"이음": {"비율": p["비목"]}}, alias, None, mark) self.lines[-1]["고정"]["비율"] = {"기준": p["기준"], "비목": p["비목"]} elif kind == "고정" and p.get("별도계상"): self.apart(line, alias, mark) elif kind == "고정": self.put(line, {"이음": dict.fromkeys(hfix.fixed.LINKS)}, alias, None, mark) else: self.put(line, {"이음": {LINK[kind]: code}}, alias, None, mark) def ledger_pick(p: dict[str, Any]) -> dict[str, Any]: """장부 짝(고른 것 꼴 · `Ledger.check_pair`) — 참조 = 만든 SK · SC 키.""" keep = ("갈래", "키", "입력값", "배", "몫", "비목", "기준", "별도계상") out = {k: p[k] for k in keep if p.get(k) is not None} return out | ({"키": p["참조"]["키"]} if "참조" in p else {}) # ── 고른 것 검사 · 다시 셈 ────────────────────────────────────────────── class Move: """한 번 옮기기 — 파일 · M01 · 만든 SC · SK 를 한 번 읽음(쓰기 = `store.root()`).""" def __init__(self, folder: Path, source: Path, key: str, who: Any) -> None: self.folder, self.source, self.key, self.who = Path(folder), Path(source), key, who self.hp = hopyo_store.read(source, key) self.units = {int(h["번"]): h for h in self.hp.get("호표") or []} self.m = judge.master() self.known = ledger._known() self.sk = sangeun.Maker(source, key, who) self.same = lsame.Same() self.out: dict[str, Any] = {"만듦": [], "단가산출": [], "막힘": [], "하위": []} self.out["장부"] = {"씀": 0} def _ref(self, p: dict) -> dict[str, Any]: """단가산출 · 일위대가 키 → 참조 칸(같은 HP 블록 · 호표 = 만든 것) · 없으면 막힘.""" code, kind = str(p.get("키") or ""), p["갈래"] hit = _SUB[kind].match(code) here = refs.here() if hit: n = int(hit.group(1)) made = ( self.sk.keys.get(sangeun.mark(self.key, n)) if kind == "단가산출" else hbuild.confirmed(self.folder, refs.where(self.source)).get( hbuild.mark(self.key, n) ) ) if not made: raise Stuck(f"{code} 먼저 옮김") return {"층": here[0], "주인": here[1], "키": made} head = "SK-" if kind == "단가산출" else "SC-" ref = {"층": p.get("층") or here[0], "주인": p.get("주인"), "키": code} if p.get("층") is None: ref["주인"] = here[1] if not code.startswith(head) or refs.read_ref(ref) is None: raise Stuck(f"없는 {kind} 「{code}」") return ref def pick(self, p: dict, line: dict | None, where: str) -> dict[str, Any]: """고른 것 하나 재검사 · 다시 셈 → `{갈래, 키, 입력값, 배, 몫, 수량, 참조?, …}`.""" kind = p["갈래"] got = {k: p[k] for k in ("갈래", "키", "몫", "비목", "기준", "별도계상") if k in p} if kind == "기초": full, need = judge.inputs(self.m, str(p.get("키") or ""), p.get("입력값")) if need: raise Stuck(f"{NEED} {len(need)}", need) got["입력값"] = full if kind in _SUB: got["참조"] = self._ref(p) else: plain = {k: v for k, v in got.items() if k in ledger.PICK_KEYS} ledger.check_pair(plain, where, self.known) if kind in ("재료", "유가") and not got.get("키"): raise Stuck(f"{kind} 키 없음") if line is not None: qty = judge.quantity(self.m, line, p) if qty["배"] is None: raise Stuck("단위 환산 입력") got["배"], got["수량"] = qty["배"], Decimal(str(qty["우리"] or 0)) return got # ── SC ── def hopyo(self, no: int, rows: dict[int, list[dict]]) -> None: unit = self.units[no] lines = unit.get("줄") or [] checked = {} for at, picks in sorted(rows.items()): if not 0 <= at < len(lines): raise store.StoreError(400, f"없는 줄 「제{no}호표 {at}」") where = f"제{no}호표 {at + 1}째 줄" if not picks: continue try: checked[at] = {"고른": [self.pick(p, lines[at], where) for p in picks]} except Stuck as e: self.out["막힘"].append({"호표": no, "차례": at, "까닭": e.why, "칸": e.cells}) b = hbuild.Builder(self.folder, self.source, self.key, no, self.who) b.strict, b.same = False, self.same made = b.build(checked, []) ref = made["참조"] head = {"번": no, "키": ref["키"], "이름": unit.get("이름")} head |= {k: made[k] for k in ("다시씀", "재사용")} self.out["만듦"].append({**head, "비교": compare(ref, unit)}) self.out["장부"]["씀"] += made["장부"]["씀"] def kid(self, no: int) -> None: """확인 안 된 하위 호표 — 모두 고정(705-3 길)으로 먼저.""" b = hbuild.Builder(self.folder, self.source, self.key, no, self.who) b.strict, b.same = False, self.same if b.done.get(hbuild.mark(self.key, no)): return made = b.build({}, []) self.out["하위"].append({"번": no, "키": made["참조"]["키"], "새로": True}) # ── SK ── def block(self, no: int, picks: list[dict]) -> None: blk = self.sk.blocks[no] items = blk.get("항목") or [] us = self.sk.units() made: list[dict] = [] rows: list[dict] = [] for p in picks: got = self.pick(p, None, f"단산{no}") name = str(p.get("이름") or "").strip() or str(blk.get("이름") or f"단산{no}") share = got.get("몫") or 1 at = p.get("항목") item = items[at] if isinstance(at, int) and 0 <= at < len(items) else None if got["갈래"] == "기초": made.append({"종류": "로직", "로직": got["키"], "입력값": got["입력값"], "이름": name, "수량": share}) # fmt: skip continue if got["갈래"] in _SUB: qty = float(item.get("품") or 1) if item else share made.append({"참조": got["참조"], "이름": name, "수량": qty}) continue if item is None: raise Stuck(f"{got['갈래']} — 블록 단위 줄(항목) 고름") if got["갈래"] == "고정": parts = hfix._parts(item.get("금액")) fx = {"이름": name, "단가": {k: float(v) for k, v in parts.items()}, "이음": {}} made.append({"고정": fx, "이름": name, "수량": 1}) continue old = {"기준": got.get("기준"), "비목": got.get("비목")} if got["갈래"] in OLD_PICK: old = {OLD_PICK[got["갈래"]]: got["키"], "별도계상": got.get("별도계상")} unit = skunit._picked(item, old, us)["단위"] rows.append({"차례": at, "행": item.get("행"), "상태": units.LINKED, "단위": unit}) extra, _ = skunit.adds(blk, rows, us, self.sk.ref_of) doc = {"키": "SK-000000", "이름": "옮김", "단위": blk.get("단위") or "식", "바탕": None} try: adds = sd.clean({**doc, "더한줄": made + extra}, None, self.sk.whole)["더한줄"] except (ValueError, store.StoreError) as e: raise Stuck(f"더한줄 틀림 — {e}") from e plan = {"갈래": "고름", "바탕": None, "입력값": {}, "덮개": {}, "자재표시": {}, "뺀줄": []} plan |= {"더한줄": adds, "못옮김": [], "놓음": []} mark_ = sangeun.mark(self.key, no) key = self.sk.write(no, plan, self.sk.keys.get(mark_)) self.sk.keys[mark_] = key self.out["단가산출"].append({"번": no, "키": key, "이름": blk.get("이름")}) def blocks(self, sure: dict[int, list[dict]]) -> None: """하위 단산(블록 단위 줄 하위) 먼저.""" done: set[int] = set() def visit(no: int, path: tuple = ()) -> None: if no in done or no in path: return for one in self.sk.blocks[no].get("항목") or []: sub = one.get("하위") or {} if sub.get("종류") == "단산" and int(sub.get("번") or 0) in sure: visit(int(sub["번"]), (*path, no)) done.add(no) try: self.block(no, sure[no]) except Stuck as e: self.out["막힘"].append({"블록": no, "까닭": e.why, "칸": e.cells}) for no in sorted(sure): visit(no) def move( folder: Path, source: Path, key: str, units_: dict[int, dict[int, list[dict]]], blocks: dict[int, list[dict]], who: Any = None, cover: bool = False, ) -> dict[str, Any]: """[옮기기] → `{만듦, 단가산출, 막힘, 하위, 장부}` — 블록 SK 먼저 · 호표 하위 먼저 · 이미 옮긴 호표 · 블록(같은 HP 출처 SC · SK)은 `cover`(덮음) 때만 덮음 · 아니면 409 아무것도 안 씀.""" mv = Move(folder, source, key, who) bad = [f"제{n}호표" for n in units_ if n not in mv.units] bad += [f"단산{n}" for n in blocks if n not in mv.sk.blocks] if bad: raise store.StoreError(404, f"없는 {bad[0]}") done = hbuild.confirmed(mv.folder, refs.where(mv.source)) had = { "호표": [ {"번": n, "키": done[k]} for n in sorted(units_) if (k := hbuild.mark(key, n)) in done ] } had["블록"] = [ {"번": n, "키": mv.sk.keys[k]} for n in sorted(blocks) if (k := sangeun.mark(key, n)) in mv.sk.keys ] if not cover and (had["호표"] or had["블록"]): raise store.StoreError(409, {"까닭": COVER, **had}) mv.blocks(blocks) wanted = { int(hit.group(1)) for rows in units_.values() for picks in rows.values() for p in picks if p.get("갈래") == "일위대가" and (hit := _SUB["일위대가"].match(str(p.get("키") or ""))) } order = hbuild._order(mv.units) for no in order: # 고른 것이 부르는 확인 안 된 하위 = 모두 고정으로 먼저 if no in wanted and no not in units_: mv.kid(no) for no in order: if no in units_: mv.hopyo(no, units_[no]) return mv.out