Files
Aislo/M02_MasterTemplete/M02_Hopyo_SkPatch.py
T
eomsangdonandClaude Sonnet 5.5 9e0746058e fix(751-1): 원화 꼴 흠 — M01 · M02 · 서버 글 — 공용 꼴 갈음 · 율 「4.0」 · 중간 값 · 마법사 콤마 · 서고 금액
- 파이썬 짝을 common_util/common_util_money.py 로 옮김(TS 와 같은 이름 · 서버 어디서나 불러옴)
- 시험 계산: 율 글자 「4.0 %」 → 숫자 4(서버 _pct) · 중간 값 경비 · 공구손료 등 비목 이름은 1,000 미만도 돈 꼴(870.2455 → 870)
- 기초 편집 단가 = 공용 단가 꼴 · 금액 = 소수 1 · 서고 기준 금액 · 구조물 조합 일위대가 금액 = 공용 정수 꼴(124,419.85 → 124,420)
- 마법사(확인 · 고르기 · 별칭 장부 · 엑셀 · 우리 금액): 콤마 없음 · 소수 셋 기본 · 부동소수 꼬리 걷음 · 품 비교 꼬리표 지수 꼴(1.235e+05) 걷음
- 다시 잇기 · 붙여넣기 대조 · 찾아 넣기 금액 열 끝 0 없음(22.9 원이 23 으로 숨지 않음) · 흐름 로직 텍스트 수식 비목 소계 정수
- 낡은 시험 고침: test_m02_hopyo_screen 품 비교 보통인부 0.0364 → 0.03636(수량 꼴 유효숫자 넷 규칙으로 파이썬 · TS 통일)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-02 22:54:02 +09:00

478 lines
25 KiB
Python

