"""M02 줄 쌓기 — 다른 단가산출 · 일위대가 복사해 펼침(PLAN 804-4 · 요구 680-1 · 681) — 서버 단독. M02 단가산출(SK) 줄 쌓기에서 M01 단가산출 · M02 단가산출 · M02 일위대가(SC)를 고름 = 그 문서 전체를 복사해 이 단가산출의 항목 · 줄로 펼침(참조 줄 「단산 N 참조」 없음 · 원본과 연결 없음) — 이 길은 복사할 조각만 줌, 문서에 넣는 것은 화면(`common_util_line_stack_copy.ts` · 옵션 = 고른 값으로 굳힘 · 기호 겹침 = 새 이름). 조각(`take`) — {갈래: 문서, 문서, 입력(옵션 값), 곱(수량 식 · 없으면 그대로), 이름} | {갈래: 인력 · 재료 · 기계, 고른, 수량} | {갈래: 율 · 손, 줄} | {갈래: 글, 글}(못 옮김 = 「⚠」) M01 단가산출 = 지금 M01 문서 · SK = 그 SK 수식 사본 문서(`M02_Doc_Sk_Path.current`) · SC = 담은로직 줄마다(로직 = M01 문서 × 수량 ÷ 배 · 참조 SK = 그 SK 문서 × 수량 ÷ 그 배 · 참조 SC = 그 안 조각 × 수량 · 고정 = 인력(노임) · 재료(자재 · 유가) · 기계(시간당 경비 로직) · 손 단가 · 율(비목 기준 · 끝에 — 밑 = 앞 줄 누계)) + 자재 = 재료 줄. M01 단가산출은 원천 기준값 — M01 줄 쌓기는 이 길이 없음(M01 찾기 창 = M01 전체만). [단가 갱신] = 사본 값 줄(자원 단가)만 지금 M01(`M02_Doc_Copy.refresh` 그대로 · 구조 · 품 · 식 그대로). """ from __future__ import annotations from decimal import Decimal from typing import Any from M02_MasterTemplete import M02_Labor_Ref as refs from M02_MasterTemplete import M02_MasterTemplete_Store as store COSTS = ("노무비", "재료비", "경비") _LAYERS = ("system", "company", "personal") DEPTH = refs.DEPTH def _bad(text: str, status: int = 400) -> store.StoreError: return store.StoreError(status, text) def parse(src: Any) -> dict | None: """`참조|층|주인|키`(`refs.ref_id` 꼴 · SK · SC) → {층, 주인, 키} · 아니면 None(M01 키).""" parts = str(src or "").split("|") if len(parts) != 4 or parts[0] != "참조" or parts[1] not in _LAYERS: return None _, layer, owner, code = parts if not refs._KEY.match(code) or code[:2] not in ("SK", "SC"): return None if layer != "system" and not refs._OWNER[layer].match(owner): return None return {"층": layer, "주인": owner or None, "키": code} def _text(v: Any) -> str: return format(Decimal(str(v)).normalize(), "f") def _times(q: str | None, more: str | None) -> str | None: """수량 식 곱 — 없음 · 1 = 그대로.""" q, more = (None if x in (None, "1") else x for x in (q, more)) if q is None or more is None: return q if more is None else more return f"({q}) × ({more})" def _warn(text: str) -> dict: return {"갈래": "글", "글": f"⚠ {text}"} def _plain(given: dict) -> dict: out = {} for k, v in (given or {}).items(): if isinstance(v, Decimal): v = int(v) if v == v.to_integral_value() else float(v) out[str(k)] = v return out # ── 문서 하나 ───────────────────────────────────────────────────────── def _m01(code: str, given: Any = None, factor: str | None = None) -> dict: from M02_MasterTemplete import M02_Doc_Copy as dc whole = dc.skc.whole_() doc = dc.doc_of(whole, code) if doc is None: return _warn(f"수식 문서 없는 단가산출 「{code}」") values = dc.given_of(whole, code, given) if given is not None else {} piece = {"갈래": "문서", "문서": doc, "입력": _plain(values), "이름": doc.get("이름") or code} factor = _times(factor, None) return {**piece, "곱": factor} if factor else piece def _sk(ref: dict, factor: str | None = None, per_unit: bool = False) -> list[dict]: from M02_MasterTemplete import M02_Doc_Sk_Path as sp from M02_MasterTemplete import M02_Sangeun_Calc as calc raw = refs.read_ref(ref) if not isinstance(raw, dict): return [_warn(f"없는 단가산출 「{ref['키']}」")] try: with store.at(refs.folder_of(ref["층"], ref.get("주인"))): copy = sp.current(raw) if per_unit: # 일위대가 안 SK = 1 단위(기준 ÷ 배) clean, whole = sp._settled(raw) times = Decimal(str(calc.run(clean, whole).get("배") or 1)) if times != 1: factor = _times(factor, f"1 ÷ {_text(times)}") except Exception as e: # noqa: BLE001 — 가져온 식 · 바탕 참조 SK 등 = 못 복사(까닭 글) why = getattr(e, "detail", None) or str(e) return [_warn(f"단가산출 「{raw.get('이름') or ref['키']}」 못 복사 — {why}")] doc = copy["문서"] out = [{"갈래": "문서", "문서": doc, "입력": {}, "이름": raw.get("이름") or ref["키"]}] factor = _times(factor, None) if factor: out[0]["곱"] = factor if copy.get("끊음"): out.append(_warn(f"「{raw.get('이름')}」 안에서 끊어 고친 부름 문서는 지금 M01 판으로 셈")) return out # ── 일위대가(SC) ────────────────────────────────────────────────────── def _pick(kind: str, qty: str | None, **picked: Any) -> list[dict]: one = {"갈래": kind, "고른": {k: v for k, v in picked.items() if v not in (None, "", {})}} if qty is None: return [{**one, "수량": "0"}, _warn(f"{picked.get('이름')} — 수량 없음(0 으로 넣음)")] return [{**one, "수량": qty}] def _fixed(fx: dict, qty: str | None, rates: list[dict]) -> list[dict]: from M02_MasterTemplete import M02_Machine_Hourly as mh name, spec, unit = fx.get("이름") or "", fx.get("규격") or "", fx.get("단위") or "" rate, link = fx.get("비율"), fx.get("이음") or {} if rate: if rate.get("기준") not in COSTS: return [ _warn(f"율 줄 「{name}」 — 기준 「{rate.get('기준')}」 는 줄 쌓기 율로 못 옮김") ] line = {"이름": name, "단위": "식", "비목": rate.get("비목") or "경비"} rates.append({"갈래": "율", "줄": {**line, "단가": f"{rate['기준']} × {qty or 0} %"}}) return [] if link.get("노임"): return _pick("인력", qty, 키=link["노임"], 이름=name, 규격=spec, 단위="인") if link.get("자재") or link.get("유가"): code = link.get("자재") or link.get("유가") return _pick("재료", qty, 키=code, 이름=name, 규격=spec, 단위=unit) if link.get("기계"): got = mh.hourly(str(link["기계"])) if not got.get("ok") or not got.get("로직"): return [_warn(f"기계 「{name}」 — {got.get('reason') or '시간당 경비 못 셈'}")] machine = {"기계": f"'{got['원문번호']}'"} return _pick("기계", qty, 키=got["로직"], 이름=name, 규격=spec, 입력=machine) price = fx.get("단가") or {} costs = [c for c in COSTS if Decimal(str(price.get(c) or 0))] or ["재료비"] return [ { "갈래": "손", "줄": { "이름": name, **({"규격": spec} if spec else {}), "단위": unit, "비목": c, "수량": qty or "0", "단가": _text(price.get(c) or 0), }, } # fmt: skip for c in costs ] def _scaled(pieces: list[dict], factor: str | None) -> list[dict]: """안 SC 조각 × 수량 — 문서 = 곱 · 줄 = 수량 식 · 율 = 그대로(밑이 이미 곱해짐).""" if factor is None: return pieces out = [] for p in pieces: if p["갈래"] == "문서": out.append({**p, "곱": _times(p.get("곱"), factor)}) elif p["갈래"] in ("인력", "재료", "기계"): out.append({**p, "수량": _times(p["수량"], factor)}) elif p["갈래"] == "손": out.append({**p, "줄": {**p["줄"], "수량": _times(p["줄"]["수량"], factor)}}) else: out.append(p) return out def _combo(ref: dict, trail: tuple[str, ...] = ()) -> list[dict]: from M02_MasterTemplete import M02_Labor_Doc as labor rid = refs.ref_id(ref) if rid in trail or len(trail) >= DEPTH: return [_warn(f"일위대가 참조 돌고 돎 · 너무 깊음 「{ref['키']}」")] doc = refs.read_ref(ref) if not isinstance(doc, dict): return [_warn(f"없는 일위대가 「{ref['키']}」")] with store.at(refs.folder_of(ref["층"], ref.get("주인"))): ans = labor.calc(doc, {}) out, rates = [], [] for one, got in zip(doc.get("담은로직") or [], ans.get("줄") or []): qty = None if got.get("수량") is None else _text(got["수량"]) if isinstance(one.get("고정"), dict): out += _fixed(one["고정"], qty, rates) elif isinstance(one.get("참조"), dict): sub = one["참조"] if qty is None: out.append(_warn(f"「{got.get('이름') or sub.get('키')}」 — 수량 없음(안 옮김)")) elif str(sub.get("키", "")).startswith("SK"): out += _sk(sub, qty, per_unit=True) elif str(sub.get("키", "")).startswith("SC"): out += _scaled(_combo(sub, (*trail, rid)), qty) else: out.append(_warn(f"옛 단가승격 「{sub.get('키')}」 — 줄 쌓기로 못 옮김")) elif one.get("패치"): out.append(_warn(f"줄 덮개로 고친 로직 「{one.get('로직')}」 — 줄 쌓기로 못 옮김")) else: times, r = Decimal(str(got.get("배") or 1)), one.get("켜쌓기율") q = qty if qty is None or times == 1 else f"{qty} ÷ {_text(times)}" if q is not None and r not in (None, 100): q = f"({q}) × {r} ÷ 100" if q is None: out.append(_warn(f"로직 「{one.get('로직')}」 — 수량 없음(안 옮김)")) continue out.append(_m01(str(one.get("로직") or ""), one.get("입력값") or {}, q)) shown = (one.get("자재표시") or {}).values() if one.get("고름") or any(labor.apart(x) for x in shown): out.append(_warn(f"「{one.get('로직')}」 고른 자재 · 별도계상 표시는 안 옮김")) whole = labor.m01.loaded()[1] for one, got in zip(doc.get("자재") or [], ans.get("자재") or []): code = str(one.get("코드") or "") try: row = whole.get(code) or {} except Exception: # noqa: BLE001 — 지워진 자재 = 코드만 row = {} name = row.get("이름") or code if labor.apart(one): out.append(_warn(f"별도계상 자재 「{name}」 — 합에서 빠지는 줄이라 안 옮김")) continue qty = None if got.get("수량") is None else _text(got["수량"]) unit = one.get("단위") or row.get("단위") out += _pick("재료", qty, 키=code, 이름=name, 규격=row.get("규격"), 단위=unit) return out + rates # ── 길 ──────────────────────────────────────────────────────────────── def take(src: str) -> dict[str, Any]: """복사할 조각 — {이름, 출처, 조각, 계?(일위대가 반영합 · 대조)}.""" from M02_MasterTemplete import M02_Labor_Doc as labor ref = parse(src) if ref is None: # M01 단가산출 piece = _m01(str(src or "")) if piece["갈래"] != "문서": raise _bad(piece["글"][2:], 404) return {"이름": piece["이름"], "출처": f"M01 {src}", "조각": [piece]} if not refs.readable(refs.here(), ref): raise _bad("위 층(마스터 · 내 회사 · 나) 문서만 복사", 403) doc = refs.read_ref(ref) if not isinstance(doc, dict): raise _bad(f"없는 문서 「{ref['키']}」", 404) layer = {"system": "마스터", "company": "회사", "personal": "개인"}[ref["층"]] head = {"이름": str(doc.get("이름") or ref["키"]), "출처": f"{layer} {ref['키']}"} if ref["키"].startswith("SK"): return {**head, "조각": _sk(ref)} with store.at(refs.folder_of(ref["층"], ref.get("주인"))): total = (labor.calc(doc, {}).get("반영합") or {}).get("계") return {**head, "단위": doc.get("단위"), "조각": _combo(ref), "계": total}