- M02_Doc_Match_Note(새) — 율 = 율 줄 셈값 ÷ 대상 줄 금액 합(대상 비목) · 짝 줄 다른 항 대 참고 가르기 - M02_Doc_Match — 같은 줄 = 로직 같은 줄(코드 다름은 짝 대조로) · 연결후보 `참고` 칸 · 참고 적은 조합 · 후보 먼저 - 띠 글 「확정 — 로직 같음 · 참고: 단가 다름 n 줄 · 우리 단가로 셈 ○○ 원」 - test_810_4 기대 고침(단가 차이 · 끝수 = 참고로 옮김) · 전수 도우미 확정 다시 셈 = 우리 금액 · 확정 참고 셈 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
188 lines
7.9 KiB
Python
188 lines
7.9 KiB
Python
"""연결 찾기 — 로직 차이(다른 항) 대 값 차이(참고) 가르기(PLAN 812-2 · 요구 712) — `M02_Doc_Match` 가 부름.
|
|
|
|
확정을 막는 「다른 항」 = 로직 차이만 — 자원 갈래 · 기계 규격 · 노임 종류 · 자재 이름 종류 · 비목 · 산출 단가 ·
|
|
율 구조(있고 없음 · 대상 비목 · 율 = 비율) · 수량(옵션 풀이 뒤 · 원 단위 같아야 함).
|
|
「참고」 = 값 차이 — 인력 · 재료 · 기계 시간당 단가 · 끝수(합만 다름) · 그로 인한 비목 금액 · 계.
|
|
율 = 율 줄 셈값 ÷ 대상 줄 금액 합 — 대상 = 율 식이 (거쳐서) 부르는 금액 줄 · 비목 이름이면 그 비목 줄 ·
|
|
대상 금액 끝수 자리 차이만 지움(유효 6 자리 · 두 바탕 중 짧은 꼴) — 지어 맞추기 아님.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from decimal import Decimal, InvalidOperation
|
|
from typing import Any
|
|
|
|
from M02_MasterTemplete import M02_Doc_Copy as dc
|
|
|
|
md = dc.md
|
|
PRICED = ("노임", "재료", "기계") # 시간당 · 단위 단가 = 값(참고)
|
|
|
|
|
|
# ── 율 구조 ───────────────────────────────────────────────────────────
|
|
def _names(text: Any) -> set[str]:
|
|
if text in (None, ""):
|
|
return set()
|
|
try:
|
|
return set(md.names(md.parse(text)))
|
|
except md.DocError:
|
|
return set()
|
|
|
|
|
|
def _reach(doc: dict) -> dict[str, set[str]]:
|
|
"""기호 → 바로 부르는 이름(변수 연결 · 식 줄 · 금액 줄 수량 · 단가)."""
|
|
direct: dict[str, set[str]] = {}
|
|
for v in doc.get("변수") or []:
|
|
direct[md.var_id(v)] = set(md.link_names(v.get("연결") or {}))
|
|
for line in doc.get("식") or []:
|
|
direct[str(line.get("기호"))] = _names(line.get("식"))
|
|
for line in doc.get("금액") or []:
|
|
if line.get("기호"):
|
|
direct[str(line["기호"])] = _names(line.get("수량")) | _names(line.get("단가"))
|
|
return direct
|
|
|
|
|
|
def _close(start: set[str], direct: dict[str, set[str]]) -> set[str]:
|
|
seen, todo = set(start), list(start)
|
|
while todo:
|
|
for n in direct.get(todo.pop(), ()):
|
|
if n not in seen:
|
|
seen.add(n)
|
|
todo.append(n)
|
|
return seen
|
|
|
|
|
|
def _short(r: Decimal) -> Decimal:
|
|
return Decimal(f"{r:.6g}").normalize()
|
|
|
|
|
|
def rates(doc: dict, got: dict, facts: list[dict]) -> None:
|
|
"""율 사실에 `율`(비율 · 못 셈 None) · `대상`(대상 비목 묶음)을 채움(제자리)."""
|
|
money, rows = doc.get("금액") or [], got.get("줄") or []
|
|
direct = _reach(doc)
|
|
core = [f for f in facts if f["갈래"] != "율" and not f["합밖"]]
|
|
for f in facts:
|
|
if f["갈래"] != "율":
|
|
continue
|
|
line, row = money[f["번"]], rows[f["번"]]
|
|
reach = _close(_names(line.get("수량")) | _names(line.get("단가")), direct)
|
|
costs = reach & set(md.COSTS)
|
|
hits = []
|
|
for c in core:
|
|
mine = money[c["번"]]
|
|
parts = _names(mine.get("수량")) | _names(mine.get("단가"))
|
|
if (
|
|
(mine.get("기호") and str(mine["기호"]) in reach)
|
|
or (parts and parts <= reach)
|
|
or (set(c["비목"]) & costs)
|
|
):
|
|
hits.append(c)
|
|
f["대상"] = frozenset(k for c in hits for k in c["비목"])
|
|
f["율"] = None
|
|
amount = _mul(row.get("수량"), row.get("단가"))
|
|
if amount is None:
|
|
continue
|
|
bases = [
|
|
sum((_num(rows[c["번"]].get("금액")) for c in hits), Decimal(0)),
|
|
sum(
|
|
(_mul(rows[c["번"]].get("수량"), rows[c["번"]].get("단가")) or 0 for c in hits),
|
|
Decimal(0),
|
|
),
|
|
]
|
|
found = [_short(amount / b) for b in bases if b]
|
|
if found:
|
|
f["율"] = min(found, key=lambda r: len(r.as_tuple().digits))
|
|
|
|
|
|
def _num(v: Any) -> Decimal:
|
|
if isinstance(v, dict):
|
|
return sum((_num(v.get(k)) for k in md.COSTS), Decimal(0))
|
|
try:
|
|
return Decimal(str(v)) if v is not None else Decimal(0)
|
|
except InvalidOperation:
|
|
return Decimal(0)
|
|
|
|
|
|
def _mul(q: Any, p: Any) -> Decimal | None:
|
|
if q is None or p is None:
|
|
return None
|
|
return _num(q) * _num(p)
|
|
|
|
|
|
def pct(r: Any) -> str:
|
|
"""비율 → 「9 %」."""
|
|
if not isinstance(r, Decimal):
|
|
return "?"
|
|
return f"{(r * 100).normalize():f} %"
|
|
|
|
|
|
# ── 짝 줄 ─────────────────────────────────────────────────────────────
|
|
def txt(v: Any) -> str:
|
|
"""단가 글 — 수 = 원 단위 쉼표 · 기계 묶음 = 노 · 재 · 경."""
|
|
if isinstance(v, tuple):
|
|
return " · ".join(f"{k[0]} {txt(x)}" for k, x in zip(md.COSTS, v) if x)
|
|
if isinstance(v, Decimal):
|
|
return f"{v.normalize():,f}"
|
|
return str(v)
|
|
|
|
|
|
def _grams(text: str) -> set[str]:
|
|
t = re.sub(r"\s", "", text)
|
|
return {run[i : i + 2] for run in re.findall(r"[가-힣]+", t) for i in range(len(run) - 1)}
|
|
|
|
|
|
def pair(a: dict, b: dict) -> tuple[list[dict], list[dict]]:
|
|
"""수량 같은 짝 줄 → (다른 항, 참고) — 단가만 다르면 참고 · 자원 · 비목이 다르면 다른 항."""
|
|
diffs: list[dict] = []
|
|
if a["갈래"] == b["갈래"] == "기계" and a["코드"] != b["코드"]:
|
|
diffs.append({"항": "기계 규격", "글": f"{a['이름']} {a['코드']} ↔ {b['코드']}"})
|
|
elif a["갈래"] == b["갈래"] == "노임" and a["코드"] != b["코드"]:
|
|
diffs.append({"항": "자원", "글": f"{a['이름']} {a['코드']} ↔ {b['이름']} {b['코드']}"})
|
|
elif a["갈래"] == b["갈래"] == "재료" and not _same_stuff(a, b):
|
|
diffs.append({"항": "자원", "글": f"{a['이름']} ↔ {b['이름']}"})
|
|
if set(a["비목"]) != set(b["비목"]):
|
|
diffs.append(
|
|
{"항": "비목", "글": f"{a['이름']} 비목 {'·'.join(a['비목'])} ↔ {'·'.join(b['비목'])}"}
|
|
)
|
|
if a["단가"] == b["단가"]:
|
|
return diffs, []
|
|
say = f"{a['이름']} 단가 {txt(a['단가'])} ↔ {txt(b['단가'])}"
|
|
if a["갈래"] == b["갈래"] and a["갈래"] in PRICED:
|
|
return diffs, [{"항": "단가 차이", "글": say}]
|
|
return [*diffs, {"항": "산출 단가", "글": say}], []
|
|
|
|
|
|
def _same_stuff(a: dict, b: dict) -> bool:
|
|
"""자재 종류 같음 — 두 코드가 있으면 코드 · 없으면 이름 낱말(한글 두 글자) 겹침."""
|
|
if a["코드"] and b["코드"] and a["코드"] == b["코드"]:
|
|
return True
|
|
return bool(_grams(a["이름"]) & _grams(b["이름"]))
|
|
|
|
|
|
def rate_pairs(left: list[dict], right: list[dict], take: Any) -> list[dict]:
|
|
"""율 줄 짝 — 같은 구조(율 · 대상 · 비목) 먼저 · 그다음 비목만 · 대상만 · 율만 다름 순."""
|
|
diffs: list[dict] = []
|
|
|
|
def is_rate(a: dict, b: dict) -> bool:
|
|
return a["갈래"] == b["갈래"] == "율"
|
|
|
|
def same(a: dict, b: dict, *keys: str) -> bool:
|
|
return is_rate(a, b) and all(
|
|
(set(a[k]) == set(b[k])) if k == "비목" else a.get(k) == b.get(k) for k in keys
|
|
)
|
|
|
|
def name(a: dict, b: dict) -> str:
|
|
return f"{a['이름']} {pct(a.get('율'))} ↔ {b['이름']} {pct(b.get('율'))}"
|
|
|
|
def target(x: dict) -> str:
|
|
return "·".join(sorted(x.get("대상") or ())) or "없음"
|
|
|
|
take(lambda a, b: same(a, b, "율", "대상", "비목")) # 로직 같음
|
|
for a, b in take(lambda a, b: same(a, b, "율", "대상")):
|
|
diffs.append({"항": "비목", "글": f"율 줄 {name(a, b)} 비목 다름"})
|
|
for a, b in take(lambda a, b: same(a, b, "율")):
|
|
diffs.append({"항": "율 대상", "글": f"{name(a, b)} 대상 {target(a)} ↔ {target(b)}"})
|
|
for a, b in take(lambda a, b: same(a, b, "대상") or is_rate(a, b)):
|
|
diffs.append({"항": "율 값", "글": f"{name(a, b)} · 대상 {target(a)} ↔ {target(b)}"})
|
|
return diffs
|