- 새 M02_Doc_NativeLayout — 원본 글줄 차림 표지로 금액 줄 묶음(= 항목 제목) + 문서 쌓기(기본과 다를 때만) · EST 금액 차례 = 원본 차례(셈 같을 때만) · 합계 절 뺌 · 줄 쌓기 모델 stackOf · saveOf · hiddenOf 파이썬 거울 - M02_Doc_NativeEst.tags — 글줄 차림 머리(단 > 0 · 소계 구간 첫 글 · 번호 글) · 합계 · 글(분수 그림 위아래 줄 뺌) - M02_San_Emit.stmate — 글 줄 표지 · 원본 차림 표지(계 줄마다 항목) - M02_Doc_Import_Shape.shape — 차림 tag · apply 부름 - 줄 쌓기 hiddenOf — 가져온 값 옵션 칸도 공식 묶음에 보임(원파일 꼬리표 · 같은 값) · 옵션 가로 칸 고침 = 수 그대로(글이면 셈 「—」 흠 고침) - 시험 test_812_3 — 견본 다섯(거창 1 · 2 · 41 · 15 · 봉화 14) · 전수 2,220 블록 금액 같음 · TS 왕복 · 거울 100 % · M01 1,518 차림 그대로 - 낡은 시험 고침 test_810_3 — 옵션 칸 ∩ 공식 묶음 = 빈 → 옵션 칸 ⊆ 공식 묶음(812-3 으로 가져온 값이 공식 묶음에도 보임) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
215 lines
8.8 KiB
Python
215 lines
8.8 KiB
Python
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
|
|
"""810-3 지킴 — 원파일 가져온 식 화면(줄 쌓기 · 옵션 칸 · 추정 칸 · 카드).
|
|
|
|
(a) 화면 셈 거울 — EST 여섯 · STC 일곱 산근 블록 모두: 화면 길(TS importView → calcDoc) = 서버 길
|
|
(`M02_Doc_Import.run` 계산기) 답 전부 같음(줄 · 비목 · 합 · 표지)
|
|
(b) 옵션 칸 — 원파일 수 하나 변수(출처 「원파일」) = 옵션 가로 칸(`sourceOf`) ·
|
|
줄 쌓기 공식 묶음에도 보임(812-3) · 금액 줄 단가로 쓰는 수 · 엑셀 단가 기억 연결 = 옵션 칸 아님
|
|
(c) 추정 칸 — 문서 머리 `가져온.추정` 줄 수 = 원본 확정 추정 수(EST) · 원코드 · 규격 머리
|
|
(d) 소계 줄 — 합 밖 · 비목 없음(화면 비목 칸 빈칸 · 종류 「소계」 —
|
|
`line_stack_edit` · `line_stack_out` 글 대조)
|
|
원파일 없으면 그 시험만 skip(다른 PC). 추정 표지 수 표 = tmp/s8103/guess.json(일회 기록).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from collections import Counter
|
|
from functools import lru_cache
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
HERE = Path(__file__).resolve().parent
|
|
for p in (ROOT, HERE):
|
|
if str(p) not in sys.path:
|
|
sys.path.insert(0, str(p))
|
|
|
|
import test_802_1_formula_doc as t1 # noqa: E402
|
|
import test_810_2_native_doc as t2 # noqa: E402
|
|
from test_810_1_estx import FILES as EST # noqa: E402
|
|
from test_810_1_stc import FILES as STC # noqa: E402
|
|
|
|
from M02_MasterTemplete import M02_Doc_Import as dim # noqa: E402
|
|
|
|
HELPER = HERE / "helper_810_3_screen.cjs"
|
|
|
|
|
|
class Keep(dim.Links):
|
|
"""서버 연결 + 기록(요소 · 표 · 문서) — 같은 값을 화면 셈에 넘김."""
|
|
|
|
def __init__(self, master, basis):
|
|
super().__init__(master, basis)
|
|
self.log, self.docs = {}, {}
|
|
self.rec = t1.Rec(self)
|
|
|
|
def element(self, p):
|
|
return self.rec._keep("요소", p, super().element)
|
|
|
|
def table(self, p):
|
|
return self.rec._keep("표", p, super().table)
|
|
|
|
def doc(self, key):
|
|
got = super().doc(key)
|
|
if got is not None:
|
|
self.docs[key] = got
|
|
return got
|
|
|
|
|
|
def _plain(v):
|
|
return json.loads(json.dumps(t1.ml._norm(v), ensure_ascii=False, default=str))
|
|
|
|
|
|
@lru_cache(maxsize=16)
|
|
def _file(kind: str, tag: str) -> list[tuple[int, dict, dict, dict]]:
|
|
"""블록마다 (번, 원본, 문서, 서버 답 · 기록) — 서버 길 그대로."""
|
|
saved = t2.native(kind, tag)
|
|
out = []
|
|
for no in t2.fn.numbers(saved):
|
|
emitted = t2.fn.emit(saved, no)
|
|
sk = t2.sk_of(emitted)
|
|
doc = dim.derive(sk, None, t2.whole())
|
|
links = Keep(t2.whole(), dim.basis_of(sk))
|
|
mine = dim.view(doc, dim.basis_of(sk))
|
|
links.learn(mine)
|
|
want = t1.mc.calc(mine, links)
|
|
out.append(
|
|
(no, emitted["원본"], doc, {"답": want, "기록": links.rec.log, "문서": links.docs})
|
|
)
|
|
return out
|
|
|
|
|
|
def _screen(rows) -> dict:
|
|
docs, log, keys = {}, {}, []
|
|
for no, _, doc, srv in rows:
|
|
key = f"블록{no}"
|
|
docs[key] = _plain(doc)
|
|
keys.append(key)
|
|
log |= srv["기록"]
|
|
docs |= {k: _plain(v) for k, v in srv["문서"].items()}
|
|
pay = {"문서들": docs, "대상": keys, "기록": _plain(log), "기준": "엑셀"}
|
|
proc = subprocess.run(
|
|
["node", str(HELPER)],
|
|
input=json.dumps(pay, ensure_ascii=False),
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
check=True,
|
|
)
|
|
return json.loads(proc.stdout)
|
|
|
|
|
|
def _cases():
|
|
return [("EST", k) for k in EST] + [("STC", k) for k in STC]
|
|
|
|
|
|
@pytest.mark.parametrize(("kind", "tag"), _cases())
|
|
def test_screen_calc_mirror_and_option_cells(kind: str, tag: str) -> None:
|
|
rows = _file(kind, tag)
|
|
got = _screen(rows)
|
|
for no, src, doc, srv in rows:
|
|
key = f"블록{no}"
|
|
t1._same(t1.ml._norm(srv["답"]), got["답"][key], f"{tag} {key}")
|
|
shape = got["꼴"][key]
|
|
nums = [
|
|
v["열쇠"]
|
|
for v in doc["변수"]
|
|
if v.get("출처") == "원파일"
|
|
and not any(str(m.get("단가")) == v["열쇠"] for m in doc.get("금액") or [])
|
|
]
|
|
assert set(nums) <= set(shape["옵션"]), (tag, key, nums, shape["옵션"])
|
|
# 옵션 칸 = 공식 묶음에도 보임(812-3 · 같은 값)
|
|
assert set(shape["옵션"]) <= set(shape["공식"]), (tag, key)
|
|
for v in doc["변수"]:
|
|
if v["열쇠"] in shape["옵션"]:
|
|
assert v["연결"]["갈래"] == "숫자" and "엑셀" not in v["연결"]
|
|
|
|
|
|
def test_guess_head_counts_and_table() -> None:
|
|
table, total = {}, Counter()
|
|
for kind, tag in _cases():
|
|
try:
|
|
rows = _file(kind, tag)
|
|
except pytest.skip.Exception:
|
|
continue
|
|
n = Counter()
|
|
for _, src, doc, _ in rows:
|
|
head = doc["가져온"]
|
|
assert head["원파일"] is True
|
|
guess = head.get("추정") or []
|
|
assert len(guess) == (src.get("확정") or {}).get("추정", 0), (tag, doc["이름"])
|
|
assert all(g["글"] and isinstance(g["행"], int) for g in guess)
|
|
if kind == "EST":
|
|
assert head.get("원코드") # EST 블록 코드(D… · AAA…)
|
|
n["블록"] += 1
|
|
n["추정 칸"] += len(guess)
|
|
n["추정 블록"] += bool(guess)
|
|
n["값 옵션"] += sum(1 for v in doc["변수"] if v.get("출처") == "원파일")
|
|
n["소계 줄"] += sum(1 for m in doc["금액"] if m.get("합밖") and not m.get("비목"))
|
|
for k in ("확정", "표지"):
|
|
n[k] += (src.get("확정") or {}).get(k, 0)
|
|
table[f"{kind} {tag}"] = dict(n)
|
|
total.update(n)
|
|
if not table:
|
|
pytest.skip("원파일 없음")
|
|
table["합"] = dict(total)
|
|
(ROOT / "tmp" / "s8103").mkdir(parents=True, exist_ok=True)
|
|
(ROOT / "tmp" / "s8103" / "guess.json").write_text(
|
|
json.dumps(table, ensure_ascii=False, indent=1), encoding="utf-8"
|
|
)
|
|
assert total["블록"] and total["추정 칸"] and total["값 옵션"] and total["소계 줄"]
|
|
|
|
|
|
def test_subtotal_shape_and_screen_text() -> None:
|
|
out, doc, got = t2._block(7) # 지장목 제거 — 인부소계
|
|
sub = next(x for x in doc["금액"] if x["이름"] == "인부소계")
|
|
assert sub["합밖"] and "비목" not in sub
|
|
edit = (ROOT / "A00_Common" / "line_stack_edit.ts").read_text(encoding="utf-8")
|
|
assert "const bare = !!line.합밖 && !line.비목" in edit and '"K_소계"' in edit
|
|
outx = (ROOT / "A00_Common" / "line_stack_out.ts").read_text(encoding="utf-8")
|
|
assert "cols: bare ? {} :" in outx
|
|
side = (ROOT / "M02_MasterTemplete" / "M02_MasterTemplete_Hopyo_Side.ts").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
assert 'accept: ".xls,.xlsx,.estx,.stc"' in side
|
|
|
|
|
|
def test_origin_card_native_line() -> None:
|
|
js = r"""
|
|
const fs = require("fs"), path = require("path");
|
|
const ts = require(path.join(process.cwd(), "config", "node_modules", "typescript"));
|
|
const at = path.join(process.cwd(), "M02_MasterTemplete", "logic", "M02_Sangeun_Origin.ts");
|
|
const js = ts.transpileModule(fs.readFileSync(at, "utf8"), {
|
|
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
|
}).outputText;
|
|
const box = { exports: {} };
|
|
new Function("exports", "module", "require", js)(box.exports, box, () => ({}));
|
|
const got = JSON.parse(fs.readFileSync(0, "utf8"));
|
|
process.stdout.write(JSON.stringify(got.map((m) => box.exports.originOf(m))));
|
|
"""
|
|
est = {"로직": {}, "원본": {"갈래": "EST", "시트": "산근", "코드": 7, "원파일": True,
|
|
"원코드": "D0000000118", "행": [2, 49],
|
|
"확정": {"확정": 13, "표지": 0, "추정": 2}}} # fmt: skip
|
|
xl = {"로직": {}, "원본": {"갈래": "EST", "시트": "산근", "코드": 7, "행": [2, 49]}}
|
|
stc = {"로직": {}, "원본": {"갈래": "STmate", "시트": "단가산출근거", "코드": "D00014",
|
|
"원파일": True, "행": [2, 25],
|
|
"확정": {"확정": 199, "표지": 1}}} # fmt: skip
|
|
proc = subprocess.run(
|
|
["node", "-e", js],
|
|
input=json.dumps([est, xl, stc], ensure_ascii=False),
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
cwd=ROOT,
|
|
check=True,
|
|
)
|
|
a, b, c = json.loads(proc.stdout)
|
|
assert a["요약"] == "원파일 · EST 산근 D0000000118 · 행 2~49"
|
|
assert a["확정"] == "확정 13 · 표지 0 · 추정 2 · 확정률 100.0 %"
|
|
assert "요약" not in b and "확정" not in b # 엑셀 길 = 그대로
|
|
assert c["요약"] == "원파일 · STmate 단가산출근거 D00014 · 행 2~25"
|
|
assert c["확정"].endswith("확정률 99.5 %")
|