"""M02 실무 단가산출(산근 블록) → 단가산출(SK) 꼴(PLAN 729-1 · 요구서 내역19 1장 7 · 3장 6 ·
분석 19-B D-1).
블록 맞춤셈 한 칸(`M02_Hopyo_Verdict.block` 꼴 · 캐시 `M02_Hopyo_Fit.result`) → 갈래 셋:
기초 = 판정 맞음(또는 견줘 차이 없음) — 바탕 = 1위 기초 · 입력값 = 옵션 거꾸로 찾은 값 · 덮개 없음
SK = 다른 점 — 바탕 = 1위 기초(조합 둘째부터 = 더한줄 로직 · 수량 = 몫) + 차이 = 엑셀 품 그대로
인력 = 직종마다 덮개 qty(줄 하나) · 배율(여럿) · 엑셀만 직종 = 더한줄 인력 · 우리만 = 뺀줄
장비 = 시간 같은 같은 갈래 줄은 그대로 · 같은 갈래 남은 줄 = 덮개 qty · 엑셀만 = 더한줄
기계경비 로직(굴착기 GC000267 · 그 밖 GC000268 · 기계 = 옵션 거꾸로 찾기) · 남은 우리 = 뺀줄
자재 = 같은 이름 우리 줄 수량 덮개 · 옮기지 못한 차이 = `못옮김`(버리지 않고 본뜬에 적음)
못옮김 = `[{id, 갈래 노임|기계|자재, 이름, 규격, 단위, 수량(엑셀 1 단위 품), 글, 후보}]` · 사용자 `잇기`
`[{id, 노임|기계|자재: 키}]` = SK 더한줄 인력 · 기계 · 재료(엑셀 수량 그대로) · `{id, 뺌: 까닭}` = 놓음.
SK 단위(728-2) = 엑셀 줄 단위 · 바탕 기준단위와 성격 다름 = 환산 계수 = 우리 품 ÷ 엑셀 품(인력 · 장비
둘 이상이 같은 비일 때만 · `환산` {계수}) · 못 구함 = 고름 대기(까닭 「단위 성격 다름 · 계수 필요」).
고름 대기 = 후보 · 숫자 없음 — SK 안 만듦 · 후보 다섯(사용자가 고른 기초 `pick` = 견줘 SK · 기초) ·
앞 셋 `줄비교`(746-1) = 그 기초로 견준 줄마다 꼴(같음 · 품다름 · 엑셀만 · 기초만 · 1 단위 품) · 못옮김 ·
견줌 못함(단위 성격 다름) = null — 판정 · 순위 · 미리 고름은 안 바꿈.
못옮김 `후보` = M01 요소 셋(`M02_Hopyo_Elem` · 보여주기만).
cid = `kind:키#n`(첫 기초 바탕 줄만 · `M02_Sangeun_Doc.line_ids`) · 더한줄 · 덮개 qty = 바탕 행 몫
(1 단위 품 × 바탕 결과단위 앞 수 · 셈이 ÷ 배) · 더한줄 로직 수량 = 몫 × 바탕 배 ÷ 그 기초 배.
품 견줌 = 우리 셈(`M02_Sangeun_Calc.run` 줄 수량 ÷ 배) · 같음 = `Option.near`(0.5 % + 엑셀 자릿수 반올림).
우리만 자재 줄은 그대로(판정 견줌도 안 봄 · 엑셀은 자재를 일위대가 줄로 따로 적는 일이 많음).
서버 단독 · 읽기만(쓰기 = `M02_Hopyo_Sangeun`).
"""
from __future__ import annotations
import re
from decimal import Decimal
from typing import Any
from common_util import common_util_money as money_fmt
from M02_MasterTemplete import M02_Hopyo_Combine as combine
from M02_MasterTemplete import M02_Hopyo_Elem as elem
from M02_MasterTemplete import M02_Hopyo_Finger as finger
from M02_MasterTemplete import M02_Hopyo_Fit as fit
from M02_MasterTemplete import M02_Hopyo_Option as option
from M02_MasterTemplete import M02_Hopyo_Ours as ours
from M02_MasterTemplete import M02_Sangeun_Calc as calc
from M02_MasterTemplete import M02_Sangeun_Doc as sd
from M02_MasterTemplete.M02_Hopyo_Words import mtypes, specnums
BASE, SK, WAIT = "기초", "SK", "고름 대기"
KEYS = ("덮개", "자재표시", "더한줄", "뺀줄", "못옮김")
TINY = 1e-9
_ADD = re.compile(r"^add:#(\d+)$")
UNIT_WHY = "단위 성격 다름 · 계수 필요"
LOOKS = ("같음", "품다름", "엑셀만", "기초만") # 줄비교 꼴(746-1)
COMPARE = 3 # 고름 대기 후보 앞 몇에 줄비교
LINK = {
"노임": ("인력", "LB"),
"기계": ("기계", "EQ"),
"자재": ("재료", "MT"),
} # 잇기 칸 → 더한줄 · 키 머리
class LinkError(ValueError):
"""잇기 틀림(없는 id · 갈래 다름 · 없는 키) — 부르는 쪽이 400."""
def _near(a: float, b: float) -> bool:
return abs(a - b) <= TINY or option.near(a, b, fit.TOL[0], option.ru(a))
def _num(x: float) -> int | float:
got = Decimal(repr(float(x))).normalize()
return int(got) if got == got.to_integral_value() else float(got)
# ── 우리 셈 → 1 단위 품 ──────────────────────────────────────────────
def _types(sub: str, item: dict, args: str | None, given: dict, auto: dict) -> tuple[set, set]:
"""기계경비 줄 갈래 · 규격 — `M02_Hopyo_Ours._engine` 과 같은 읽기(중간값 없이)."""
ts = set(ours.FIXED_MACH.get(sub, set())) | mtypes(item.get("이름"))
sp = set(specnums(item.get("이름")))
for v in ours._args(args).values():
lit = v.strip("'\"")
if not ours.EQ_CODE.match(lit):
lit = str(given.get(lit) or auto.get(lit) or "")
if ours.EQ_CODE.match(lit):
a, b = ours.eq_of(lit)
ts, sp = ts | a, sp | b
return ts, sp
def parts(doc: dict, whole: Any) -> list[dict[str, Any]]:
"""SK 문서 → 줄마다 `{cid, 갈래 인력|장비|자재|기초|그밖, 이름 · 종류 · 규격, 품(1 단위)}` ·
더한줄 기초 = 그 기초 1 단위 품 × 줄 몫(`L` · `X`)."""
got = calc.resolve(doc, whole)
items = got["줄"]["호표"]
ran = calc.run(doc, whole)
times = Decimal(str(ran["배"]))
labor = ours._labor_names()
auto = {}
if (doc.get("바탕") or {}).get("로직"):
auto = option.m01.mau.values(whole, whole.logic(doc["바탕"]["로직"]))
qty = {x["cid"]: x.get("수량") for x in ran.get("줄") or [] if x.get("cid")}
out = []
for item, cid in zip(items, got["정체"]): # 별도계상 재료 = 셈 줄에서 빠짐 → 품 None
raw = qty.get(cid)
one: dict[str, Any] = {"cid": cid, "갈래": "그밖"}
one["품"] = None if raw is None else float(Decimal(str(raw)) / times)
kind, found = item.get("종류"), ours._SUB.match(str(item.get("요소") or ""))
if kind == "인력":
one.update(갈래="인력", 이름=labor.get(str(item.get("요소")), str(item.get("이름"))))
elif kind == "재료":
one.update(갈래="자재", 이름=str(item.get("이름") or ""), 단위=item.get("단위") or "")
elif kind == "로직" and found:
sub = found.group(1)
if ours.is_mach_logic(whole.logic(sub)):
ts, sp = _types(sub, item, found.group(2), got["입력값"], auto)
one.update(갈래="장비", 종류=ts, 규격=sp)
elif hit := _ADD.match(cid):
given = (doc.get("더한줄") or [])[int(hit[1])].get("입력값") or {}
v = ours.run(sub, given) or {"L": {}, "X": []}
k = (one["품"] or 0.0) * ours.times_of(whole.logic(sub))
one.update(갈래="기초", 로직=sub, L={t: q * k for t, q in v["L"].items()})
one["X"] = [(set(a), set(b), q * k) for a, b, q in v["X"]]
out.append(one)
return out
def measure(doc: dict, whole: Any) -> dict[str, Any]:
"""SK 1 단위 품 — `{인력 {직종: 품}, 장비 [{갈래, 시간}], 자재 {이름: 수량}}`(시험 · 견줌)."""
labor: dict[str, float] = {}
machines, mats = [], {}
for one in parts(doc, whole):
if one["갈래"] == "인력" and one["품"]:
labor[one["이름"]] = labor.get(one["이름"], 0.0) + one["품"]
elif one["갈래"] == "장비" and one["품"]:
machines.append({"갈래": sorted(one["종류"]), "시간": one["품"]})
elif one["갈래"] == "자재" and one["품"]:
mats[one["이름"]] = mats.get(one["이름"], 0.0) + one["품"]
elif one["갈래"] == "기초":
for t, q in one["L"].items():
labor[t] = labor.get(t, 0.0) + q
machines += [{"갈래": sorted(a), "시간": q} for a, _, q in one["X"] if q]
return {"인력": labor, "장비": machines, "자재": mats}
# ── 차이 → 덮개 · 더한줄 · 뺀줄 ──────────────────────────────────────
class _Plan:
def __init__(self, doc: dict, whole: Any) -> None:
self.doc, self.whole = doc, whole
self.tb = float(calc.ratio_of(doc, whole.logic(doc["바탕"]["로직"]), whole))
self.lines = parts(doc, whole)
self.lost: list[dict[str, Any]] = []
self.lb = {name: key for key, name in ours._labor_names().items()}
self.rows: list[dict[str, Any]] = [] # 줄비교(746-1)
self.prices: dict[str, float] = {} # 엑셀 단가(못옮김 후보 차례)
def note(self, kind: str, name: str, excel: float, mine: float | None, **more: Any) -> None:
"""줄비교 한 줄 — 꼴 = 같음 · 품다름 · 엑셀만 · 기초만(`꼴` 주면 그대로)."""
look = more.pop("꼴", None)
if look is None:
if excel and mine:
look = LOOKS[0] if _near(excel, mine) else LOOKS[1]
else:
look = LOOKS[2] if excel else LOOKS[3]
num = lambda x: _num(float(f"{x:.10g}")) if x else None # noqa: E731
one = {
"갈래": kind,
"이름": name,
"규격": "",
"단위": "",
"엑셀": num(excel),
"기초": num(mine),
}
self.rows.append({**one, **more, "꼴": look})
def cover(self, cid: str, **one: float) -> None:
self.doc["덮개"][cid] = {k: _num(v) for k, v in one.items()}
def drop(self, cid: str) -> None:
if cid not in self.doc["뺀줄"]:
self.doc["뺀줄"].append(cid)
def add(self, one: dict[str, Any]) -> None:
self.doc["더한줄"].append(one)
def miss(self, kind: str, name: str, qty: float, text: str, **more: Any) -> None:
"""못옮김 한 줄 — id = 「갈래:이름」(같은 것 = #n) · 수량 = 엑셀 1 단위 품."""
base = f"{kind}:{name}"
n = sum(1 for x in self.lost if (x["갈래"], x["이름"]) == (kind, name))
one = {"id": base if not n else f"{base}#{n}", "갈래": kind, "이름": name, "규격": "",
"단위": "", "수량": _num(float(f"{qty:.10g}")), "글": text, "후보": []} # fmt: skip
one = {**one, **more}
self.lost.append({**one, "후보": elem.lost(one, self.prices.get(name))})
def link(self, picks: list[dict[str, Any]]) -> None:
"""사용자 잇기 — 이은 줄 = 더한줄(엑셀 수량 × 배) · 뺌 = 놓음 · 나머지 못옮김 그대로."""
for raw in picks:
got = next((x for x in self.lost if x["id"] == str(raw.get("id"))), None)
if got is None:
raise LinkError(f"없는 못옮김 「{raw.get('id')}」")
if raw.get("뺌"):
self.lost.remove(got)
self.doc.setdefault("놓음", []).append({**got, "뺌": str(raw["뺌"])})
continue
kind, head = LINK.get(got["갈래"], ("", ""))
key = str(raw.get(got["갈래"]) or "")
if not kind or not key.startswith(head):
raise LinkError(f"「{got['id']}」 · {got['갈래']} 키({head}…)로 이음")
try:
elem = self.whole.get(key) or {}
except Exception as e: # noqa: BLE001 — 없는 키
raise LinkError(f"「{got['id']}」 · 없는 키 「{key}」") from e
self.lost.remove(got)
name = str(elem.get("이름") or got["이름"])
unit = got["단위"] or ("인" if kind == "인력" else str(elem.get("단위") or ""))
self.add({"종류": kind, "요소": key, "이름": name, "단위": unit,
"수량": _num(float(got["수량"]) * self.tb)}) # fmt: skip
def labor(self, excel: dict[str, float]) -> None:
mine = [x for x in self.lines if x["갈래"] == "인력" and not x["cid"].startswith("add:")]
mine = [{**x, "품": x["품"] or 0.0} for x in mine]
extra: dict[str, float] = {}
for x in self.lines:
for t, q in (x.get("L") or {}).items():
extra[t] = extra.get(t, 0.0) + q
for t in sorted(set(excel) | {x["이름"] for x in mine if x["품"]} | set(extra)):
a, rows = excel.get(t, 0.0), [x for x in mine if x["이름"] == t]
b, f = sum(x["품"] for x in rows), extra.get(t, 0.0)
self.note("인력", t, a, b + f, 단위="인")
if a and _near(a, b + f):
continue
want = a - f
if want <= TINY:
for x in rows:
self.drop(x["cid"])
if want < -TINY:
self.miss(
"노임",
t,
a,
f"엑셀 {money_fmt.fmt(a)} · 더한 기초만 {money_fmt.fmt(f)}",
단위="인",
)
elif not rows:
if t in self.lb:
unit = "인"
self.add({"종류": "인력", "요소": self.lb[t], "이름": t, "단위": unit,
"수량": _num(want * self.tb)}) # fmt: skip
else:
self.miss(
"노임", t, want, f"엑셀 {money_fmt.fmt(a)} — 우리 인력에 없음", 단위="인"
)
elif len(rows) == 1 or not b:
self.cover(rows[0]["cid"], qty=want * self.tb)
for x in rows[1:]:
self.drop(x["cid"])
else:
for x in rows:
self.cover(x["cid"], 배율=want / b)
def machines(self, excel: list[dict[str, Any]], line: dict[str, Any]) -> None:
mine = [x for x in self.lines if x["갈래"] == "장비" and x["품"]]
fixed = [(a, q) for x in self.lines for a, _, q in x.get("X") or [] if q]
used, rest = set(), []
for m in excel:
kinds, h = set(m["갈래"]), m["시간"]
hit = next((x for x in mine if x["cid"] not in used and x["종류"] & kinds
and _near(h, x["품"])), None) # fmt: skip
if hit is not None:
used.add(hit["cid"])
self._mnote(m, h, hit["품"])
continue
at = next((i for i, (a, q) in enumerate(fixed) if a & kinds and _near(h, q)), None)
if at is not None:
self._mnote(m, h, fixed.pop(at)[1])
continue
rest.append(m)
for m in rest:
kinds, sp = set(m["갈래"]), set(m.get("규격") or [])
same = [x for x in mine if x["cid"] not in used and x["종류"] & kinds]
same.sort(key=lambda x: not (sp and sp & x["규격"]))
if same:
used.add(same[0]["cid"])
self._mnote(m, m["시간"], same[0]["품"])
self.cover(same[0]["cid"], qty=m["시간"] * self.tb)
else:
self._mnote(m, m["시간"], None)
self._add_machine(m, line)
for x in mine:
if x["cid"] not in used:
self._mnote({"갈래": sorted(x["종류"]), "규격": sorted(x["규격"])}, 0.0, x["품"])
self.drop(x["cid"])
for a, q in fixed:
self._mnote({"갈래": sorted(a)}, 0.0, q)
self.miss("기계", "·".join(sorted(a)), q, "우리만 장비 — 더한 기초 안", 단위="시간")
def _mnote(self, m: dict[str, Any], excel: float, mine: float | None) -> None:
spec = " ".join(f"{x:g}" for x in sorted(m.get("규격") or []))
self.note("장비", "·".join(sorted(m["갈래"])), excel, mine, 규격=spec, 단위="시간")
def _add_machine(self, m: dict[str, Any], line: dict[str, Any]) -> None:
"""엑셀만 장비 → 기계경비 로직 줄(기계 = 그 갈래 · 규격 옵션 거꾸로 찾기)."""
fp = finger.empty(line)
fp["장비"] = [{**m, "시간": 1.0, "밑시간": 1.0, "율": []}]
kinds = set(m["갈래"])
first = next((c for k, c in combine.MACHINE.items() if k in kinds), combine.MACHINE_ANY)
for code in [first, *sorted(combine.MACHINE_SET - {first})]:
got = option.find(code, fp, [])
if got["수준"] <= 1:
name = " ".join([*m["갈래"], *(f"{x:g}" for x in m.get("규격") or [])])
self.add({"종류": "로직", "로직": code, "입력값": got["입력"], "이름": name,
"단위": "시간", "수량": _num(m["시간"] * self.tb)}) # fmt: skip
return
spec = " ".join(f"{x:g}" for x in m.get("규격") or [])
name = "·".join(m["갈래"])
self.miss("기계", name, m["시간"], "엑셀만 장비 — 기계 못 찾음", 규격=spec, 단위="시간")
def materials(self, excel: list[dict[str, Any]]) -> None:
mine = [x for x in self.lines if x["갈래"] == "자재" and not x["cid"].startswith("add:")]
for m in excel:
words = option.mat_words(m["이름"])
rows = [x for x in mine if option._named(words, x["이름"])]
unit = next((x["단위"] for x in rows if x.get("단위")), "")
if rows and all(x["품"] is None for x in rows): # 우리 = 별도계상 · 엑셀 = 블록 안
self.note("자재", m["이름"], m["품"], None, 단위=unit, 꼴=LOOKS[1])
self.doc["자재표시"][rows[0]["cid"]] = {"별도계상": "직접계산"}
self.cover(rows[0]["cid"], qty=m["품"] * self.tb)
continue
rows = [x for x in rows if x["품"] is not None]
self.note("자재", m["이름"], m["품"], sum(x["품"] for x in rows) or None, 단위=unit)
if rows and _near(m["품"], sum(x["품"] for x in rows)):
continue
if rows:
self.cover(rows[0]["cid"], qty=m["품"] * self.tb)
for x in rows[1:]:
self.drop(x["cid"])
else:
self.miss("자재", m["이름"], m["품"], "엑셀만 자재")
def _extras(entry: dict, code: str, whole: Any, tb: float) -> list[dict[str, Any]]:
"""조합 둘째부터(체크된 로직) = 더한줄 로직 — 수량 = 몫 × 바탕 배 ÷ 그 기초 배."""
out = []
for c in entry.get("조합") or []:
if not c.get("체크") or c["로직"] == code:
continue
given = ((entry.get("옵션") or {}).get(c["로직"]) or {}).get("입력") or {}
times = ours.times_of(whole.logic(c["로직"]))
qty = float(c.get("몫") or 1) * tb / times
out.append({"종류": "로직", "로직": c["로직"], "입력값": given, "수량": _num(qty)})
return out
def _need(code: str, unit: str, whole: Any) -> dict[str, Any] | None:
"""후보 기초 기준단위 ↔ 엑셀 단위가 성격 다름 = 계수 칸 `{가, 나, 갈래 사용자|단위중량}` · 아니면 None."""
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
try:
got = calc.base_unit(whole.logic(code))
except Exception: # noqa: BLE001 — 없는 로직
return None
if not got or str(unit or "").strip() in ("", "식"):
return None
swap = combo_store.mu.swap(got[0], unit)
return None if swap["배"] is not None else {"가": got[0], "나": unit, "갈래": swap["계수"]}
def compare(
entry: dict, block: dict | None, line: dict, whole: Any, code: str, efp: dict | None = None
) -> dict | None:
"""고른 기초 `code` 로 견준 줄비교 `{줄, 수, 못옮김, 환산?}`(만들지 않음) · 견줌 못함 = None."""
got = plan(entry, block, line, whole, {"로직": code}, efp)
if got["갈래"] == WAIT:
return None
rows = got["줄비교"]
out = {"줄": rows, "수": {k: sum(x["꼴"] == k for x in rows) for k in LOOKS}}
out["못옮김"] = got["못옮김"]
return out | ({"환산": got["환산"]} if got.get("환산") else {})
def _wait(
entry: dict, whole: Any, unit: str, why: str | None = None, at: tuple | None = None
) -> dict[str, Any]:
"""고름 대기 — `at` = (맞춤셈, 블록, 줄 · 사용자 고름 없을 때) 이면 앞 셋에 줄비교."""
keep = ("로직", "이름", "단위", "판정", "글")
cands = [{k: c.get(k) for k in keep} for c in (entry.get("후보") or [])[: fit.TOP_OPT]]
for i, c in enumerate(cands):
c["계수"] = _need(str(c.get("로직") or ""), unit, whole)
if at and i < COMPARE and c.get("로직"):
c["줄비교"] = compare(*at[:3], whole, str(c["로직"]), *at[3:])
if why is None:
why = "맞는 기초 없음 — " + ((entry.get("판정") or {}).get("글") or "후보 없음")
return {"갈래": WAIT, "까닭": why, "후보": cands}
def _by_type(rows: list[dict[str, Any]]) -> dict[str, float]:
out: dict[str, float] = {}
for x in rows:
k = "+".join(sorted(x["갈래"]))
out[k] = out.get(k, 0.0) + x["시간"]
return out
def _coef(doc: dict, whole: Any, excel: dict[str, Any]) -> float | None:
"""성격 다른 단위 계수(바탕 앞 수 뗀 1 단위 = SK 몇 단위) = 우리 품 ÷ 엑셀 품 — 인력 직종 · 장비 갈래
둘 이상이 같은 비(맞춤 허용 폭)일 때만 · 아니면 None(고름 대기)."""
mine = measure({**doc, "단위": ""}, whole) # 「」 = 앞 수 뗀 바탕 단위(옛 셈)
pairs = [(mine["인력"].get(t, 0.0), q) for t, q in excel["인력"].items()]
ours_m = _by_type(mine["장비"])
pairs += [(ours_m.get(k, 0.0), q) for k, q in _by_type(excel["장비"]).items()]
rates = [a / b for a, b in pairs if a > TINY and b > TINY]
if len(rates) < 2 or max(rates) > min(rates) * (1 + fit.TOL[0]):
return None
return float(f"{sum(rates) / len(rates):.6g}")
def plan(
entry: dict | None,
block: dict | None,
line: dict[str, Any],
whole: Any,
pick: dict | None = None,
efp: dict | None = None,
) -> dict[str, Any]:
"""블록 한 칸 → `{갈래 기초|SK|고름 대기, 바탕, 입력값, 환산?, 덮개, 자재표시, 더한줄, 뺀줄, 못옮김,
놓음, 엑셀품, 후보?}` · `pick` = 사용자 `{로직?, 입력값?, 환산?, 잇기?}` — 로직 = 고른 기초(옵션 없으면
거꾸로 찾음 · 늘 견줌) · 환산 = SK `환산` 꼴 · 잇기 = 못옮김 잇기(`_Plan.link` · 틀림 `LinkError`)."""
entry, pick = entry or {}, pick or {}
state = (entry.get("판정") or {}).get("상태")
efp = efp or finger.excel(block, line) # 줄비교 = 한 번 푼 지문
unit = str(line.get("단위") or "").strip() or "식"
if pick.get("로직"):
code = str(pick["로직"])
given = pick.get("입력값")
if given is None:
given = option.find(code, efp, (block or {}).get("글") or [])["입력"]
elif state in ("맞음", "다른 점") and entry.get("차례"):
code = entry["차례"][0]
given = ((entry.get("옵션") or {}).get(code) or {}).get("입력") or {}
else:
return _wait(entry, whole, unit, at=(entry, block, line, efp))
doc = {"키": "SK-000000", "이름": "견줌", "단위": unit, "바탕": {"로직": code},
"입력값": dict(given), "덮개": {}, "뺀줄": [], "자재표시": {}} # fmt: skip
if pick.get("환산"):
doc["환산"] = dict(pick["환산"])
from M02_MasterTemplete import M02_Hopyo_Verdict as verdict # 판정이 Fit 을 부름 — 늦게 읽음
excel = verdict.excel_qty(efp)
row = whole.logic(code)
def extras(tb: float) -> list[dict[str, Any]]:
return [] if pick.get("로직") else _extras(entry, code, whole, tb)
try:
tb = float(calc.ratio_of(doc, row, whole))
except sd.DocError as e:
old = {**doc, "더한줄": extras(float(calc.times_of(row)))}
coef = None if pick.get("환산") else _coef(old, whole, excel)
if coef is None:
cands = entry.get("후보") or []
if code not in [c.get("로직") for c in cands]:
cands = [{"로직": code, "이름": row.get("이름")}, *cands]
why = f"{UNIT_WHY} — {str(e).split(' · ', 1)[-1]}"
at = None if pick else (entry, block, line, efp)
return _wait({"후보": cands}, whole, unit, why, at)
doc["환산"] = {"계수": coef}
tb = float(calc.ratio_of(doc, row, whole))
doc["더한줄"] = extras(tb)
head = {"바탕": doc["바탕"], "입력값": doc["입력값"], "엑셀품": excel}
head |= {"환산": doc["환산"]} if doc.get("환산") else {}
if state == "맞음" and not pick and not doc["더한줄"]:
base = {**dict.fromkeys(KEYS[:2], {}), **dict.fromkeys(KEYS[2:], []), "놓음": []}
return {**head, "갈래": BASE, **base}
work = _Plan(doc, whole)
work.prices = elem.prices(block)
work.labor({t: x["품"] for t, x in efp["인력"].items() if x.get("품")})
work.machines([m for m in efp["장비"] if m.get("시간")], line)
work.materials([m for m in efp["자재"] if m.get("품")])
work.link(pick.get("잇기") or [])
out = {k: doc[k] for k in KEYS[:4]}
changed = any(out.values()) or bool(work.lost)
kept = {"못옮김": work.lost, "놓음": doc.get("놓음") or [], "줄비교": work.rows}
return {**head, **out, "갈래": SK if changed else BASE, **kept}