"""M02 엑셀 줄 → M01 요소 후보 · 환산(PLAN 785-3 · 809-1) — 서버 단독 · 읽기만. `m01_picks` = 갈래(인력 · 재료 · 유가 · 기계) + 줄 이름 · 규격 · 단위 → M01 표마다(LB · MT · MO · EQ) 후보 (가져온 식 이음 `M02_San_Emit` 이 부름) · `CURRENCY` = 환율 낱말 · `factor` · `quantity` = 단위 환산 배 · 우리 수량 = 엑셀 수량 × 몫 × 배. 옛 1차 줄 판정(엑셀 사전 · 제안 · 2차 필요 · 기초 후보)은 809-1 에서 지움(경쟁사 로직 인정 · 요구 703). """ from __future__ import annotations import copy import re import unicodedata from decimal import Decimal from functools import lru_cache from typing import Any from M01_MasterData import M01_MasterData_Store as m01 from M02_MasterTemplete import M02_Hopyo_Elem as elem from M02_MasterTemplete import M02_Hopyo_Match as hm from M02_MasterTemplete import M02_Hopyo_Material as hmat from M02_MasterTemplete import M02_Hopyo_ReadEst as est mm, mp = hm.mm, hm.mp COSTS = ("노무비", "재료비", "경비") MACHINE_UNITS = ("hr", "시간", "h") TOP = 5 CURRENCY = (("달러", "USD"), ("$", "USD"), ("엔", "JPY"), ("유로", "EUR"), ("파운드", "GBP")) def flat(value: Any) -> str: return re.sub(r"\s+", "", unicodedata.normalize("NFKC", str(value or ""))).lower() def _num(value: Any) -> float | None: if isinstance(value, bool) or value in (None, ""): return None try: return float(str(value).replace(",", "").strip()) except ValueError: return None def _plain(value: Any) -> int | float | None: if value is None: return None f = float(value) return int(f) if f == int(f) else round(f, 6) # ── 엑셀 사전 ─────────────────────────────────────────────────────────── # ── M01 키 후보(표마다) ─────────────────────────────────────────────────── def pick(kind: str, key: Any = None, name: Any = "", spec: Any = "", unit: Any = "", **more: Any): """고른 것 한 꼴(설계 2-1).""" head = {"갈래": kind, "키": key, "이름": str(name or ""), "규격": str(spec or "")} return {**head, "단위": str(unit or ""), "단가": more.pop("단가", None), "입력값": {}, "출처": more.pop("출처", {}), "몫": None, **more} # fmt: skip def _rows(table: str) -> dict[str, dict[str, Any]]: return m01.loaded()[1].numbers.get(table, {}) def m01_picks(kind: str, line: dict[str, Any]) -> list[dict[str, Any]]: """갈래 하나 → M01 고른 것 후보(LB · MT · MO · EQ · FX) — 유일 짝이면 하나 · 같은 글 한 번.""" text = tuple(str(line.get(k) or "") for k in ("이름", "규격", "단위")) return copy.deepcopy(_m01_picks(kind, *text, id(m01.loaded()[1]))) @lru_cache(maxsize=4096) def _m01_picks(kind: str, name: str, spec: str, unit: str, _whole: int) -> list[dict[str, Any]]: if kind == "인력": trade = est.trade(name) hit = [r for r in _rows("LB").values() if trade and r.get("이름") == trade] if hit: return [ pick("인력", hit[0]["키"], hit[0]["이름"], "", "인", 단가=_plain(hit[0].get("값"))) ] got = elem.labor(str(name or "")) return [pick("인력", x["키"], x["이름"], "", "인", 단가=x["단가"]) for x in got] if kind == "기계": text = f"{name} {spec}" one = elem.machine(text) rows = [one] if one else [] got = [{"키": r["키"], "이름": r["이름"], "규격": r.get("규격")} for r in rows] got = got or elem.machines(text) return [pick("기계", x["키"], x["이름"], x.get("규격"), unit or "hr") for x in got] if kind == "유가": return [ pick("유가", x["키"], x["이름"], x["규격"], x["단위"], 단가=x["단가"]) for x in elem.oils(str(name or "")) ] if kind == "환율": code = next((c for w, c in CURRENCY if w in str(name)), flat(name).upper()) rows = [r for r in _rows("FX").values() if str(r.get("이름")).startswith(code)] return [ pick("환율", r["키"], r["이름"], "", r.get("단위"), 단가=_plain(r["값"])) for r in rows ] if kind == "재료": clean, apart = hmat.clean({"이름": name, "규격": spec, "단위": unit}) sure, _ = hmat.sure(clean) got = [r["one"] for r in sure[:1] if not r["one"].get("전국")] got = got or hmat.candidates(clean, TOP) more = {"별도계상": True} if apart else {} return [ pick("재료", x.get("자재"), x["이름"], x.get("규격"), x.get("단위"), **more) for x in got if x.get("자재") ] return [] # ── M02 층 SK · SC(같은 이름 · 참고) ──────────────────────────────────────── # ── 줄 판정 ───────────────────────────────────────────────────────────── # 별도계상 줄 표지 `품목` — 품목은 프로젝트에 복사할 때 정함(요구 645-2) # ── 환산(`M02_Hopyo_Match.factor` 옮김) ───────────────────────────────────── def factor(have: Any, want: Any, spec: Any = "") -> dict[str, Any]: """`{배, 갈래}` — 우리 수량 = 엑셀 수량 × 배 · 배 None = 사용자 입력.""" units = [u.strip() for u in str(want or "").split("·") if u.strip()] plain = mm.plain_unit(have) if len(units) > 1: ok = plain in {mm.plain_unit(u) for u in units} return {"배": 1 if ok else None, "갈래": "두 단위" if ok else "입력"} if plain.replace("/", "") == mm.plain_unit(want).replace("/", ""): # 공/㎥ ≡ 공㎥ return {"배": 1, "갈래": "같음"} if str(want or "").strip() == "자재": # 결과단위 「원/자재 단위」 = 줄 단위 그대로 return {"배": 1, "갈래": "자재 단위"} a, b = mp.kind_of(have), mp.kind_of(want) if a is not None and a == b: scale = mp.UNITS[a] ratio = Decimal(scale[plain]) / Decimal(scale[mm.plain_unit(want)]) return {"배": hm._json(ratio), "갈래": "단위표"} found = hm.numbers(spec) width, thick = hm._meters(found.get("B")), hm._meters(found.get("T")) by = {("넓이", "부피"): thick, ("길이", "넓이"): width} if (a, b) == ("길이", "부피") and width is not None and thick is not None: by[(a, b)] = width * thick got = by.get((a, b)) if got: return {"배": hm._json(got), "갈래": "추정"} return {"배": None, "갈래": "입력"} def quantity(line: dict[str, Any], want: Any, share: Any = None) -> dict[str, Any]: """단가산출 줄 수량 `{엑셀, 몫, 배, 갈래, 우리}` — 우리 = 엑셀 × 몫 × 배(배 None = 입력).""" got = factor(line.get("단위"), want, line.get("규격")) qty = line.get("수량") ours = None if qty is not None and got["배"] is not None: ours = _plain(Decimal(str(qty)) * Decimal(str(share or 1)) * Decimal(str(got["배"]))) return {"엑셀": qty, "몫": share, **got, "우리": ours}