- A 옵션 찾기 못 맞춤(수준 9 · 거리 ≥ 1) = 우리 품 · 입력 안 냄 · 「못맞춤」 표지 · 꼬리표 「→ –(우리 셈 조건 못 찾음)」 - B 자동값 「만약(X > 0, X, 기본)」 칸 = 나눗수 퍼뜨림에서 뺌(0 = 원문 기본) · 옵션 후보엔 옛 1 도 둠 - C 하나씩 조합 — 후보 ≤ 4 수 칸도 둘씩 짝 - D 엑셀이 값으로 넣은 「0 = 식」 칸 출처 「엑셀 값 · 식 아님」 · 꼬리표 「원문 식이면 품 · 금액(조건)」 - E 결과단위 「자재」 = 배 1 · 공/㎥ ≡ 공㎥ - F 품 비교 우리 품 × 대상 몫 - G 절 하나 블록 = 덧 로직은 후보만(이중 계상 막음) - H 1위 옵션 수준 ≤ 2 면 그 입력을 대상 입력값으로 - I 규격 「L=…cm」 를 거리 칸에 안 넣음 · 수 고르기는 제 단위 붙은 수만 · 엑셀 수 「1 ㎡」 를 m 로 안 읽음 - 우리지문 · 맞춤 캐시 판 올림(FP 2 · CALC 3) - 새 지킴 시험 test_635_hopyo_ours_qty - 낡은 시험 둘(test_628 표지 수 18 · test_621 S1 울진소광 해시)은 HEAD 에서도 실패 — 782-3 마스터 합침 뒤 값 · 손대지 않음 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
488 lines
26 KiB
Python
488 lines
26 KiB
Python
"""M02 일위대가 실무자료 줄 맞춤 응답(PLAN 338-1 · 설계 3-3 · 3-4) — 줄 상태 · 체크 제안 · 대상 목록
|
||
·
|
||
후보 · 품 비교 · 호표 통째 · 셈 · 만든 · 미리 셈(`guess`).
|
||
|
||
상태 차례 = ① % ② 호표 참조(만들 층에 확인된 사전조합이면 체크) ③ 노임(그릇 없음) ④ 장부 「우리에
|
||
없음 ·
|
||
안 씀」 → 뺌 ⑤ 장부 짝 → 별칭(근거 다름이면 체크 안 함 · 숫자 판정 맞음이 다른 로직이면
|
||
⑥ 판정 먼저 + 꼬리표 「장부와 다름」 · 짝은 후보 끝 덤 · 358-3) ⑥
|
||
맞춤셈
|
||
판정(맞음 = 체크 · 다른 점 · 후보) ⑦ 셈 전 · 숫자 없음 → 이름 · 근거 후보(「셈 중」 · 「숫자 판정
|
||
없음」).
|
||
장부 짝이 규격 다름(품명|단위 느슨 열쇠)이면 ⑤ 아님 — 꼬리표 「별칭 · 규격 다름」 · 짝은 후보
|
||
끝 덤만.
|
||
자재 · 별도계상 = 이름 · 규격 같음 하나 · 바탕 이름 · 규격 수 같은 한 품목이면 맞음(358-2).
|
||
로직 줄 후보 묶음 표시(358-1) = 이름후보(옛 324 다섯 `Match.name_cands`) · 후보마다
|
||
뜻겹침 · 출처같음 · 책 — 차례 · 무게 · 상태는 그대로.
|
||
입력값 출처(칸마다) = 별칭 > 블록(옵션 찾기 값이 규격 읽기와 다름) > 규격(`spec_inputs`) > 자동값.
|
||
체크 = 처음 제안일 뿐 — 들어감은 [확인하고 사전조합 만들기](다 창)만. 저장 없음 · 셈은 서버 단독.
|
||
장부 = `M02_Hopyo_Ledger`(다 창) · 확인된 사전조합 = `M02_Hopyo_Build.confirmed`(다 창).
|
||
④ ⑤ 장부 짝 = 자동 짝만(마스터 · 이 불러오기에서 이은 짝 · 739-1) · 회사 · 개인 층 짝 =
|
||
「내 제안」 — 숫자 판정 그대로 + 꼬리표 「내 제안」 + `제안 {갈래, 짝, 대상, 층, 때}`
|
||
(고르기 창 맨 위 · 한 번 눌러 고름).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
from M02_MasterTemplete import M02_Hopyo_Basis as hopyo_basis
|
||
from M02_MasterTemplete import M02_Hopyo_Build as hopyo_build
|
||
from M02_MasterTemplete import M02_Hopyo_Finger as finger
|
||
from M02_MasterTemplete import M02_Hopyo_Fit as fit
|
||
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
|
||
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
||
from M02_MasterTemplete import M02_Hopyo_Material as hmat
|
||
from M02_MasterTemplete import M02_Hopyo_Option as option
|
||
from M02_MasterTemplete import M02_Hopyo_Ours as ours
|
||
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
|
||
from M02_MasterTemplete import M02_Hopyo_Units as units
|
||
from M02_MasterTemplete import M02_Hopyo_Verdict as verdict
|
||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||
|
||
SKIP = ("우리에 없음", "안 씀")
|
||
MINE = "내 제안"
|
||
LOOSE = "별칭 · 규격 다름"
|
||
LEDGER_OFF = "장부와 다름"
|
||
OUTSIDE = ("%", "노임", "뺌") # 연결 셈 밖(358-3 ②)
|
||
|
||
|
||
class Ctx:
|
||
"""보기 한 번 — 파일 · 장부 · 맞춤셈 · 확인된 사전조합을 한 번 읽어 줄마다 넘김."""
|
||
|
||
def __init__(self, folder: Path, key: str, to: Path | None = None) -> None:
|
||
self.folder, self.key = Path(folder), key
|
||
self.to = Path(to or folder)
|
||
self.doc = hopyo_store.read(folder, key)
|
||
self.got = hm.targets()
|
||
self.units = {int(h["번"]): h for h in self.doc.get("호표") or []}
|
||
self.books = ledger.books(self.to)
|
||
self.doc_id = ledger.doc_id(self.folder, key)
|
||
self.calc = fit.ensure(folder, key)
|
||
self.cache = (fit.result(folder, key) or {}).get("블록") or {}
|
||
self.done = hopyo_build.confirmed(self.to, refs.where(self.folder))
|
||
self.names = {x["로직"]: x for x in self.got["로직"]}
|
||
self.mats = {x["자재"]: x for x in self.got["자재"]}
|
||
|
||
|
||
def _target(code: str, line: dict, ctx: Ctx, found: dict | None, use: bool, **extra) -> dict:
|
||
"""대상 하나(로직) — 입력값 · 못읽음 · 출처 · 환산 · 몫 · 배."""
|
||
one = ctx.names.get(code) or {"이름": "", "단위": ""}
|
||
vals, unread = hm.spec_inputs(line, hm._logic_specs(code))
|
||
src = {k: "규격" for k in vals}
|
||
use = use or (found or {}).get("수준", 9) <= 2 # 맞춘 옵션 입력 = 품 비교와 같은 입력
|
||
typed = ((found or {}).get("식아님") or {}).get("칸") or {} # 엑셀이 값으로 넣은 「0 = 식」 칸
|
||
for k, v in ((found or {}).get("입력") or {}).items() if use else ():
|
||
if str(vals.get(k)) != str(v):
|
||
vals[k], src[k] = v, "엑셀 값 · 식 아님" if k in typed else "블록"
|
||
conv = hm.factor(line.get("단위"), one["단위"], line.get("규격"))
|
||
share = extra.pop("몫", None)
|
||
times = conv["배"] * share if conv["배"] is not None and share else conv["배"]
|
||
head = {"로직": code, "이름": one["이름"], "단위": one["단위"], "입력값": vals}
|
||
head.update(못읽음=[u for u in unread if u not in vals], 출처=src, 환산=conv)
|
||
return {**head, "몫": share, "배": times, **extra}
|
||
|
||
|
||
def _pair(pair: dict, line: dict, ctx: Ctx) -> dict:
|
||
"""장부 짝 → 대상(입력값 출처 = 별칭)."""
|
||
vals = dict(pair.get("입력값") or {})
|
||
if pair.get("로직"):
|
||
one = ctx.names.get(pair["로직"]) or {"이름": "", "단위": ""}
|
||
head = {"로직": pair["로직"]}
|
||
elif pair.get("자재"):
|
||
one, head = ctx.mats.get(pair["자재"]) or {"이름": "", "단위": ""}, {"자재": pair["자재"]}
|
||
else:
|
||
h = ctx.units.get(int(pair.get("호표") or 0)) or {}
|
||
one, head = (
|
||
{"이름": h.get("이름", ""), "단위": h.get("단위", "")},
|
||
{"호표": pair.get("호표")},
|
||
)
|
||
conv = {"배": pair.get("배"), "갈래": "별칭"}
|
||
if "자재" in head: # 세 칸(361-2) = 만들기에 적힐 값(`Material.marks`)
|
||
head["세칸"] = hmat.marks(line, one["이름"])
|
||
return {**head, "이름": one["이름"], "단위": one["단위"], "입력값": vals, "못읽음": []} | {
|
||
"출처": {k: "별칭" for k in vals},
|
||
"환산": conv,
|
||
"몫": pair.get("몫"),
|
||
"배": pair.get("배"),
|
||
}
|
||
|
||
|
||
def _qty(entry: dict, found: dict | None, share: float | None = None) -> dict:
|
||
"""품 비교 — 엑셀 · 우리 · 같음(직종 · 장비 갈래마다 R 허용) · 같은 갈래 여러 줄은 합쳐 견줌
|
||
(화면 품 비교 열쇠 = 갈래 가나다 차례 + 로 묶음) · 몫 = 대상 몫(섞인 줄 10% = 우리 × 0.1)."""
|
||
ex = entry.get("품") or {"인력": {}, "장비": [], "자재": []}
|
||
us = (found or {}).get("우리품") or option.ours_qty(None)
|
||
if share:
|
||
k = lambda q: round(q * share, 6) # noqa: E731
|
||
us = {"인력": {t: k(q) for t, q in us["인력"].items()}} | {
|
||
"장비": [{**x, "시간": k(x["시간"])} for x in us["장비"]],
|
||
"자재": [{**x, "수량": k(x["수량"])} for x in us["자재"]],
|
||
}
|
||
same = {}
|
||
for t, a in ex["인력"].items():
|
||
b = us["인력"].get(t)
|
||
same[t] = bool(b) and option.near(a, b, fit.TOL[0], option.ru(a))
|
||
sums: dict[str, list[float]] = {} # 열쇠 → [엑셀 합, 반올림 폭 합, 우리 합]
|
||
for m in ex["장비"]:
|
||
one = sums.setdefault("+".join(sorted(m["갈래"])), [0.0, 0.0, 0.0])
|
||
one[0] += m["시간"]
|
||
one[1] += option.ru(m["시간"])
|
||
for x in us["장비"]:
|
||
if (one := sums.get("+".join(sorted(x["갈래"])))) is not None:
|
||
one[2] += x["시간"]
|
||
for name, (a, r, b) in sums.items():
|
||
same[name] = option.near(a, b, fit.TOL[0], r)
|
||
return {"엑셀": ex, "우리": us, "같음": same}
|
||
|
||
|
||
def _picked(entry: dict) -> list[dict]:
|
||
"""체크할 조합 — 로직이 하나뿐이면 조합 아님(기계 몫은 꼬리표)."""
|
||
combo = [c for c in entry.get("조합") or [] if c["체크"]]
|
||
return combo if len(combo) > 1 else []
|
||
|
||
|
||
def _check(entry: dict, line: dict, ctx: Ctx) -> bool:
|
||
"""체크 제안 한 규칙(358-3 · 목록 수 = 확인 창) — 판정 맞음 · 대상(조합 또는 1위) 있음 ·
|
||
대상마다 환산 배 있음(추정 아님)."""
|
||
codes = [c["로직"] for c in _picked(entry)] or (entry.get("차례") or [])[:1]
|
||
if entry["판정"]["상태"] != "맞음" or not codes:
|
||
return False
|
||
for code in codes:
|
||
unit = (ctx.names.get(code) or {}).get("단위", "")
|
||
conv = hm.factor(line.get("단위"), unit, line.get("규격"))
|
||
if conv["배"] is None or conv["갈래"] == "추정":
|
||
return False
|
||
return True
|
||
|
||
|
||
def _numeric(line: dict, ctx: Ctx, entry: dict) -> dict:
|
||
"""⑥ 맞춤셈 판정 → 상태 · 대상 · 후보 · 품 · 글 · 꼬리표."""
|
||
judged = entry["판정"]
|
||
state = judged["상태"]
|
||
top = (entry.get("차례") or [None])[0]
|
||
found = (entry.get("옵션") or {}).get(top)
|
||
use = state in ("맞음", "다른 점")
|
||
combo = _picked(entry)
|
||
share = None
|
||
if combo:
|
||
targets = [
|
||
_target(
|
||
c["로직"], line, ctx, entry["옵션"].get(c["로직"]), use, 몫=c["몫"], 어디=c["어디"]
|
||
)
|
||
for c in combo
|
||
]
|
||
else:
|
||
part = next((c for c in entry.get("조합") or [] if c["로직"] == top), {})
|
||
share = part.get("몫") if part.get("몫") not in (None, 1, 1.0) else None # 섞인 줄 인력 몫
|
||
extra = {"몫": share, "어디": part["어디"]} if share else {}
|
||
targets = [_target(top, line, ctx, found, use, **extra)] if top else []
|
||
tags = list(judged.get("꼬리표") or [])
|
||
tags += [f"조합 — {' + '.join(c['로직'] for c in combo)}"] if combo else []
|
||
for c in entry.get("조합") or []:
|
||
if not c["체크"]:
|
||
tags.append(f"기계 몫 {c['로직']} — 시간 = 엑셀 값(우리 셈 아님)")
|
||
if any({"블록", "엑셀 값 · 식 아님"} & set(t["출처"].values()) for t in targets):
|
||
tags.append("옵션 — 블록에서")
|
||
tags += [f"근거 {b} {n}" for b, n in (entry.get("지문") or {}).get("출처") or []]
|
||
out = {"상태": state, "체크": _check(entry, line, ctx), "대상": targets}
|
||
out.update(
|
||
후보=entry.get("후보") or [], 글=judged["글"], 꼬리표=tags, 품=_qty(entry, found, share)
|
||
)
|
||
if found and found.get("갈리는칸"):
|
||
out["갈리는칸"] = found["갈리는칸"]
|
||
return out
|
||
|
||
|
||
def _names_only(line: dict, ctx: Ctx, why: str, full: bool) -> dict:
|
||
"""⑦ 숫자 판정 없음 · 셈 전 — 이름 · 근거 후보(차례만 · 대상 = 1위)."""
|
||
if not full:
|
||
return {"상태": "후보", "체크": False, "대상": [], "후보": [], "글": why, "꼬리표": [why]}
|
||
order = hm.b_order(line, ctx.got)
|
||
top = order["top"]
|
||
cands = [{**c, "판정": "후보", "글": why} for c in order["후보"]]
|
||
targets = [_target(top[0], line, ctx, None, False)] if top else []
|
||
state = "후보" if top else "없음"
|
||
return {
|
||
"상태": state,
|
||
"체크": False,
|
||
"대상": targets,
|
||
"후보": cands,
|
||
"글": why,
|
||
"꼬리표": [why],
|
||
}
|
||
|
||
|
||
def _material(line: dict, ctx: Ctx, full: bool) -> dict:
|
||
"""자재 · 별도계상 줄(358-2 · `M02_Hopyo_Material`) — 「별도계상」 글 = 꼬리표 · 맞음 =
|
||
이름 · 규격 같음 하나 · 또는 바탕 이름 · 규격 수 같은 한 품목(줄 글 지역 · 지역 없는 것 ·
|
||
「전국」 자리표 · 361-1) · 후보 = 그 품목 변형 모두(`변형` · 고르기 창이 접음)."""
|
||
me, _ = hmat.clean(line)
|
||
hit = units.material(line, ctx.got, full) # 줄 잇기 한 벌(778-5 · 단가산출 블록 자재와 같음)
|
||
found, text, tags = hit["찾음"], hit["글"], hit["꼬리표"]
|
||
if hit["상태"] == units.PICK and hit["후보"] and not found: # 글자 규격 = 후보로만(요구 624-2)
|
||
cands = hit["후보"] if hit["글"] == units.LETTERS else []
|
||
cands = cands or (hmat.candidates(me) if full else [])
|
||
return {"상태": "후보", "체크": False, "대상": [], "후보": cands if full else [], "글": text,
|
||
"꼬리표": tags} # fmt: skip
|
||
if found:
|
||
target = _mat_target(found[0], line)
|
||
conv = target["환산"]
|
||
ok = conv["배"] is not None and conv["갈래"] != "추정"
|
||
many = len(found) > 1
|
||
cands = [
|
||
{**hm._public(x), "점수": None, "판정": "맞음" if not at else "후보", "글": text}
|
||
| ({"변형": True} if many and "자재" in x else {})
|
||
for at, x in enumerate(found)
|
||
]
|
||
out = {"상태": "맞음", "체크": ok, "대상": [target], "후보": cands if full else []}
|
||
return {**out, "글": text, "꼬리표": tags}
|
||
cands = hmat.candidates(me) if full else []
|
||
state = "후보" if cands or not full else "없음"
|
||
return {"상태": state, "체크": False, "대상": [], "후보": cands, "꼬리표": tags}
|
||
|
||
|
||
def _mat_target(one: dict, line: dict, times: Any = None) -> dict:
|
||
"""자재 · 「전국」 자리표 → 대상(배 = 환산 · `times` 주면 그것 · 세칸 = 만들기에 적힐 세 칸)."""
|
||
conv = hm.factor(line.get("단위"), one["단위"], line.get("규격"))
|
||
target = {**hm._public(one), "입력값": {}, "못읽음": [], "출처": {}, "환산": conv}
|
||
target["세칸"] = hmat.marks(line, one["이름"])
|
||
ok = isinstance(times, (int, float)) and not isinstance(times, bool) and times > 0
|
||
return {**target, "몫": None, "배": times if ok else conv["배"]}
|
||
|
||
|
||
def match_line(line: dict[str, Any], ctx: Ctx, full: bool = True) -> dict[str, Any]:
|
||
"""줄 → 맞춤(설계 3-3) — `full` 아니면 상태 · 체크만(호표 목록 수)."""
|
||
out: dict[str, Any] = {"열쇠": hm.line_key(line), "체크": False, "대상": [], "후보": []}
|
||
kind = finger.kind_of(line)
|
||
ref = line.get("참조") or {}
|
||
if kind == "%":
|
||
return {**out, "상태": "%", "글": "% 줄 — 우리 로직 덧줄이 셈"}
|
||
if kind == "호표" or ref.get("종류") == "일위":
|
||
no = int(ref.get("번") or 0)
|
||
h = ctx.units.get(no) or {}
|
||
conv = hm.factor(line.get("단위"), h.get("단위"), line.get("규격"))
|
||
made = ctx.done.get(hopyo_build.mark(ctx.key, no))
|
||
target = {"호표": no, "이름": h.get("이름", ""), "단위": h.get("단위"), "입력값": {}}
|
||
target.update(못읽음=[], 출처={}, 환산=conv, 몫=None, 배=conv["배"])
|
||
ok = bool(made) and conv["배"] is not None and conv["갈래"] != "추정"
|
||
text = f"하위 일위대가 {made} 사본" if made else f"제{no}호표 — 저장 때 하위로 함께 만듦"
|
||
return {**out, "상태": "호표", "체크": ok, "대상": [target], "글": text}
|
||
if kind == "노임":
|
||
return {**out, "상태": "노임", "글": "노임 · 그릇 없음"}
|
||
alias = ledger.recall(line, ctx.to, ctx.books, doc=ctx.doc_id)
|
||
mine, alias = (alias, None) if alias and alias.get("제안") else (None, alias)
|
||
loose, alias = (alias, None) if alias and alias.get("규격다름") else (None, alias)
|
||
entry = ctx.cache.get(fit.unit_key(line)) if kind not in hm.MATERIAL_KINDS else None
|
||
if alias and alias.get("갈래") in SKIP:
|
||
return {**out, "상태": "뺌", "뺌": alias["갈래"], "별칭": _alias_info(alias, False)}
|
||
if alias and alias.get("갈래") == "짝" and alias.get("짝") and _ledger_off(entry, alias):
|
||
loose, alias = alias, None # 숫자 판정 맞음 = 장부 짝 아닌 로직 → 판정 먼저(358-3 ①)
|
||
loose = {**loose, "_까닭": LEDGER_OFF}
|
||
if alias and alias.get("갈래") == "짝" and alias.get("짝"):
|
||
got = _aliased(line, ctx, alias, entry)
|
||
elif kind in hm.MATERIAL_KINDS:
|
||
got = _material(line, ctx, full)
|
||
elif entry and entry["판정"]["글"] not in (fit.NO_NUMBER, "우리 로직 준비 중"):
|
||
got = _numeric(line, ctx, entry) if full else _brief(entry, line, ctx)
|
||
else:
|
||
why = "셈 중" if not ctx.calc["끝"] else fit.NO_NUMBER
|
||
got = _names_only(line, ctx, why, full)
|
||
got = _loose(got, loose, line, ctx) if loose and full else got
|
||
got = _mine(got, mine, line, ctx) if mine else got
|
||
return {**out, **(_grouped(got, line, ctx) if full and kind not in hm.MATERIAL_KINDS else got)}
|
||
|
||
|
||
def _mine(got: dict, mine: dict, line: dict, ctx: Ctx) -> dict:
|
||
"""내 제안(739-1 · 승인 전 회사 · 개인 층 짝) — 자동 아님 · 꼬리표 + `제안`(고르기 맨 위)."""
|
||
pairs = [p for p in mine.get("짝") or [] if p.get("로직") or p.get("자재")]
|
||
kind = mine.get("갈래")
|
||
word = MINE if kind == "짝" else f"{MINE} — {kind}"
|
||
head = {"갈래": kind, "짝": pairs, "층": mine.get("층"), "때": mine.get("때")}
|
||
targets = [_pair(p, line, ctx) for p in pairs]
|
||
return {**got, "꼬리표": [*(got.get("꼬리표") or []), word], "제안": {**head, "대상": targets}}
|
||
|
||
|
||
def _grouped(got: dict, line: dict, ctx: Ctx) -> dict:
|
||
"""묶음 표시(358-1) — 후보 · 이름후보(옛 324 다섯)마다 뜻겹침 · 출처같음(조항 ≥ 0.9) · 책 ·
|
||
차례 · 무게 · 상태 그대로(아무것도 숨기지 않음 · 묶음은 화면 몫)."""
|
||
mine = hm.words(line.get("이름"))
|
||
theirs = hm.fix_typo(((line.get("근거") or {}).get("출처")) or [])
|
||
table = hopyo_basis.ours()
|
||
|
||
def mark(c: dict) -> dict:
|
||
one, our = ctx.names.get(c.get("로직")), table.get(c.get("로직"))
|
||
if one is None:
|
||
return c
|
||
same = bool(theirs and our) and hm.clause(theirs, our)[0] >= 0.9
|
||
book = (our or {}).get("책", "")
|
||
return {**c, "뜻겹침": hm.near(mine, one["words"]), "출처같음": same, "책": book}
|
||
|
||
named = [mark(c) for c in hm.name_cands(line, ctx.got)]
|
||
cands = [mark(c) for c in got.get("후보") or []]
|
||
# 745-1 미리 고름 점 — 「다른 점」 1위 + 출처 같음 · 누르기는 사용자
|
||
top = cands[0] if cands else {}
|
||
if top.get("판정") == "다른 점" and top.get("출처같음"):
|
||
top["미리"] = True
|
||
return {**got, "후보": cands, "이름후보": named}
|
||
|
||
|
||
def _ledger_off(entry: dict | None, alias: dict) -> bool:
|
||
"""장부 짝(정확 열쇠)이 숫자 판정 맞음 1위를 안 담음."""
|
||
codes = {p.get("로직") for p in alias.get("짝") or []}
|
||
return bool(entry and entry["판정"]["상태"] == "맞음" and entry["차례"][0] not in codes)
|
||
|
||
|
||
def _loose(got: dict, alias: dict, line: dict, ctx: Ctx) -> dict:
|
||
"""규격 다른 별칭 · 숫자 판정과 다른 장부 짝 — 꼬리표(「별칭 · 규격 다름」 · 「장부와 다름」) +
|
||
짝 로직 · 자재가 후보에 없으면 끝에 덤(상태 · 대상 · 옵션은 이 줄 숫자 그대로)."""
|
||
word = alias.get("_까닭") or LOOSE
|
||
tags = [*(got.get("꼬리표") or []), word]
|
||
have = {c.get("로직") or c.get("자재") for c in got.get("후보") or []}
|
||
have |= {t.get("로직") or t.get("자재") for t in got.get("대상") or []}
|
||
extra = []
|
||
for p in alias.get("짝") or []:
|
||
code = p.get("로직") or p.get("자재")
|
||
if code and code not in have:
|
||
one = {
|
||
k: v
|
||
for k, v in _pair(p, line, ctx).items()
|
||
if k in ("로직", "자재", "이름", "단위")
|
||
}
|
||
extra.append({**one, "점수": None, "판정": "후보", "글": word})
|
||
return {**got, "꼬리표": tags, "후보": [*(got.get("후보") or []), *extra]}
|
||
|
||
|
||
def _brief(entry: dict, line: dict, ctx: Ctx) -> dict:
|
||
"""목록 수(`full` 아님) — 체크는 확인 창과 같은 `_check`."""
|
||
return {
|
||
"상태": entry["판정"]["상태"],
|
||
"체크": _check(entry, line, ctx),
|
||
"글": entry["판정"]["글"],
|
||
}
|
||
|
||
|
||
def _alias_info(alias: dict, basis_off: bool) -> dict:
|
||
keep = ("층", "주인", "때", "누가", "처음파일", "규격다름")
|
||
return {**{k: alias.get(k) for k in keep}, "근거다름": basis_off}
|
||
|
||
|
||
def _aliased(line: dict, ctx: Ctx, alias: dict, entry: dict | None) -> dict:
|
||
"""⑤ 장부 짝(정확 열쇠 · 숫자 판정 맞음 1위를 담음 또는 판정 맞음 아님) — 체크 = 근거 같음."""
|
||
targets = [_pair(p, line, ctx) for p in alias["짝"]]
|
||
codes = {t["로직"] for t in targets if t.get("로직")}
|
||
theirs = ((line.get("근거") or {}).get("출처")) or []
|
||
table = hopyo_basis.ours()
|
||
off = bool(theirs and codes) and all(
|
||
hm.clause(theirs, table[c])[0] < 0.9 for c in codes if c in table
|
||
)
|
||
tags = ["별칭 — " + " · ".join(str(x) for x in (alias.get("층"), alias.get("때")) if x)]
|
||
tags += ["근거 다름"] * off
|
||
out = {"상태": "별칭", "체크": not off, "대상": targets, "글": "별칭 장부 짝", "꼬리표": tags}
|
||
out["별칭"] = _alias_info(alias, off)
|
||
if entry and entry.get("품"):
|
||
out["품"] = _qty(entry, (entry.get("옵션") or {}).get(next(iter(codes), None)))
|
||
return out
|
||
|
||
|
||
def whole(hopyo: dict, ctx: Ctx) -> dict | None:
|
||
"""직접 줄 호표 통째 제안 — 장부 짝(호표 열쇠) 먼저 · 맞춤셈 「호표:번」."""
|
||
head = {k: hopyo.get(k) for k in ("이름", "규격", "단위")}
|
||
alias = ledger.recall(head, ctx.to, ctx.books, whole=True, doc=ctx.doc_id)
|
||
if alias and alias.get("갈래") == "짝" and alias.get("짝") and not alias.get("규격다름"):
|
||
return { # 내 제안(739-1) = 체크 안 함 · 한 번 눌러 고름
|
||
"대상": [_pair(p, head, ctx) for p in alias["짝"]],
|
||
"글": MINE if alias.get("제안") else "별칭 장부 짝",
|
||
"체크": not alias.get("제안"),
|
||
}
|
||
entry = ctx.cache.get(f"호표:{hopyo['번']}")
|
||
if not entry or not entry.get("차례"):
|
||
return None
|
||
top = entry["차례"][0]
|
||
state = entry["판정"]["상태"]
|
||
target = _target(top, head, ctx, entry["옵션"].get(top), state in ("맞음", "다른 점"))
|
||
return {"대상": [target], "글": f"통째 = {entry['판정']['글']}", "체크": state == "맞음"}
|
||
|
||
|
||
def hopyo_view(hopyo: dict[str, Any], ctx: Ctx, full: bool = True) -> dict[str, Any]:
|
||
"""호표 하나 — 줄마다 맞춤 · `맞춤` = [체크 제안 수, 줄 수] · 연결할(% · 노임 · 뺌 밖 줄 수) ·
|
||
연결된(그중 체크 제안 수 · 358-3) · 통째 · 셈 · 만든."""
|
||
lines = [{**one, "맞춤": match_line(one, ctx, full)} for one in hopyo.get("줄") or []]
|
||
check = sum(1 for one in lines if one["맞춤"]["체크"])
|
||
need = [one for one in lines if one["맞춤"]["상태"] not in OUTSIDE]
|
||
out = {**hopyo, "줄": lines, "맞춤": [check, len(lines)], "연결할": len(need)}
|
||
out["연결된"] = sum(1 for one in need if one["맞춤"]["체크"])
|
||
if full:
|
||
made = ctx.done.get(hopyo_build.mark(ctx.key, int(hopyo["번"])))
|
||
out.update(통째=whole(hopyo, ctx), 셈=ctx.calc)
|
||
out["만든"] = {"키": made, "확인": True, **_made(ctx.to, made)} if made else None
|
||
return out
|
||
|
||
|
||
def _made(folder: Path, key: str) -> dict[str, Any]:
|
||
"""만든 사전조합의 줄 갈래(705-3) — `품셈` = 품셈으로 넣은 엑셀 줄 차례(옛 것 = None) · `고정` 줄 수."""
|
||
try:
|
||
doc = json.loads((Path(folder) / "precombo" / f"{key}.json").read_bytes())
|
||
except (OSError, ValueError):
|
||
return {"품셈": None, "고정": 0}
|
||
fixed = sum(1 for x in doc.get("담은로직") or [] if isinstance(x, dict) and "고정" in x)
|
||
return {"품셈": (doc.get("본뜬") or {}).get("품셈"), "고정": fixed}
|
||
|
||
|
||
def guess(
|
||
folder: Path, key: str, no: int, at: int, target: Any, to: Path | None = None
|
||
) -> dict[str, Any]:
|
||
"""미리 셈(저장 안 함) — 고른 대상 · 옵션으로 엔진 한 번 → {대상, 품: {엑셀, 우리, 같음},
|
||
글}."""
|
||
ctx = Ctx(folder, key, to)
|
||
hopyo = ctx.units.get(int(no))
|
||
lines = (hopyo or {}).get("줄") or []
|
||
if not 0 <= int(at) < len(lines):
|
||
raise store.StoreError(404, f"없는 줄 「제{no}호표 {at}」")
|
||
line = lines[int(at)]
|
||
picked = target if isinstance(target, list) else [target]
|
||
if not picked or not all(isinstance(t, dict) for t in picked):
|
||
raise store.StoreError(400, "대상 꼴이 틀림")
|
||
entry = ctx.cache.get(fit.unit_key(line))
|
||
if entry is None:
|
||
block = next(
|
||
(
|
||
b
|
||
for b in ctx.doc.get("단산") or []
|
||
if b.get("번") == (line.get("참조") or {}).get("번")
|
||
),
|
||
None,
|
||
)
|
||
entry = {"품": verdict.excel_qty(finger.excel(block, line))}
|
||
targets, total = [], {"L": {}, "X": [], "M": {}, "S": {}}
|
||
for t in picked:
|
||
if t.get("로직"):
|
||
if t["로직"] not in ctx.names:
|
||
raise store.StoreError(400, "돈 로직 · 우리 자재가 아님")
|
||
given = t.get("입력값")
|
||
one = _target(t["로직"], line, ctx, None, False, 몫=t.get("몫"))
|
||
if isinstance(t.get("배"), (int, float)) and t["배"] > 0:
|
||
one["배"] = t["배"] # 사용자가 정한 배는 그대로
|
||
if isinstance(given, dict):
|
||
one["입력값"] = {**one["입력값"], **given}
|
||
one["출처"].update({k: "고름" for k in given})
|
||
v = ours.run(t["로직"], one["입력값"]) or {"L": {}, "X": [], "M": {}, "S": {}}
|
||
k = float(t.get("몫") or 1)
|
||
for name, q in v["L"].items():
|
||
total["L"][name] = total["L"].get(name, 0.0) + q * k
|
||
total["X"] += [[a, b, q * k] for a, b, q in v["X"]]
|
||
for name, q in v["M"].items():
|
||
total["M"][name] = total["M"].get(name, 0.0) + q * k
|
||
targets.append(one)
|
||
elif t.get("자재") in ctx.mats:
|
||
targets.append(_pair({"자재": t["자재"], "배": t.get("배")}, line, ctx))
|
||
elif t.get("전국") and (one := hmat.national(line)):
|
||
targets.append(_mat_target(one, line, t.get("배")))
|
||
else:
|
||
raise store.StoreError(400, "돈 로직 · 우리 자재가 아님")
|
||
qty = _qty(entry, {"우리품": option.ours_qty(total)})
|
||
return {"대상": targets, "품": qty, "글": "우리 품 — 엔진 한 번(저장 안 함)"}
|