- 로직이 가리키던 나라장터 자리 121(정적 키 18 · 고르기 13벌)을 자재품목(MT) 줄로 갈아 끼움 — 이름·규격·단위가 맞는 줄 · 지역이 갈리는 품목은 서울 줄. - 자재품목에 맞는 줄이 없는 넷은 품셈재료 미연결로 돌림 — 시너(MP000266) · 동 및 동합금용 용접봉 BCUP3 · 폴리에틸렌필름 0.04㎜ · 아스팔트싱글(새 줄 셋). - 엔진이 자재품목 키를 바로 받으면 값 열 다섯 가운데 낮은 값(시험 계산용) · 출처는 「키.열」. - 재료_나라장터자재.json 과 그 빌더를 old 로 옮기고 키 대장에 MN 폐기 · check_master · 키 대장 · M01 서버·화면 · _틀.md · compare_estx 의 나라장터 자리 정리. - 끝 검사: check_master 틀 0 · 로직 0 · 일괄 시험 계산 통과 988 · 멈춤 363(앞 989 · 362 — 아스팔트싱글 한 줄) · pytest 67 통과 · typecheck 깨끗. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgUhN55Z3BCYhUJTjwTNfj
98 lines
4.3 KiB
Python
98 lines
4.3 KiB
Python
# -*- 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
|
||
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"{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 _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]:
|
||
"""요소 값과 출처 — 자재품목은 제 줄, 품셈재료는 연결을 따라가 낮은 값 · 그 밖은 줄의 `값`."""
|
||
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 — 서로 부름 · 이 모듈 이름이 다 선 뒤에 읽음
|