"""「이 공사 단가로 다시 잇기」(PLAN 705-2 · 요구서 4장 2 · EST 「신 · 구 단가 비교」 꼴) — 고정 줄마다 붙인 단가 ↔ 단가 책(M02 = M01 최신 판 노임 「값」 · 자재 기준단가) · 이음 후보 · 고른 줄만 이음 바꿈 · 되돌리기. 후보 = 705-3 이음과 같은 짝(`M02_Hopyo_Fixed.Linker`) — 노임 = 이름이 우리 직종(단위 인) · 자재 = 이름 · 규격 같은 품목 + 단위 배 1 · 단가가 달라도 후보(바꿀지는 사람이 고름) · 기계 = 없음(B09 중기 몫). 줄 = 비율 줄 뺀 고정 줄 · 줄 가름 = `fixed_id` · 붙인 이름 끝 「 (2)」(줄 가름)은 떼고 찾음. 바꿈 = `{키, 줄, 노임?, 자재?}` — 키 = 이 층 사전조합(SC) · 값 null = 이음 풂(붙인 단가로) · 서버가 문서를 읽어 이음 칸만 고쳐 씀(숫자 안 받음) · 전후 1 단위 값(`Labor_Ref.Expand`) · `되돌림` = 옛 이음. 열린 문서(저장 전) = `doc` 로 줄 · 후보만 — 바꿈은 화면이 문서에 넣고 [저장]. """ from __future__ import annotations import re from typing import Any from M01_MasterData import M01_MasterData_Store as m01 from M02_MasterTemplete import M02_Hopyo_Fixed as hfix from M02_MasterTemplete import M02_Hopyo_Shares as shares from M02_MasterTemplete import M02_Labor_Fixed as fixed from M02_MasterTemplete import M02_Labor_Ref as refs from M02_MasterTemplete import M02_MasterTemplete_Store as store KINDS = ("노임", "자재") CHANGES_MAX = 2000 _SUFFIX = re.compile(r"\s*\(\d+\)$") def _num(raw: Any) -> float | None: got = fixed._num(raw) return float(got) if got is not None else None class Finder: """후보 찾기 — 단가 책(M01 최신 판) 한 번 읽음.""" def __init__(self) -> None: self.linker = hfix.Linker() self.whole = self.linker.whole def _name(self, code: str) -> dict[str, str]: try: row = self.whole.get(code) or {} except fixed.mf.FormulaError: row = {} return {k: str(row.get(k) or "") for k in ("이름", "규격", "단위")} def price(self, kind: str, code: str | None, unit: str | None) -> float | None: if not code: return None try: if kind == "노임": return _num((self.whole.get(code) or {}).get("값")) return _num(fixed.mm.element(self.whole, code, {}, unit or None)[0]) except fixed.mf.FormulaError: return None def candidates(self, fx: dict[str, Any]) -> list[dict[str, Any]]: name = _SUFFIX.sub("", str(fx.get("이름") or "")) line = {"이름": name, "규격": fx.get("규격") or "", "단위": fx.get("단위") or ""} out = [] trade = shares.trade(name) if line["단위"].strip() == "인" else None for code, value in self.linker.labor.get(trade or "", []): out.append({"비목": "노임", "키": code, **self._name(code), "단가": _num(value)}) price = fx.get("단가") or {} if not trade and (_num(price.get("재료비")) or not any(_num(v) for v in price.values())): for code, value in self.linker._mats({**line, "갈래": "자재"}): out.append({"비목": "자재", "키": code, **self._name(code), "단가": _num(value)}) return out def row(self, key: str | None, one: dict[str, Any]) -> dict[str, Any]: fx = one[fixed.FIXED] link = fx.get("이음") or {} qty = _num((one.get("수량") or {}).get("손")) now = { k: self.price(k, link.get(k), fx.get("단위")) for k in KINDS if link.get(k) # 이은 비목 지금 단가 } return { "키": key, "줄": fixed.fixed_id(fx), **{k: fx.get(k) or "" for k in ("이름", "규격", "단위")}, "수량": qty, "붙인": {k: _num((fx.get("단가") or {}).get(k)) or 0 for k in fixed.COSTS}, "이음": {k: link.get(k) for k in KINDS}, "지금": now, "후보": self.candidates(fx), } def _lines(doc: dict[str, Any]) -> list[dict[str, Any]]: return [ one for one in doc.get("담은로직") or [] if fixed.is_fixed(one) and not one[fixed.FIXED].get("비율") ] def rows(keys: list[str] | None = None, doc: dict[str, Any] | None = None) -> dict[str, Any]: """고정 줄 표 — `keys` = 이 층 사전조합들 · `doc` = 열린 문서 → `{줄 [...], 문서 [{키, 이름}]}`.""" finder, out, docs = Finder(), [], [] if doc is not None: out = [finder.row(None, one) for one in _lines(doc)] for key in keys or []: refs.check_key("precombo", key) got = store.read("precombo", key)["문서"] docs.append({"키": key, "이름": got.get("이름") or key}) out += [finder.row(key, one) for one in _lines(got)] return {"줄": out, "문서": docs} def _total(key: str) -> float | None: at = refs.here() got = refs.Expand(m01.loaded()[1])({"층": at[0], "주인": at[1], "키": key}) return _num((got.get("기준") or {}).get("계")) def apply(changes: list[dict[str, Any]]) -> dict[str, Any]: """바꿈 → 이 층 사전조합 이음 칸만 고쳐 씀 · M01 에 없는 키 400(`Labor_Fixed.clean`) → `{바뀜, 문서 [{키, 이름, 전, 후}], 되돌림 [{키, 줄, 노임, 자재}]}`.""" if len(changes) > CHANGES_MAX: raise store.StoreError(400, f"바꿈은 {CHANGES_MAX} 줄 안") by_key: dict[str, list[dict[str, Any]]] = {} for one in changes: if not isinstance(one, dict) or not isinstance(one.get("줄"), str): raise store.StoreError(400, "바꿈 줄은 {키, 줄, 노임?, 자재?}") refs.check_key("precombo", str(one.get("키") or "")) by_key.setdefault(str(one["키"]), []).append(one) whole, done, back, docs = m01.loaded()[1], 0, [], [] for key, wants in by_key.items(): got = store.read("precombo", key) doc, before = got["문서"], _total(key) lines = {fixed.fixed_id(x[fixed.FIXED]): x for x in _lines(doc)} for want in wants: one = lines.get(want["줄"]) if one is None: raise store.StoreError(404, f"없는 고정 줄 「{want['줄']}」 ({key})") link = dict(one[fixed.FIXED].get("이음") or {}) back.append({"키": key, "줄": want["줄"], **{k: link.get(k) for k in KINDS}}) for k in KINDS: if k in want: link[k] = want[k] or None one[fixed.FIXED] = fixed.clean({**one[fixed.FIXED], "이음": link}, want["줄"], whole) done += 1 store.write("precombo", key, got["판"], doc) docs.append({"키": key, "이름": doc.get("이름") or key, "전": before, "후": _total(key)}) return {"바뀜": done, "문서": docs, "되돌림": back}