"""M02 단가산출(SK) 수식 탭 — SK 셈 답 기준 소계 · 합계 · 고친 칸 표지(PLAN 796-2c · 요구 657). 수식 탭 줄(`M02_Sangeun_Equation`) = 바탕 사본 줄(+ 수식고침). 그 위에 SK 셈 답 (`M02_Sangeun_Calc.calc_doc` = 화면 · 저장 · B09 가 쓰는 한 벌)을 얹어 화면 금액 = SK 금액 (셈은 안 바꿈 · 보이기만): 합계 = 셈 답 `기준`(노 · 재 · 경 · 계 · 1 단위) — 바탕 줄만 셈한 합계 대신 소계 = 셈 답 `줄` → 돈 묶음(801-3 · `master_equation_group`) — 더한 · 부른 단가산출(갈래 블록) 하나씩 {cid, 이름, 상태, 수량, 노 · 재 · 경 · 계, 덮개?, 원래?, 까닭?, 나무?} + 직접 줄(인력 · 재료 · 기계 · 덧줄 · 율) 한 묶음 {갈래 직접, 이름, 노 · 재 · 경 · 계, 낱줄?(더함 · 덮음 줄)} + 끝수(합계 − 묶음 합 · 까닭 글) · 밖(합계 밖 줄 — 엔진이 합계에서 뺌 · 이름 · 밖금액) 상태 = 바탕(그대로) · 덮음(품 · 단가 · 상수 · 표값 · 식 덮개 · 변수줄) · 더함(더한줄) · 뺌(뺀줄 · 금액 0) 나무 = 더한 기초(더한줄 로직)의 수식(세부 덮개 · 뺀줄 얹은 줄) — 화면은 접힘 블록 표지 = 수식 탭 줄에 고친 칸 {표지[갈래], 원래, 지금, 뺌} — 줄 값 = 식 덮개 · 상수 · 표값 · 변수줄을 얹은 식(`M02_Sangeun_Const.formulas`) · 품 · 단가 덮개는 줄 값에 안 얹고 `지금` 으로 보임(고침 주소 = 바탕 차례 그대로) 셈 답이 안 서는 SK(틀린 문서 · 돈 아닌 로직) = 바탕 줄 합계 그대로 + `답까닭`. 시험 `resources/tester/test_796_2c_m02_equation_full.py`. """ from __future__ import annotations import re from decimal import Decimal from typing import Any from M02_MasterTemplete import M02_Sangeun_Doc as sd COSTS = ("노무비", "재료비", "경비") SUMS = (*COSTS, "계") _ADD = re.compile(r"^add:#(\d+)$") _HEAD = {"labor": "자원", "material": "자원", "machine": "자원", "logic": "블록", "pct": "덧줄"} _ADD_KIND = {"인력": "자원", "재료": "자원", "기계": "자원"} _WORD = {"labor": "인력", "material": "재료", "machine": "기계"} # 직접 묶음 이름(801-3) def _d(value: Any) -> Decimal: return Decimal(str(value)) if value is not None else Decimal(0) def _errors() -> tuple: from M02_MasterTemplete import M02_MasterTemplete_Store as store return (sd.DocError, store.StoreError, ArithmeticError, KeyError, TypeError, ValueError) def answer(doc: dict, inputs: dict | None) -> dict | None: """SK 셈 답 — 못 셈(틀린 문서 · 돈 아닌 로직) = None.""" from M02_MasterTemplete import M02_Sangeun_Calc as calc try: got = calc.calc_doc(doc, inputs or None, False) except _errors(): return None return got if got.get("기준") else None def total(ans: dict | None) -> dict | None: return {k: _d(ans["기준"].get(k)) for k in SUMS} if ans else None # ── 행 ──────────────────────────────────────────────────────────────── def _kind(cid: str, adds: list) -> str: hit = _ADD.match(cid) if not hit: return _HEAD.get(cid.split(":", 1)[0], "자원") one = adds[int(hit[1])] if int(hit[1]) < len(adds) else {} if sd.is_rate(one): return "율" return _ADD_KIND.get(str(one.get("종류")), "블록") def _word(cid: str, adds: list) -> str: """직접 줄의 자원 갈래 말 — 인력 · 재료 · 기계(그 밖 = 빈 글).""" hit = _ADD.match(cid) if not hit: return _WORD.get(cid.split(":", 1)[0], "") one = adds[int(hit[1])] if int(hit[1]) < len(adds) else {} return str(one.get("종류")) if one.get("종류") in _ADD_KIND else "" def _row(cid: str, kind: str, name: str, state: str, parts: dict | None, **more: Any) -> dict: cost = {k: _d((parts or {}).get(k)) for k in COSTS} return { "cid": cid, "갈래": kind, "이름": name, "상태": state, **cost, "계": sum(cost.values(), Decimal(0)), **{k: v for k, v in more.items() if v not in (None, [], "")}, } def rows(doc: dict, ans: dict, tree: Any, out_ids: set[str] = frozenset()) -> list[dict]: """셈 답 줄 → 행(셈 답 순서) + 뺀줄(금액 0) — `tree(더한줄 차례)` = 더한 기초 수식.""" adds = doc.get("더한줄") or [] spots = (ans.get("시트") or {}).get("자리") or {} out = [] for line in ans.get("줄") or []: cid = str(line.get("cid") or "") hit = _ADD.match(cid) info = spots.get(cid) or {} cover = [k for k in info.get("덮개") or [] if k != "뺌"] or [ k for k in line.get("덮개") or [] ] k = int(hit[1]) if hit else -1 state = "더함" if hit else "덮음" if cover else "바탕" one = adds[k] if 0 <= k < len(adds) else {} if hit and one.get("종류") == "로직" and sd.sub_patched({**one, "끊음": None}): cover = cover or ["세부"] outside = cid in out_ids # 합계 밖 줄(가져온 식 `합밖` · 제외 단계) — 합에 안 듦 out.append( _row( cid, _kind(cid, adds), str(line.get("이름") or info.get("이름") or ""), state, None if outside else line.get("비목"), 합밖=outside or None, 밖금액=sum((_d(v) for v in (line.get("비목") or {}).values()), Decimal(0)) if outside else None, 수량=line.get("수량"), 종류=_word(cid, adds), 덮개=cover, 원래=info.get("원래") if state == "덮음" else None, 까닭=line.get("까닭") or info.get("까닭"), 나무=tree(k) if hit and one.get("종류") == "로직" else None, 블록=k if hit and one.get("종류") == "로직" else None, # 더한 기초 차례(연결 끊기) 끊음=True if one.get("끊음") else None, ) ) for cid in doc.get("뺀줄") or []: # 지운 줄 — 금액 0 · 원래 금액만 보임 info = spots.get(cid) or {} out.append( _row( cid, _kind(cid, adds), str(info.get("이름") or ""), "뺌", None, 원래=info.get("원래"), 까닭=info.get("까닭"), ) ) return out def _why(ans: dict) -> str: """끝수 까닭 — 합계는 엔진이 비목마다 합쳐 버린 값 · 행은 줄마다 버린 값.""" text = ["줄마다 버린 값의 합 ≠ 엔진 비목 합(품셈 1-2-2 끝수)"] if _d(ans.get("화면배") or 1) != 1: text.append(f"단위 환산 ÷ {ans['화면배']}") rate = ans.get("켜쌓기율") if rate not in (None, 100): text.append(f"켜쌓기율 {rate} %") return " · ".join(text) # ── 더한 기초 수식 ───────────────────────────────────────────────────── def _basic_tree(whole: Any, doc: dict, one: dict, inputs: dict | None) -> dict | None: """더한줄 로직 하나의 수식 — 세부 덮개 · 뺀줄 얹은 줄 · 입력 = 줄 값 › 공통 옵션 › 자동.""" from M02_MasterTemplete import M02_Sangeun_Equation as eqm if any(one.get(k) not in (None, {}, []) for k in sd.BODY): return None if one.get("끊음"): # 끊은 기초(796-2e) = 자기 사본 줄 + 수식 고침(고침 주소 그대로) from M02_MasterTemplete import M02_Sangeun_Block as blk return blk.view(whole, doc, one, None, inputs)[0] code = str(one["로직"]) mine = {k: v for k, v in (one.get("입력값") or {}).items() if str(v).strip()} given = {**(doc.get("입력값") or {}), **(inputs or {}), **mine} try: row = sd.patched(whole.logic(code), one)["줄"] return eqm.export(whole, {**row, "키": code}, given) except Exception: # noqa: BLE001 — 엔진 FormulaError · 기초 하나가 틀려도 나머지는 그림 return None # ── 줄 표지 ──────────────────────────────────────────────────────────── def _mark(eq: dict, base: dict, ans: dict, doc: dict) -> None: """수식 탭 줄에 고친 칸 표지 — 자리 = 바탕 cid(호표 · 덧줄) · `mid:이름`.""" spots = (ans.get("시트") or {}).get("자리") or {} ids = sd.line_ids(base) heads = len(base.get("호표") or []) for line in eq.get("줄") or []: if line["갈래"] == "중간": spot = f"mid:{line['열쇠']}" elif line["갈래"] == "호표": spot = ids[line["차례"]] if line.get("차례", 0) < len(ids) else "" elif line["갈래"] == "덧줄": at = heads + line.get("차례", 0) spot = ids[at] if at < len(ids) else "" else: continue info = spots.get(spot) or {} kinds = list(info.get("덮개") or []) if not kinds: continue line["표지"] = [k for k in kinds if k != "뺌"] line["원래"], line["지금"] = info.get("원래"), info.get("지금") if "뺌" in kinds: line["뺌"] = True def _outside(base: dict) -> set[str]: """합계 밖 줄 cid — 엔진이 비목 합에서 빼는 줄(`master_formula_sum.skipped`).""" import master_cut as mcut # M01 이 길을 둔 뒤(수식 탭 export 가 먼저) if not mcut.ms.uses(base): return set() ids = sd.line_ids(base) return {ids[i] for i in mcut.ms.skipped(base) if i < len(ids)} # ── 붙이기 ──────────────────────────────────────────────────────────── def attach( eq: dict, doc: dict, whole: Any, base: dict, inputs: dict | None, ans: dict | None ) -> dict: """수식 `eq` 에 SK 셈 답 합계 · 소계 · 표지를 얹음(제자리) — 답 없음 = `답까닭` 만.""" import master_equation_group as grp # M01 이 길을 둔 뒤 if ans is None: return eq | {"답까닭": "단가산출 셈 답이 서지 않음 — 바탕 줄 합계를 보임"} _mark(eq, base, ans, doc) adds = doc.get("더한줄") or [] table = rows( doc, ans, lambda k: _basic_tree(whole, doc, adds[k], inputs) if k >= 0 else None, _outside(base), ) got = grp.bundles(table) # 801-3 묶음 — 부른 · 더한 단가산출 하나씩 + 직접 줄 한 묶음 now = total(ans) or {} rest = {k: now[k] - sum((r[k] for r in got["줄"]), Decimal(0)) for k in SUMS} eq["합계"] = now eq["소계"] = {"줄": got["줄"], "밖": got["밖"], "끝수": rest, "끝수까닭": _why(ans)} return eq