- 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>
227 lines
10 KiB
Python
227 lines
10 KiB
Python
"""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
|