Files
Aislo/resources/master_data/scripts/master_material.py
T
eomsangdonandClaude Opus 5 76ee8acb4b knowledge(마스터): 전력을 재료_유가전력으로 연결 — 자재품목 전력 2줄 뺌
- 품셈재료 「전력」 연결 = {이름: 전력량요금, 계약종별: 입력} — 유가전력 전력 줄을 가리킴
- 전력 쓰는 로직 8 줄에 입력 「전력 계약종별」(주택용 저압 · 일반용(갑)Ⅰ 저압 · 산업용(갑)Ⅰ 저압) — 품셈·계약예규에 계약종별 규정 없어 기본값 두지 않음
- 임시전력(갑)은 주택용 저압 요금 그대로(요금표 6.가) — 따로 고르기에 두지 않음
- master_material: 자재품목 아닌 줄은 「값」 한 칸으로 읽고 계약종별 후보를 고름 · check_master 연결 모양·끊김 검사도 맞춤
- 자재품목 MT000006 · MT000007 삭제 → _키대장.json 「폐기」 · 빌더 DROP 에 더해 다시 만들어도 같음
- _틀.md 재료 절 · 시험 둘 고침(전력 계약종별 · 연결 낮은 값은 시멘트로)

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

123 lines
5.5 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
MARKET = "MT" # 자재품목 테이블ID
PRICED = "MO" # 유가전력 테이블ID — 값 한 칸
SLOTS = ("물가자료", "유통물가", "물가정보", "거래가격", "관급") # 자재품목 값 열
REGION = "자재지역" # 로직 입력 — 「지역」 은 유가 고르기가 씀
TARIFF = "전력 계약종별" # 로직 입력 — 품셈·계약예규에 계약종별 규정 없음
_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)
if key[:2] != MARKET:
return [(row["값"], f"{row['키']}.값")] if row.get("값") is not None else []
return [(row[slot], f"{row['키']}.{slot}") for slot in SLOTS if row.get(slot) is not None]
def _lowest(master: mf.Master, key: str) -> tuple[object, str | None]:
"""자재품목 줄의 낮은 값 — ⚠ 시험 계산용."""
offers = _offers(master, key)
return min(offers, key=lambda x: x[0]) if offers else (None, 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 _tariff(master: mf.Master, ref: str, row: dict, env: dict) -> list[str]:
"""후보 조건{이름 · 계약종별: 입력} — 유가전력 전력 줄 가운데 그 계약종별·이름으로 시작하는 줄."""
cond = row["연결"]
if TARIFF not in env:
raise mf.FormulaError(f"「{master.label(ref)}」 입력 「{TARIFF}」 없음")
want, kind = plain(cond["이름"]), plain(env[TARIFF])
hits = [
key
for key, one in master.index.get(PRICED, {}).items()
if one.get("구분") == "전력"
and plain(one.get("상세구분")) == kind
and plain(one.get("이름")).startswith(want)
and one.get("값") is not None
]
if not hits:
raise mf.FormulaError(f"「{master.label(ref)}」 계약종별 값 없음 — {env[TARIFF]}")
return hits
def _candidates(master: mf.Master, ref: str, row: dict, env: dict) -> list[str]:
"""후보 조건{이름 · 규격 · 지역: 입력} — 뿌리가 이름으로 끝나고 이름에 지역 글자 · 단위 같음."""
cond = row["연결"]
if cond.get("계약종별") == "입력":
return _tariff(master, ref, row, env)
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]:
"""요소 값과 출처 — 자재품목은 제 줄, 품셈재료는 연결을 따라가 낮은 값 · 그 밖은 줄의 `값`."""
row = master.get(ref)
if ref[:2] == MARKET: # 자재품목을 바로 가리키는 호표 줄
return _lowest(master, 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 — 서로 부름 · 이 모듈 이름이 다 선 뒤에 읽음