Merge branch 'sub_laptop_3' of C:/Aislo_wt/sub_laptop_3 into main_laptop_1

This commit is contained in:
2026-11-11 15:10:39 +09:00
15 changed files with 554 additions and 129 deletions
+4 -1
View File
@@ -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["변수"]
+10 -1
View File
@@ -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))
+116
View File
@@ -0,0 +1,116 @@
"""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…」 같은 줄 ·
공사 직접 번호(M0000000…)는 빼고 이름 길
- `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"
PRICES = ("물가자료", "유통물가", "물가정보", "거래가격", "관급") # 자재 값 칸 다섯
BASE_REGION = "서울"
_CODE = re.compile(r"\bM\d{10}\b")
LOCAL = "M0000000" # 공사 직접 번호(파일마다 다른 품목 · 거창 치즐 = 우리 표 Air Hammer) = 코드로 안 이음
_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 priced(key: Any) -> bool:
"""우리 자재 줄에 값 칸 다섯 중 하나라도 있음."""
return any(row(MARKET, key).get(c) is not None for c in PRICES)
def est_code(code: Any) -> list[str]:
"""EST 자재 코드(M + 수 열) → 우리 자재 키들(출처 칸 같은 코드)."""
text = str(code or "").strip()
if not _CODE.fullmatch(text) or text.startswith(LOCAL):
return []
return list(book()["코드"].get(text, []))
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, "")
+19
View File
@@ -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(같은 이름 · 참고) ────────────────────────────────────────
+2 -4
View File
@@ -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:
+21 -13
View File
@@ -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("이름"))
+19 -48
View File
@@ -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 });
+30 -31
View File
@@ -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,29 @@ 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("비목") and ea.priced(key): # 우리 값 없는 자재 = 견적 값 그대로
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 +153,7 @@ def why(item: dict) -> str:
return "노임 없음"
if kind == "단가산출":
return "안쪽 단가산출 못 이음"
return "갈래 밖(견적 · 고정 값)"
return QUOTE
def tally(logic: dict) -> dict:
+3 -1
View File
@@ -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("참조")
]
Binary file not shown.
@@ -0,0 +1,77 @@
# 815-1 불러오기 이음 — 전후 전수 보고(요구 753 ~ 765 · 방향 장부 K 장 K1 · K2 · K3 · K5)
- 대상 = 814-1 과 같은 자 — 원파일 EST 6 · STC 7(산근 2,220 블록) + 실무 엑셀 11(1,021 블록)
- 전 = 814-1 뒤 판(지금 코드로 다시 잼 · 814-1 보고 「후」와 수 같음) · 후 = 815-1 판(커밋 55ad1bf · 030a843 · 8edd8ad)
- 자세한 표 = `_목록_815-1_정합성.xlsx`(요약 전후 · 못 이은 줄 = 별칭 넣을 후보 · 견적 줄 · 0 원 줄)
- 마스터 자료 · 별칭 값 · storage 층 사본 안 건드림 · 별칭 이음은 시험 안 임시 별칭으로만 확인
## 요약
| 잣대 | 전(814-1 뒤) | 후(815-1) | 판정 |
|---|---|---|---|
| 금액(엑셀 단가 그대로 셈 = 원본) | 원파일 2,220 · 엑셀 1,021 = 100 % | 같음 100 % | 그대로 |
| 못 읽음 표지 | 0 | 0 | 그대로 |
| 인력 · 형제 · 유가 이음 | 100 % · 100 % · 100 % | 같음 | 그대로 |
| 기계 이음 | 95.7 %(4,601 / 4,807) | 94.9 %(4,564) | 옛 마스터 별칭 다섯이 아직 기계 줄 「별칭」 칸에 안 옮겨짐(815-2 몫) — 옮기면 37 줄 돌아와 95.7 % |
| 재료 이음 | 87.6 %(1,784 / 2,036) | **88.0 %**(1,792) | 표기 맞추기로 8 줄 더 |
| 못 이은 재료 | 252 줄 · 105 종 | **244 줄 · 100 종** | 다섯 종 풀림 |
| 견적 · 운임 · 수수료 줄 | 59 줄 = 「갈래 밖」 못 이음 | **59 줄 = 「견적」 그대로**(못 이음에서 뺌) | 우리 값 있는 자재로 이어진 것 0 |
| [우리 단가로] 모두 우리 값 | 원파일 1,899(85.5 %) · 엑셀 814(79.7 %) | 원파일 1,853(83.5 %) + 견적만 남음 19 · 엑셀 815(79.8 %) + 12 | 아래 「줄어든 까닭」 |
| [우리 단가로] 0 원 줄 있는 블록 | 원파일 115 · 엑셀 4 | 원파일 155 · 엑셀 9 | 아래 「줄어든 까닭」 |
| 차림 왕복 · 시험 | 100 % | 100 %(812-3 · 810-2 · 810-3 통과) | 그대로 |
### 「모두 우리 값」이 줄어든 까닭(블록 옮김 전수)
| 옮김 | 블록 | 까닭 |
|---|---|---|
| 우리 값 → 엑셀 단가 남음 | 30 | 아스팔트페이버(피니셔) 29 · 버킷준설기 1 — 옛 마스터 별칭 장부를 안 읽게 됨 · 815-2 가 다섯을 기계 줄 칸으로 옮기면 그대로 돌아옴 |
| → 0 원 줄 | 45 | 치즐(0.7㎥) 39 · 치즐(0.4㎥) 6 — 전에는 「바탕 이름」 길이 **엉뚱한 치즐 0.2㎥ 줄**로 이어 값이 있어 보였음(지어 맞추기) · 이제 정확히 같은 치즐 0.7 · 0.4 줄로 잇는데 그 줄 값 칸 다섯이 빔(EST DB 자체에 값 없음 · 비고에만 「가격정보 223,000」) = 자재표 보강(J 장) 대상 |
| 엑셀 단가 남음 → 우리 값 | 4 | 표기 맞추기 |
## K1 EST 원파일 자재 = 참조 코드
| 원파일 | 자재 자원 | 공사 직접 번호(M0000000…) | 우리 표 코드와 같음 → 그 줄로 이음 |
|---|---|---|---|
| main.estx(EST DB 판 그 자체) | 23,980 | 8 | **23,965 / 23,965**(다른 줄 0) |
| 거창 · 장수 · 진안 · 임도용역 · 통영 | 239 | **235** | 0 — 나머지 4 도 우리 표에 없는 코드(아연도철선 M0004113991 …) |
- 찾은 것: 방향 장부 G4 의 「24,219 중 23,981(99 %)」은 거의 다 main.estx(우리 자재표를 만든 바로 그 EST DB 파일) 몫 · **실제 공사 파일 다섯의 자재는 99 % 가 공사가 매긴 직접 번호** → 코드로 잇는 몫 0 · 이름 · 별칭 길로 이음
- 공사 직접 번호는 파일마다 다른 품목(거창 「치즐(0.7㎥)」 = M0000000001 ↔ 우리 표 M0000000001 = 「Air Hammer」) → 코드 잇기에서 뺌(시험에 굳힘)
- 화면: 거창 .estx 단산10 줄떼심기 = 천연잔디 이름 길로 이음 · 이음 없는 자원 0
## K2 「별칭」 칸 읽기(풀이 정본 `master_alias.py` · 잇기 `M02_Elem_Alias.py`)
- 풀이 = 콤마 · 전각 콤마 「,」 · 큰따옴표 조각 통째(속 콤마 보존) · 앞뒤 빈칸 뗌 · 같은 것 한 번
- 뜻 = 이름만 적은 별칭 → 가져온 규격이 그 줄 규격과 정규화해 같을 때 · 「이름 규격」을 적은 별칭 → 통째 같을 때 · 같은 별칭이 여러 줄 = 같은 품목 지역 줄이면 줄 지역 › 서울 · 아니면 「후보 여럿」
- 읽는 곳(모두 같은 길) = 단가산출 불러오기 재료 · 인력 · 유가 · 기계 찾기 · 일위대가 호표 줄(자재 · 노임 · 기계)
- 옛 층 별칭 장부를 요소 잇기에서 읽는 곳 걷음 — 기계 찾기 · 호표 자재 짝 · 단가산출 호표(장부 import 뗌)
- 남긴 곳(815-2 몫) — 기계 [저장] 때 고른 짝을 옛 장부에 쓰는 `M02_Machine_Find.learn` · 일위대가 [확정]의 장부 쓰기(`M02_Hopyo_Build`) · 장부 화면 · 길
- 시험(임시 별칭): 「"크레인(트럭) 15Ton"」 → 그 크레인 · 「아스팔트페이버(피니셔)」 → 규격으로 피니셔 3m · 「결속선(철선)」(서울 · 부산 줄) + #20, 0.9M/M → 서울 줄 · 규격 #8 = 안 이음 · 「특인」 → 특별인부 · 「디젤유」 → 경유 전국평균 · 같은 별칭 두 기계 = 후보 여럿
## K3 재료 표기 맞추기
- 정규화 = M/M · m/m · ㎜ = mm · 곱셈표 × · x · X · * · 지름 Φ · φ · ø · D= · 괄호 · 빈칸 · 전각 · # · 지역 꼬리(「, 서울」) — 정확히 같을 때만(가까운 규격 대신 잇지 않음) · 낱말 순서 뒤집힘(결속선(철선) ↔ 보통철선(결속선))은 별칭 몫
- 정확 일치를 옛 「바탕 이름」 길보다 먼저 봄(엉뚱한 치즐 0.2 잇기 바로잡음)
- 풀린 다섯 종 = PVC파이프(일반관),서울 D=100mm(VG1) · 강관동바리 Φ48.6x2.4,L=2.6~4.0 · 보통철선(결속선) #20 , 0.9M/M · 산림종자(비수리) · 산림종자(안고초)
- 남은 못 이은 재료 244 줄 · 100 종 = 후보 여럿 230 · 자재표에 없음 14 → 목록 「못 이은 줄」 시트(가져온 이름 · 규격 · 쓰임 수 · 가까운 우리 줄 셋 · 넣을 별칭 예)
## K5 견적 줄
- 대상 = 원파일 EST 견적(국가지점번호판 · 측량 수수료 · 도선료) · STmate · 엑셀 고정 코드 줄(구역화물 · 하차비 · 덤프자동덮개 · 치즐 견적) — 59 줄
- 우리 자재에 이어지고(코드 · 별칭 · 정확 일치) **우리 값이 있으면** 재료 줄(우리 값 · 엑셀 값은 검산) · 아니면 그대로(이름 · 값) + 표지 「견적 — 그대로(이름 · 값) · [단가 갱신] 대상 아님」 · 문서 줄 설명 같은 글 · 화면 「견적 n」 배지(「이음 없는 자원」 수에서 뺌)
- 지금 59 줄 모두 그대로 — 치즐(0.7m3) 견적 5 줄은 우리 치즐 0.7 줄이 있으나 값 칸이 비어 견적 값 유지(0 원 막음)
## 화면(8005 · 5178 · 일반 계정 개인 층 · 「sub3임시_거창.estx」)
- 단산22 국가지점번호판 → 띠 「견적 2」(파랑) · 「국가지점번호판 (견적 — 그대로(이름 · 값) · [단가 갱신] 대상 아님) · 이음 고르기」 · 검산 같음 · 이음 없는 자원 배지 없음
- 밝음 · 어두움 · 1366×768 가로 넘침 0 · 콘솔 오류 0(로그인 전 세션 401 하나 뺌) · ORCA 자기 탭 확인 · 사진 = tmp(셋)
- 만든 SK 셋 · 올린 HP 둘 지움(같은 계정 다른 창이 만든 SK-000004 ~ 7 은 그대로)
## 남은 흠 · 알림
| # | 내용 | 갈래 |
|---|---|---|
| 1 | 옛 마스터 기계 별칭 다섯이 칸으로 옮겨지기 전까지 기계 37 줄 · 30 블록 못 이음 | 815-2 옮김 뒤 풀림(시험은 칸이 비면 다섯을 임시로 넣어 봄) |
| 2 | 치즐 0.7 · 0.4 등 우리 자재 줄 값 칸 빔 — 정확히 이으니 0 원 줄 102 → 153 | 자재표 보강(J 장 · 값 출처 미정) |
| 3 | 견적 값묶음 줄 공식 칸에 「[object Object]」 글(814-1 전부터 · 값묶음 줄 꼴) | 화면 흠 · 이번 고침 밖 |
| 4 | test_778_5 일위대가 기계 줄 표(이어짐 162 · 고를 것 3 · 못 이음 25)가 굳힌 수(180 · 10 · 0)와 다름 — 고치기 전 판도 같은 수(812-9 뒤부터) | 낡은 굳힘 수 · 이번 고침 무관 |
+24 -26
View File
@@ -5,8 +5,8 @@
(물탱크 · 크레인(무한궤도) · 머캐덤 · 콘크리트 펌프차 …) · 지어 맞추기 없음(1.0Ton = 우리 표에 없음)
(b) 표기 정규화 · 괄호 낱말(무한궤도 · 타이어 · 일반용 · 표에 없는 낱말 발파 = 안 봄) · 범위 · 복수 규격
(c) 조합 기계 = 둘(대형브레이카 +굴착기 · 0.7+브레이카 · +부착용집게 · +진동콤팩터 · 덤프트럭(자동덮개))
(d) 별칭 먼저 — 지금 층 장부 기계 짝 = 그 EQ · 이름만 열쇠 = 그 기종으로 · 후보 여럿 표지 ·
[저장] 때 고른 EQ = 지금 층 장부 줄(`learn`)
(d) 별칭 먼저 — M01 기계 줄 「별칭」 칸(815-1 · 시험 안 임시 별칭) · 이름 + 규격 통째 = 그 EQ · 이름만 =
그 기종으로 · 후보 여럿 표지 · [저장] 때 고른 EQ = 지금 층 옛 장부 줄(`learn` — 승인 길 바뀔 때까지)
(e) 금액 전후 같음 — 조합 줄 둘(첫 줄 엑셀 값 · 다음 줄 0) · 원파일 저장 금액 그대로
"""
@@ -23,10 +23,12 @@ for p in (ROOT, HERE):
if str(p) not in sys.path:
sys.path.insert(0, str(p))
from M02_MasterTemplete import M02_Elem_Alias as ea # noqa: E402
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger # noqa: E402
from M02_MasterTemplete import M02_Machine_Find as mf # noqa: E402
from M02_MasterTemplete import M02_MasterTemplete_Store as store # noqa: E402
from resources.tester.test_m02_labor_ref import world # noqa: E402, F401 — 같은 임시 층
from test_815_1_import_link import aliases # noqa: E402 — M01 별칭 칸 임시
def one(name: str, spec: str = "") -> tuple[str, str, str]:
@@ -55,7 +57,6 @@ LINKED = [ # 낱말 모름 · 후보 여럿이던 예(812-8) → 하나
("경운기", "1000kg", "경운기", "1,000㎏"),
("건설용펌프(자흡식)", "50mm", "건설용펌프(자흡식)", "50(1.49×10)"),
("강연선인장기", "120Ton", "강연선인장기", "120ton"),
("디젤 엔진", "11.19kW", "엔진(디젤)", "기종 디젤 엔진 · 규격 11.19㎾"),
("물탱크", "5500ℓ", "물탱크(살수차)", "5,500ℓ"),
("물탱크", "16000ℓ", "물탱크(살수차)", "16,000ℓ"),
("콘크리트 펌프차", "32m,80~95㎥/hr", "콘크리트 펌프차", "32 [80∼95]"),
@@ -92,9 +93,16 @@ MASTER = [ # 마스터 층 승인 별칭(요구 735-5 · 736) — 이름 별칭
]
# 마스터 기계 별칭 다섯(815-2 가 기계 줄 「별칭」 칸으로 옮김) — 칸이 비었으면 시험 동안만 넣음
FIVE = {"EQ000193": "아스팔트페이버(피니셔)", "EQ000500": "디젤 엔진", "EQ000400": "워터제트",
"EQ000425": "버킷준설기", "EQ000248": '"바이브레이트 봉상후렉시블"'} # fmt: skip
@pytest.mark.parametrize("name,spec,eq", MASTER)
def test_master_alias(name: str, spec: str, eq: str) -> None:
got = mf.find(name, spec)
rows = ea.book()["줄"]["EQ"]
with aliases("EQ", {k: v for k, v in FIVE.items() if not rows[k].get("별칭")}):
got = mf.find(name, spec)
assert got["키"] == eq and got["판정"] in (mf.LINKED, mf.ALIAS)
@@ -153,39 +161,29 @@ def test_combo_many_part() -> None:
assert mf.find_all("덤프트럭", "15Ton") == [{**mf.find("덤프트럭", "15Ton"), "이름": "덤프트럭", "규격": "15Ton"}] # fmt: skip
def _pair(eq: str, name: str, spec: str, unit: str = "hr") -> dict:
full = ledger.key({"이름": name, "규격": spec, "단위": unit})
row = {"원래": {"품명": name, "규격": spec, "단위": unit}, "갈래": "짝"}
return {full: row | {"짝": [{"갈래": "기계", "키": eq, "입력값": {}, "배": 1}]}}
def test_alias_first_and_learn(world) -> None: # noqa: F811
me = world["personal"]
me.mkdir(parents=True, exist_ok=True)
with store.at(me):
assert mf.find("아스팔트페이버(피니셔)", "3.0m")["판정"] == mf.NONE
ledger.write(me, _pair("EQ000193", "아스팔트페이버(피니셔)", ""), {"id": "sub3임시"})
got = mf.find("아스팔트페이버(피니셔)", "3.0m") # 이름만 열쇠 = 그 기종 → 규격으로 하나
assert (got["판정"], got["후보"][0]["이름"], got["후보"][0]["규격"]) == (
mf.ALIAS, "아스팔트 피니셔", "3m") # fmt: skip
ledger.write(me, _pair("EQ000129", "크레인(트럭)", "15Ton"), {"id": "sub3임시"})
got = mf.find("크레인(트럭)", "15Ton") # 이름 + 규격 열쇠 = 그 EQ(찾기보다 먼저)
assert (got["판정"], got["키"]) == (mf.ALIAS, "EQ000129")
assert (
mf.find("크레인(트럭)", "15Ton(할증125%)")["키"] == "EQ000129"
) # 규격 속 할증 글 = 안 봄
assert mf.same_kind("아스팔트페이버(피니셔)", "아스팔트 피니셔") is True
pairs = {"EQ000193": "아스팔트페이버(피니셔)", "EQ000129": '"크레인(트럭) 15Ton"'}
with aliases("EQ", pairs):
got = mf.find("아스팔트페이버(피니셔)", "3.0m") # 이름만 별칭 = 그 기종 → 규격으로 하나
assert (got["판정"], got["후보"][0]["이름"], got["후보"][0]["규격"]) == (
mf.ALIAS, "아스팔트 피니셔", "3m") # fmt: skip
got = mf.find("크레인(트럭)", "15Ton") # 이름 + 규격 별칭 = 그 EQ(찾기보다 먼저)
assert (got["판정"], got["키"]) == (mf.ALIAS, "EQ000129")
assert mf.find("크레인(트럭)", "15Ton(할증125%)")["키"] == "EQ000129" # 할증 글 안 봄
assert mf.same_kind("아스팔트페이버(피니셔)", "아스팔트 피니셔") is True
doc = {
"변수": [{"열쇠": "P_크", "기호": "P", "아래": "크", "연결": {
"갈래": "단가산출", "키": "GC000270", "입력": {"기계": "'2104-0010'"}}}],
"가져온": {"기계": [{"기호": "P_크", "판정": mf.MANY, "키": None,
"원래": {"이름": "크레인(트럭)", "규격": "10Ton", "단위": "hr"}}]},
} # fmt: skip
assert mf.learn(doc, {"id": "sub3임시"}) == 1
got = mf.find("크레인(트럭)", "10Ton")
assert got["판정"] == mf.ALIAS and got["후보"][0]["원문번호"] == "2104-0010"
assert mf.learn(doc, {"id": "sub3임시"}) == 0 # 이미 같은 EQ = 안 씀
assert mf.find("크레인(트럭)", "10Ton")["판정"] == mf.MANY # 다른 층(마스터) = 안 봄
assert mf.learn(doc, {"id": "sub3임시"}) == 1 # 옛 층 장부 줄(이음은 별칭 칸만 봄)
assert any(r.get("원래", {}).get("규격") == "10Ton" for r in ledger.read(me).values())
assert mf.find("크레인(트럭)", "10Ton")["판정"] == mf.MANY
def test_combo_money_same() -> None:
+219
View File
@@ -0,0 +1,219 @@
"""815-1 지킴 — 불러오기 이음(방향 장부 `_방향_불러오기.md` K 장 · 요구 753 ~ 765).
K1 EST 원파일 자재 = 우리 자재 줄 `출처` 칸 EST 코드로 바로(글 대조 없음) · 코드 없음 = 이름 길
K2 M01 요소 줄 「별칭」 칸 풀이(`master_alias` — 콤마 · 전각 콤마 · 큰따옴표 조각 · 규격) → 기계 · 재료 ·
인력 · 유가 이음(시험 안 임시 별칭 — 마스터 자료 안 고침)
K3 표기 정규화(M/M = mm · 곱셈표 · 지름 · 괄호 · 빈칸 · 전각 · 지역 꼬리) · 정확 일치만
K5 견적 줄 = 우리 자재에 이어지면 우리 값(재료 줄) · 아니면 그대로 + 「견적」 표시 · 못 이음 수에서 뺌
금액 = 엑셀 단가 그대로 셈이 원파일 저장 금액과 같음(견적 · 코드 이음 블록).
원파일 없으면 그 시험만 skip(다른 PC).
"""
from __future__ import annotations
import sys
from contextlib import contextmanager
from pathlib import Path
HERE = Path(__file__).resolve().parent
ROOT = HERE.parents[1]
for p in (ROOT, HERE):
if str(p) not in sys.path:
sys.path.insert(0, str(p))
import test_810_2_native_doc as t # noqa: E402
from M02_MasterTemplete import M02_Doc_FromNative as fn # noqa: E402
from M02_MasterTemplete import M02_Elem_Alias as ea # noqa: E402
from M02_MasterTemplete import M02_Hopyo_Row as hrow # noqa: E402
from M02_MasterTemplete import M02_Hopyo_Units as units # noqa: E402
from M02_MasterTemplete import M02_Machine_Find as mf # noqa: E402
from M02_MasterTemplete import M02_San_Link as link # noqa: E402
from M02_MasterTemplete import M02_Sangeun_Imported as imp # noqa: E402
am = ea.am
GONE = object()
@contextmanager
def aliases(table: str, pairs: dict[str, str]):
"""M01 줄 `별칭` 칸을 시험 동안만 바꿈(읽은 판 안 · 파일 안 씀) — 끝나면 되돌림."""
rows = ea.book()["줄"][table]
old = {k: rows[k].get("별칭", GONE) for k in pairs}
def clear() -> None:
ea._book.cache_clear()
hrow._m01_picks.cache_clear()
for k, v in pairs.items():
rows[k]["별칭"] = v
clear()
try:
yield
finally:
for k, v in old.items():
if v is GONE:
rows[k].pop("별칭", None)
else:
rows[k]["별칭"] = v
clear()
# ── 별칭 풀이 ─────────────────────────────────────────────────────────────
def test_alias_names_rule() -> None:
assert am.names("") == [] and am.names(None) == []
assert am.names(" 철선 , 결속선,보통철선 ,, 철선") == ["철선", "결속선", "보통철선"]
assert am.names('"결속선(철선) #20, 0.9M/M", 철선') == ["결속선(철선) #20, 0.9M/M", "철선"]
assert am.names("“크레인(트럭) 15Ton, 할증”") == ["크레인(트럭) 15Ton, 할증"]
idx = am.index([{"키": "A", "별칭": "철선, 결속선"}, {"키": "B", "별칭": "결속선"}])
assert idx == {"철선": ["A"], "결속선": ["A", "B"]}
def test_norm_samples() -> None:
same = [
("0.9M/M", "0.9mm", "0.9m/m", "0.9㎜", "0.9 MM"),
("600×1200", "600x1200", "600X1200", "600*1200", "600 × 1200"),
("Φ100", "φ100", "ø100", "D=100", "d = 100"),
("유로폼(패널)", "유로폼 패널", "유로폼[패널]", "유로폼패널"),
("#20", "#20", "# 20"),
]
for group in same:
assert len({am.norm(x) for x in group}) == 1, group
assert am.norm("결속선(철선)") != am.norm("철선(결속선)") # 낱말 순서 = 별칭 몫
# ── K1 EST 코드 ────────────────────────────────────────────────────────────
def test_est_code_first() -> None:
assert ea.est_code("M0005250069") == ["MT000011"] # 치 즐(0.7㎥)
src = {"꼴": "EST", "종류": "자재", "갈래": "재료", "코드": "M0005250069", "이름": "엉뚱한 글", "규격": "", "단위": "EA"} # fmt: skip
assert link.m01("재료", src) == ("MT000011", None) # 글 대조 없이 코드
gone = {**src, "코드": "M0000000123", "이름": "치즐(0.7m3)"} # 공사가 넣은 코드 = 이름 길
assert link.m01("재료", gone) == ("MT000011", None)
assert ea.est_code("자재품목 M0005250069") == [] and ea.est_code(None) == []
# 공사 직접 번호 = 파일마다 다른 품목(거창 치즐 ↔ 우리 표 Air Hammer) → 코드로 안 이음 · 이름 길
assert ea.est_code("M0000000001") == []
local = {**src, "코드": "M0000000001", "이름": "치즐(0.7㎥)"}
assert link.m01("재료", local) == ("MT000011", None)
# ── K2 별칭 → 요소 ───────────────────────────────────────────────────────────
def test_alias_machine() -> None:
assert mf.find("에어 호스", "1.91cm")["판정"] == mf.NONE
with aliases("EQ", {"EQ000129": '"크레인(트럭) 15Ton"', "EQ000193": "아스팔트페이버(피니셔)"}):
got = mf.find("크레인(트럭)", "15Ton") # 이름 + 규격 통째 별칭 = 그 EQ
assert (got["판정"], got["키"]) == (mf.ALIAS, "EQ000129")
assert mf.find("크레인(트럭)", "15Ton(할증125%)")["키"] == "EQ000129" # 할증 글 안 봄
got = mf.find("아스팔트페이버(피니셔)", "3.0m") # 이름 별칭 = 그 기종 → 규격으로 하나
assert (got["판정"], got["후보"][0]["이름"], got["후보"][0]["규격"]) == (mf.ALIAS, "아스팔트 피니셔", "3m") # fmt: skip
with aliases("EQ", {"EQ000129": "크레인 갑", "EQ000130": "크레인 갑"}):
assert mf.find("크레인 갑")["판정"] == mf.MANY # 같은 별칭 둘 = 후보 여럿
assert mf.find("크레인(트럭)", "15Ton")["판정"] != mf.ALIAS # 되돌림
def test_alias_material() -> None:
line = {"이름": "결속선(철선)", "규격": "#20, 0.9M/M", "단위": "kg"}
assert link.m01("재료", dict(line))[0] is None # 별칭 전 = 못 이음(낱말 순서 다름)
with aliases("MT", {"MT000174": "결속선(철선)", "MT000175": "결속선(철선)"}):
assert link.m01("재료", dict(line)) == ("MT000174", None) # 같은 품목 지역 줄 = 서울
assert hrow.m01_picks("재료", line)[0]["키"] == "MT000174"
assert units.Units()._M({"묶음": "M", **line})["단위"] == {"자재": "MT000174"}
assert link.m01("재료", {**line, "규격": "#8"})[0] is None # 규격 다름 = 안 이음
with aliases("MT", {"MT000194": '"결속선(철선) #20, 0.9M/M(200m/kg)"'}):
got = link.m01("재료", {**line, "규격": "#20, 0.9M/M(200m/kg)", "단위": "M"})
assert got == ("MT000194", None) # 이름 + 규격 통째 별칭
def test_alias_labor_oil() -> None:
with aliases("LB", {"LB000003": "산림특인, 특인"}):
assert [x["키"] for x in hrow.m01_picks("인력", {"이름": "특인"})] == ["LB000003"]
assert link.m01("인력", {"이름": "산림특인", "규격": "", "단위": "인"}) == (
"LB000003",
None,
)
assert units.Units()._L({"이름": "특인"})["단위"] == {"노임": "LB000003"}
with aliases("MO", {"MO000002": "디젤유"}):
assert link.m01("재료", {"이름": "디젤유", "규격": "", "단위": "L"}) == ("MO000002", None)
# ── K3 표기 맞추기 ──────────────────────────────────────────────────────────
def test_exact_norm() -> None:
assert link.m01("재료", {"이름": "치즐(0.7m3)", "규격": "", "단위": "EA"}) == ("MT000011", None)
got = link.m01("재료", {"이름": "보통철선(결속선)", "규격": "#20, 0.9M/M", "단위": "kg"})
assert got == ("MT000174", None)
got = link.m01("재료", {"이름": "유로폼 패널", "규격": "600X1200mm", "단위": "매"})
assert (
got[0] is None or got[0] == "MT000467"
) # 규격 단위 꼬리(㎜) 다름 = 정확 일치 아님 · 지어 맞추기 없음
assert (
link.m01("재료", {"이름": "보통철선(결속선)", "규격": "#20, 1.0mm", "단위": "kg"})[0]
is None
)
# ── K5 견적 ───────────────────────────────────────────────────────────────
def test_quote_line() -> None:
ours = {"꼴": "STmate", "갈래": "고정", "코드": "Q1", "이름": "치즐(0.6m3)", "규격": "", "단위": "EA", "비목": "M", "단가": {"M": 9}} # fmt: skip
assert link.m01("고정", ours) == (
"MT000010",
None,
) # 우리 값 있는 자재 = 재료 줄(우리 값 · 엑셀 = 검산)
assert ours["갈래"] == "재료" and ours["견적"] is True
empty = {
**ours,
"갈래": "고정",
"이름": "치즐(0.7m3)",
} # 우리 줄 값 칸 빔(MT000011) = 견적 값 그대로
assert link.m01("고정", empty)[0] is None and empty["갈래"] == "고정"
keep = {"꼴": "EST", "갈래": "고정", "종류": "견적", "코드": "W0000001", "이름": "도선료", "규격": "8.0ton", "비목": "S"} # fmt: skip
key, mark = link.m01("고정", keep)
assert key is None and mark == f"{link.NOTE}{link.QUOTE}" and keep["갈래"] == "고정"
assert link.why({"종류": "로직", "자원": keep}) == link.QUOTE
def test_quote_money_and_tag() -> None:
saved = t.native("EST", "거창")
no = next(n for n, b in fn.numbers(saved).items() if b.get("이름") == "국가지점번호판")
out = fn.emit(saved, no)
quotes = [h for h in out["로직"]["호표"] if (h.get("자원") or {}).get("견적")]
assert len(quotes) == 2 and all(not h.get("이음") for h in quotes)
assert sum(str(m).endswith(link.QUOTE) for m in out["원본"]["표지"]) == 2
_sk, doc, got = t.doc_and_money(out)
want = fn.numbers(saved)[no]["저장"]
assert all(abs(float(got[k]) - float(want.get(c) or 0)) < 0.5 for c, k in t.COST)
free = [x for x in imp.unlinked(_sk) if x.get("견적")]
assert len(free) == 2 and all(x["까닭"] == link.QUOTE for x in free)
notes = [v.get("설명") for v in doc["변수"] if v.get("설명") == link.QUOTE]
assert len(notes) == 2
def test_quote_kept_money_same() -> None:
saved = t.native("STC", "소광22")
out = fn.emit(
saved, 96
) # 구조물헐기 — 치즐(0.7m3) 견적 · 우리 치즐 0.7 값 칸 빔 = 견적 값 그대로 + 표지
hit = [h for h in out["로직"]["호표"] if (h.get("자원") or {}).get("견적")]
assert [(h["종류"], h["이음"]) for h in hit] == [("로직", None)]
assert any(str(m).endswith(link.QUOTE) for m in out["원본"]["표지"])
_sk, _doc, got = t.doc_and_money(out)
want = fn.numbers(saved)[96]["저장"]
assert all(abs(float(got[k]) - float(want.get(c) or 0)) < 0.5 for c, k in t.COST)
def test_est_local_code_block_money() -> None:
"""거창 = 자재가 모두 공사 직접 번호(M0000000…) → 이름 길 · 치즐(0.7㎥) = 우리 치즐 0.7(Air Hammer 아님) ·
금액 = 원파일 저장 그대로(전 블록)."""
saved = t.native("EST", "거창")
chisel = set()
for no in fn.numbers(saved):
out = fn.emit(saved, no)
for h in out["로직"]["호표"]:
r = h.get("자원") or {}
if r.get("종류") == "자재":
assert str(r.get("코드")).startswith(ea.LOCAL) or r.get("코드") == "M0004113991"
if r.get("코드") == "M0000000001":
chisel.add(h["이음"])
_sk, _doc, got = t.doc_and_money(out)
want = fn.numbers(saved)[no]["저장"]
assert all(abs(float(got[k]) - float(want.get(c) or 0)) < 0.5 for c, k in t.COST), no
assert chisel == {"MT000011"}