"""연결 찾기 값 기준 넓힘(PLAN 812-11 · 요구 714 · 733 · 734 · 735) — `M02_Doc_Match` 가 부름. 값(참고 · 확정을 안 막음 · 바꾸면 우리 값): 같은 기종 규격만 다름(굴착기 0.7㎥ ↔ 1.0㎥ — 기종 = 기계표 이름 머리 + 괄호 낱말 · 무한궤도 대 타이어 = 다른 기종 · 조합 기계는 구성 줄마다) = 「규격」(바꾸면 가져온 규격 — [바꾸기] 때 그 줄 기계를 갈아 끼움 `바꿈`) · 수량 차이(같은 자원 줄) = 「수량 끝자리」(1 % 안) · 「품 숫자」(가져온 수량 = 수 하나) · 「수량」(조건 값 · 비율 · 표 값). 로직(다른 항): 자원 줄 구성(갈래 · 기종 · 노임 직종 · 재료 종류 · 비목) · 줄 없음 · 율 구조 · 단위 환산 = 같은 배(× 6 등)가 두 줄 넘게 같이 걸림(결과 단위 기준이 다름) — 한 줄만 배가 맞는 것은 수량 값. 기계 작업(`work`) = 가져온 문서가 기계 한 묶음(한 기계 줄 · 조합 기계면 그 구성 줄들)뿐 · 수량이 작업량 식(Q) = 우리 기초에 안 붙임(요구 728 · 735-4 — 수량 · 규격까지 같은 우리 기초가 있을 때만 확정) · 공식 꼴이 그 기종 「작업량 틀」(813-2 부품)과 같으면 참고에 틀 이름. """ from __future__ import annotations import re from decimal import Decimal from functools import lru_cache from typing import Any from M02_MasterTemplete import M02_Machine_Find as mfind WORK = "기계 작업" SPEC, QTY, TAIL, POOM = "규격", "수량", "수량 끝자리", "품 숫자" VALUE = (SPEC, QTY, TAIL, POOM) # 확정 순위 — 값 참고가 적은 조합 · 기초 먼저 CHECK = (*VALUE, "옵션 확인") # 기계 작업 꼴이 확정으로 붙지 않는 참고 _NUMBER = re.compile(r"^\s*\d+(?:\.\d+)?\s*$") # ── 기종 ────────────────────────────────────────────────────────────── def row_of(code: Any) -> Any: """원문번호 → 기계표 줄(`M02_Machine_Find.Row`) · 없음 None.""" key = mfind.by_code(code) return mfind.table()[2].get(key) if key else None def same_kind(a: Any, b: Any) -> bool: """두 원문번호 = 같은 기종 — 이름 머리 + 괄호 낱말 같음(규격만 다름).""" x, y = row_of(a), row_of(b) return bool(x and y) and x.base == y.base and x.words == y.words @lru_cache(maxsize=4096) def _combo(name: str) -> tuple | None: """조합 기계 이름 → 구성 기종 묶음(정렬) · 낱 기계 = None.""" got = mfind.parts(name) return tuple(sorted(mfind.split_name(n)[0] for n, _ in got)) if len(got) > 1 else None def mark_combo(doc: dict, facts: list[dict]) -> None: """기계 사실에 `조합`(구성 묶음 · 낱 = None) · `갈라짐`(가져온 조합을 둘로 가른 줄 = 낱 기계로도 맞음).""" head = _machines(doc) for f in facts: if f["갈래"] in ("기계", "산출"): f["조합"] = _combo(f["이름"]) f["갈라짐"] = any(head.get(str(f.get("기호")), {}).get(k) for k in ("조합", "조합첫")) def same_combo(a: dict, b: dict) -> bool: """조합 여부 같음 — 구성 묶음 같음 · 가른 줄 ↔ 낱 기계도 같음(「굴착기+부착용 집게」 ↔ 굴착기 하나 = 다름).""" x, y = a.get("조합"), b.get("조합") return x == y or (a.get("갈라짐") and y is None) or (b.get("갈라짐") and x is None) def same_machine(a: dict, b: dict) -> bool: """같은 기종 기계 줄 — 원문번호 기종 같음 + 조합 구성 같음.""" return same_kind(a["코드"], b["코드"]) and same_combo(a, b) def _eq(code: Any) -> str: r = row_of(code) return f"{r.raw.get('이름')} {r.raw.get('규격') or ''}".strip() if r else str(code) def spec_note(a: dict, b: dict) -> dict: """같은 기종 규격 다름 → 참고 「규격」 + 바꿈(우리 줄 단가 기호 → 가져온 원문번호).""" ra = row_of(a["코드"]) say = f"규격 다름 {_eq(a['코드'])} ↔ {(row_of(b['코드']).raw.get('규격') or '')}" swap = {"기호": b.get("기호"), "기계": a["코드"], "이름": ra.raw.get("이름"), "규격": ra.raw.get("규격") or ""} # fmt: skip return {"항": SPEC, "글": f"{say} — 바꾸면 가져온 규격", "바꿈": swap} def loose_note(a: dict, b: dict) -> dict | None: """못 이은 가져온 기계(원파일 묶음 값) ↔ 우리 기계 — 규격 글이 다르면 「안 바꿈」 참고.""" loose, ours = (a, b) if a["갈래"] == "산출" else (b, a) row = row_of(ours["코드"]) if not row or not loose.get("규격"): return None if mfind.spec_fit(mfind.tokens(loose["규격"]), row.spec) == "같음": return None say = f"규격 다름 {loose['이름']} {loose['규격']} ↔ {_eq(ours['코드'])}" return { "항": SPEC, "글": f"{say} — 가져온 기계를 우리 표에서 못 찾음 · 바꾸면 우리 기계 그대로", } # ── 수량 ────────────────────────────────────────────────────────────── def _show(v: Any) -> str: return f"{v:.4g}" if isinstance(v, Decimal) else str(v) def qty_note(a: dict, b: dict) -> dict: """같은 자원 줄 수량 다름 → 참고(끝자리 · 품 숫자 · 수량).""" x, y = a["수량"], b["수량"] if isinstance(x, Decimal) and isinstance(y, Decimal) and y and abs(x - y) <= abs(y) / 100: what = TAIL elif _NUMBER.match(str(a.get("수량글") or "")): what = POOM else: what = QTY return {"항": what, "글": f"{a['이름']} 수량 다름 {_show(x)} ↔ {_show(y)} — 바꾸면 우리 값"} def pairs( take: Any, core: Any, cls: Any, pair: Any, ratio: Any ) -> tuple[list[dict], list[dict], int]: """수량 다른 같은 자원 짝(로직 같음 · `pair` 다른 항 0) → (다른 항 = 단위 환산 무리, 참고, 같은 줄 수) · `ratio` = 단위 환산 배 글(「× 6」) · 없음 None.""" got = take( lambda a, b: core(a, b) and cls(a, b) and a["수량"] != b["수량"] and not pair(a, b)[0] ) hows = [ratio(a["수량"], b["수량"]) for a, b in got] many = { h for h in hows if h and hows.count(h) >= 2 } # 같은 배가 두 줄 넘게 = 결과 단위 기준 다름 diffs: list[dict] = [] notes: list[dict] = [] for (a, b), how in zip(got, hows): if how in many: diffs.append( {"항": "단위 환산", "글": f"{a['이름']} 수량 {a['수량']} ↔ {b['수량']} {how}"} ) else: notes.append(qty_note(a, b)) notes += pair(a, b)[1] return diffs, notes, len(got) - sum(1 for h in hows if h in many) # ── 기계 작업 ───────────────────────────────────────────────────────── def _machines(doc: dict) -> dict[str, dict]: head = (doc.get("가져온") or {}).get("기계") return {str(x.get("기호")): x for x in head or [] if isinstance(x, dict)} def _one_group(rows: list[dict]) -> bool: if len(rows) == 1: return True return sum(1 for x in rows if x.get("조합첫")) == 1 and all( x.get("조합") or x.get("조합첫") for x in rows ) def _form(text: Any) -> str: """작업량 식 꼴 — 끝수 함수 벗김 · 빈칸 뺌 · 작은 글자(Cm = cm).""" t = re.sub(r"\s", "", str(text or "")) m = re.match(r"^(?:반올림|버림|올림)\((.*),\d+\)$", t) return (m.group(1) if m else t).lower() def _q_form(doc: dict, names: set[str]) -> str: for line in doc.get("식") or []: if str(line.get("기호")) in names and str(line.get("기호")).lower().startswith("q"): return _form(line.get("식")) return "" def work(doc: dict, mine: list[dict], names: Any) -> dict | None: """가져온 문서가 기계 한 묶음 + 작업량 식 수량이면 {판정 기계 작업, 참고} · 아니면 None (`names` = 식 글 → 쓰는 이름 · 인력 · 재료 · 율 줄이 섞이면 보통 길).""" heads = _machines(doc) if not mine or not heads or any(f["갈래"] == "율" for f in mine): return None rows = [f for f in mine if not f["합밖"]] if not rows or any( str(f.get("기호")) not in heads or f["갈래"] not in ("기계", "산출") for f in rows ): return None if not _one_group([heads[str(f["기호"])] for f in rows]): return None used = [names(f.get("수량글")) for f in rows] if not all(used): # 수량이 수뿐 = 작업량 식 아님 return None notes = [] form = _q_form(doc, set().union(*used)) from M01_MasterData import M01_MasterData_Parts as parts # M02 가 M01 을 부름 — 늦게 for f in rows: if not form or not row_of(f["코드"]): continue got = parts.of_machine(str(f["코드"])) for t in got["틀"]: if any( _form(x.get("식")) == form for x in t["문서"].get("식") or [] if x.get("기호") == "Q" ): say = f"{got['기계']['이름']} — 작업량 공식 꼴 = {t['키']} {t['이름']}" notes.append({"항": "작업량 틀", "글": say}) break return {"판정": WORK, "참고": notes, "후보수": 0} # ── 옵션 확인 ───────────────────────────────────────────────────────── def option_check(doc: dict, pick: dict, same: list[dict], qty_vars: set[str]) -> list[dict]: """값이 정확히 맞는 조합이 없고 수량을 정하는 선택이 값으로 안 갈림(같은 순위 조합에 값 여럿) = 고른 값(기본 먼저) + 참고 「옵션 확인」.""" from M02_MasterTemplete.M02_Doc_Copy import md out = [] for v in doc.get("변수") or []: vid, link = md.var_id(v), v.get("연결") or {} if link.get("갈래") != "선택" or vid not in qty_vars: continue if len({str(p.get(vid)) for p in same}) > 1: how = "기본값" if pick.get(vid) == link.get("값") else "고른 값" say = f"옵션 확인 — {v.get('이름') or vid} = {pick.get(vid)}({how} · 값으로 못 가름)" out.append({"항": "옵션 확인", "글": say}) return out