"""M02 불러오기 요소 잇기 색인(PLAN 815-1 · 요구 755 ~ 764) — M01 요소 줄 「별칭」 칸 · EST 참조 코드 · 재료 정확 일치. 서버 단독 · 읽기만(M01 판이 바뀌면 다시 읽음 · `id(whole)`). - `keys(표, 이름, 규격)` = 별칭으로 이어지는 키들(풀이 = `master_alias` · 표 LB 인력 · EQ 기계 · MT 재료 · MO 유가) — 부르는 곳: 단가산출 불러오기(`M02_San_Link.m01`) · 1차 줄 판정(`M02_Hopyo_Row.m01_picks`) · 기계 찾기(`M02_Machine_Find.alias`) · 일위대가 호표 줄(`M02_Hopyo_Units`) - `est_code(코드)` = EST 원파일 자재 자원 코드(M…) ↔ 우리 자재 줄 `출처` 칸 「자재품목 M…」 같은 줄 · 공사 직접 번호(M0000000…)는 빼고 이름 길 - `exact(이름, 규격)` = 지역 꼬리 뗀 이름 · 규격이 `master_alias.norm` 으로 같은 자재 줄(가까운 규격 아님) - `one(줄, 키들)` = 같은 품목(지역 꼬리 뗀 이름 · 규격 같음)의 지역 줄 여럿 → 줄 지역 › 서울 › 같은 단위 › 키 차례 하나 · 다른 품목이 섞이면 None(후보 여럿) """ from __future__ import annotations import re from functools import lru_cache from typing import Any from M01_MasterData import M01_MasterData_Store as m01 import master_alias as am # noqa: E402 — M01 store 가 scripts 경로를 넣음 MARKET = "MT" PRICES = ("물가자료", "유통물가", "물가정보", "거래가격", "관급") # 자재 값 칸 다섯 BASE_REGION = "서울" _CODE = re.compile(r"\bM\d{10}\b") LOCAL = "M0000000" # 공사 직접 번호(파일마다 다른 품목 · 거창 치즐 = 우리 표 Air Hammer) = 코드로 안 이음 _REGION = re.compile(r"\s*,\s*([가-힣]{2,4})\s*$") # 자재 이름 지역 꼬리 「, 서울」 def region_of(name: Any) -> tuple[str, str]: """자재 이름 → (지역 꼬리 뗀 이름, 지역 | 빈 글).""" text = str(name or "") m = _REGION.search(text) return (text[: m.start()], m.group(1)) if m else (text, "") def _same(name: Any, spec: Any) -> tuple[str, str]: return am.norm(region_of(name)[0]), am.norm(spec) @lru_cache(maxsize=2) def _book(_whole: int) -> dict[str, Any]: whole = m01.loaded()[1] tables = {t: whole.numbers.get(t, {}) for t in ("LB", "EQ", "MO")} tables[MARKET] = whole.index.get(MARKET, {}) rows = {t: {str(r.get("키") or k): r for k, r in got.items()} for t, got in tables.items()} alias = {t: am.index({"키": k, "별칭": r.get("별칭")} for k, r in got.items()) for t, got in rows.items()} # fmt: skip code: dict[str, list[str]] = {} exact: dict[tuple[str, str], list[str]] = {} for k, r in rows[MARKET].items(): for c in _CODE.findall(str(r.get("출처") or "")): code.setdefault(c, []).append(k) exact.setdefault(_same(r.get("이름"), r.get("규격")), []).append(k) return {"줄": rows, "별칭": alias, "코드": code, "같음": exact} def book() -> dict[str, Any]: return _book(id(m01.loaded()[1])) def row(table: str, key: Any) -> dict[str, Any]: return book()["줄"].get(table, {}).get(str(key)) or {} def keys(table: str, name: Any, spec: Any = "") -> list[str]: """별칭 칸으로 이어지는 키들(없음 = []).""" b = book() got = b["줄"].get(table, {}) return am.match(b["별칭"].get(table, {}), lambda k: (got.get(k) or {}).get("규격"), name, spec) def priced(key: Any) -> bool: """우리 자재 줄에 값 칸 다섯 중 하나라도 있음.""" return any(row(MARKET, key).get(c) is not None for c in PRICES) def est_code(code: Any) -> list[str]: """EST 자재 코드(M + 수 열) → 우리 자재 키들(출처 칸 같은 코드).""" text = str(code or "").strip() if not _CODE.fullmatch(text) or text.startswith(LOCAL): return [] return list(book()["코드"].get(text, [])) def exact(name: Any, spec: Any) -> list[str]: """지역 꼬리 뗀 이름 · 규격 정규화 같음 자재 키들.""" return list(book()["같음"].get(_same(name, spec), [])) def one(line: dict[str, Any], found: list[str]) -> str | None: """같은 품목 지역 줄 여럿 → 하나(줄 지역 › 서울 › 같은 단위 › 키) · 다른 품목 섞임 = None.""" rows = {k: row(MARKET, k) for k in found} if not rows or len({_same(r.get("이름"), r.get("규격")) for r in rows.values()}) != 1: return None mine, unit = region_of(line.get("이름"))[1], am.norm(line.get("단위")) def rank(k: str) -> tuple: where = region_of(rows[k].get("이름"))[1] return (where != mine if mine else True, where != BASE_REGION, am.norm(rows[k].get("단위")) != unit, k) # fmt: skip return min(rows, key=rank) def material(line: dict[str, Any], code: Any = None) -> tuple[str | None, str]: """재료 줄 → (우리 자재 키, 길 「코드」 · 「별칭」 · 「정확 일치」) · 별칭 여럿 = (None, 「별칭 후보 여럿」) · 없음 = (None, "").""" if hit := est_code(code): return one(line, hit) or hit[0], "코드" hit = keys(MARKET, line.get("이름"), line.get("규격")) if hit: got = hit[0] if len(hit) == 1 else one(line, hit) return (got, "별칭") if got else (None, "별칭 후보 여럿") got = one(line, exact(line.get("이름"), line.get("규격"))) return (got, "정확 일치") if got else (None, "")