feat(812-11): 연결 찾기 값 기준 넓힘 — 같은 기종 규격 · 수량 = 참고 · 바꾸기 때 기계 줄 갈아 끼움 · 기계 작업 갈래
- 새 M02_Doc_Match_Value — 같은 기종(이름 머리 + 괄호 낱말 · 조합 구성) 규격만 다름 = 참고 「규격」 + 바꿈 · 같은 자원 수량 다름 = 참고 「수량 끝자리」 · 「품 숫자」 · 「수량」 · 같은 배 두 줄 넘음 = 단위 환산(로직) · 기계 한 묶음 + 작업량 식 = 「기계 작업」(수량 · 규격까지 같은 기초만 확정) · 작업량 틀 꼴 참고 · 옵션 값으로 못 가름 = 「옵션 확인」 - M02_Doc_Match — 사실에 단가 기호 · 규격 · 수량 글 · 조합 표지 · 값 참고 적은 조합 먼저 · 선택 글 근거 쉼표 무시(D1,800 = D1800) · 같은 자원 아닌 수량 짝 = 그 로직 까닭 - M02_Doc_MatchSk — 바꾸기 뒤 사본 문서 기계 입력(원문번호) · 줄 이름 · 규격 · 묶음 제목 = 가져온 규격(수식 길 저장) · 되돌리기 = 보관 몸 수식 사본 그대로 - 띠 — 기계 작업 「기계 한 줄 단가산출 — 그대로 씀 · 값은 [단가 갱신]으로 우리 값」 · 참고 요약에 규격 · 수량 · 옵션 확인 - helper_810_4_sweep — 과녁 = 부품 뺀 문서(화면과 같음) - 시험 고침: 810_4 단위 환산 = 같은 배 두 줄(한 줄만 배 = 수량 값) · 812_2 기계 규격 = 무한궤도 ↔ 타이어(같은 기종 규격 = 값) - 전수 2,220: 확정 184 → 337 · 근접 576 → 848 · 없음 1,460 → 837 · 기계 작업 198 · 확정 다시 셈 다름 0 · 목록 _목록_812-11_연결판정.xlsx Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,10 +7,13 @@
|
||||
② 옵션 풀이 = 그 문서의 선택 · 입력 변수 값을 줄마다 좁힘(줄 수량 · 단가가 의존하는 변수만 돌림 ·
|
||||
값이 가져온 줄 수량 · 단가 묶음에 있거나 0(꺼진 줄)인 값만 남김) → 남은 조합(상한 `CAP_ALL`)을
|
||||
문서 셈 · 입력 변수 후보 = 기본값 · 0 · 가져온 식에 나온 수 · 선택은 가져온 글에 든 값부터.
|
||||
③ 판정 = 줄 대조 = 로직(812-2 · 요구 712 — `M02_Doc_Match_Note`): 다른 항 = 자원 · 기계 규격 · 수량 ·
|
||||
비목 · 율 구조(대상 · 비목) · 줄 없음 · 이름 · 참고 = 단가 · 끝수 · 율 값 %(값 — 확정을 안 막음) —
|
||||
다른 항 0 = 「확정」 · 같은 줄이 하나 넘고 핵심 줄 반 넘게 짝 = 「근접」(다른 항 목록) · 그 밖 「없음」.
|
||||
결과 `{판정, 키, 이름, 옵션, 다른, 참고, 금액 {가져온, 우리}, 후보수}` = 가져온 식 원본 `연결후보`.
|
||||
③ 판정 = 줄 대조 = 로직(812-2 · 요구 712 — `M02_Doc_Match_Note`): 다른 항 = 자원 · 기종 · 단위 환산 ·
|
||||
비목 · 율 구조(대상 · 비목) · 줄 없음 · 이름 · 참고 = 단가 · 끝수 · 율 값 % · 같은 기종 규격 · 수량
|
||||
(값 — 확정을 안 막음 · 812-11 `M02_Doc_Match_Value`) — 다른 항 0 = 「확정」 · 같은 줄이 하나 넘고
|
||||
핵심 줄 반 넘게 짝 = 「근접」(다른 항 목록) · 그 밖 「없음」 · 값이 맞는 조합 없으면 기본 옵션 + 「옵션 확인」.
|
||||
⓪ 기계 한 묶음 + 작업량 식 수량 = 「기계 작업」(우리 기초에 안 붙임 · 요구 728 · 735-4).
|
||||
결과 `{판정, 키, 이름, 옵션, 다른, 참고, 바꿈, 금액 {가져온, 우리}, 후보수}` = 가져온 식 원본 `연결후보` ·
|
||||
`바꿈` = [바꾸기] 때 갈아 끼울 기계 줄 [{기호(우리 단가 기호), 기계(가져온 원문번호), 이름, 규격}].
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -24,6 +27,7 @@ from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Doc_Copy as dc
|
||||
from M02_MasterTemplete import M02_Doc_Match_Note as note
|
||||
from M02_MasterTemplete import M02_Doc_Match_Value as val
|
||||
|
||||
md, mc, ml = dc.md, dc.mc, dc.ml
|
||||
if not hasattr(
|
||||
@@ -133,8 +137,12 @@ def facts(doc: dict, got: dict) -> list[dict]:
|
||||
"비목": {k: _key(v) for k, v in (row.get("비목") or {}).items() if v},
|
||||
"금액": _key(row.get("금액")),
|
||||
"합밖": bool(row.get("합밖")),
|
||||
"기호": _single(line.get("단가")),
|
||||
"규격": str(line.get("규격") or ""),
|
||||
"수량글": str(line.get("수량") or ""),
|
||||
}
|
||||
)
|
||||
val.mark_combo(doc, out)
|
||||
note.rates(doc, got, out)
|
||||
return out
|
||||
|
||||
@@ -284,7 +292,11 @@ def _words(doc: dict) -> str:
|
||||
bits += [x.get("규격") for x in doc.get("금액") or []]
|
||||
for v in doc.get("변수") or []:
|
||||
bits += [str(x) for x in ((v.get("연결") or {}).get("입력") or {}).values()]
|
||||
return " ".join(str(b or "") for b in bits).replace(" ", "")
|
||||
return " ".join(str(b or "") for b in bits).replace(" ", "").replace(",", "")
|
||||
|
||||
|
||||
def _bare(o: Any) -> str:
|
||||
return str(o).replace(" ", "").replace(",", "") # 「D1,800」 = 글 「D1800」
|
||||
|
||||
|
||||
def domains(
|
||||
@@ -300,7 +312,7 @@ def domains(
|
||||
kind, base = link.get("갈래"), link.get("값")
|
||||
if kind == "선택":
|
||||
opts = list(link.get("목록") or [])
|
||||
seen = [o for o in opts if str(o).replace(" ", "") in words and len(str(o)) > 1]
|
||||
seen = [o for o in opts if _bare(o) in words and len(str(o)) > 1]
|
||||
seen.sort(key=lambda o: -len(str(o)))
|
||||
if seen:
|
||||
ev[md.var_id(v)] = set(seen)
|
||||
@@ -437,6 +449,7 @@ def compare(mine: list[dict], ours: list[dict]) -> dict[str, Any]:
|
||||
and a["단가"] == b["단가"]
|
||||
and a["비목"] == b["비목"]
|
||||
and (a["코드"] == b["코드"] or not (a["코드"] and b["코드"])) # 코드 다름 = 짝 대조
|
||||
and val.same_combo(a, b)
|
||||
)
|
||||
)
|
||||
same += len(exact)
|
||||
@@ -452,11 +465,14 @@ def compare(mine: list[dict], ours: list[dict]) -> dict[str, Any]:
|
||||
same, paired = (same + 1, paired) if not bad else (same, paired + 1)
|
||||
diffs += bad
|
||||
notes += ref
|
||||
bad, ref, n = val.pairs(take, core, cls, note.pair, _ratio) # 같은 자원 수량 다름 = 값(812-11)
|
||||
same, paired, diffs, notes = same + n, paired + len(bad), diffs + bad, notes + ref
|
||||
for a, b in take(lambda a, b: core(a, b) and cls(a, b) and a["단가"] == b["단가"]):
|
||||
paired += 1
|
||||
how = _ratio(a["수량"], b["수량"])
|
||||
what = "단위 환산" if how else "수량"
|
||||
diffs.append({"항": what, "글": f"{a['이름']} 수량 {a['수량']} ↔ {b['수량']} {how or ''}"})
|
||||
say = {"항": what, "글": f"{a['이름']} 수량 {a['수량']} ↔ {b['수량']} {how or ''}"}
|
||||
diffs += note.pair(a, b)[0] or [say] # 같은 자원 아님 = 그 로직 까닭(비목 · 자원)
|
||||
for a, b in take(lambda a, b: core(a, b) and a["갈래"] == b["갈래"] == "기계"):
|
||||
paired += 1
|
||||
diffs.append(
|
||||
@@ -473,7 +489,15 @@ def compare(mine: list[dict], ours: list[dict]) -> dict[str, Any]:
|
||||
what = "줄 없음"
|
||||
diffs.append({"항": what, "글": f"{side} — {x['이름']}"})
|
||||
n_core = sum(1 for a in mine if a["갈래"] != "율")
|
||||
return {"같음": same, "짝": same + paired, "핵심": n_core, "다른": diffs, "참고": notes}
|
||||
swaps = [x.pop("바꿈") for x in notes if "바꿈" in x]
|
||||
return {
|
||||
"같음": same,
|
||||
"짝": same + paired,
|
||||
"핵심": n_core,
|
||||
"다른": diffs,
|
||||
"참고": notes,
|
||||
"바꿈": swaps,
|
||||
}
|
||||
|
||||
|
||||
def _totals(got: dict) -> dict:
|
||||
@@ -542,6 +566,7 @@ def try_doc(
|
||||
dom = narrow(base, links, dom, want, ev)
|
||||
named = [] if same_name(imported, base) else [_unnamed(imported, base)]
|
||||
best: dict | None = None
|
||||
ties: list[tuple] = []
|
||||
for pick in combos(base, dom) or [{}]:
|
||||
got = _calc(base, links, _given(base, pick))
|
||||
if got is None:
|
||||
@@ -549,9 +574,11 @@ def try_doc(
|
||||
off = [_off(base, k, pick[k], ev[k]) for k in ev if k in pick and pick[k] not in ev[k]]
|
||||
off += named
|
||||
res = verdict(mine, facts(base, got), got_imp, got, off)
|
||||
values = sum(1 for x in res["참고"] if x["항"] in val.VALUE) if res["판정"] == SURE else 0
|
||||
rank = (
|
||||
res["판정"] == SURE,
|
||||
-len(off),
|
||||
-values, # 값 참고(규격 · 수량)가 적은 조합 먼저(812-11)
|
||||
res["같음"],
|
||||
res["짝"],
|
||||
-len(res["다른"]),
|
||||
@@ -559,18 +586,28 @@ def try_doc(
|
||||
)
|
||||
if best is None or rank > best["순위"]:
|
||||
best = {**res, "순위": rank, "옵션": pick, "금액": _totals(got)}
|
||||
ties.append((rank, pick))
|
||||
if res["판정"] == SURE and not res["참고"]:
|
||||
break
|
||||
if best and best["판정"] == SURE and any(x["항"] in val.VALUE[1:] for x in best["참고"]):
|
||||
same = [p for r, p in ties if r == best["순위"]] # 값으로 못 가른 조합(요구 733 ㉢)
|
||||
best["참고"] += val.option_check(base, best["옵션"], same, _qty_vars(base))
|
||||
return best or {
|
||||
"판정": NONE,
|
||||
"같음": 0,
|
||||
"짝": 0,
|
||||
"다른": [],
|
||||
"참고": [],
|
||||
"순위": (False, -9, 0, 0, 0, 0),
|
||||
"순위": (False, -9, 0, 0, 0, 0, 0),
|
||||
}
|
||||
|
||||
|
||||
def _qty_vars(doc: dict) -> set[str]:
|
||||
"""금액 줄 수량이 (거쳐서) 부르는 변수."""
|
||||
close = _closure(doc)
|
||||
return set().union(*(_uses(x.get("수량"), close) or set() for x in doc.get("금액") or []))
|
||||
|
||||
|
||||
def _off(doc: dict, vid: str, value: Any, seen: set) -> dict:
|
||||
"""옵션 값이 가져온 글 근거와 다름 — 다른 항 「옵션 글」."""
|
||||
var = next((v for v in doc.get("변수") or [] if md.var_id(v) == vid), {})
|
||||
@@ -585,6 +622,7 @@ def match(imported: dict, links_imp: Any, index: Index, links: Any) -> dict[str,
|
||||
if got_imp is None:
|
||||
return {"판정": NONE, "까닭": "가져온 식을 우리 단가로 못 셈", "후보수": 0}
|
||||
mine = facts(imported, got_imp)
|
||||
alone = val.work(imported, mine, note._names)
|
||||
keys = index.candidates(imported)
|
||||
best, best_key = None, None
|
||||
for key in keys:
|
||||
@@ -593,6 +631,9 @@ def match(imported: dict, links_imp: Any, index: Index, links: Any) -> dict[str,
|
||||
best, best_key = res, key
|
||||
if res["판정"] == SURE and not res["참고"]:
|
||||
break
|
||||
exact = best and best["판정"] == SURE and not {x["항"] for x in best["참고"]} & set(val.CHECK)
|
||||
if alone and not exact: # 기계 작업 — 우리 기초와 수량 · 규격까지 같을 때만 확정으로 붙임
|
||||
return alone
|
||||
if best is None:
|
||||
return {"판정": NONE, "후보수": 0}
|
||||
base = index.docs[best_key]
|
||||
@@ -606,6 +647,7 @@ def match(imported: dict, links_imp: Any, index: Index, links: Any) -> dict[str,
|
||||
"옵션": _shown(base, best["옵션"]),
|
||||
"다른": best["다른"],
|
||||
"참고": best["참고"],
|
||||
"바꿈": best.get("바꿈") or [],
|
||||
"금액": {"가져온": _plain(_totals(got_imp)), "우리": _plain(best["금액"])},
|
||||
"후보수": len(keys),
|
||||
}
|
||||
|
||||
@@ -5,18 +5,24 @@
|
||||
바꾸기(`swap`) = 서버가 다시 찾아 「확정」 일 때만 — 같은 SK 키에 M01 사본 SK(바탕 = 그 기초 ·
|
||||
입력값 = 찾은 옵션 · 802-4a 꼴 · 수식 사본은 저장 때 서버가 찍음)를 씀 · 원래 가져온 식 SK 몸은
|
||||
`가져온보관` 칸에 그대로 둠 · 일위대가 줄 참조 = 키 그대로(SK 내용만 바뀜 → 금액 다시 셈).
|
||||
되돌리기(`back`) = `가져온보관` 몸을 그 키에 다시 씀(가져온 식 금액 그대로).
|
||||
우리 M01 기초는 어느 갈래에서도 안 바뀜(708) · 근접 · 없음 = 바꾸기 409(703).
|
||||
+ 찾은 `바꿈`(같은 기종 규격만 다른 기계 줄 · 812-11 · 요구 733-2) = 사본 문서 그 줄 기계 입력(원문번호)을
|
||||
가져온 규격의 우리 기계로 갈아 끼움(수식 길 저장 = 따로) · 품 · 율 · 단가 · 조건 값은 우리 값 그대로 ·
|
||||
기계가 옵션 기호 · 식이면 안 바꿈.
|
||||
되돌리기(`back`) = `가져온보관` 몸을 그 키에 다시 씀(가져온 식 금액 그대로 · 수식 사본도 보관 몸 그대로).
|
||||
우리 M01 기초는 어느 갈래에서도 안 바뀜(708) · 근접 · 없음 · 기계 작업 = 바꾸기 409(703).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy as cp
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from common_util.common_util_time import utc_now_text
|
||||
from M02_MasterTemplete import M02_Doc_Copy as dc
|
||||
from M02_MasterTemplete import M02_Doc_Import as dim
|
||||
from M02_MasterTemplete import M02_Doc_Match as dm
|
||||
from M02_MasterTemplete import M02_Doc_Sk as dsk
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_Sangeun_Doc as sd
|
||||
from M02_MasterTemplete import M02_Sangeun_Imported as imp
|
||||
@@ -92,7 +98,36 @@ def swap(key: str, version: str) -> dict[str, Any]:
|
||||
if found.get("판정") != dm.SURE:
|
||||
raise _bad(f"우리 기초와 「확정」 일치가 아님({found.get('판정')}) — 바꾸지 않음", 409)
|
||||
info = store.write(sd.KIND, key, version or got["판"], copy_body(sk, found))
|
||||
return {dm.KEY: found, "판": info.get("판")}
|
||||
done = refit(key, found.get("바꿈") or [])
|
||||
return {dm.KEY: found, "판": done[0] or info.get("판"), "바꾼기계": done[1]}
|
||||
|
||||
|
||||
def refit(key: str, swaps: list[dict]) -> tuple[str | None, list[dict]]:
|
||||
"""사본 문서 기계 줄 → 가져온 규격 우리 기계(그 줄 단가 기호 연결의 입력 「기계」 · 줄 이름 · 규격) →
|
||||
수식 길 저장 · (판, 바꾼 것)."""
|
||||
from M02_MasterTemplete import M02_Doc_Sk_Path as path # 수식 길 — 늦게(돌아 부름 피함)
|
||||
|
||||
if not swaps:
|
||||
return None, []
|
||||
got = path.read(key)
|
||||
doc = cp.deepcopy(got["문서"])
|
||||
done = []
|
||||
for one in swaps:
|
||||
var = next((v for v in doc.get("변수") or [] if dm.md.var_id(v) == one["기호"]), None)
|
||||
link = (var or {}).get("연결") or {}
|
||||
given = link.get("입력") or {}
|
||||
if link.get("갈래") != "단가산출" or not re.fullmatch(r"'[^']+'", str(given.get("기계"))):
|
||||
continue
|
||||
link["입력"] = {**given, "기계": f"'{one['기계']}'"}
|
||||
for line in doc.get("금액") or []:
|
||||
if dm._single(line.get("단가")) == one["기호"]:
|
||||
if line.get("묶음") == line.get("이름"): # 그 줄 이름을 단 묶음 제목도
|
||||
line["묶음"] = f"{one['이름']} {one['규격']}".strip()
|
||||
line["이름"], line["규격"] = one["이름"], one["규격"]
|
||||
done.append(one)
|
||||
if not done:
|
||||
return None, []
|
||||
return path.save(key, {"문서": doc, "판": got["판"]})["판"], done
|
||||
|
||||
|
||||
def back(key: str, version: str) -> dict[str, Any]:
|
||||
@@ -101,5 +136,13 @@ def back(key: str, version: str) -> dict[str, Any]:
|
||||
old = got["문서"].get(KEEP)
|
||||
if not isinstance(old, dict) or not old.get(imp.KEY):
|
||||
raise _bad("되돌릴 가져온 식 없음")
|
||||
info = store.write(sd.KIND, key, version or got["판"], {**old, "키": key})
|
||||
keep = (
|
||||
old.get(dc.KEY) if dc.has(old) else None
|
||||
) # 보관 몸 수식 사본 그대로(바꾼 기계 = 따로 버림)
|
||||
token = dsk.SAVING.set({"키": key, "사본": keep}) if keep else None
|
||||
try:
|
||||
info = store.write(sd.KIND, key, version or got["판"], {**old, "키": key})
|
||||
finally:
|
||||
if token is not None:
|
||||
dsk.SAVING.reset(token)
|
||||
return {"판": info.get("판")}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"""연결 찾기 — 로직 차이(다른 항) 대 값 차이(참고) 가르기(PLAN 812-2 · 요구 712) — `M02_Doc_Match` 가 부름.
|
||||
|
||||
확정을 막는 「다른 항」 = 로직 차이만 — 자원 갈래 · 기계 규격 · 노임 종류 · 자재 이름 종류 · 비목 · 산출 단가 ·
|
||||
율 구조(있고 없음 · 대상 비목 — 율 값 % 는 참고) · 수량(옵션 풀이 뒤 · 원 단위 같아야 함).
|
||||
「참고」 = 값 차이 — 인력 · 재료 · 기계 시간당 단가 · 끝수(합만 다름) · 율 값(%) · 그로 인한 비목 금액 · 계.
|
||||
확정을 막는 「다른 항」 = 로직 차이만 — 자원 갈래 · 기종 · 노임 종류 · 자재 이름 종류 · 비목 · 산출 단가 ·
|
||||
율 구조(있고 없음 · 대상 비목 — 율 값 % 는 참고) · 단위 환산(`M02_Doc_Match_Value`).
|
||||
「참고」 = 값 차이 — 인력 · 재료 · 기계 시간당 단가 · 끝수(합만 다름) · 율 값(%) · 같은 기종 규격 · 수량(812-11) ·
|
||||
그로 인한 비목 금액 · 계.
|
||||
율 = 율 줄 셈값 ÷ 대상 금액 합 — 대상 = 율 식이 (거쳐서) 부르는 금액 줄 · 비목 이름이면 그 비목 몫 · 번지 합 = 핵심 줄 전부 ·
|
||||
대상 금액 끝수 자리 차이만 지움(유효 6 자리 · 두 바탕 중 짧은 꼴) — 지어 맞추기 아님.
|
||||
"""
|
||||
@@ -14,6 +15,7 @@ from decimal import Decimal, InvalidOperation
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Doc_Copy as dc
|
||||
from M02_MasterTemplete import M02_Doc_Match_Value as val
|
||||
from M02_MasterTemplete import M02_Machine_Find as mfind
|
||||
|
||||
md = dc.md
|
||||
@@ -158,15 +160,24 @@ def _grams(text: str) -> set[str]:
|
||||
|
||||
|
||||
def pair(a: dict, b: dict) -> tuple[list[dict], list[dict]]:
|
||||
"""수량 같은 짝 줄 → (다른 항, 참고) — 단가만 다르면 참고 · 자원 · 비목이 다르면 다른 항."""
|
||||
"""짝 줄 → (다른 항, 참고) — 단가 · 같은 기종 규격만 다르면 참고 · 자원 · 기종 · 비목이 다르면 다른 항."""
|
||||
diffs: list[dict] = []
|
||||
spec: list[dict] = [] # 같은 기종 규격만 다름 = 값(812-11 · 요구 733-2)
|
||||
kinds = {a["갈래"], b["갈래"]}
|
||||
# 기계경비 숫자 묶음(코드 없음 · 원파일 값) ↔ 우리 기계 = 이름 종류로 같은 기계인지
|
||||
loose = kinds == {"기계", "산출"} and not (a if a["갈래"] == "산출" else b)["코드"]
|
||||
if a["갈래"] == b["갈래"] == "기계" and a["코드"] != b["코드"]:
|
||||
diffs.append({"항": "기계 규격", "글": f"{a['이름']} {a['코드']} ↔ {b['코드']}"})
|
||||
combo = not val.same_combo(a, b) # 조합 기계 ↔ 낱 기계 = 다른 기종
|
||||
if a["갈래"] == b["갈래"] == "기계" and (a["코드"] != b["코드"] or combo):
|
||||
if not combo and val.same_machine(a, b):
|
||||
spec.append(val.spec_note(a, b))
|
||||
else:
|
||||
diffs.append(
|
||||
{"항": "기계 규격", "글": f"{a['이름']} {a['코드']} ↔ {b['코드']} — 다른 기종"}
|
||||
)
|
||||
elif loose and not _same_machine(a["이름"], b["이름"]):
|
||||
diffs.append({"항": "기계 규격", "글": f"{a['이름']} ↔ {b['이름']} {b['코드']}"})
|
||||
elif loose:
|
||||
spec += [x for x in (val.loose_note(a, b),) if x]
|
||||
elif a["갈래"] == b["갈래"] == "노임" and a["코드"] != b["코드"]:
|
||||
diffs.append({"항": "자원", "글": f"{a['이름']} {a['코드']} ↔ {b['이름']} {b['코드']}"})
|
||||
elif a["갈래"] == b["갈래"] == "재료" and not _same_stuff(a, b):
|
||||
@@ -178,11 +189,11 @@ def pair(a: dict, b: dict) -> tuple[list[dict], list[dict]]:
|
||||
{"항": "비목", "글": f"{a['이름']} 비목 {'·'.join(a['비목'])} ↔ {'·'.join(b['비목'])}"}
|
||||
)
|
||||
if a["단가"] == b["단가"]:
|
||||
return diffs, []
|
||||
return diffs, spec
|
||||
say = f"{a['이름']} 단가 {txt(a['단가'])} ↔ {txt(b['단가'])}"
|
||||
if (a["갈래"] == b["갈래"] and a["갈래"] in PRICED) or loose:
|
||||
return diffs, [{"항": "단가 차이", "글": say}]
|
||||
return [*diffs, {"항": "산출 단가", "글": say}], []
|
||||
return diffs, [*spec, {"항": "단가 차이", "글": say}]
|
||||
return [*diffs, {"항": "산출 단가", "글": say}], spec
|
||||
|
||||
|
||||
def _same_stuff(a: dict, b: dict) -> bool:
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
"""연결 찾기 값 기준 넓힘(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
|
||||
@@ -34,7 +34,7 @@ const LABEL: Record<Exclude<MatchAction, null>, string> = {
|
||||
};
|
||||
const ASK: Partial<Record<Exclude<MatchAction, null>, string>> = {
|
||||
바꾸기:
|
||||
"이 단가산출을 우리 기초 사본(찾은 옵션)으로 바꿈 — 키 그대로 · 일위대가 금액이 다시 셈됨 · 원래 가져온 식은 보관",
|
||||
"이 단가산출을 우리 기초 사본(찾은 옵션 · 우리 값)으로 바꿈 — 규격만 다른 기계 줄은 가져온 규격의 우리 기계 · 키 그대로 · 일위대가 금액이 다시 셈됨 · 원래 가져온 식은 보관",
|
||||
되돌리기: "우리 기초 사본을 원래 가져온 식으로 되돌림 — 가져온 식 금액 그대로",
|
||||
};
|
||||
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
* SK `가져온.원본.연결후보`(서버 `M02_Doc_Match` 가 적음) → 띠 글 · 다른 항 · 단추 하나:
|
||||
* 확정 = [우리 기초 사본으로 바꾸기] · 근접 · 없음 = 단추 없음(703) · 안 찾음 = [연결 찾기] ·
|
||||
* 다른 항 = 로직 차이(확정을 막음) · 참고 = 단가 · 끝수 · 율 값 %(값 차이 — 「우리 단가로 셈 ○○ 원」 · 812-2 · 812-4) ·
|
||||
* 바꾼 SK(`가져온보관` 있음) = 「우리 기초 사본 ○○」 + [가져온 식으로 되돌리기]
|
||||
* 바꾼 SK(`가져온보관` 있음) = 「우리 기초 사본 ○○」 + [가져온 식으로 되돌리기] ·
|
||||
* 같은 기종 규격 · 수량 차이 = 참고(812-11) · 기계 작업 = 기계 한 줄 단가산출(우리 기초에 안 붙임 · 단추 없음)
|
||||
* 화면 · DOM · fetch 없음
|
||||
* ========================================================================== */
|
||||
|
||||
export type MatchVerdict = "확정" | "근접" | "없음";
|
||||
export type MatchVerdict = "확정" | "근접" | "없음" | "기계 작업";
|
||||
|
||||
export interface MatchFound {
|
||||
판정?: MatchVerdict;
|
||||
@@ -16,7 +17,7 @@ export interface MatchFound {
|
||||
이름?: string;
|
||||
옵션?: Record<string, string>;
|
||||
다른?: { 항: string; 글: string }[];
|
||||
/** 값 차이(단가 · 끝수 · 율 값) — 확정을 안 막음 */
|
||||
/** 값 차이(단가 · 끝수 · 율 값 · 규격 · 수량) — 확정을 안 막음 */
|
||||
참고?: { 항: string; 글: string }[];
|
||||
금액?: { 가져온?: Record<string, number | null>; 우리?: Record<string, number | null> };
|
||||
후보수?: number;
|
||||
@@ -49,15 +50,21 @@ const lines = (d?: { 항: string; 글: string }[]): string[] =>
|
||||
|
||||
const won = (n?: number | null): string =>
|
||||
n == null ? "" : `${Math.round(n).toLocaleString("ko-KR")} 원`;
|
||||
const QTY = ["수량", "수량 끝자리", "품 숫자"];
|
||||
/** 참고 요약 — 「참고: 단가 다름 3 줄 · 율 값 다름 1 줄 · 우리 단가로 셈 53,218 원」 · 참고 없음 = "" */
|
||||
const noteText = (f: MatchFound): string => {
|
||||
const n = f.참고 ?? [];
|
||||
if (!n.length) return "";
|
||||
const price = n.filter((x) => x.항 === "단가 차이").length;
|
||||
const rate = n.filter((x) => x.항 === "율 값").length;
|
||||
const spec = n.filter((x) => x.항 === "규격").length;
|
||||
const qty = n.filter((x) => QTY.includes(x.항)).length;
|
||||
const bits = [
|
||||
spec ? `규격 다름 ${spec} 줄` : "",
|
||||
qty ? `수량 다름 ${qty} 줄` : "",
|
||||
price ? `단가 다름 ${price} 줄` : "",
|
||||
rate ? `율 값 다름 ${rate} 줄` : "",
|
||||
n.some((x) => x.항 === "옵션 확인") ? "옵션 확인" : "",
|
||||
n.some((x) => x.항 === "끝수") ? "끝수 다름" : "",
|
||||
].filter(Boolean);
|
||||
const ours = won(f.금액?.우리?.계);
|
||||
@@ -80,6 +87,14 @@ export function matchBand(sk: SkLike | null | undefined): MatchBand | null {
|
||||
if (!f?.판정) {
|
||||
return { tone: "info", text: "우리 기초와 연결을 아직 안 찾음", details: [], action: "찾기" };
|
||||
}
|
||||
if (f.판정 === "기계 작업") {
|
||||
return {
|
||||
tone: "info",
|
||||
text: "기계 한 줄 단가산출 — 그대로 씀 · 값은 [단가 갱신]으로 우리 값",
|
||||
details: lines(f.참고),
|
||||
action: null,
|
||||
};
|
||||
}
|
||||
const base = `${f.키 ?? ""} ${f.이름 ?? ""} ${opts(f.옵션)}`.trim();
|
||||
if (f.판정 === "확정") {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user