feat(802-2): 옛 M01 로직 1,519 → 수식 문서 변환 빌더 · 금액 전수 대조 · 같은 것만 저장 · 다름 목록
- 빌더 build_수식문서_802_2.py(다시 돌리면 같음 · --check) — 기호 = 수식 탭 기호(800-1 · 800-5) · 기본 옵션 = 수식 탭 기본값 - 대조 = 새 계산기 대 옛 엔진 노 · 재 · 경 · 계(결과) 원 단위: 기본 옵션 1,519/1,519 같음 · 옵션 표본 18,716 회(로직마다 고르기 갈래 모두) 중 같음 18,715 · 다름 1 · 둘 다 못 셈 134(옵션 바꾸면 표에 맞는 줄 없는 기계 작업량) - 다름 1 = GC000100 지역=경북 재료비 1 원 — 식 속 단가산출 부름 값이 끝수 뒤(옛 엔진 = 끝수 전 날 값) · 보정 없음 - 저장 = 같은 것만 1,518 문서 — resources/master_data/수식문서/수식_<로직 파일 꼬리>.json 58 파일(옛 로직 JSON 안 씀) - 목록 = docs/raw/요구_1104/_목록_802-2_변환대조.xlsx(다름 · 요약 · 둘 다 못 셈 · 변환 알림) - 시험 test_802_2_convert 9(--check · 전수 기본 · 옵션 표본 모든 갈래 · 목록 · 저장본 다시 셈 · 791 실무 견본 넷 = 엑셀) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,320 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""옛 M01 로직 → 수식 문서 빌더 + 금액 전수 대조 (PLAN 802-2 · 요구서_664 1 장 3).
|
||||
|
||||
python resources/master_data/scripts/build_수식문서_802_2.py # 변환 · 대조 · 저장
|
||||
· 목록
|
||||
python resources/master_data/scripts/build_수식문서_802_2.py --check # 다시 돌려 저장본
|
||||
· 목록과 같은지
|
||||
|
||||
- 변환 = `master_doc_convert.convert`(로직마다 · 기본 옵션 = 수식 탭 기본값)
|
||||
· 셈 = `master_doc_calc.calc`.
|
||||
- 대조 = 새 계산기 대 옛 엔진(`master_formula.run` → `master_cut.engine`) 노 · 재 · 경
|
||||
· 계(돈 아닌 로직 = 결과)
|
||||
원 단위 — 기본 옵션 전부 + 옵션 표본(로직마다 고르기 입력의 다른 갈래 하나씩 · 나머지 기본).
|
||||
둘 다 못 셈 = 같음(따로 셈). 보정 없음 — 다르면 목록에 까닭.
|
||||
- 저장 = 같은 것만(요구서_664 1 장 3) — 모든 대조가 같고 부르는 단가산출도 저장되는 문서 ·
|
||||
`resources/master_data/수식문서/수식_<옛 로직 파일 꼬리>.json`(로직 파일마다
|
||||
· 옛 로직 JSON 은 안 씀).
|
||||
- 목록 = `docs/raw/요구_1104/_목록_802-2_변환대조.xlsx`(로직 · 옵션 · 옛 · 새 · 차이 · 까닭 + 요약).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
for p in (str(ROOT), str(Path(__file__).resolve().parent)):
|
||||
if p not in sys.path:
|
||||
sys.path.insert(0, p)
|
||||
|
||||
import master_cut as mcut # noqa: E402 — 옛 엔진(대조 쪽만)
|
||||
import master_doc as md # noqa: E402
|
||||
import master_doc_calc as mc # noqa: E402
|
||||
import master_doc_convert as cv # noqa: E402
|
||||
import master_doc_link as ml # noqa: E402
|
||||
import master_equation as meq # noqa: E402
|
||||
import master_formula as mf # noqa: E402
|
||||
|
||||
LIST = ROOT / "docs/raw/요구_1104/_목록_802-2_변환대조.xlsx"
|
||||
KEYS = ("노무비", "재료비", "경비", "계")
|
||||
HEAD = ("로직", "이름", "옵션", "옛", "새", "차이", "까닭")
|
||||
RAW_SUB = (
|
||||
"식 속 단가산출 부름 = 끝수 뒤 값"
|
||||
"(옛 엔진 = 끝수 전 날 값 · `master_formula.run` 결과를 식에 씀)"
|
||||
)
|
||||
|
||||
|
||||
def rows_of(files: dict) -> list[tuple[str, dict]]:
|
||||
"""(옛 로직 파일 이름, 로직 줄) — 파일 · 줄 차례."""
|
||||
return [
|
||||
(name, row)
|
||||
for name, data in sorted(files.items())
|
||||
if data.get("그룹") == "로직"
|
||||
for row in data.get("줄") or []
|
||||
]
|
||||
|
||||
|
||||
def old_answer(whole, row: dict, given: dict):
|
||||
try:
|
||||
got = mcut.engine(whole, row["키"], mf.run(whole, row["키"], mf.fill_empty(row, given)))
|
||||
except Exception as e: # 옛 엔진 멈춤도 대조 값
|
||||
return f"못 셈 — {e}"
|
||||
return {"결과": got["결과"]} if "결과" in got else {k: got.get(k) for k in KEYS}
|
||||
|
||||
|
||||
def new_answer(links, doc: dict, given: dict | None = None):
|
||||
got = mc.calc(doc, links, given)
|
||||
if not got["셈됨"]:
|
||||
return "못 셈 — " + (" · ".join(m["글"] for m in got["표지"]) or got.get("까닭") or "")
|
||||
return {"결과": got["결과"]} if doc.get("결과") else {k: got.get(k) for k in KEYS}
|
||||
|
||||
|
||||
def _text(v) -> str:
|
||||
if isinstance(v, str):
|
||||
return v
|
||||
return " · ".join(f"{k} {mc.plain_text(x) if x is not None else '—'}" for k, x in v.items())
|
||||
|
||||
|
||||
def _gap(a, b) -> str:
|
||||
if isinstance(a, str) or isinstance(b, str):
|
||||
return "한쪽 못 셈"
|
||||
return " · ".join(
|
||||
f"{k} {mc.plain_text((b.get(k) or 0) - (a.get(k) or 0))}" for k in a if a.get(k) != b.get(k)
|
||||
)
|
||||
|
||||
|
||||
def _uses_raw_sub(doc: dict) -> bool:
|
||||
"""식 줄이 단가산출 부름 기호를 씀(금액 줄 단가가 아닌 자리)."""
|
||||
subs = {
|
||||
md.var_id(v)
|
||||
for v in doc.get("변수") or []
|
||||
if (v.get("연결") or {}).get("갈래") == "단가산출"
|
||||
}
|
||||
for line in doc.get("식") or []:
|
||||
try:
|
||||
if subs & set(md.names(md.parse(line.get("식")))):
|
||||
return True
|
||||
except md.DocError:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def why(doc: dict, old, new) -> str:
|
||||
"""다름 까닭 — 문서 꼴에서 바로 보이는 것만 · 아니면 「까닭 모름」(시험이 막음)."""
|
||||
if isinstance(old, str) and not isinstance(new, str):
|
||||
return f"옛 엔진 멈춤 · 새 계산기 셈 — {old}"
|
||||
if isinstance(new, str) and not isinstance(old, str):
|
||||
return f"새 계산기 못 셈 — {new}"
|
||||
if _uses_raw_sub(doc):
|
||||
return RAW_SUB
|
||||
return "까닭 모름"
|
||||
|
||||
|
||||
def build(log=print) -> dict:
|
||||
"""변환 · 대조 전부 — {문서, 알림, 파일, 대조, 저장, 요약}."""
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
|
||||
files, whole = m01.loaded()
|
||||
items = rows_of(files)
|
||||
docs, notes, by_file = {}, {}, {}
|
||||
for name, row in items:
|
||||
key = str(row["키"])
|
||||
docs[key], notes[key] = cv.convert(whole, row)
|
||||
by_file.setdefault(name, []).append(key)
|
||||
log(f"변환 {len(docs)}")
|
||||
links = ml.Links(whole, docs)
|
||||
runs, both_out = [], 0
|
||||
for _, row in items:
|
||||
key, base = str(row["키"]), meq.given_of(whole, row)
|
||||
cases = [("", base, None)] # (옵션 글, 옛 엔진 입력, 새 문서에 넘길 바뀐 입력)
|
||||
for spec in row.get("입력") or []:
|
||||
for opt in spec.get("고르기") or []:
|
||||
if str(opt) != str(base.get(spec["이름"])):
|
||||
one = {spec["이름"]: opt}
|
||||
cases.append((f"{spec['이름']}={opt}", {**base, **one}, one))
|
||||
for label, given, changed in cases:
|
||||
old = old_answer(whole, row, given)
|
||||
new = new_answer(links, docs[key], changed)
|
||||
same = old == new or (isinstance(old, str) and isinstance(new, str))
|
||||
both_out += isinstance(old, str) and isinstance(new, str)
|
||||
runs.append(
|
||||
{
|
||||
"로직": key,
|
||||
"이름": row.get("이름") or "",
|
||||
"옵션": label,
|
||||
"옛": old,
|
||||
"새": new,
|
||||
"같음": same,
|
||||
}
|
||||
)
|
||||
for one in runs:
|
||||
if not one["같음"]:
|
||||
one["까닭"] = why(docs[one["로직"]], one["옛"], one["새"])
|
||||
keep = _keep(docs, runs)
|
||||
for one in runs: # 같아도 못 넣는 것 — 부르는 단가산출이 빠짐
|
||||
if one["같음"] and one["로직"] not in keep and not one["옵션"]:
|
||||
one["빠짐"] = "부르는 단가산출이 빠짐 — " + " · ".join(
|
||||
sorted(_calls(docs[one["로직"]]) - keep)
|
||||
)
|
||||
summary = {
|
||||
"로직": len(docs),
|
||||
"기본 같음": sum(1 for x in runs if not x["옵션"] and x["같음"]),
|
||||
"기본 다름": sum(1 for x in runs if not x["옵션"] and not x["같음"]),
|
||||
"옵션 표본": sum(1 for x in runs if x["옵션"]),
|
||||
"옵션 같음": sum(1 for x in runs if x["옵션"] and x["같음"]),
|
||||
"옵션 다름": sum(1 for x in runs if x["옵션"] and not x["같음"]),
|
||||
"둘 다 못 셈": both_out,
|
||||
"저장": len(keep),
|
||||
"변환 알림": sum(1 for v in notes.values() if v),
|
||||
}
|
||||
log(" · ".join(f"{k} {v}" for k, v in summary.items()))
|
||||
return {
|
||||
"문서": docs,
|
||||
"알림": notes,
|
||||
"파일": by_file,
|
||||
"대조": runs,
|
||||
"저장": keep,
|
||||
"요약": summary,
|
||||
}
|
||||
|
||||
|
||||
def _calls(doc: dict) -> set[str]:
|
||||
out = set()
|
||||
for v in doc.get("변수") or []:
|
||||
link = v.get("연결") or {}
|
||||
if link.get("갈래") == "단가산출" and "{" not in str(link.get("키")):
|
||||
out.add(str(link["키"]))
|
||||
return out
|
||||
|
||||
|
||||
def _keep(docs: dict, runs: list[dict]) -> set[str]:
|
||||
"""저장할 문서 — 모든 대조 같음 + 부르는 단가산출도 저장(닫힘).
|
||||
키 구멍 부름은 그 로직 대조로 판정."""
|
||||
keep = set(docs) - {x["로직"] for x in runs if not x["같음"]}
|
||||
while True:
|
||||
drop = {k for k in keep if not _calls(docs[k]) <= keep}
|
||||
if not drop:
|
||||
return keep
|
||||
keep -= drop
|
||||
|
||||
|
||||
def files_of(result: dict) -> dict[str, str]:
|
||||
"""저장 파일 이름 → 글."""
|
||||
out = {}
|
||||
for name, keys in result["파일"].items():
|
||||
kept = [result["문서"][k] for k in keys if k in result["저장"]]
|
||||
body = {"그룹": "수식문서", "로직파일": name, "줄": kept}
|
||||
out[ml.file_of(name)] = ml.dump(body)
|
||||
return out
|
||||
|
||||
|
||||
def table_rows(result: dict) -> list[list[str]]:
|
||||
rows = []
|
||||
for one in result["대조"]:
|
||||
if one["같음"] and not one.get("빠짐"):
|
||||
continue
|
||||
rows.append(
|
||||
[
|
||||
one["로직"],
|
||||
one["이름"],
|
||||
one["옵션"] or "기본",
|
||||
_text(one["옛"]),
|
||||
_text(one["새"]),
|
||||
"" if one["같음"] else _gap(one["옛"], one["새"]),
|
||||
one.get("까닭") or one.get("빠짐") or "",
|
||||
]
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def write_list(result: dict, path: Path = LIST) -> None:
|
||||
from openpyxl import Workbook
|
||||
|
||||
book = Workbook()
|
||||
sheet = book.active
|
||||
sheet.title = "다름"
|
||||
sheet.append(list(HEAD))
|
||||
for row in table_rows(result):
|
||||
sheet.append(row)
|
||||
for col, width in zip("ABCDEFG", (11, 30, 26, 46, 46, 22, 70)):
|
||||
sheet.column_dimensions[col].width = width
|
||||
for title, rows in list(_sheets(result).items())[1:]:
|
||||
sheet = book.create_sheet(title)
|
||||
for row in rows:
|
||||
sheet.append(row)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
book.save(path)
|
||||
|
||||
|
||||
def read_list(path: Path = LIST) -> dict[str, list[list[str]]]:
|
||||
from openpyxl import load_workbook
|
||||
|
||||
book = load_workbook(path, read_only=True)
|
||||
out = {
|
||||
s.title: [
|
||||
["" if c is None else str(c) for c in row] for row in s.iter_rows(values_only=True)
|
||||
]
|
||||
for s in book.worksheets
|
||||
}
|
||||
book.close()
|
||||
return out
|
||||
|
||||
|
||||
def _sheets(result: dict) -> dict[str, list[list[str]]]:
|
||||
notes = [["로직", "알림"]] + [[k, t] for k, v in sorted(result["알림"].items()) for t in v]
|
||||
both = [["로직", "이름", "옵션", "옛", "새"]] + [
|
||||
[x["로직"], x["이름"], x["옵션"] or "기본", x["옛"], x["새"]]
|
||||
for x in result["대조"]
|
||||
if isinstance(x["옛"], str) and isinstance(x["새"], str)
|
||||
]
|
||||
return {
|
||||
"다름": [list(HEAD), *table_rows(result)],
|
||||
"요약": [[k, str(v)] for k, v in result["요약"].items()],
|
||||
"둘 다 못 셈": both,
|
||||
"변환 알림": notes,
|
||||
}
|
||||
|
||||
|
||||
def check(result: dict) -> list[str]:
|
||||
"""저장본 · 목록이 지금 빌드와 같은지 — 다른 것 목록."""
|
||||
out = []
|
||||
want = files_of(result)
|
||||
have = {p.name for p in ml.FOLDER.glob("수식_*.json")}
|
||||
for name in sorted(have - set(want)):
|
||||
out.append(f"남은 파일 {name}")
|
||||
for name, text in sorted(want.items()):
|
||||
path = ml.FOLDER / name
|
||||
if not path.exists() or path.read_text(encoding="utf-8") != text:
|
||||
out.append(f"다른 파일 {name}")
|
||||
if not LIST.exists() or read_list() != _sheets(result):
|
||||
out.append(f"다른 목록 {LIST.name}")
|
||||
return out
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
parser.add_argument("--check", action="store_true", help="저장본 · 목록과 같은지만")
|
||||
args = parser.parse_args()
|
||||
result = build()
|
||||
if args.check:
|
||||
bad = check(result)
|
||||
for line in bad:
|
||||
print(line)
|
||||
print("같음" if not bad else f"다름 {len(bad)}")
|
||||
return 1 if bad else 0
|
||||
ml.FOLDER.mkdir(parents=True, exist_ok=True)
|
||||
want = files_of(result)
|
||||
for path in ml.FOLDER.glob("수식_*.json"):
|
||||
if path.name not in want:
|
||||
path.unlink()
|
||||
for name, text in want.items():
|
||||
(ml.FOLDER / name).write_text(text, encoding="utf-8", newline="\n")
|
||||
write_list(result)
|
||||
print(f"저장 {len(want)} 파일 · 목록 {LIST}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user