Files
Aislo/resources/master_data/scripts/master_material.py
T
eomsangdonandClaude Opus 5 9de61eb251 knowledge(마스터): 재료 열 한 벌 정리 — 시중물가 → 자재품목 · 값 다섯 열 · 유가·환율도 같은 열
- 재료_시중물가.json → 재료_자재품목.json(테이블ID MT · 키 그대로) · 모든 줄이 같은 열 한 벌(키 · 원문번호 · 구분 · 상세구분 · 이름 · 규격 · 단위 · 물가자료 · 유통물가 · 물가정보 · 거래가격 · 관급 · 출처 · 비고 · 면수)
- 값 묶음(시중·조달)을 풀어 다섯 값을 열로 · 관급 = 조달 값(4,552 줄 · 기간은 비고 「조달 26.7」) · 원문 가격정보만 남은 72 줄은 값 열에 안 넣고 비고 「가격정보 ○○」 · 구분·상세구분은 원문에 분류가 없어 비움
- 재료_오피넷유가 · 환율_한국은행환율도 열 한 벌(기준일 → 규격 · 유가 구분 = 전국평균/시도별 · 상세구분 = 지역) · 상품코드·지역코드 삭제
- 엔진 단가(연결된 줄의 낮은 값 · 출처 = 「<키>.<열>」) · check_master 열 검사 · M01 고르기(procure 걷어냄) · 빌더 · _틀.md · 화면 계약 · 시험 같이 고침

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgUhN55Z3BCYhUJTjwTNfj
2026-09-20 15:55:40 +09:00

89 lines
4.0 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 -*-
"""품셈재료 단가 — 연결을 따라 자재품목 줄의 값 열 다섯 가운데 낮은 값 (`_틀.md` 4장 재료 값).
⚠ 낮은 값 고르기는 **시험 계산용**. 최저가·견적 고르기는 프로젝트 몫 — 마스터는 값을 모으기만 함.
`master_formula` 가 요소 값을 풀 때 부름. 서로 부르는 모듈이라 `master_formula` 는 끝에서 읽음.
"""
from __future__ import annotations
import re
import unicodedata
from decimal import Decimal
LINKED = "MP" # 품셈재료 테이블ID
SLOTS = ("물가자료", "유통물가", "물가정보", "거래가격", "관급") # 자재품목 값 열
REGION = "자재지역" # 로직 입력 — 「지역」 은 유가 고르기가 씀
_UNITS = {"ea": "개", "ton": "톤", "t": "톤", "l": "", "리터": "", "set": "조", "세트": "조"}
def plain(s) -> str:
"""이름 맞대기 — NFKC · 소문자 · 빈칸 없앰 · 지름 기호 하나로."""
s = unicodedata.normalize("NFKC", str(s or "")).lower()
return re.sub(r"\s+", "", re.sub(r"[φΦ∅⌀]", "ø", s))
def plain_spec(s) -> str:
return plain(s).replace("×", "x").replace("*", "x")
def plain_unit(s) -> str:
s = plain(s).replace("", "l")
return _UNITS.get(s, s)
def _offers(master: mf.Master, key: str) -> list[tuple[Decimal, str]]:
"""자재품목 줄 하나의 (값, 출처) — 값이 있는 열마다 하나 · 출처는 그 열 이름."""
row = master.get(key)
return [(row[slot], f"{key}.{slot}") for slot in SLOTS if row.get(slot) is not None]
def _market_index(master: mf.Master) -> list[tuple[str, str, str, str, str]]:
"""(품명 뿌리, 이름, 규격, 단위, 키) — 「레미콘(관급)-경상북도(울진군)」 뿌리 = 레미콘."""
if not hasattr(master, "_market"):
master._market = []
for key, row in master.index.get("MT", {}).items():
name = plain(row.get("이름"))
root = re.sub(r"\(.*?\)", "", name.split(",")[0]).split("-")[0]
master._market.append(
(root, name, plain_spec(row.get("규격")), plain_unit(row.get("단위")), key)
)
return master._market
def _candidates(master: mf.Master, ref: str, row: dict, env: dict) -> list[str]:
"""후보 조건{이름 · 규격 · 지역: 입력} — 뿌리가 이름으로 끝나고 이름에 지역 글자 · 단위 같음."""
cond = row["연결"]
if cond.get("지역") == "입력" and REGION not in env:
raise mf.FormulaError(f"「{master.label(ref)}」 입력 「{REGION}」 없음")
region = plain(env.get(REGION, "")) if cond.get("지역") == "입력" else ""
want, spec, unit = plain(cond["이름"]), plain_spec(cond.get("규격")), plain_unit(row["단위"])
hits = [
(s, key)
for root, name, s, u, key in _market_index(master)
if root.endswith(want) and region in name and u == unit and (not spec or s == spec)
]
hits = [(s, key) for s, key in hits if _offers(master, key)]
if not hits:
raise mf.FormulaError(f"「{master.label(ref)}」 지역 값 없음 — {env.get(REGION)}")
if len({s for s, _ in hits}) > 1:
raise mf.FormulaError(f"「{master.label(ref)}」 후보 여럿 — 규격 선택 필요 ({len(hits)}줄)")
return [key for _, key in hits]
def element(master: mf.Master, ref: str, env: dict) -> tuple[object, str | None]:
"""요소 값과 출처 — 품셈재료는 연결을 따라가 낮은 값 · 그 밖은 줄의 `값` 그대로(출처 None)."""
row = master.get(ref)
if ref[:2] != LINKED:
return master.value(ref)
link = row.get("연결")
if not link:
return None, None
keys = [link] if isinstance(link, str) else _candidates(master, ref, row, env)
offers = [x for key in keys for x in _offers(master, key)]
return min(offers, key=lambda x: x[0]) if offers else (None, None)
import master_formula as mf # noqa: E402 — 서로 부름 · 이 모듈 이름이 다 선 뒤에 읽음