Files
Aislo/M02_MasterTemplete/M02_Sangeun_Equation_Full.py
T
eomsangdonandClaude Sonnet 5.5 be43dad64e feat(796-2e-1): 더한 기초 블록 [연결 끊기] · [다시 잇기] 서버 — 끊음 = 이 SK 안 자기 사본 줄 · 수식 고침 · 재생성 때 줄 안 바꿈
- M02_Sangeun_Block 신설: 끊기(닫힌 묶음 굳힘 · 금액 그대로) · 다시 잇기(고침 버림 + 원본 갈음) · 끊은 기초 수식 읽기 · 미리 셈 · 저장 검사
- Calc.sub_run 이 끊은 기초 자기 줄 + 수식 고침을 얹어 셈 · Copy.settle 이 다시 잇기 표지 처리 · [단가 갱신] · [재생성] 응답 끊은기초 한 줄
- Router: block/unlink · relink · equation 몸 블록 · 새 시험 test_796_2e_unlink_basis 6

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 15:38:36 +09:00

227 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""M02 단가산출(SK) 수식 탭 — SK 셈 답 기준 소계 · 합계 · 고친 칸 표지(PLAN 796-2c · 요구 657).
수식 탭 줄(`M02_Sangeun_Equation`) = 바탕 사본 줄(+ 수식고침). 그 위에 SK 셈 답
(`M02_Sangeun_Calc.calc_doc` = 화면 · 저장 · B09 가 쓰는 한 벌)을 얹어 화면 금액 = SK 금액
(셈은 안 바꿈 · 보이기만):
합계 = 셈 답 `기준`(노 · 재 · 경 · 계 · 1 단위) — 바탕 줄만 셈한 합계 대신
소계 = 셈 답 `줄` 한 줄마다 한 행 {cid, 갈래(자원 | 블록 | 덧줄 | 율), 이름, 상태, 수량,
노 · 재 · 경 · 계, 덮개?, 원래?, 까닭?, 나무?, 합밖?, 밖금액?} +
끝수(합계 − 행 합 · 까닭 글)
합밖 줄(엔진이 합계에서 뺌) = 행 금액 0 + 밖금액
상태 = 바탕(그대로) · 덮음(품 · 단가 · 상수 · 표값 · 식 덮개 · 변수줄) ·
더함(더한줄) · 뺌(뺀줄 · 금액 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 = {"인력": "자원", "재료": "자원", "기계": "자원"}
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 _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("수량"),
덮개=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 셈 답 합계 · 소계 · 표지를 얹음(제자리) — 답 없음 = `답까닭` 만."""
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),
)
now = total(ans) or {}
rest = {k: now[k] - sum((r[k] for r in table), Decimal(0)) for k in SUMS}
eq["합계"] = now
eq["소계"] = {"줄": table, "끝수": rest, "끝수까닭": _why(ans)}
return eq