feat(815-1 K1·K2·K3·K5): 불러오기 이음 — EST 코드 · 별칭 칸 · 표기 맞추기 · 견적 줄
- K1 EST 원파일 자재 = 우리 자재 출처 칸 EST 코드로 바로 이음(새 M02_Elem_Alias · 자재 자원 24,219 중 코드 같음 23,981 모두 그 줄) - K2 M01 요소 줄 「별칭」 칸 읽기 — 기계 찾기(옛 별칭 장부 읽기 걷음) · 재료 · 인력 · 유가 · 일위대가 호표 줄(Units · Hopyo_Sangeun 장부 import 뗌) - K3 재료 표기 정규화 정확 일치(M/M · 곱셈표 · 지름 · 괄호 · 빈칸 · 전각 · 지역 꼬리) — 바탕 이름 길보다 먼저(치즐(0.7m3) → 0.2㎥ 줄로 잘못 잇던 것 바로잡음) - K5 견적 · 운임 · 수수료 줄 = 우리 자재면 재료 줄(우리 값) · 아니면 그대로 + 「견적」 표지 · 문서 설명 · 화면 「견적 n」 배지(못 이음 수에서 뺌) - 시험 test_815_1_import_link 새로 · test_812_9 = 옛 장부 별칭 대신 기계 줄 별칭 칸(마스터 다섯은 칸이 비면 시험 동안만 넣음) · 「디젤 엔진」 은 마스터 별칭 묶음으로 옮김 - test_778_5 기계 표(162 · 3 · 25)는 고치기 전 판과 같음 — 812-9 뒤부터 굳힌 수와 다름(이번 고침 무관 · 안 고침) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -332,7 +332,10 @@ def _attach(doc: dict, row: dict, ids: list[str], src: dict[str, dict]) -> None:
|
||||
if not note and not item.get("이음"): # 못 이은 까닭(814-1 — 흠 아님 · 우리 정보 없음)
|
||||
from M02_MasterTemplete import M02_San_Link as san_link
|
||||
|
||||
var["설명"] = f"이음 없음 — {san_link.why(item)} · 엑셀 단가 그대로"
|
||||
why = san_link.why(item)
|
||||
var["설명"] = (
|
||||
why if why == san_link.QUOTE else f"이음 없음 — {why} · 엑셀 단가 그대로"
|
||||
)
|
||||
continue
|
||||
if (opt := link.pop(OPTION, None)) and opt["열쇠"] not in {
|
||||
v.get("열쇠") for v in doc["변수"]
|
||||
|
||||
@@ -274,6 +274,10 @@ class Block:
|
||||
out["옮김"] = part if part != nat else None
|
||||
return out
|
||||
|
||||
def quote(self, r: dict) -> tuple[str | None, str | None]:
|
||||
"""견적 자원(갈래 고정) → 우리 자재 이음 · 표지(`M02_San_Link.quote`) · 그 밖 = (None, None)."""
|
||||
return em._m01("고정", r) if self.keys and r.get("갈래") == "고정" else (None, None)
|
||||
|
||||
def register(self, ln: dict, call: str) -> None:
|
||||
if ln.get("결과"):
|
||||
self.done.append((ln["결과"][-1], call, ln))
|
||||
@@ -314,6 +318,8 @@ class Block:
|
||||
prices = {ln["비목"]: (ln.get("연결단가") or [0])[0] for ln in rows}
|
||||
r = self.res(rows[0], None, None)
|
||||
r |= {"단가": prices}
|
||||
if quoted := self.quote(r)[1]: # 815-1 K5 — 세 줄 견적 = 값묶음 그대로 · 표지 한 번
|
||||
self.mark(rows[0], quoted.removeprefix(NOTE))
|
||||
parts = em.machine_parts(r) if self.keys else [] # 812-9 기계 찾기 · 조합 = 줄 둘
|
||||
r |= {"찾기": parts[0]} if parts else {}
|
||||
r |= {"조합첫": True} if len(parts) > 1 else {}
|
||||
@@ -363,10 +369,13 @@ class Block:
|
||||
parts = em.machine_parts(r) if self.keys else [] # 812-9 기계 찾기 · 조합 = 줄 둘
|
||||
r |= {"찾기": parts[0]} if parts else {}
|
||||
r |= {"조합첫": True} if len(parts) > 1 else {}
|
||||
one = em.resource(name, r.get("단위"), qty, r, parts[0]["키"] if parts else None)
|
||||
link, many = (parts[0]["키"], None) if parts else self.quote(r)
|
||||
one = em.resource(name, r.get("단위"), qty, r, link)
|
||||
em.first_spec(one, parts)
|
||||
one |= {"자리": place} if place else {}
|
||||
self.o.add_line(one, True, ln["행"], ln.get("보임") or "", ln["결과"][-1])
|
||||
if many:
|
||||
self.mark(ln, many.removeprefix(NOTE))
|
||||
self.got["확정"] += bool(got)
|
||||
self.register(ln, self.parts(one, parts, ln, ln))
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
"""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…」 같은 줄
|
||||
- `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"
|
||||
BASE_REGION = "서울"
|
||||
_CODE = re.compile(r"\bM\d{10}\b")
|
||||
_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 est_code(code: Any) -> list[str]:
|
||||
"""EST 자재 코드(M + 수 열) → 우리 자재 키들(출처 칸 같은 코드)."""
|
||||
text = str(code or "").strip()
|
||||
return list(book()["코드"].get(text, [])) if _CODE.fullmatch(text) else []
|
||||
|
||||
|
||||
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, "")
|
||||
@@ -77,6 +77,8 @@ def m01_picks(kind: str, line: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
|
||||
@lru_cache(maxsize=4096)
|
||||
def _m01_picks(kind: str, name: str, spec: str, unit: str, _whole: int) -> list[dict[str, Any]]:
|
||||
if kind in ("인력", "재료") and (named := _alias(kind, name, spec)):
|
||||
return named # 815-1 K2 — M01 줄 「별칭」 칸 먼저
|
||||
if kind == "인력":
|
||||
trade = est.trade(name)
|
||||
hit = [r for r in _rows("LB").values() if trade and r.get("이름") == trade]
|
||||
@@ -111,6 +113,23 @@ def _m01_picks(kind: str, name: str, spec: str, unit: str, _whole: int) -> list[
|
||||
return []
|
||||
|
||||
|
||||
def _alias(kind: str, name: str, spec: str) -> list[dict[str, Any]]:
|
||||
"""M01 별칭 칸 이음(`M02_Elem_Alias`) — 인력 = 이름 · 재료 = 이름 + 규격 › 표기 정규화 정확 일치 ·
|
||||
같은 품목 지역 줄은 하나."""
|
||||
from M02_MasterTemplete import M02_Elem_Alias as ea
|
||||
|
||||
table = "LB" if kind == "인력" else ea.MARKET
|
||||
keys = ea.keys(table, name, "" if kind == "인력" else spec)
|
||||
if kind == "재료" and len(keys) > 1:
|
||||
keys = [k] if (k := ea.one({"이름": name, "규격": spec}, keys)) else keys
|
||||
if kind == "재료" and not keys: # 815-1 K3 — 표기 정규화 정확 일치(바탕 이름 길보다 먼저)
|
||||
keys = [k] if (k := ea.one({"이름": name, "규격": spec}, ea.exact(name, spec))) else []
|
||||
rows = [(k, ea.row(table, k)) for k in keys]
|
||||
if kind == "인력":
|
||||
return [pick("인력", k, r.get("이름"), "", "인", 단가=_plain(r.get("값"))) for k, r in rows]
|
||||
return [pick("재료", k, r.get("이름"), r.get("규격"), r.get("단위")) for k, r in rows]
|
||||
|
||||
|
||||
# ── M02 층 SK · SC(같은 이름 · 참고) ────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ from common_util.common_util_time import utc_now_text
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Hopyo_Elem as elem
|
||||
from M02_MasterTemplete import M02_Hopyo_Fixed as hfix
|
||||
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
|
||||
from M02_MasterTemplete import M02_Hopyo_Units as units
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
@@ -144,9 +143,8 @@ class Maker:
|
||||
self._units: units.Units | None = None
|
||||
|
||||
def units(self) -> units.Units:
|
||||
if self._units is None:
|
||||
books = ledger.books(self.source)
|
||||
self._units = units.Units(books, ledger.doc_id(self.source, self.key))
|
||||
if self._units is None: # 815-1 — 옛 별칭 장부 안 읽음(M01 별칭 칸은 Units 가 읽음)
|
||||
self._units = units.Units()
|
||||
return self._units
|
||||
|
||||
def ref_of(self, sub: dict[str, Any]) -> dict[str, Any] | None:
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
L 노무 = 직종 LB(이름 · 옛이름) · 같은 이름 여럿 = 건설업 먼저(작업반장 · 요구 624-1) · 단위 `{노임}`
|
||||
X 기계 = EQ(`Fixed.Linker._machine` · 778-1 괄호 낱말) → 기계경비 기초 로직(`mach_logics` ·
|
||||
778-2) · 단위 `{기계, 로직, 할증?}` · 없는 규격 = 가까운 규격 후보만(`Elem.machines` · 자동 고름 없음)
|
||||
M 자재 = MT(`material` — 별칭 장부 › 이름 · 규격 같음 › 바탕 이름 · 숫자 규격 같음(치수 차례 · 구분자
|
||||
M 자재 = MT(`material` — M01 별칭 칸 › 이름 · 규격 같음 › 바탕 이름 · 숫자 규격 같음(치수 차례 · 구분자
|
||||
무관) · 글자 든 규격 = 후보로만) · 손료율 = 방법 「수량 × 손료율 × 단가」 · 별도 = 단위 `{자재?,
|
||||
별도계상}` · 단위 `{자재}`
|
||||
R 율 = 778-4 꼴 `{기준, 비목}` — 기준 = `기준꼴`(인력 · 재료 묶음 · 노무비 · 줄 목록 `{행: [...]}`) ·
|
||||
우리에 없는 율 항목(설치비 · 제경비)도 율 줄 · 기준 못 찾음 = 고를 것(글 기준 제안)
|
||||
S 하위 = `{하위 {종류 단산|일위, 번}}`(SK · SC 참조) · E 값 = `{값: 금액}`(고정 금액 줄)
|
||||
별칭 장부 짝(자재) = 숫자보다 먼저. 서버 단독 · 읽기만.
|
||||
M01 줄 「별칭」 칸(자재 · 노임 · 기계 = 기계 찾기 안) = 숫자보다 먼저(815-1). 서버 단독 · 읽기만.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -22,7 +22,7 @@ from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Elem as elem
|
||||
from M02_MasterTemplete import M02_Hopyo_Fixed as hfix
|
||||
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
|
||||
from M02_MasterTemplete import M02_Elem_Alias as ea
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
||||
from M02_MasterTemplete import M02_Hopyo_Material as hmat
|
||||
from M02_MasterTemplete import M02_Hopyo_Shares as shares
|
||||
@@ -109,6 +109,16 @@ def material(line: dict[str, Any], got: dict[str, Any], full: bool = True) -> di
|
||||
found = hm._exact(me, "자재", got)
|
||||
if len(found) == 1:
|
||||
return {"상태": LINKED, "찾음": found, "글": "이름 · 규격 같음", "꼬리표": tags, "후보": []}
|
||||
hits = ea.exact(
|
||||
me.get("이름"), me.get("규격")
|
||||
) # 815-1 K3 표기 정규화 같음 한 줄(지역 변형 = 아래 길)
|
||||
if len(hits) == 1:
|
||||
key = hits[0]
|
||||
one = ea.row(ea.MARKET, key)
|
||||
found = [
|
||||
{"자재": key, "이름": one.get("이름"), "규격": one.get("규격"), "단위": one.get("단위")}
|
||||
]
|
||||
return {"상태": LINKED, "찾음": found, "글": "표기 정규화 같음", "꼬리표": tags, "후보": []}
|
||||
tries = [me, *_split(me)]
|
||||
for one in tries:
|
||||
sure, more = hmat.sure(one)
|
||||
@@ -150,7 +160,7 @@ def _text_base(one: dict[str, Any]) -> str:
|
||||
|
||||
|
||||
class Units:
|
||||
"""한 번 잇기 — M01 · 목록 · 장부를 한 번 읽음(`books` = `Ledger.books` · 없으면 장부 안 봄)."""
|
||||
"""한 번 잇기 — M01 · 목록을 한 번 읽음(`books` · `doc` = 옛 장부 자리 · 815-1 뒤 안 씀)."""
|
||||
|
||||
def __init__(self, books: list | None = None, doc: str | None = None) -> None:
|
||||
self.linker = hfix.Linker(by_name=True)
|
||||
@@ -168,6 +178,9 @@ class Units:
|
||||
return {"방법": how, "후보": [], "꼬리표": [], "글": "", **got}
|
||||
|
||||
def _L(self, one: dict[str, Any]) -> dict[str, Any]:
|
||||
named = ea.keys("LB", one.get("직종") or one.get("이름")) # 815-1 M01 별칭 칸 먼저
|
||||
if len(named) == 1:
|
||||
return {"상태": LINKED, "단위": {"노임": named[0]}, "꼬리표": ["별칭"]}
|
||||
name = shares.trade(one.get("직종") or one.get("이름")) or ""
|
||||
rows = self.lb.get(name, [])
|
||||
cands = [{"키": str(r.get("키")), "갈래": "노임", "이름": r.get("이름"),
|
||||
@@ -211,7 +224,7 @@ class Units:
|
||||
pair = self._alias(line)
|
||||
if pair:
|
||||
unit = {"자재": pair} | ({"별도계상": True} if apart else {})
|
||||
return {"상태": LINKED, "방법": how, "단위": unit, "꼬리표": ["별칭 장부 짝"]}
|
||||
return {"상태": LINKED, "방법": how, "단위": unit, "꼬리표": ["별칭"]}
|
||||
got = material(line, self.got)
|
||||
apart = apart or hmat.APART in got["꼬리표"]
|
||||
found = [r for r in got["찾음"] if r.get("자재")]
|
||||
@@ -231,14 +244,9 @@ class Units:
|
||||
"글": got["글"], "꼬리표": got["꼬리표"]} # fmt: skip
|
||||
|
||||
def _alias(self, line: dict[str, Any]) -> str | None:
|
||||
"""별칭 장부 자재 짝(자동 정확 열쇠 · 하나) — 숫자보다 먼저."""
|
||||
if self.books is None:
|
||||
return None
|
||||
got = ledger.recall(line, "", self.books, doc=self.doc)
|
||||
if not got or got.get("제안") or got.get("규격다름") or got.get("갈래") != "짝":
|
||||
return None
|
||||
pairs = [p.get("자재") for p in got.get("짝") or []]
|
||||
return pairs[0] if len(pairs) == 1 and pairs[0] else None
|
||||
"""M01 자재 줄 「별칭」 칸(815-1 · 이름 + 규격 · 같은 품목 지역 줄은 하나) — 숫자보다 먼저."""
|
||||
keys = ea.keys(ea.MARKET, line.get("이름"), line.get("규격"))
|
||||
return keys[0] if len(keys) == 1 else ea.one(line, keys) if keys else None
|
||||
|
||||
def _R(self, one: dict[str, Any]) -> dict[str, Any]:
|
||||
part = rate_part(one.get("이름"))
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
과녁 = M01 기계표 전체(EQ) · 부르는 곳 = 불러오기 이음(`M02_Hopyo_Row.m01_picks` · `M02_San_Emit._m01`) ·
|
||||
일위대가 호표(`M02_Hopyo_Elem.machine(s)` · `M02_Hopyo_Fixed._eqs`) · 연결 찾기(`M02_Doc_Match_Note`).
|
||||
차례:
|
||||
1. 별칭 장부(`M02_Hopyo_Ledger` · 마스터 층 + 지금 층 · 짝 갈래 기계) — 이름 + 규격 열쇠 = 그 EQ ·
|
||||
이름만(규격 빈) 열쇠 = 그 EQ 이름으로 바꿔 2 로
|
||||
1. 별칭(M01 기계 줄 「별칭」 칸 · 815-1 `M02_Elem_Alias`) — 「이름 규격」 통째 별칭 = 그 EQ(여럿 =
|
||||
후보 여럿) · 이름만 별칭 = 그 EQ 기종으로 바꿔 2 로
|
||||
2. 기종 = EQ 이름(괄호 뗌) · 괄호 붙임 · 상세구분과 같은 글 › 그 글을 품은 EQ(표기 정규화 `norm`)
|
||||
3. 괄호 낱말 — 기계표 괄호에 있는 낱말(무한궤도 · 타이어 · 자주식 …)만 거름 · 표에 없는 낱말(발파 · 리핑)은 안 봄
|
||||
4. 규격 = 수 + 단위(범위 · 여럿) 모두 EQ 규격에 같은 값(단위 한쪽 없음 = 봄) › 없으면 EQ 범위 안
|
||||
@@ -20,7 +20,6 @@ import re
|
||||
import time
|
||||
import unicodedata
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
@@ -222,38 +221,14 @@ def parts(name: Any, spec: Any = "") -> list[tuple[str, str]]:
|
||||
return [(n, s or first) for n, s in out]
|
||||
|
||||
|
||||
# ── 별칭 장부 ─────────────────────────────────────────────────────────
|
||||
def _alias_rows() -> dict[tuple[str, str], list[str]]:
|
||||
"""(이름, 규격) 정규화 → EQ 키들 — 마스터 층 + 지금 층 별칭 장부 기계 짝."""
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
|
||||
here, top = Path(store.root()), Path(store.FOLDER)
|
||||
folders = [here] + ([top] if here != top else [])
|
||||
files = [(f, hopyo_store._dir(f) / f"{hopyo_store.MEMORY}.json") for f in folders]
|
||||
stamp = tuple((str(f), p.stat().st_mtime_ns if p.is_file() else 0) for f, p in files)
|
||||
return _alias_read(stamp)
|
||||
|
||||
|
||||
@lru_cache(maxsize=8)
|
||||
def _alias_read(stamp: tuple) -> dict[tuple[str, str], list[str]]:
|
||||
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
|
||||
|
||||
out: dict[tuple[str, str], list[str]] = {}
|
||||
for folder, _ in stamp:
|
||||
for row in ledger.read(folder).values():
|
||||
pairs = row.get("짝") or []
|
||||
eqs = [str(p["키"]) for p in pairs if p.get("갈래") == "기계" and p.get("키")]
|
||||
if row.get("갈래") != "짝" or not eqs:
|
||||
continue
|
||||
origin = row.get("원래") or {}
|
||||
out.setdefault((norm(origin.get("품명")), norm(_clean(origin.get("규격")))), eqs)
|
||||
return out
|
||||
|
||||
|
||||
# ── 별칭(M01 기계 줄 「별칭」 칸 · 815-1) ───────────────────────────────────
|
||||
def alias(name: Any, spec: Any) -> list[str]:
|
||||
"""별칭 장부 기계 짝 — 이름 + 규격 열쇠(규격 속 할증 글은 뺌) · 없으면 []."""
|
||||
return _alias_rows().get((norm(name), norm(_clean(spec))), [])
|
||||
"""M01 기계 줄 별칭(`M02_Elem_Alias`) — 규격이 있으면 「이름 규격」 통째 별칭(규격 속 할증 글은 뺌) ·
|
||||
규격 빔 = 이름 별칭 · 없으면 []."""
|
||||
from M02_MasterTemplete import M02_Elem_Alias as ea
|
||||
|
||||
rows = ea.book()["별칭"].get("EQ", {})
|
||||
return list(rows.get(ea.am.norm(name) + ea.am.norm(_clean(spec)), []))
|
||||
|
||||
|
||||
# ── 찾기 ──────────────────────────────────────────────────────────────
|
||||
@@ -280,18 +255,19 @@ def _ans(kind: str, rows: list[Row], query: str, why: str = "") -> dict[str, Any
|
||||
def find(name: Any, spec: Any = "") -> dict[str, Any]:
|
||||
"""기계 하나(조합 아님) → `{판정, 키, 후보, 찾기어, 까닭, 수}`."""
|
||||
rows_by = table()[2]
|
||||
hit = alias(name, spec)
|
||||
if len(hit) == 1 and hit[0] in rows_by:
|
||||
r = rows_by[hit[0]]
|
||||
return _ans(ALIAS, [r], str(r.raw.get("이름")))
|
||||
hit = [rows_by[k] for k in alias(name, spec) if k in rows_by]
|
||||
if hit: # 별칭 하나 = 그 EQ · 여럿 = 후보 여럿
|
||||
if len(hit) == 1:
|
||||
return _ans(ALIAS, hit, str(hit[0].raw.get("이름")))
|
||||
return _ans(MANY, hit, _query(hit, hit[0].base), f"후보 {len(hit)}")
|
||||
base = split_name(name)[0]
|
||||
_, groups, specs = split_name(f"{name or ''} {spec or ''}") # 규격 괄호 낱말 「3.6m(일반용)」
|
||||
groups.append(
|
||||
_words(_TOKEN.sub(" ", _PAREN.sub(" ", _clean(spec))))
|
||||
) # 규격 글 낱말 「탑승유압식」
|
||||
named = alias(name, "")
|
||||
if len(named) == 1 and named[0] in rows_by: # 이름만 별칭 = 그 EQ 기종으로
|
||||
base = rows_by[named[0]].base
|
||||
named = [rows_by[k] for k in alias(name, "") if k in rows_by]
|
||||
if named and len({r.base for r in named}) == 1: # 이름만 별칭 = 그 EQ 기종으로
|
||||
base = named[0].base
|
||||
mine = tokens(" ".join(specs))
|
||||
kinds = kind_rows(base)
|
||||
known = table()[1]
|
||||
@@ -307,7 +283,7 @@ def find(name: Any, spec: Any = "") -> dict[str, Any]:
|
||||
same = [r for r in rows if spec_fit(mine, r.spec) == "같음"]
|
||||
got = same or [r for r in rows if spec_fit(mine, r.spec) == "안"]
|
||||
if len(got) == 1: # 이름만 별칭으로 기종을 정했으면 「별칭」
|
||||
return _ans(ALIAS if len(named) == 1 else LINKED, got, str(got[0].raw.get("이름")))
|
||||
return _ans(ALIAS if named else LINKED, got, str(got[0].raw.get("이름")))
|
||||
if got:
|
||||
return _ans(MANY, got, _query(got, base), f"후보 {len(got)}")
|
||||
if not kinds:
|
||||
@@ -326,12 +302,7 @@ def _gap(mine: list, theirs: list) -> float:
|
||||
|
||||
|
||||
def find_all(name: Any, spec: Any = "") -> list[dict[str, Any]]:
|
||||
"""기계 글 → 부분마다 `find` + {이름, 규격}(조합 = 둘 이상) · 글 통째 별칭(짝 여럿)이 먼저."""
|
||||
whole = alias(name, spec)
|
||||
rows_by = table()[2]
|
||||
if len(whole) > 1 and all(k in rows_by for k in whole):
|
||||
made = [_ans(ALIAS, [rows_by[k]], str(rows_by[k].raw.get("이름"))) for k in whole]
|
||||
return [{**x, "이름": str(name or ""), "규격": str(spec or "")} for x in made]
|
||||
"""기계 글 → 부분마다 `find` + {이름, 규격}(조합 = 둘 이상)."""
|
||||
out: list[dict[str, Any]] = []
|
||||
for n, s in parts(name, spec):
|
||||
if out and s and not split_name(n)[2] and s == out[0]["규격"] and out[0]["키"]:
|
||||
|
||||
@@ -61,7 +61,7 @@ interface EditAnswer {
|
||||
문서: ImportedDoc;
|
||||
전?: Money4 | null;
|
||||
후?: Money4 | null;
|
||||
이음없음?: { 자리: string; 이름: string; 까닭?: string }[];
|
||||
이음없음?: { 자리: string; 이름: string; 까닭?: string; 견적?: boolean }[];
|
||||
/** qty 답(797-2) */
|
||||
줄?: QtyRow[];
|
||||
집계?: QtyTally;
|
||||
|
||||
@@ -42,6 +42,7 @@ const TEXT = {
|
||||
Excel: ["엑셀", "Excel"],
|
||||
Now: ["지금", "Now"],
|
||||
Unlinked: ["이음 없는 자원 {n}", "Unlinked resources {n}"],
|
||||
Quote: ["견적 {n}", "Quotes {n}"],
|
||||
Link: ["이음 고르기", "Pick link"],
|
||||
Gap: ["바꾸기 전 {a} → 후 {b} ({d})", "Before {a} → after {b} ({d})"],
|
||||
QtyLinks: ["소요량 · 계수 이음 {n}", "Quantity / coefficient links {n}"],
|
||||
@@ -54,7 +55,7 @@ export const tx = (k: keyof typeof TEXT): string =>
|
||||
export interface PanelState {
|
||||
doc: ImportedDoc;
|
||||
/** 이음 없는 자원(서버 price 답) */
|
||||
unlinked: { 자리: string; 이름: string; 까닭?: string }[];
|
||||
unlinked: { 자리: string; 이름: string; 까닭?: string; 견적?: boolean }[];
|
||||
/** 소요량 · 계수 이음 집계(서버 qty 답 · 797-2) */
|
||||
qty: QtyTally | null;
|
||||
/** 마지막 단가 바꿈 전후 */
|
||||
@@ -154,8 +155,13 @@ export function importedPanel(s: PanelState, a: PanelActs): HTMLElement {
|
||||
b.dataset.importedLink = u.자리;
|
||||
return b;
|
||||
});
|
||||
const tag = createBadge(tx("Unlinked").replace("{n}", String(s.unlinked.length)), "warning");
|
||||
kids.push(createToolbar({ items: [tag, ...items] }));
|
||||
const quotes = s.unlinked.filter((u) => u.견적).length; // 815-1 견적 = 그대로 쓰는 값 · 못 이음 수에서 뺌
|
||||
const left = s.unlinked.length - quotes;
|
||||
const tags = [
|
||||
...(left ? [createBadge(tx("Unlinked").replace("{n}", String(left)), "warning")] : []),
|
||||
...(quotes ? [createBadge(tx("Quote").replace("{n}", String(quotes)), "info")] : []),
|
||||
];
|
||||
kids.push(createToolbar({ items: [...tags, ...items] }));
|
||||
}
|
||||
if (here && s.qty?.자리) {
|
||||
const open = createButton({ label: tx("QtyOpen"), size: 24, variant: "ghost", onClick: a.qty });
|
||||
|
||||
@@ -6,21 +6,20 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import unicodedata
|
||||
|
||||
NOTE = "참고 · " # `M02_San_Emit.NOTE` 와 같음(사람 확인 표지 머리)
|
||||
OIL_BASE = ("전국평균", "일반용(갑)Ⅰ 저압") # 유가 · 전력 기본(요구 748 · 우리 기초 기본값)
|
||||
BASE_REGION = "서울" # 같은 이름 · 규격 자재가 지역마다 있을 때 기본(우리 기초 검색어 꼴)
|
||||
_REGION = re.compile(r"\s*,\s*([가-힣]{2,4})\s*$") # 자재 이름 지역 꼬리 「, 서울」 · 「,서울」
|
||||
QUOTE = "견적 — 그대로(이름 · 값) · [단가 갱신] 대상 아님" # 815-1 K5 · 요구 762-3
|
||||
|
||||
|
||||
def m01(kind: str, src: dict) -> tuple[str | None, str | None]:
|
||||
"""1차 줄 판정 유일 짝 M01 키 → (키, 표지) — 없음 = (None, None) · 여럿 = (None, 표지 · 유가 ·
|
||||
환율만 — 재료 · 인력은 조용히 이음 없음) · 기계 = 기계 찾기(812-9) 못 이으면 늘 표지. 재료 줄 이름이 M01 유가전력(MO)이면
|
||||
유가 · 화폐 이름 그대로(달러 · USD …)면 환율(FX)로 판정(797-1 · 「유로폼」 같은 글 속 낱말은 아님)."""
|
||||
if kind == "고정":
|
||||
return quote(src)
|
||||
if kind not in ("인력", "재료", "기계"):
|
||||
return None, None
|
||||
from M02_MasterTemplete import M02_Elem_Alias as ea
|
||||
from M02_MasterTemplete import M02_Hopyo_Elem as elem
|
||||
from M02_MasterTemplete import M02_Hopyo_Row as hrow
|
||||
|
||||
@@ -34,16 +33,17 @@ def m01(kind: str, src: dict) -> tuple[str | None, str | None]:
|
||||
# 찾기 판정 없는 기계 줄 막음(814-1 · 줄 쌓기 표지 · 별칭)
|
||||
src.setdefault("찾기", {**got, "이름": got.get("이름") or line["이름"], "규격": got.get("규격") or line["규격"]}) # fmt: skip
|
||||
return got["키"], mfind.mark(got, line["이름"], line["규격"])
|
||||
if kind == "재료" and src.get("종류") == "자재" and (hit := ea.est_code(src.get("코드"))):
|
||||
return ea.one(line, hit) or hit[0], None # 815-1 K1 — EST 원파일 자재 = 우리 자재 출처 코드
|
||||
oil = ea.keys("MO", line["이름"], line["규격"]) if kind == "재료" else []
|
||||
if kind == "재료":
|
||||
if elem.oil_rows(line["이름"]):
|
||||
if oil or elem.oil_rows(line["이름"]):
|
||||
kind = "유가"
|
||||
elif line["이름"].strip().upper() in {x.upper() for pair in hrow.CURRENCY for x in pair}:
|
||||
kind = "환율"
|
||||
if kind == "유가": # MO 전부 → 줄 규격이 상세구분(전국 · 지역)이면 그것만
|
||||
got = [
|
||||
{"키": r["키"], "규격": str(r.get("상세구분") or "")}
|
||||
for r in elem.oil_rows(line["이름"])
|
||||
]
|
||||
rows = [ea.row("MO", k) for k in oil] or elem.oil_rows(line["이름"]) # 815-1 별칭 먼저
|
||||
got = [{"키": r["키"], "규격": str(r.get("상세구분") or "")} for r in rows]
|
||||
spec = line["규격"].replace(" ", "")
|
||||
got = (
|
||||
[x for x in got if x["규격"] == spec]
|
||||
@@ -59,6 +59,8 @@ def m01(kind: str, src: dict) -> tuple[str | None, str | None]:
|
||||
keys = {x["키"] for x in got if x["규격"] in OIL_BASE} or keys
|
||||
elif len(keys) > 1 and kind == "재료": # 814-1 — 지역 꼬리 뗀 이름 · 규격 정확 일치
|
||||
keys = exact(line, got) or keys
|
||||
if not keys and kind == "재료": # 815-1 K3 — 자재표 전체에서 표기 정규화 정확 일치
|
||||
keys = {k} if (k := ea.one(line, ea.exact(line["이름"], line["규격"]))) else keys
|
||||
if len(keys) == 1:
|
||||
return keys.pop(), None
|
||||
if len(keys) > 1 and kind in ("유가", "환율"):
|
||||
@@ -66,32 +68,28 @@ def m01(kind: str, src: dict) -> tuple[str | None, str | None]:
|
||||
return None, None
|
||||
|
||||
|
||||
def _flat(text: object) -> str:
|
||||
return re.sub(r"\s+", "", unicodedata.normalize("NFKC", str(text or ""))).lower()
|
||||
def quote(src: dict) -> tuple[str | None, str | None]:
|
||||
"""견적 · 운임 · 수수료 꼴 줄(갈래 밖 — EST 견적 · STmate 고정 코드 · 815-1 K5) → 우리 자재에 이어지면
|
||||
(코드 · 별칭 · 정확 일치 · 단가 줄일 때) 그 키 + 갈래 재료(우리 값 · 엑셀 값은 검산) · 아니면 그대로 + 표지."""
|
||||
from M02_MasterTemplete import M02_Elem_Alias as ea
|
||||
|
||||
|
||||
def region_of(name: object) -> tuple[str, str]:
|
||||
"""자재 이름 → (지역 꼬리 뗀 이름, 지역 | 빈 글)."""
|
||||
text = str(name or "")
|
||||
m = _REGION.search(text)
|
||||
return (text[: m.start()], m.group(1)) if m else (text, "")
|
||||
src["견적"] = True
|
||||
line = {k: src.get(k) or "" for k in ("이름", "규격", "단위")}
|
||||
key, _how = ea.material(line, src.get("코드"))
|
||||
if key and src.get("비목"):
|
||||
src["갈래"] = "재료"
|
||||
return key, None
|
||||
return None, f"{NOTE}{QUOTE}"
|
||||
|
||||
|
||||
def exact(line: dict, got: list[dict]) -> set[str]:
|
||||
"""재료 후보 가운데 지역 꼬리 뗀 이름 · 규격이 줄과 정확히 같은 것(빈칸 · 전각 · 대소문자 정규화) —
|
||||
"""재료 후보 가운데 지역 꼬리 뗀 이름 · 규격이 줄과 정확히 같은 것(표기 정규화 `master_alias.norm`) —
|
||||
지역마다 여럿이면 줄 지역 › 서울 › 같은 단위 › 키 차례 첫째 하나 · 없음 = 빈 모음(사람 확인)."""
|
||||
name, mine = region_of(line.get("이름"))
|
||||
want = (_flat(name), _flat(line.get("규격")))
|
||||
hits = [x for x in got if x.get("키") and (_flat(region_of(x["이름"])[0]), _flat(x.get("규격"))) == want] # fmt: skip
|
||||
if not hits:
|
||||
return set()
|
||||
unit = _flat(line.get("단위"))
|
||||
from M02_MasterTemplete import M02_Elem_Alias as ea
|
||||
|
||||
def rank(x: dict) -> tuple:
|
||||
where = region_of(x["이름"])[1]
|
||||
return (where != mine if mine else True, where != BASE_REGION, _flat(x.get("단위")) != unit, str(x["키"])) # fmt: skip
|
||||
|
||||
return {min(hits, key=rank)["키"]}
|
||||
want = ea._same(line.get("이름"), line.get("규격"))
|
||||
hits = [x["키"] for x in got if x.get("키") and ea._same(x.get("이름"), x.get("규격")) == want]
|
||||
return {k} if (k := ea.one(line, hits)) else set()
|
||||
|
||||
|
||||
def hourly_text(key: str | None) -> str | None:
|
||||
@@ -154,7 +152,7 @@ def why(item: dict) -> str:
|
||||
return "노임 없음"
|
||||
if kind == "단가산출":
|
||||
return "안쪽 단가산출 못 이음"
|
||||
return "갈래 밖(견적 · 고정 값)"
|
||||
return QUOTE
|
||||
|
||||
|
||||
def tally(logic: dict) -> dict:
|
||||
|
||||
@@ -304,13 +304,15 @@ def same_part(doc: Any) -> dict | None:
|
||||
|
||||
|
||||
def unlinked(doc: dict) -> list[dict]:
|
||||
"""이음 없는 자원 줄(표지 · 사람이 고름) `[{자리, 이름, 까닭}]`(까닭 = 814-1 `M02_San_Link.why`)."""
|
||||
"""이음 없는 자원 줄(표지 · 사람이 고름) `[{자리, 이름, 까닭, 견적?}]`(까닭 = 814-1 `M02_San_Link.why` ·
|
||||
견적 = 815-1 K5 그대로 쓰는 견적 · 운임 · 수수료 줄 — 못 이음 수에서 뺌)."""
|
||||
from M02_MasterTemplete import M02_San_Link as link
|
||||
|
||||
row = row_of(doc) or {}
|
||||
ids = sd.line_ids(row)
|
||||
return [
|
||||
{"자리": ids[n], "이름": one.get("이름") or "", "까닭": link.why(one)}
|
||||
| ({"견적": True} if (one.get("자원") or {}).get("갈래") == "고정" else {})
|
||||
for n, one in enumerate(doc[KEY]["로직"].get("호표") or [])
|
||||
if not one.get("이음") and not one.get("참조")
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user