"""M02 일위대가 고정 줄(PLAN 705-1 · 요구서 1장 · 4장 1 · 2 · 3) — 품 · 수량 · 단가 숫자를 그대로 적어 둔 줄. 줄 = `{고정: {이름, 규격, 단위, 단가: {노무비, 재료비, 경비}, 이음: {노임, 자재, 기계}, 출처?}, 별칭, 메모, 수량}` — 담은로직 안 로직 · 참조 줄과 섞임(줄 단위 꼬리표 · 기본 = 품셈). 단가 = 그 줄 단위 1 개 값 · 빈 비목 = 0 · 합계만 있는 줄 = 고른 비목 하나(기본 재료비 · 붙이는 쪽 몫). 이음 = 이 공사 노임(LB) · 자재(MT) · 기계(EQ) 키 · 못 이음 = null(「손 단가」) — 이은 비목만 단가 책 값 (노임 새 판 때 따라감) · 품(수량)은 그대로. M02 셈(층 · 사본) = 노임 「값」 · 자재 기준단가로 이음 · 기계 = 줄 단가(기계 시간 단가 = B09 단가 책 몫). 출처 = `{파일, 호표, 줄, 판}`(글 · 수만) · 줄 가름 = 「고정|이름|규격|단위」(짝 `Labor_Calc.ts` `lineId`). B09 대가 갈래 `fixed` = `B09_Estimation_Labor_Source.fixed_line` · 셈 `B09_Estimation_Calc_Labor`. """ from __future__ import annotations from decimal import Decimal, InvalidOperation from typing import Any from M01_MasterData import M01_MasterData_Store as m01 mf = m01.cm.mf mm = mf.mp.mm COSTS = mf.COST_ITEMS # 노무비 · 재료비 · 경비 FIXED = "고정" LINKS = {"노임": "LB", "자재": "MT", "기계": "EQ"} SOURCE_KEYS = ("파일", "호표", "줄", "판") def _bad(text: str) -> Exception: from M02_MasterTemplete import M02_MasterTemplete_Store as store return store.StoreError(400, text) def _num(raw: Any) -> Decimal | None: if raw is None or isinstance(raw, bool): return None try: got = Decimal(str(raw).replace(",", "").strip()) except InvalidOperation: return None return got if got.is_finite() else None def _plain(value: Decimal) -> int | float: return int(value) if value == value.to_integral_value() else float(value) def is_fixed(one: Any) -> bool: return isinstance(one, dict) and isinstance(one.get(FIXED), dict) def fixed_id(fx: dict) -> str: return f"고정|{fx.get('이름') or ''}|{fx.get('규격') or ''}|{fx.get('단위') or ''}" def clean(fx: Any, where: str, whole: Any) -> dict[str, Any]: """고정 칸 정리 — 이름 없음 · 단가가 수 아님 · 이음 키 머리 틀림 · M01 에 없는 이음 = 400.""" if not isinstance(fx, dict): raise _bad(f"{where} · 고정 줄이 묶음 아님") name = str(fx.get("이름") or "").strip() if not name: raise _bad(f"{where} · 고정 줄 이름 없음") raw = fx.get("단가") or {} if not isinstance(raw, dict): raise _bad(f"{where} · 고정 줄 단가는 {{노무비, 재료비, 경비}}") price = {} for k in COSTS: got = _num(raw.get(k)) if got is None and raw.get(k) not in (None, ""): raise _bad(f"{where} · 고정 줄 {k} 단가가 수 아님") price[k] = _plain(got) if got is not None else 0 given = fx.get("이음") or {} link = {} for k, head in LINKS.items(): code = str(given.get(k) or "") if isinstance(given, dict) else "" if code and (code[:2] != head or not _has(whole, code)): raise _bad(f"{where} · 고정 줄 {k} 이음 「{code}」 M01 에 없음") link[k] = code or None src = fx.get("출처") if isinstance(fx.get("출처"), dict) else {} kept = {k: v for k in SOURCE_KEYS if isinstance(v := src.get(k), (str, int, float))} out = {"이름": name, "규격": str(fx.get("규격") or ""), "단위": str(fx.get("단위") or "")} return {**out, "단가": price, "이음": link, **({"출처": kept} if kept else {})} def _has(whole: Any, code: str) -> bool: try: return bool(whole.get(code)) except mf.FormulaError: return False def link_keys(doc: Any) -> list[str]: """문서 고정 줄이 잇는 키 — 사본 `단가.json` · B09 단가 책 수요(705-1).""" out: list[str] = [] for one in (doc or {}).get("담은로직") or []: if is_fixed(one): out += [v for v in (one[FIXED].get("이음") or {}).values() if v and v not in out] return out def line(whole: Any, fx: dict) -> dict[str, Any]: """고정 줄 계산 답 — 기준 = 줄 단가 · 이은 노임 · 자재는 `whole` 값(없으면 줄 단가 그대로) · 분모 = 줄 단위 · 배 1 · `이은` = 단가 책 값을 쓴 비목.""" base = {k: _num((fx.get("단가") or {}).get(k)) or Decimal(0) for k in COSTS} link, used = fx.get("이음") or {}, [] if code := link.get("노임"): try: got = _num((whole.get(code) or {}).get("값")) except mf.FormulaError: got = None if got is not None: base["노무비"], used = got, [*used, "노무비"] if code := link.get("자재"): try: got = _num(mm.element(whole, code, {}, fx.get("단위") or None)[0]) except mf.FormulaError: got = None if got is not None: base["재료비"], used = got, [*used, "재료비"] # ponytail: 기계 이음은 M02 셈에서 줄 단가 그대로 — 기계 시간 단가 셈은 B09 중기목록표 몫 money = {**base, "계": sum(base.values())} head = {FIXED: fx, "이름": fx.get("이름"), "알약": None, "분모": fx.get("단위"), "배": 1} return {**head, "기준": money, "이은": used, "까닭": None}