Files
Aislo/resources/master_data/scripts/master_pick.py
T
eomsangdonandClaude Sonnet 5 c1eeede597 feat(M01): 기계 입력이 EQ 키로 와도 원문 분류번호로 풀어 받음 (PLAN 8-2)
- 고르기 목록(원문번호)과 같은 꼴로 풀어 계산 · 없는 키는 그대로 막힘
- 시험 1개 더함

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgUhN55Z3BCYhUJTjwTNfj
2026-09-24 21:12:29 +09:00

175 lines
8.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""재료 줄의 기본 검색어 찾기 · 고른 품목 · 단위 환산 · 단위 경고 (PLAN 8-1 계약).
`master_formula.run` 이 재료 줄 단가를 풀 때 부름 — 요소 객체에 `검색어` 가 있으면 여기서 찾고,
없으면 옛 길(`master_material` 조건 · 대표). 서로 부르는 모듈이라 `master_formula` 는 끝에서 읽음.
"""
from __future__ import annotations
from contextlib import contextmanager
from contextvars import ContextVar
from decimal import Decimal
import master_material as mm
# 「같은 성격」 단위 — {성격: {정규 단위: 기준 단위 몇 배}} (키는 `plain_unit` 을 거친 꼴)
UNITS = {
"무게": {"g": 1, "kg": 1000, "톤": 1000000},
"길이": {"mm": 1, "cm": 10, "m": 1000, "km": 1000000},
"넓이": {"cm2": 1, "m2": 10000},
"부피": {"cm3": 1, "ℓ": 1000, "m3": 1000000},
}
_SCALE = {u: (kind, n) for kind, group in UNITS.items() for u, n in group.items()}
REGIONS = (
"전국평균 전국 서울 경기 강원 충북 충남 전북 전남 경북 경남 부산 대구 인천 대전 울산 세종 제주 광주"
" 전남광주"
).split()
CHOSEN: ContextVar[dict | None] = ContextVar("고름", default=None) # {호표 줄 차례(0부터): 자재 키}
EQ: ContextVar = ContextVar("기계번호", default=None) # 지금 쓰는 마스터 — 기계 줄을 찾음
@contextmanager
def choosing(picks: dict | None):
"""`/calc` · `/text` 몸의 `고름` — 그 요청 동안만 · 저장 안 함."""
token = CHOSEN.set({str(k): str(v) for k, v in (picks or {}).items()})
try:
yield
finally:
CHOSEN.reset(token)
def _numbers() -> dict:
return EQ.get().numbers.get("EQ", {}) if EQ.get() else {}
def resolve(spec: dict, value):
"""기계 입력이 EQ 키로 오면 원문 분류번호로 풂 — 고르기 목록(원문번호)과 같은 꼴."""
if not (isinstance(value, str) and value.strip()[:2] == "EQ" and EQ.get()):
return value
row = EQ.get().index.get("EQ", {}).get(value.strip())
number = str(row.get("원문번호") or "") if row else ""
return number if number and machine(spec, number) else value
def machine(spec: dict, value) -> bool:
"""기계 입력 — 고르기 목록 밖 원문 분류번호도 기계 마스터에 있으면 받음."""
known = _numbers()
picks = spec.get("고르기") or []
return bool(picks) and str(value).strip() in known and all(str(o) in known for o in picks)
# ── 단위 ───────────────────────────────────────────────────────────────
def kind_of(unit) -> str | None:
got = _SCALE.get(mm.plain_unit(unit))
return got[0] if got else None
def same_kind(a, b) -> bool:
"""단위가 같거나 같은 성격(무게 · 길이 · 넓이 · 부피)."""
pa, pb = mm.plain_unit(a), mm.plain_unit(b)
return pa == pb or (kind_of(a) is not None and kind_of(a) == kind_of(b))
def convert(price, have, want):
"""`have` 단위 단가 → `want` 단위 단가 — 같은 성격이 아니면 그대로."""
if price is None or not want or mm.plain_unit(have) == mm.plain_unit(want):
return price
if not same_kind(have, want) or kind_of(have) is None:
return price
return price * Decimal(_SCALE[mm.plain_unit(want)][1]) / Decimal(_SCALE[mm.plain_unit(have)][1])
# ── 찾기 ───────────────────────────────────────────────────────────────
def _words(term) -> tuple[list[str], list[str]]:
"""(이름·규격에 든 낱말, 지역 낱말) — 지역 낱말은 고르는 차례에만 쓰임."""
every = [mm.plain_spec(w) for w in str(term or "").split()]
region = [w for w in every if w in REGIONS]
return [w for w in every if w not in REGIONS], region
def search(master, cond: dict, env: dict | None = None, unit: str | None = None) -> list[str]:
"""검색어 안 자재품목 키 — 낱말이 모두 이름 또는 규격에 든 줄(요소의 구분 · 상세구분 안에서) ·
단위는 같은 성격까지(하나도 없으면 다 — 경고가 붙음) · 고르는 차례:
값 있는 줄 → 줄 단위와 똑같은 단위 → 지역 낱말(입력 `자재지역` 포함)과 맞는 줄 → 키 차례."""
words, region = _words(cond.get("검색어"))
region += [mm.plain(env[mm.REGION])] if (env or {}).get(mm.REGION) else []
rows = master.index.get(mm.MARKET, {})
hits = [
key
for _, name, spec, _, key in mm._market_index(master)
if all(w in name.replace("×", "x") or w in spec for w in words)
and (not cond.get("구분") or rows[key].get("구분") == cond["구분"])
and (not cond.get("상세구분") or rows[key].get("상세구분") == cond["상세구분"])
]
if unit:
kin = [k for k in hits if same_kind(rows[k].get("단위"), unit)]
hits = kin or hits
want = mm.plain_unit(unit) if unit else None
def rank(key):
one = rows[key]
name = mm.plain(one.get("이름"))
return (
not mm._offers(master, key),
bool(want) and mm.plain_unit(one.get("단위")) != want,
not (region and all(r in name for r in region)),
)
return sorted(hits, key=rank) # 정렬은 안정 — 남는 차례는 파일 차례
def pick(master, cond: dict, env: dict, unit: str | None = None) -> str:
keys = search(master, cond, env, unit)
if not keys:
raise mm.mf.FormulaError(f"재료 검색어 후보 없음 — {cond.get('검색어')}")
return keys[0]
def priced(master, item: dict, ref, env: dict, index: int | None):
"""재료 줄 단가 — (단가, 출처, 까닭, 단위경고). 고른 품목(`고름`)이 있으면 그것 · 검색어가 있으면 찾기 ·
없으면 옛 길. 자재 단가는 줄 단위로 환산 · 성격이 다르면 그대로 두고 경고."""
unit = item.get("단위")
picked = (CHOSEN.get() or {}).get(str(index)) if index is not None else None
try:
if picked or (isinstance(ref, dict) and ref.get("검색어")):
ref = picked or pick(master, ref, env, unit)
have = master.get(ref).get("단위") if ref[:2] == mm.MARKET else None
price, source = mm.element(master, ref, env, None)
warn = None
if have and unit and not same_kind(have, unit):
warn = f"줄 {unit} · 자재 {have}"
price = convert(price, have, unit) if have and unit else price
else:
price, source = mm.element(master, ref, env, unit)
warn = None
except mm.mf.FormulaError as e:
return None, None, str(e), None
why = None if price is not None else f"「{master.label(ref)}」 값 없음 — 관리자가 채울 값"
return price, source, why, warn
# ── 검사 ───────────────────────────────────────────────────────────────
def check_zero(master, where: str, cond: dict) -> list[str]:
"""검색어 후보 0 — `{입력}` 이 든 검색어는 계산 때 채워져 여기서 못 봄."""
term = str(cond.get("검색어") or "")
if not term.strip() or "{" in term or any("{" in str(v) for v in cond.values()):
return [] if term.strip() else [f"{where} · 검색어 비어 있음"]
return [] if search(master, cond) else [f"{where} · 검색어 「{term}」 후보 0"]
def check_warn(master, where: str, cond: dict, unit: str | None) -> list[str]:
"""단위 경고 줄 — 검색어 후보가 있으나 줄 단위와 같은 성격이 하나도 없음."""
term = str(cond.get("검색어") or "")
if not unit or "{" in term or "{" in str(cond.get("구분") or ""):
return []
keys = search(master, cond)
rows = master.index.get(mm.MARKET, {})
if keys and not any(same_kind(rows[k].get("단위"), unit) for k in keys):
return [f"{where} · 검색어 「{term}」 후보 단위가 줄 단위 「{unit}」 과 성격이 다름"]
return []
import master_formula as mf # noqa: E402, F401 — 서로 부름 · 이 모듈 이름이 다 선 뒤에 읽음