- 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>
178 lines
8.6 KiB
Python
178 lines
8.6 KiB
Python
"""M02 엑셀 줄 → M01 요소 후보 · 환산(PLAN 785-3 · 809-1) — 서버 단독 · 읽기만.
|
|
|
|
`m01_picks` = 갈래(인력 · 재료 · 유가 · 기계) + 줄 이름 · 규격 · 단위 → M01 표마다(LB · MT · MO · EQ) 후보
|
|
(가져온 식 이음 `M02_San_Emit` 이 부름) · `CURRENCY` = 환율 낱말 · `factor` · `quantity` = 단위 환산 배 ·
|
|
우리 수량 = 엑셀 수량 × 몫 × 배.
|
|
옛 1차 줄 판정(엑셀 사전 · 제안 · 2차 필요 · 기초 후보)은 809-1 에서 지움(경쟁사 로직 인정 · 요구 703).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import re
|
|
import unicodedata
|
|
from decimal import Decimal
|
|
from functools import lru_cache
|
|
from typing import Any
|
|
|
|
from M01_MasterData import M01_MasterData_Store as m01
|
|
from M02_MasterTemplete import M02_Hopyo_Elem as elem
|
|
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
|
from M02_MasterTemplete import M02_Hopyo_Material as hmat
|
|
from M02_MasterTemplete import M02_Hopyo_ReadEst as est
|
|
from M02_MasterTemplete import M02_Machine_Find as mfind
|
|
|
|
mm, mp = hm.mm, hm.mp
|
|
COSTS = ("노무비", "재료비", "경비")
|
|
MACHINE_UNITS = ("hr", "시간", "h")
|
|
TOP = 5
|
|
CURRENCY = (("달러", "USD"), ("$", "USD"), ("엔", "JPY"), ("유로", "EUR"), ("파운드", "GBP"))
|
|
|
|
|
|
def flat(value: Any) -> str:
|
|
return re.sub(r"\s+", "", unicodedata.normalize("NFKC", str(value or ""))).lower()
|
|
|
|
|
|
def _num(value: Any) -> float | None:
|
|
if isinstance(value, bool) or value in (None, ""):
|
|
return None
|
|
try:
|
|
return float(str(value).replace(",", "").strip())
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
def _plain(value: Any) -> int | float | None:
|
|
if value is None:
|
|
return None
|
|
f = float(value)
|
|
return int(f) if f == int(f) else round(f, 6)
|
|
|
|
|
|
# ── 엑셀 사전 ───────────────────────────────────────────────────────────
|
|
|
|
|
|
# ── M01 키 후보(표마다) ───────────────────────────────────────────────────
|
|
def pick(kind: str, key: Any = None, name: Any = "", spec: Any = "", unit: Any = "", **more: Any):
|
|
"""고른 것 한 꼴(설계 2-1)."""
|
|
head = {"갈래": kind, "키": key, "이름": str(name or ""), "규격": str(spec or "")}
|
|
return {**head, "단위": str(unit or ""), "단가": more.pop("단가", None), "입력값": {},
|
|
"출처": more.pop("출처", {}), "몫": None, **more} # fmt: skip
|
|
|
|
|
|
def _rows(table: str) -> dict[str, dict[str, Any]]:
|
|
return m01.loaded()[1].numbers.get(table, {})
|
|
|
|
|
|
def m01_picks(kind: str, line: dict[str, Any]) -> list[dict[str, Any]]:
|
|
"""갈래 하나 → M01 고른 것 후보(LB · MT · MO · EQ · FX) — 유일 짝이면 하나 · 같은 글 한 번."""
|
|
if kind == "기계": # 812-9 기계 찾기 — 별칭 장부가 바뀌므로 lru 밖(찾기가 기계표를 기억)
|
|
got = mfind.find(line.get("이름"), line.get("규격"))
|
|
rows = [] if got["판정"] == mfind.NONE else got["후보"]
|
|
unit = str(line.get("단위") or "") or "hr"
|
|
return [pick("기계", x["키"], x["이름"], x.get("규격"), unit) for x in rows]
|
|
text = tuple(str(line.get(k) or "") for k in ("이름", "규격", "단위"))
|
|
return copy.deepcopy(_m01_picks(kind, *text, id(m01.loaded()[1])))
|
|
|
|
|
|
@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]
|
|
if hit:
|
|
return [
|
|
pick("인력", hit[0]["키"], hit[0]["이름"], "", "인", 단가=_plain(hit[0].get("값")))
|
|
]
|
|
got = elem.labor(str(name or ""))
|
|
return [pick("인력", x["키"], x["이름"], "", "인", 단가=x["단가"]) for x in got]
|
|
if kind == "유가":
|
|
return [
|
|
pick("유가", x["키"], x["이름"], x["규격"], x["단위"], 단가=x["단가"])
|
|
for x in elem.oils(str(name or ""))
|
|
]
|
|
if kind == "환율":
|
|
code = next((c for w, c in CURRENCY if w in str(name)), flat(name).upper())
|
|
rows = [r for r in _rows("FX").values() if str(r.get("이름")).startswith(code)]
|
|
return [
|
|
pick("환율", r["키"], r["이름"], "", r.get("단위"), 단가=_plain(r["값"])) for r in rows
|
|
]
|
|
if kind == "재료":
|
|
clean, apart = hmat.clean({"이름": name, "규격": spec, "단위": unit})
|
|
sure, _ = hmat.sure(clean)
|
|
got = [r["one"] for r in sure[:1] if not r["one"].get("전국")]
|
|
got = got or hmat.candidates(clean, TOP)
|
|
more = {"별도계상": True} if apart else {}
|
|
return [
|
|
pick("재료", x.get("자재"), x["이름"], x.get("규격"), x.get("단위"), **more)
|
|
for x in got
|
|
if x.get("자재")
|
|
]
|
|
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(같은 이름 · 참고) ────────────────────────────────────────
|
|
|
|
|
|
# ── 줄 판정 ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
# 별도계상 줄 표지 `품목` — 품목은 프로젝트에 복사할 때 정함(요구 645-2)
|
|
|
|
|
|
# ── 환산(`M02_Hopyo_Match.factor` 옮김) ─────────────────────────────────────
|
|
def factor(have: Any, want: Any, spec: Any = "") -> dict[str, Any]:
|
|
"""`{배, 갈래}` — 우리 수량 = 엑셀 수량 × 배 · 배 None = 사용자 입력."""
|
|
units = [u.strip() for u in str(want or "").split("·") if u.strip()]
|
|
plain = mm.plain_unit(have)
|
|
if len(units) > 1:
|
|
ok = plain in {mm.plain_unit(u) for u in units}
|
|
return {"배": 1 if ok else None, "갈래": "두 단위" if ok else "입력"}
|
|
if plain.replace("/", "") == mm.plain_unit(want).replace("/", ""): # 공/㎥ ≡ 공㎥
|
|
return {"배": 1, "갈래": "같음"}
|
|
if str(want or "").strip() == "자재": # 결과단위 「원/자재 단위」 = 줄 단위 그대로
|
|
return {"배": 1, "갈래": "자재 단위"}
|
|
a, b = mp.kind_of(have), mp.kind_of(want)
|
|
if a is not None and a == b:
|
|
scale = mp.UNITS[a]
|
|
ratio = Decimal(scale[plain]) / Decimal(scale[mm.plain_unit(want)])
|
|
return {"배": hm._json(ratio), "갈래": "단위표"}
|
|
found = hm.numbers(spec)
|
|
width, thick = hm._meters(found.get("B")), hm._meters(found.get("T"))
|
|
by = {("넓이", "부피"): thick, ("길이", "넓이"): width}
|
|
if (a, b) == ("길이", "부피") and width is not None and thick is not None:
|
|
by[(a, b)] = width * thick
|
|
got = by.get((a, b))
|
|
if got:
|
|
return {"배": hm._json(got), "갈래": "추정"}
|
|
return {"배": None, "갈래": "입력"}
|
|
|
|
|
|
def quantity(line: dict[str, Any], want: Any, share: Any = None) -> dict[str, Any]:
|
|
"""단가산출 줄 수량 `{엑셀, 몫, 배, 갈래, 우리}` — 우리 = 엑셀 × 몫 × 배(배 None = 입력)."""
|
|
got = factor(line.get("단위"), want, line.get("규격"))
|
|
qty = line.get("수량")
|
|
ours = None
|
|
if qty is not None and got["배"] is not None:
|
|
ours = _plain(Decimal(str(qty)) * Decimal(str(share or 1)) * Decimal(str(got["배"])))
|
|
return {"엑셀": qty, "몫": share, **got, "우리": ours}
|