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

This commit is contained in:
2026-11-15 16:20:15 +09:00
8 changed files with 157 additions and 12 deletions
+17
View File
@@ -257,9 +257,26 @@ def _link_of(item: dict) -> tuple[dict | None, str]:
link = {"갈래": "요소", "키": key}
if key[:2] == dc.cv.mm.LINKED: # 품셈재료 = 줄 단위로 값이 갈림(M01 변환과 같음)
link["단위"] = str(item.get("단위") or "")
if key[:2] == dc.cv.mm.MARKET and (unit := str(item.get("단위") or "")):
link["단위"] = unit # 840-3 자재품목 = 셈이 줄 단위로 환산(`master_pick.priced`)
return link, _unit_note(key, unit)
return link, ""
def _unit_note(key: str, unit: str) -> str:
"""840-3 자재 단위 ↔ 줄 단위 글 — 같음 · 1 : 1 = 빈 글 · 같은 성격 = 환산 · 성격 다름 = 경고(사람이 고른 줄도)."""
mu = dc.cv.mm.mu
try:
have = str(imp._whole().get(key).get("단위") or "")
except Exception: # noqa: BLE001 — 없는 키 = 셈이 끊김 표지
return ""
if not have or mu.factor(have, unit) == 1:
return ""
if mu.same_kind(have, unit):
return f"단위 환산 {have} → {unit}"
return f"단위 다름 {unit} ↔ {have} — 값은 자재 단위 그대로"
OPTION = "_옵션" # 연결이 부르는 옵션 변수(문서에 없으면 `_attach` 가 더함)
+9 -5
View File
@@ -77,7 +77,7 @@ 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)):
if kind in ("인력", "재료") and (named := _alias(kind, name, spec, unit)):
return named # 815-1 K2 — M01 줄 「별칭」 칸 먼저
if kind == "인력":
trade = est.trade(name)
@@ -130,17 +130,21 @@ def _found(line: dict[str, Any]) -> list[dict[str, Any]]:
return out
def _alias(kind: str, name: str, spec: str) -> list[dict[str, Any]]:
def _alias(kind: str, name: str, spec: str, unit: str = "") -> list[dict[str, Any]]:
"""M01 별칭 칸 이음(`M02_Elem_Alias`) — 인력 = 이름 · 재료 = 이름 + 규격 › 표기 정규화 정확 일치 ·
같은 품목 지역 줄은 하나."""
같은 품목 지역 줄은 하나(840-3 줄 단위 넘김 = 같은 단위 줄 먼저)."""
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
keys = [k] if (k := ea.one({"이름": name, "규격": spec, "단위": unit}, keys)) else keys
if kind == "재료" and not keys: # 815-1 K3 — 표기 정규화 정확 일치(바탕 이름 길보다 먼저)
keys = [k] if (k := ea.one({"이름": name, "규격": spec}, ea.exact(name, spec))) else []
keys = (
[k]
if (k := ea.one({"이름": name, "규격": spec, "단위": unit}, 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]
+4 -1
View File
@@ -44,7 +44,7 @@ def m01(kind: str, src: dict, label: str = "") -> tuple[str | None, str | None]:
return got["키"], mfind.mark(got, line["이름"], line["규격"])
if kind == "재료" and src.get("종류") == "자재" and (hit := ea.est_code(src.get("코드"))):
key = ea.one(line, hit) or hit[0] # 815-1 K1 — EST 원파일 자재 = 우리 자재 출처 코드
return mf.done(key, "코드", src)["키"], None
return mf.done(key, "코드", src, line["단위"])["키"], None # 840-3 단위 맞춤
oil = ea.keys("MO", line["이름"], line["규격"]) if kind == "재료" else []
if kind == "재료":
if oil or elem.oil_rows(line["이름"]):
@@ -183,6 +183,9 @@ def why(item: dict) -> str:
return mp.apart_note(res.get("재료찾기") or {})
if elem.oil_rows(name):
return "유가 후보 여럿 — 고름"
found = res.get("재료찾기") or {}
if found.get("판정") == "단위 다름 — 고르기": # 840-3 `M02_San_MatFind.UNIT`
return f"{found.get('방법')} — 고름"
line = {"이름": name, "규격": res.get("규격") or "", "단위": res.get("단위") or ""}
if picks := hrow.m01_picks("재료", line): # 817-1 ㉯ 1 순위를 글에(고르기 창은 이름 찾기)
return f"자재 후보 여럿 — 고름 · 1 순위 {picks[0]['이름']} {picks[0]['규격']}".strip()
+34 -3
View File
@@ -9,6 +9,9 @@
- 「규격 없음 — 고르기」 — 가져온 규격 빔 · 별도계상 · 바탕 이름 같은 후보 있음
- 「후보 여럿」 — 규격은 있는데 하나로 못 정함
- 「우리 표에 없음」 — 바탕 이름 같은 후보 없음 · 규격 수가 하나도 안 닮음(후보 = 닮은 줄 · 참고)
- 「단위 다름 — 고르기」 — 이은 우리 줄 단위가 줄 단위와 성격 다름(840-3 · 방법 = 「단위 다름 ㎡ ↔ 매」 · 후보 첫째 = 그 줄)
단위 맞춤(840-3 `_by_unit` · 이은 판정 모두) = ㉠ 이름 · 규격 같은 쌍둥이 중 줄과 같은 단위 줄 › ㉡ 같은 성격(kg ↔ 톤 등)
= 그대로 이음 · 값은 셈이 줄 단위로 환산(`master_pick.priced`) · ㉢ 성격 다름 = 이음 풂.
후보 = [{키, 이름, 규격, 단위, 단서?}] 차례 = 단서로 좁힌 것 › 이름 같은 묶음 › 옛 후보.
별도계상 줄(요구 811-2 — 품목은 정해짐 · 셈만 프로젝트에서) = 같은 찾기 · 키는 판정에만(줄 이음 · 단가 0 그대로).
@@ -35,6 +38,7 @@ from M02_MasterTemplete import M02_Hopyo_Material as hmat
from M02_MasterTemplete import M02_Hopyo_MatSame as ms
LINKED, NO_SPEC, MANY, NONE = "이어짐", "규격 없음 — 고르기", "후보 여럿", "우리 표에 없음"
UNIT = "단위 다름 — 고르기"
TOP = 8
_CTX: ContextVar[dict] = ContextVar("재료단서", default={})
_LEAD = re.compile(r"^[\s\-·\d.)]+")
@@ -126,16 +130,16 @@ def find(
code = str((src or {}).get("코드") or "")
me = ms._item(text[0], text[1])
keys = tuple(sorted(k for k in keys if k))
got = dict(_find(*text, keys, _texts(me, code, label), apart, id(hm.targets())))
got = _by_unit(line, dict(_find(*text, keys, _texts(me, code, label), apart, id(hm.targets()))))
got |= {"후보": [dict(x) for x in got["후보"]], "단서": list(got["단서"])}
if src is not None:
src["재료찾기"] = got
return got
def done(key: str, how: str, src: dict | None = None) -> dict:
def done(key: str, how: str, src: dict | None = None, unit: str = "") -> dict:
"""이미 정한 이음(코드 길) 판정."""
got = {"판정": LINKED, "방법": how, "키": key, "후보": [], "단서": []}
got = _by_unit({"단위": unit}, {"판정": LINKED, "방법": how, "키": key, "후보": [], "단서": []})
if src is not None:
src["재료찾기"] = got
return got
@@ -169,6 +173,33 @@ def _find(
return _out(verdict, "", None, cands, used)
def _by_unit(line: dict, got: dict) -> dict:
"""이은 판정 단위 맞춤(840-3) — ㉠ 같은 단위 쌍둥이 › ㉡ 같은 성격 = 그대로 › ㉢ 성격 다름 = 고르기."""
from M02_MasterTemplete import M02_Elem_Alias as ea
key, want = got.get("키"), str(line.get("단위") or "")
if got["판정"] != LINKED or not key or not want:
return got
row = ea.row(ea.MARKET, key)
have, plain = str(row.get("단위") or ""), hm.mm.plain_unit
if not have or plain(have) == plain(want):
return got
twins = [k for k in ea.exact(row.get("이름"), row.get("규격")) if plain(ea.row(ea.MARKET, k).get("단위")) == plain(want)] # fmt: skip
if twin := ea.one({"이름": line.get("이름") or row.get("이름"), "규격": line.get("규격") or row.get("규격"), "단위": want}, twins): # fmt: skip
return {**got, "키": twin, "방법": f"{got['방법']} · 같은 단위 줄"}
if hm.mp.same_kind(have, want):
return got # 값 = 셈이 줄 단위로 환산 · 표시도 셈(`단위경고`)
first = dict(_pub({**row, "자재": key}, ""))
rest = [dict(c) for c in got["후보"] if dict(c).get("키") != key]
return {
**got,
"판정": UNIT,
"방법": f"단위 다름 {want} ↔ {have}",
"키": None,
"후보": [first, *rest],
}
def _alike(me: dict, x: dict) -> bool:
"""이름 같은 우리 줄이 이 물건일 수 있음 — 우리 규격 빔 · 가져온 수 없음 · 수 하나라도 같음(BUTTON BIT Ø38 ↔ 105mm = 아님)."""
return (
+1
View File
@@ -31,6 +31,7 @@ DIRNAME = "재료고름"
CENSUS = "재료판"
KINDS = (
"규격 없음 — 고르기",
"단위 다름 — 고르기", # 840-3
"후보 여럿",
"우리 표에 없음",
"이어짐",
+16 -2
View File
@@ -12,7 +12,7 @@ from contextvars import ContextVar
from decimal import Decimal
import master_material as mm
from master_unit import UNITS, convert, kind_of, same_kind # noqa: F401 — 단위 한 벌(712-2)
from master_unit import UNITS, convert, factor, kind_of, same_kind # noqa: F401 — 단위 한 벌(712-2)
REGIONS = (
"전국평균 전국 서울 경기 강원 충북 충남 전북 전남 경북 경남 부산 대구 인천 대전 울산 세종 제주 광주"
@@ -163,7 +163,9 @@ def _priced(master, item: dict, ref, env: dict, index: int | None):
price = convert(price, have, unit) if have and unit else price
else:
price, source = mm.element(master, ref, env, unit)
warn = None
warn = _direct(master, ref, unit)
if warn and warn.startswith(SWAP): # 840-3 자재품목 바로 이음 = 줄 단위로 환산
price = convert(price, master.get(ref).get("단위"), unit)
except mm.mf.FormulaError as e:
return None, None, str(e), None
why = None if price is not None else f"「{master.label(ref)}」 값 없음 — 관리자가 채울 값"
@@ -171,6 +173,18 @@ def _priced(master, item: dict, ref, env: dict, index: int | None):
HAND = "손 단가"
SWAP = "단위 환산"
def _direct(master, ref, unit) -> str | None:
"""자재품목 바로 이음 줄(불러오기 · 840-3) 단위 글 — 같음 · 1 : 1 = None · 같은 성격 = 「단위 환산 톤 → kg」 ·
성격 다름 = 검색어 길과 같은 경고(값 그대로)."""
if not unit or not isinstance(ref, str) or ref[:2] != mm.MARKET:
return None
have = master.get(ref).get("단위")
if not have or factor(have, unit) == 1:
return None
return f"{SWAP} {have} → {unit}" if same_kind(have, unit) else f"줄 {unit} · 자재 {have}"
def bundled(item: dict, base):
+2 -1
View File
@@ -83,7 +83,8 @@ def test_pick_whole_file(world) -> None:
conv = dim.derive(doc, None, imp._whole())
e = _entry(conv, WIRE)
assert e["고름"]["키"] == pick["키"] and e["처음"]["갈래"] == "숫자"
assert _var(conv, e["기호"])["연결"] == {"갈래": "요소", "키": pick["키"]} # 우리 값
got = _var(conv, e["기호"])["연결"] # 우리 값(엑셀 기억 없음) · 840-3 줄 단위 실음
assert got == {"갈래": "요소", "키": pick["키"], "단위": got["단위"]}
assert sorted(mp.made(me, fp)) == sorted(made)
assert mp.uses(fp, WIRE, (made[0],)) == [made[1]]
# 다른 파일 = 영향 0
@@ -0,0 +1,74 @@
"""840-3 불러오기 재료 단위 — ㉠ 같은 이름 · 규격 쌍둥이 중 경쟁사 줄과 같은 단위 줄 먼저 · ㉡ 같은 성격
단위는 이은 뒤 셈이 줄 단위로 환산(표시 = `단위경고` 「단위 환산 톤 → kg」) · ㉢ 성격 다르면 잇지 않고 고르기(요구 860 · 862).
판정 = `M02_San_MatFind`(`M02_San_Link.m01`) · 값 = `master_pick.priced`(자재품목 바로 이음) · 문서 연결 = `M02_Doc_Import._link_of`.
"""
import sys
from decimal import Decimal
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path[:0] = [str(ROOT), str(ROOT / "resources" / "master_data" / "scripts")]
import master_material as mm # noqa: E402
import master_pick as mp # noqa: E402
from M01_MasterData import M01_MasterData_Store as m01 # noqa: E402
from M02_MasterTemplete import M02_Doc_Import as dim # noqa: E402
from M02_MasterTemplete import M02_San_Link as sl # noqa: E402
from M02_MasterTemplete import M02_San_MatFind as mf # noqa: E402
def _go(name, spec, unit, code="", kind=""):
src = {"이름": name, "규격": spec, "단위": unit, "갈래": "재료", "코드": code, "종류": kind}
key, _mark = sl.m01("재료", src)
return key, src["재료찾기"]
def test_twin_same_unit_first():
"""잔디 ㎡ 줄 — 매 줄(MT006796)이 아니라 ㎡ 쌍둥이(MT006799)."""
key, found = _go("천연잔디(한국형)", "0.18×0.18×0.01", "㎡")
assert key == "MT006799" and found["판정"] == mf.LINKED
key, _ = _go("천연잔디(한국형)", "0.18×0.18×0.01", "매")
assert key == "MT006796"
def test_unit_kind_differs_goes_to_pick():
"""골재시험 회 ↔ 자재 kg · 각재 ㎥ ↔ 재 = 이음 없음 · 판정 고르기 · 까닭 글 · 후보에 그 줄."""
for name, spec, unit, was in (
("골재시험", "0.08mm체 통과량시험", "회", "MT009890"),
("각재(외송)", "3.6×3.0×3.0", "㎥", "MT000393"),
):
key, found = _go(name, spec, unit)
assert key is None and found["판정"] == mf.UNIT
assert found["방법"].startswith(f"단위 다름 {unit} ↔ ")
assert was in {c["키"] for c in found["후보"]}
def test_same_meaning_units_stay_linked():
"""본 ↔ EA · 개소 ↔ 개(개수 1 : 1) = 같은 단위로 봄 — 이음 그대로 · 표시 없음."""
key, found = _go("시스템동바리(수직재)", "D60.5×2.6T P1725", "본")
assert key == "MT000512" and found["판정"] == mf.LINKED and "단위" not in found["방법"]
def test_ton_to_kg_price():
"""자재 Ton 줄을 kg 줄에 이음 → 단가 ÷ 1,000 · 표시 「단위 환산 Ton → kg」 · 성격 다름 = 값 그대로 + 경고."""
whole = m01.loaded()[1]
base, _ = mm._lowest(whole, "MT000811")
price, _src, _why, extra = mp.priced(whole, {"단위": "kg"}, "MT000811", {}, None)
assert price == base * Decimal(1000) / Decimal(1000000)
assert extra["단위경고"] == "단위 환산 Ton → kg"
price, *_rest, extra = mp.priced(whole, {"단위": "㎡"}, "MT000811", {}, None)
assert price == base and extra["단위경고"] == "줄 ㎡ · 자재 Ton"
price, *_rest, extra = mp.priced(whole, {"단위": "ton"}, "MT000811", {}, None)
assert price == base and "단위경고" not in extra
def test_doc_link_carries_unit():
"""문서 연결(호표 길) — 자재 이음에 줄 단위 · 사람이 고른 줄도 성격 다르면 설명에 경고."""
link, note = dim._link_of({"이음": "MT000811", "단위": "kg"})
assert link == {"갈래": "요소", "키": "MT000811", "단위": "kg"} and note == "단위 환산 Ton → kg"
link, note = dim._link_of({"이음": "MT000811", "단위": "㎡"})
assert link["단위"] == "㎡" and note == "단위 다름 ㎡ ↔ Ton — 값은 자재 단위 그대로"
assert dim._link_of({"이음": "MT000811", "단위": "Ton"})[1] == ""