feat(802-1): 단가산출 수식 문서 꼴 · 계산기(파이썬 · TS 짝) · 표지 · 꼴 문서
- 꼴: 변수표(기호 · 첨자 · 이름 · 단위 · 연결 = 숫자 · 선택 · 입력 · 요소 · 표 · 단가산출 · 식) + 식 줄 + 금액 줄 + 끝수 칸 — resources/master_data/_수식문서_틀.md - 풀이 · 글로 되돌림 · 기호 · 이름 바꿈 전파 · 정적 검사 = master_doc.py ↔ common_util_formula_doc.ts - 셈 = master_doc_calc.py ↔ common_util_formula_doc_calc.ts — 기호는 필요할 때 셈 · 정의 필요 · 순환 · 0 나눔 · 식 틀림 · 빈 문서 = 표지 + 금액 없음 · 값 없음 = 그 줄만 비움 · 묶음 소계 · 합계 · 줄 자리 · 끝전 큰나머지 · 계 끝수 · 조정 · 번지 · 단계 · 합밖 - M01 연결 풀이(표 찾기 · 요소 · 단가산출 문서) = master_doc_link.py ↔ common_util_formula_doc_table.ts(표 찾기) · 옛 엔진 셈 안 부름 - 옛 로직 변환 = master_doc_convert.py(802-2 빌더가 부름) - 시험 test_802_1_formula_doc 48(꼴 · 셈 · 지움 견딤 300 넘는 지운 문서 예외 0 · 표지 · 거울 파이썬 ↔ TS 60 문서) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,564 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""옛 M01 로직 줄 → 수식 문서 변환 (PLAN 802-2 · 요구서_664 1 장 3 · 꼴 `_수식문서_틀.md`).
|
||||
|
||||
빌더 `build_수식문서_802_2.py` 가 부름 · 다시 돌리면 같은 결과.
|
||||
- 기호 = 800-1 · 800-5 기호(수식 탭이 보이는 것
|
||||
· `master_equation.export` + `master_equation_sign.settle`) ·
|
||||
못 내보내는 로직은 같은 규칙(`master_symbol`)으로 · 한 문서 안 겹침 = 아래첨자 끝 숫자.
|
||||
- 입력 = 선택(고르기) · 입력 · 값 = 기본 옵션(`master_equation.given_of`) · 중간 = 식 줄
|
||||
· 한 마디 중간
|
||||
(표 찾기 · 요소 · 로직 부름 하나) = 그 기호에 바로 연결 · 표 찾기 · 요소
|
||||
· 로직 부름 = 기호 + 연결.
|
||||
- 호표 = 금액 줄(수량 × 단가 · 단가 = 요소 · 재료 고르기 · 단가산출 묶음)
|
||||
· 덧줄 = 금액 줄(단가 = 식) ·
|
||||
`줄.'이름'` · 앞 덧줄 이름 = 그 금액 줄 기호 · 끝수 결과 · 표시 = 식 안 반올림
|
||||
· 금액 끝수 = 문서 `끝수`.
|
||||
옛 엔진 셈은 안 함(기호 · 이름 보기만 export 를 씀).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from decimal import Decimal
|
||||
|
||||
import master_doc as md
|
||||
import master_equation as meq
|
||||
import master_equation_edit as mee
|
||||
import master_equation_sign as mes
|
||||
import master_formula as mf
|
||||
import master_material as mm
|
||||
import master_symbol as msym
|
||||
|
||||
_ERR = (mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueError, IndexError)
|
||||
SUBMAX = msym.SUBMAX
|
||||
|
||||
|
||||
def _harvest(master, key: str) -> dict:
|
||||
"""수식 탭 내보내기 기호 — (꼴, 갈래, 식 | 열쇠) → 마디 · 못 내보내면 빈 것."""
|
||||
try:
|
||||
ex = mes.settle(meq.export(master, key))
|
||||
except _ERR:
|
||||
return {}
|
||||
out: dict = {}
|
||||
|
||||
def visit(t):
|
||||
if isinstance(t, list):
|
||||
for x in t:
|
||||
visit(x)
|
||||
elif isinstance(t, dict):
|
||||
if t.get("꼴") in ("변수", "블록"):
|
||||
out.setdefault((t["꼴"], t.get("갈래"), str(t.get("식") or t.get("열쇠"))), t)
|
||||
for k, v in t.items():
|
||||
if k != "나무" and isinstance(v, (dict, list)):
|
||||
visit(v)
|
||||
|
||||
for line in ex.get("줄") or []:
|
||||
visit(line.get("이름식"))
|
||||
if isinstance(line.get("결과"), dict):
|
||||
head = line["결과"]
|
||||
out.setdefault(("머리", line.get("갈래"), str(head.get("열쇠"))), head)
|
||||
return out
|
||||
|
||||
|
||||
def _clean(text: str) -> str:
|
||||
"""기호 · 첨자 글 다듬기 — 낱말 아닌 글 뺌."""
|
||||
return re.sub(r"[^\w′'·]|_", "", str(text or ""))
|
||||
|
||||
|
||||
class _Reg:
|
||||
"""기호 자리 — 열쇠마다 (기호, 아래, 위) 제안 → 겹침 없는 이름."""
|
||||
|
||||
def __init__(self):
|
||||
self.props: dict[tuple, tuple[str, str, str]] = {}
|
||||
self.ids: dict[tuple, str] = {}
|
||||
self.used: set[str] = set()
|
||||
self.notes: list[str] = []
|
||||
|
||||
def want(self, key: tuple, parts: tuple[str, str, str]) -> None:
|
||||
self.props.setdefault(key, parts)
|
||||
|
||||
def settle(self) -> None:
|
||||
for key, (sign, low, up) in self.props.items():
|
||||
if key in self.ids:
|
||||
continue
|
||||
if not md.good_id(md.sym_id(sign, "", "")):
|
||||
fixed = _clean(sign)
|
||||
fixed = fixed if fixed and md.good_id(fixed) else "X"
|
||||
self.notes.append(f"기호 「{sign}」 → 「{fixed}」")
|
||||
sign = fixed
|
||||
n, base = 1, low
|
||||
while md.sym_id(sign, low, up) in self.used or not md.good_id(md.sym_id(sign, low, up)):
|
||||
n += 1
|
||||
low = (base or "")[: SUBMAX - len(str(n))] + str(n)
|
||||
if n > 99:
|
||||
raise ValueError(f"기호 못 가름 {key}")
|
||||
self.used.add(md.sym_id(sign, low, up))
|
||||
self.ids[key] = md.sym_id(sign, low, up)
|
||||
|
||||
def id(self, key: tuple) -> str:
|
||||
return self.ids.get(key, "?")
|
||||
|
||||
|
||||
class _Conv:
|
||||
def __init__(self, master, row: dict, given: dict):
|
||||
self.master, self.row, self.given = master, row, given
|
||||
self.key = str(row["키"])
|
||||
self.found = _harvest(master, self.key)
|
||||
self.table = {v["열쇠"]: v for v in meq.table(row, master)}
|
||||
self.reg = _Reg()
|
||||
self.links: dict[tuple, dict] = {} # 열쇠 → 변수 칸(이름 · 단위 · 연결)
|
||||
self.order: list[tuple] = []
|
||||
self.alias: dict[str, tuple] = {} # 한 마디 중간 식 → ('중간', 이름)
|
||||
self.names = {s["이름"] for s in row.get("입력") or []} | {
|
||||
s["이름"] for s in row.get("중간") or []
|
||||
}
|
||||
self.kinds = {s["이름"]: "입력" for s in row.get("입력") or []}
|
||||
self.kinds |= {s["이름"]: "중간" for s in row.get("중간") or []}
|
||||
self.notes: list[str] = []
|
||||
self.used_lines: set[tuple] = set() # 식이 부른 금액 줄 — ('합' | '덧', 이름)
|
||||
self.quiet = "" # 한 마디 중간 연결만 뜰 때(그 마디 식) — 기호 자리 안 잡음
|
||||
self.last: dict = {}
|
||||
|
||||
# 기호 제안
|
||||
def _look(self, kind: str, raw: str) -> dict | None:
|
||||
for k in (("변수", kind, raw), ("블록", None, raw), ("변수", "요소", raw)):
|
||||
if k in self.found:
|
||||
return self.found[k]
|
||||
return None
|
||||
|
||||
def _var(self, key: tuple, family: str, raw: str, name: str, link: dict, unit: str = "") -> str:
|
||||
if self.quiet and self.quiet == raw:
|
||||
self.quiet, self.last = "", link
|
||||
return "?"
|
||||
if key not in self.links:
|
||||
node = self._look(family, raw) or {}
|
||||
sign = str(node.get("기호") or "")
|
||||
if sign and node.get("기호") != node.get("열쇠"):
|
||||
parts = _merge(sign, str(node.get("아래") or ""), str(node.get("위") or ""))
|
||||
else:
|
||||
fam = {"블록": "결과"}.get(family, family)
|
||||
parts = msym.lookup(name or raw, fam if fam in msym.FAMILY else "요소")
|
||||
self.reg.want(key, parts)
|
||||
self.order.append(key)
|
||||
label = str(node.get("이름") or name or raw)
|
||||
self.links[key] = {
|
||||
"이름": label,
|
||||
"단위": str(node.get("단위") or unit or ""),
|
||||
"연결": link,
|
||||
}
|
||||
self.links[key]["연결"] = link # 두 번째 돌이 = 겹침 가른 이름으로 쓴 연결
|
||||
return self.reg.id(key)
|
||||
|
||||
def own(self, name: str) -> str:
|
||||
"""입력 · 중간 이름 → 기호."""
|
||||
if name not in self.names:
|
||||
self.notes.append(f"모르는 이름 「{name}」")
|
||||
return name if md.good_id(name) else "X_" + _clean(name)
|
||||
return self.reg.id((self.kinds[name], name))
|
||||
|
||||
def holes(self, text):
|
||||
if not isinstance(text, str):
|
||||
return text
|
||||
return md.HOLE.sub(lambda m: "{" + self.own(m.group(1)) + "}", text)
|
||||
|
||||
def _place(self) -> dict:
|
||||
out = {}
|
||||
if mm.REGION in self.kinds:
|
||||
out["지역"] = self.own(mm.REGION)
|
||||
if mm.TARIFF in self.kinds:
|
||||
out["계약종별"] = self.own(mm.TARIFF)
|
||||
return out
|
||||
|
||||
# 식
|
||||
def emit(self, node, money: dict | None = None):
|
||||
kind = node[0]
|
||||
if kind in ("num", "str"):
|
||||
return node
|
||||
if kind == "name":
|
||||
name = node[1]
|
||||
if money is not None and name in md.COSTS:
|
||||
return ("var", name)
|
||||
if money is not None and name in money:
|
||||
self.used_lines.add(money[name])
|
||||
return ("var", self.reg.id(money[name]))
|
||||
return ("var", self.own(name))
|
||||
if kind == "ref":
|
||||
return ("var", self.element(node[1]))
|
||||
if kind == "neg":
|
||||
return ("neg", self.emit(node[1], money))
|
||||
if kind == "bin":
|
||||
return ("bin", node[1], self.emit(node[2], money), self.emit(node[3], money))
|
||||
if kind == "call":
|
||||
return ("call", node[1], [self.emit(x, money) for x in node[2]])
|
||||
if kind == "find":
|
||||
return ("var", self.find(node, None))
|
||||
if kind == "logic":
|
||||
return ("var", self.block(node, None))
|
||||
base, col = node[1], node[2]
|
||||
if base[0] == "find":
|
||||
return ("var", self.find(base, col))
|
||||
if base[0] == "logic":
|
||||
return ("var", self.block(base, col))
|
||||
if base[0] == "ref":
|
||||
return ("var", self.element(base[1], col))
|
||||
if base[0] == "name" and base[1] == "줄" and money is not None:
|
||||
self.used_lines.add(("합", col))
|
||||
return ("var", self.reg.id(("합", col)))
|
||||
if base[0] == "field" and base[1][0] == "name" and base[1][1] in ("번지", "단계"):
|
||||
return ("field", ("field", ("var", base[1][1]), base[2]), col)
|
||||
return ("field", self.emit(base, money), col)
|
||||
|
||||
def text(self, raw, money: dict | None = None) -> str:
|
||||
return md.text_of(self.emit(mf.parse(raw), money))
|
||||
|
||||
# 연결 마디
|
||||
def find(self, node, col) -> str:
|
||||
raw = mee.raw_text(node if col is None else ("field", node, col))
|
||||
if raw in self.alias:
|
||||
return self.reg.id(self.alias[raw])
|
||||
link = {"갈래": "표", "표": self.holes(node[1])}
|
||||
link["조건"] = {k: md.text_of(self.emit(v)) for k, v in node[2]}
|
||||
if col is None:
|
||||
self.notes.append(f"칸 없는 찾기 「{raw}」")
|
||||
else:
|
||||
link["칸"] = col
|
||||
return self._var(("찾기", raw), "찾기", raw, col or raw, link)
|
||||
|
||||
def block(self, node, col) -> str:
|
||||
raw = mee.raw_text(node if col is None else ("field", node, col))
|
||||
if raw in self.alias:
|
||||
return self.reg.id(self.alias[raw])
|
||||
link = {"갈래": "단가산출", "키": self.holes(node[1])}
|
||||
link["입력"] = {k: md.text_of(self.emit(v)) for k, v in node[2]}
|
||||
if col is not None:
|
||||
link["칸"] = col
|
||||
try:
|
||||
sub = self.master.logic(mf.fill(node[1], {})) if "{" not in node[1] else {}
|
||||
except _ERR:
|
||||
sub = {}
|
||||
unit = str(sub.get("결과단위") or "")
|
||||
return self._var(("블록", raw), "블록", raw, str(sub.get("이름") or node[1]), link, unit)
|
||||
|
||||
def element(self, ref: str, col: str | None = None, unit: str | None = None) -> str:
|
||||
raw = ref if col is None else mee.raw_text(("field", ("ref", ref), col))
|
||||
if raw in self.alias and unit is None:
|
||||
return self.reg.id(self.alias[raw])
|
||||
link = {"갈래": "요소", "키": self.holes(ref)}
|
||||
if col is not None:
|
||||
link["칸"] = col
|
||||
if unit is not None:
|
||||
link["단위"] = unit
|
||||
link |= self._place()
|
||||
try:
|
||||
label = self.master.label(ref) if "{" not in ref else ref
|
||||
except _ERR:
|
||||
label = ref
|
||||
return self._var(("요소", raw, unit), "요소", raw, label, link)
|
||||
|
||||
def material(self, at: int, item: dict) -> str:
|
||||
cond = {k: self.holes(v) for k, v in item["요소"].items()}
|
||||
link = {"갈래": "요소", "재료": cond, "단위": item.get("단위") or ""} | self._place()
|
||||
name = str(item.get("이름") or "")
|
||||
return self._var(("재료", at), "요소", f"단가:{name}", f"{name}단가", link)
|
||||
|
||||
def lone(self, raw: str):
|
||||
"""한 마디 식(표 찾기 · 요소 · 로직 부름 하나) — 마디 · 아니면 None."""
|
||||
try:
|
||||
node = mf.parse(raw)
|
||||
except mf.FormulaError:
|
||||
return None
|
||||
if node[0] in ("ref", "logic"):
|
||||
return node
|
||||
if node[0] == "field" and node[1][0] in ("find", "logic", "ref"):
|
||||
return node
|
||||
return None
|
||||
|
||||
|
||||
def _merge(sign: str, low: str, up: str) -> tuple[str, str, str]:
|
||||
"""기호 칸에 첨자까지 든 것(「t_1」 · 「C_m」)을 가름."""
|
||||
if "_" in sign or "^" in sign:
|
||||
head, low0, up0 = md.split_id(sign)
|
||||
if head != sign:
|
||||
return head, low0 + low, up or up0
|
||||
return sign, low, up
|
||||
|
||||
|
||||
def _parts(var: dict) -> tuple[str, str, str]:
|
||||
return _merge(
|
||||
str(var.get("기호") or var["열쇠"]), str(var.get("아래") or ""), str(var.get("위") or "")
|
||||
)
|
||||
|
||||
|
||||
def _line_parts(c: _Conv, name: str) -> tuple[str, str, str]:
|
||||
head = c.found.get(("머리", "호표", name)) or c.found.get(("머리", "덧줄", name)) or {}
|
||||
if head.get("기호") and head.get("기호") != name:
|
||||
return str(head["기호"]), str(head.get("아래") or ""), str(head.get("위") or "")
|
||||
return msym.lookup(name, "합")
|
||||
|
||||
|
||||
def convert(master, row: dict, given: dict | None = None) -> tuple[dict, list[str]]:
|
||||
"""로직 줄 → (수식 문서, 알림). given = 기본 옵션(없으면 `given_of`)."""
|
||||
given = meq.given_of(master, row) if given is None else given
|
||||
c = _Conv(master, row, given)
|
||||
for spec in row.get("입력") or []:
|
||||
c.reg.want(
|
||||
("입력", spec["이름"]), _parts(c.table.get(spec["이름"], {"열쇠": spec["이름"]}))
|
||||
)
|
||||
for step in row.get("중간") or []:
|
||||
c.reg.want(
|
||||
("중간", step["이름"]), _parts(c.table.get(step["이름"], {"열쇠": step["이름"]}))
|
||||
)
|
||||
for item in row.get("호표") or []:
|
||||
c.reg.want(("합", str(item.get("이름") or "")), _line_parts(c, str(item.get("이름") or "")))
|
||||
for item in row.get("덧줄") or []:
|
||||
c.reg.want(("덧", str(item.get("이름") or "")), _line_parts(c, str(item.get("이름") or "")))
|
||||
lone_steps = {}
|
||||
for step in row.get("중간") or []:
|
||||
node = c.lone(step["식"])
|
||||
if node is not None and step.get("표시") is None:
|
||||
raw = mee.raw_text(node)
|
||||
if raw not in c.alias:
|
||||
c.alias[raw] = ("중간", step["이름"])
|
||||
lone_steps[step["이름"]] = node
|
||||
doc = None
|
||||
for _ in range(2): # 한 번 = 기호 모음 · 두 번 = 겹침 가른 이름으로 글
|
||||
c.reg.settle()
|
||||
doc = _build(c, row, given, lone_steps)
|
||||
c.notes += c.reg.notes
|
||||
return doc, sorted(set(c.notes))
|
||||
|
||||
|
||||
def _link_of(c: _Conv, node) -> dict:
|
||||
"""한 마디 중간의 연결 — 찾기 · 요소 · 로직 부름."""
|
||||
alias = dict(c.alias)
|
||||
c.alias, c.quiet, c.last = {}, mee.raw_text(node), {}
|
||||
try:
|
||||
c.emit(node)
|
||||
return dict(c.last)
|
||||
finally:
|
||||
c.alias, c.quiet = alias, ""
|
||||
|
||||
|
||||
def _build(c: _Conv, row: dict, given: dict, lone_steps: dict) -> dict:
|
||||
variables: list[dict] = []
|
||||
for spec in row.get("입력") or []:
|
||||
name = spec["이름"]
|
||||
look = c.table.get(name, {})
|
||||
value = given.get(name)
|
||||
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
||||
value = Decimal(str(value))
|
||||
if "고르기" in spec:
|
||||
link = {"갈래": "선택", "목록": list(spec["고르기"]), "값": value}
|
||||
else:
|
||||
link = {"갈래": "입력", "값": value}
|
||||
if spec.get("범위"):
|
||||
link["범위"] = list(spec["범위"])
|
||||
variables.append(_var_row(c.reg.id(("입력", name)), name, look, spec, link))
|
||||
lines: list[dict] = []
|
||||
for step in row.get("중간") or []:
|
||||
name = step["이름"]
|
||||
look = c.table.get(name, {})
|
||||
me = c.reg.id(("중간", name))
|
||||
if name in lone_steps:
|
||||
variables.append(_var_row(me, name, look, step, _link_of(c, lone_steps[name])))
|
||||
continue
|
||||
try:
|
||||
text = c.text(step["식"])
|
||||
except mf.FormulaError as e:
|
||||
c.notes.append(f"중간 「{name}」 식 못 읽음 — {e}")
|
||||
text = str(step["식"])
|
||||
if step.get("표시") is not None:
|
||||
text = f"반올림({text}, {int(step['표시'])})"
|
||||
variables.append(_var_row(me, name, look, step, {"갈래": "식"}))
|
||||
lines.append({"기호": me, "식": text})
|
||||
doc = {
|
||||
"꼴": "수식문서",
|
||||
"판": 1,
|
||||
"키": c.key,
|
||||
"이름": row.get("이름") or "",
|
||||
"결과단위": row.get("결과단위") or "",
|
||||
"출처": row.get("출처") or "",
|
||||
}
|
||||
money: list[dict] = []
|
||||
if "결과" in row:
|
||||
head = c.found.get(("머리", "결과", "결과")) or {}
|
||||
sign = head.get("기호") if msym.is_symbol(str(head.get("기호") or "")) else ""
|
||||
parts = (sign, str(head.get("아래") or ""), str(head.get("위") or "")) if sign else None
|
||||
parts = parts or msym.rule(str(row.get("결과단위") or ""), "결과")[:3]
|
||||
c.reg.want(("결과", "결과"), parts)
|
||||
text = c.text(row["결과"]["식"])
|
||||
rule = row.get("끝수")
|
||||
if isinstance(rule, dict) and rule.get("대상") == "결과":
|
||||
text = f"{rule['방법']}({text}, {int(rule.get('자리') or 0)})"
|
||||
me = c.reg.id(("결과", "결과"))
|
||||
variables.append(
|
||||
{
|
||||
"열쇠": "결과",
|
||||
**_split(me),
|
||||
"이름": "결과",
|
||||
"단위": row.get("결과단위") or "",
|
||||
"연결": {"갈래": "식"},
|
||||
}
|
||||
)
|
||||
lines.append({"기호": me, "식": text})
|
||||
doc["결과"] = me
|
||||
else:
|
||||
money = _money(c, row)
|
||||
for key in c.order:
|
||||
if key[0] in ("입력", "중간") or key in [("결과", "결과")]:
|
||||
continue
|
||||
if any(alias == key for alias in c.alias.values()):
|
||||
continue
|
||||
one = c.links[key]
|
||||
me = c.reg.id(key)
|
||||
variables.append(
|
||||
{
|
||||
"열쇠": me,
|
||||
**_split(me),
|
||||
"이름": one["이름"],
|
||||
"단위": one["단위"],
|
||||
"연결": one["연결"],
|
||||
}
|
||||
)
|
||||
doc |= {"변수": variables, "식": lines, "금액": money}
|
||||
cut = {k: (dict(v) if isinstance(v, dict) else v) for k, v in md.CUT.items()}
|
||||
rule = row.get("끝수")
|
||||
if isinstance(rule, dict) and rule.get("대상") == "계":
|
||||
cut["계"] = {"방법": rule["방법"], "자리": int(rule.get("자리") or 0)}
|
||||
if row.get("끝전"):
|
||||
cut["끝전"] = row["끝전"]
|
||||
doc["끝수"] = cut
|
||||
for k in ("단계", "조정"):
|
||||
if row.get(k):
|
||||
doc[k] = row[k]
|
||||
return doc
|
||||
|
||||
|
||||
def _split(me: str) -> dict:
|
||||
sign, low, up = md.split_id(me)
|
||||
return {"기호": sign, "아래": low, "위": up}
|
||||
|
||||
|
||||
def _var_row(me: str, name: str, look: dict, spec: dict, link: dict) -> dict:
|
||||
out = {
|
||||
"열쇠": name,
|
||||
**_split(me),
|
||||
"이름": look.get("이름") or name,
|
||||
"단위": spec.get("단위") or "",
|
||||
}
|
||||
for k in ("설명", "출처"):
|
||||
if spec.get(k):
|
||||
out[k] = spec[k]
|
||||
out["연결"] = link
|
||||
return out
|
||||
|
||||
|
||||
def _hand(c: _Conv, at: int, item: dict) -> str:
|
||||
"""손 값 줄 — 값묶음(노 · 재 · 경) 또는 손 단가 = 숫자 연결."""
|
||||
name = str(item.get("이름") or "")
|
||||
given = item.get("값묶음")
|
||||
if given:
|
||||
if set(given) < set(md.COSTS):
|
||||
c.notes.append(f"호표 「{name}」 값묶음 일부 — 빠진 비목 0")
|
||||
value = {k: Decimal(str(given.get(k) or 0)) for k in md.COSTS}
|
||||
else:
|
||||
value = Decimal(str(item["단가"]))
|
||||
link = {"갈래": "숫자", "값": value}
|
||||
return c._var(("손", at), "요소", f"단가:{name}", f"{name}단가", link)
|
||||
|
||||
|
||||
def _group_name(items: list[dict]) -> str:
|
||||
kinds = []
|
||||
for x in items:
|
||||
k = x.get("종류") or str(x.get("이름") or "")
|
||||
if k not in kinds:
|
||||
kinds.append(k)
|
||||
return " · ".join(kinds)
|
||||
|
||||
|
||||
def _money(c: _Conv, row: dict) -> list[dict]:
|
||||
items, adds = row.get("호표") or [], row.get("덧줄") or []
|
||||
out: list[dict] = []
|
||||
for at, item in enumerate(items):
|
||||
name = str(item.get("이름") or "")
|
||||
line = {"이름": name}
|
||||
if item.get("규격"):
|
||||
line["규격"] = item["규격"]
|
||||
line["단위"] = item.get("단위") or ""
|
||||
try:
|
||||
qty = c.text(item["수량"])
|
||||
except mf.FormulaError as e:
|
||||
c.notes.append(f"호표 「{name}」 수량 못 읽음 — {e}")
|
||||
qty = str(item["수량"])
|
||||
kind = item.get("종류")
|
||||
if item.get("값묶음") or item.get("단가") is not None: # M02 손 값(값묶음 · 손 단가)
|
||||
price, line["비목"] = _hand(c, at, item), item.get("비목") or "묶음"
|
||||
if line["비목"] != "묶음" and item.get("값묶음"):
|
||||
price = f"{price}.계"
|
||||
elif kind == "로직":
|
||||
node = mf.parse(str(item["요소"]))
|
||||
price = md.text_of(c.emit(node))
|
||||
bundle = node[0] == "logic"
|
||||
if "비목" in item:
|
||||
line["비목"] = item["비목"]
|
||||
if bundle:
|
||||
price = f"{price}.계"
|
||||
else:
|
||||
line["비목"] = "묶음"
|
||||
elif isinstance(item.get("요소"), dict):
|
||||
line["비목"] = item["비목"]
|
||||
price = c.material(at, item)
|
||||
else:
|
||||
line["비목"] = item["비목"]
|
||||
ref = str(
|
||||
item["요소"]
|
||||
) # 품셈재료(MP)만 줄 단위로 값이 갈림 — 그 밖은 식 속 요소와 한 기호
|
||||
price = c.element(
|
||||
ref, None, str(item.get("단위") or "") if ref[:2] == mm.LINKED else None
|
||||
)
|
||||
line |= {"수량": qty, "단가": price}
|
||||
for k in ("번지", "합밖", "자리", "출처"):
|
||||
if k in item:
|
||||
line[k] = item[k]
|
||||
out.append(line)
|
||||
seen: dict[str, tuple] = {}
|
||||
for add in adds:
|
||||
name = str(add.get("이름") or "")
|
||||
try:
|
||||
price = c.text(add["식"], dict(seen))
|
||||
except mf.FormulaError as e:
|
||||
c.notes.append(f"덧줄 「{name}」 식 못 읽음 — {e}")
|
||||
price = str(add["식"])
|
||||
line = {"이름": name, "단위": "식", "비목": add["비목"], "단가": price}
|
||||
for k in ("번지", "합밖", "자리", "출처"):
|
||||
if k in add:
|
||||
line[k] = add[k]
|
||||
out.append(line)
|
||||
seen[name] = ("덧", name)
|
||||
last = {
|
||||
str(x.get("이름") or ""): i for i, x in enumerate(items)
|
||||
} # 같은 이름 = 뒤 줄(옛 `줄` 묶음)
|
||||
for i, line in enumerate(out):
|
||||
key = ("합", line["이름"]) if i < len(items) else ("덧", line["이름"])
|
||||
if key in c.used_lines and (i >= len(items) or last[line["이름"]] == i):
|
||||
line["기호"] = c.reg.id(key)
|
||||
_groups(out, [*items, *adds])
|
||||
return out
|
||||
|
||||
|
||||
def _groups(out: list[dict], items: list[dict]) -> None:
|
||||
"""묶음 — 단가산출 부름 줄마다 한 묶음 · 그 사이 다른 줄은 이어진 것끼리 · 묶음 하나면 안 둠."""
|
||||
runs: list[tuple[str, list[int]]] = []
|
||||
for i, item in enumerate(items):
|
||||
if item.get("종류") == "로직":
|
||||
runs.append((str(item.get("이름") or ""), [i]))
|
||||
elif runs and runs[-1][0] == "" and items[runs[-1][1][-1]].get("종류") != "로직":
|
||||
runs[-1][1].append(i)
|
||||
else:
|
||||
runs.append(("", [i]))
|
||||
if len(runs) < 2:
|
||||
return
|
||||
for name, members in runs:
|
||||
label = name or _group_name([items[i] for i in members])
|
||||
for i in members:
|
||||
out[i]["묶음"] = label
|
||||
Reference in New Issue
Block a user