"""M02 일위대가 불러오기 — 못옮김 · 안 이은 기계 줄 → M01 요소 후보 셋(PLAN 746-1 · 요구 585). 후보 = `[{키, 갈래 노임|기계|자재|유가, 이름, 규격, 단위, 단가}]` 셋까지 — 보여주기만(자동 이음 없음 · 고르면 지금 잇기 길 `{노임|기계|자재|유가: 키}`). 자재 = `M02_Hopyo_Material.candidates` 점수(이름 글자 묶음 · 규격) · 단가 = M01 자재 값(엑셀 줄 단위) 노임 = LB 이름 글자 묶음 닮음 · 단가 = LB 값 기계 = 기계 찾기(812-9 `M02_Machine_Find` — 기계표 전체 · 표기 정규화 · 괄호 낱말 · 규격) 후보 · 단가 없음(기계경비는 로직 셈) · `machine` = 하나로 정할 때만 유가(785-3) = MO 이름에 글 이름이 듦(경유 ⊂ 자동차용경유) · 전국 먼저 · 단가 = MO 값 차례 = 점수(0.1 단위로 접음) › 엑셀 단가에 가까운 것(엑셀 단가 = 블록 단위 줄 단가 · 모르면 점수만). 서버 단독 · 읽기만. """ from __future__ import annotations import math import re from typing import Any from M01_MasterData import M01_MasterData_Store as m01 from M02_MasterTemplete import M02_Hopyo_Match as hm from M02_MasterTemplete import M02_Hopyo_Material as hmat from M02_MasterTemplete import M02_Hopyo_UnitLine as unit_line from M02_MasterTemplete import M02_Machine_Find as mfind from M02_MasterTemplete.M02_Hopyo_Words import nfkc TOP = 3 POOL = 5 def prices(block: dict[str, Any] | None) -> dict[str, float]: """블록 단위 줄(노무 · 자재 · 값) → `{이름: 엑셀 단가}`(첫 것 · 776-3 — 글 다시 안 풂).""" out: dict[str, float] = {} for one in (block or {}).get("항목") or []: price = unit_line.money(one.get("단가")) if one.get("묶음") in ("L", "M", "E") else None if price: out.setdefault(re.sub(r"^[0-9가-힣]\.", "", str(one.get("이름") or "")).strip(), price) return out def _gap(price: Any, want: float | None) -> float: try: p = float(price) except (TypeError, ValueError): return math.inf return abs(math.log(p / want)) if want and p > 0 else math.inf def _order(rows: list[tuple[float, dict]], want: float | None) -> list[dict]: rows.sort(key=lambda x: -x[0]) pool = rows[:POOL] pool.sort(key=lambda x: (-round(x[0], 1), _gap(x[1].get("단가"), want))) return [one for _, one in pool[:TOP]] def _num(x: Any) -> int | float | None: if x is None: return None f = float(x) return int(f) if f == int(f) else round(f, 4) def materials(name: str, unit: str = "", want: float | None = None) -> list[dict[str, Any]]: whole = m01.loaded()[1] rows = [] for c in hmat.candidates({"이름": name, "규격": "", "단위": unit}, POOL): code = c.get("자재") if not code: continue try: price = hmat.mm.element(whole, code, {}, unit or None)[0] except Exception: # noqa: BLE001 — 값 못 읽음 = 단가 없음 price = None one = {"키": code, "갈래": "자재", "이름": c.get("이름"), "규격": c.get("규격") or ""} rows.append((c.get("점수") or 0.0, {**one, "단위": c.get("단위"), "단가": _num(price)})) return _order(rows, want) def labor(name: str, want: float | None = None) -> list[dict[str, Any]]: whole = m01.loaded()[1] me = hm._grams(re.sub(r"\s", "", name)) rows = [] for row in whole.numbers.get("LB", {}).values(): them = hm._grams(re.sub(r"\s", "", str(row.get("이름") or ""))) score = 2 * len(me & them) / (len(me) + len(them)) if score > 0: one = {"키": str(row.get("키")), "갈래": "노임", "이름": row.get("이름"), "규격": ""} rows.append((score, {**one, "단위": "인", "단가": _num(row.get("값"))})) return _order(rows, want) def oil_rows(name: Any) -> list[dict[str, Any]]: """유가전력 MO 줄 — 정리 이름(빈칸 뺌)이 MO 이름에 듦 · 전국 먼저 · 키 차례.""" me = re.sub(r"\s", "", nfkc(name)) if len(me) < 2: return [] rows = [ r for r in m01.loaded()[1].numbers.get("MO", {}).values() if me in re.sub(r"\s", "", nfkc(r.get("이름"))) ] rows.sort(key=lambda r: (not str(r.get("상세구분") or "").startswith("전국"), str(r["키"]))) return rows def oils(name: str) -> list[dict[str, Any]]: """유가전력 후보 셋 — `{키, 갈래 유가, 이름, 규격 = 상세구분, 단위, 단가}`.""" return [ { "키": str(r["키"]), "갈래": "유가", "이름": r.get("이름"), "규격": r.get("상세구분") or "", "단위": r.get("단위"), "단가": _num(r.get("값")), } # fmt: skip for r in oil_rows(name)[:TOP] ] def _eq(one: dict[str, Any]) -> dict[str, Any]: raw = m01.loaded()[1].numbers.get("EQ", {}).get(str(one.get("원문번호")), {}) unit = (raw.get("단위") or {}).get("내용시간") if isinstance(raw.get("단위"), dict) else "" return {"키": one["키"], "갈래": "기계", "이름": one["이름"], "규격": one.get("규격") or "", "단위": unit or "시간", "단가": None} # fmt: skip def machine(text: str, *_: Any) -> dict | None: """기계 글(이름 규격) → EQ 한 줄 — 기계 찾기(812-9 `M02_Machine_Find`)가 하나로 정할 때만.""" got = mfind.find(text) if got["판정"] not in (mfind.LINKED, mfind.ALIAS): return None return m01.loaded()[1].numbers.get("EQ", {}).get(str(got["후보"][0]["원문번호"])) def machines(text: str, *_: Any) -> list[dict]: """기계 글 → EQ 후보(보여주기만 · 812-9) — 이음 · 후보 여럿 = 그것 · 못 찾음 = 같은 기종(가까운 규격 먼저).""" return [_eq(one) for one in mfind.find(text)["후보"][:TOP]] def lost(one: dict[str, Any], want: float | None = None) -> list[dict[str, Any]]: """못옮김 한 줄(`M02_Hopyo_SkPatch` 꼴) → 후보.""" kind, name = one.get("갈래"), str(one.get("이름") or "") if kind == "자재": return materials(name, str(one.get("단위") or ""), want) if kind == "유가": return oils(name) if kind == "노임": return labor(name, want) if kind == "기계": return machines(f"{name} {one.get('규격') or ''}", set(name.split("·"))) return []