Files
Aislo/resources/tester/test_802_4b1_import_doc.py
T
eomsangdonandClaude Opus 5.5 61bb15fbde refactor(812-1): 옛 엑셀 엔진 걷기 2 · 실무 11 금액 100 % · NativeEst 자리 dict 묶음 고침 · 옛 시험 정리
- M02_Doc_NativeEst.triple: 세 줄 수량 식이 같은지 = repr 로 맞댐(자리 규칙 dict 가 set 에 안 들어가 TypeError — 엑셀 줄은 보인 값이 반올림 자리를 타 원파일에 없던 꼴 · 812-3 끝나 브레인이 맡김)
- 엑셀 읽기기: 절 소계 덧셈이 뺀 오타 줄 = 비목 없음(블록 금액도 그래야 맞을 때만) · 세 줄 계 금액 칸 비목 하나 = 옮김 · 소계 61 = 금액 칸 비목 하나 · 「.1」 수 · 총괄표 당초 칸 표지 · STmate 엑셀 줄 금액 · 계 꼬리(검산 줄)
- 화면: 가져온 식 규격 칸 꼬리표 = 원파일 · 엑셀 갈림(line_stack_sourced)
- 시험: 793_1 지움(옛 EST · STmate 엑셀 글 추정 엔진 · 812_1 골든이 대신) · 793_2_san_emit EST 시험 지움(STmate 남김) · 797_1a 옛 표지 ① ② · 줄 단위 합격 · 표본 밖 수 · 버림1 감시 지움(옛 엔진) · 810_1 (c) 엑셀 엔진 대조 지움 · 793_2 실무 블록 · 793_3b 끝전 = 새 길 · 812_1 금액 엇갈림 없음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 08:38:12 +09:00

310 lines
16 KiB
Python

# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
"""802-4b-1 엑셀 불러오기 → 수식 문서(요구 664 · 665 · 요구서_664 0 장) — 임시 storage · M01 그대로.
① 가져온 식 SK = 수식 문서(손 값 = 숫자 · 이음 = 연결 + 엑셀 값 기억 · 중간 = 식 줄 ·
얼린 변수 = 숫자 · 금액 줄 = 노무비 · 재료비 · 경비 · 단계 · 끝전 · 머리 `가져온` 표지)
② [엑셀 단가 그대로] = 숫자 값 셈 = 엑셀 · [우리 단가로] = 연결 값 셈(기계 = 시간당 그 비목)
③ 산근 속 산근 = 형제 SK 단가산출 연결(안쪽이 바뀌면 따라감 · 부르는 쪽 단가기준)
④ 소요량 · 계수 고른 칸 = 그 수 자리에 표 연결 기호(금액 그대로)
⑤ SK 금액 = 문서 셈 = 옛 엔진 셈 · 수식 길 읽기 · 고침(따로) · 옛 칸 고침 409 · 옮기기 대조
실무 엑셀 없으면 그 시험만 skip.
"""
from __future__ import annotations
import copy
import json
import subprocess
from decimal import Decimal
from pathlib import Path
import helper_m02_hopyo_golden as golden
import pytest
from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_Doc_Import as dim
from M02_MasterTemplete import M02_Doc_Migrate as mig
from M02_MasterTemplete import M02_Doc_Sk as dsk
from M02_MasterTemplete import M02_Doc_Sk_Path as path
from M02_MasterTemplete import M02_Hopyo_Store as hs
from M02_MasterTemplete import M02_Machine_Hourly as machine
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_Sangeun_FromHopyo as fh
from M02_MasterTemplete import M02_Sangeun_Imported as imp
from M02_MasterTemplete import M02_Sangeun_QtyLinks as qty
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
KEY = "HP-000001"
def _doc(items: list[dict], extra: dict | None = None, src: dict | None = None) -> dict:
row = {"이름": "견본 sub2임시", "결과단위": "원/식", "호표": items, **(extra or {})}
meta = {"로직": row, "단가기준": "엑셀", "원본": {"엑셀금액": {}, **(src or {})}}
return {"키": "SK-000001", "이름": "견본", "단위": "식", "바탕": None, imp.KEY: meta}
def _item(kind: str, name: str, price: float, part: str, link: str | None = None, qty_="1"):
out = {"종류": kind, "이름": name, "단위": "식", "수량": qty_, "비목": part, "단가": price}
return out | ({"이음": link} if link else {})
def _ours(doc: dict) -> dict:
return {**doc, imp.KEY: {**doc[imp.KEY], "단가기준": "우리"}}
def _var(doc: dict, part: str) -> dict:
"""금액 줄 `이름` 의 단가 기호 변수."""
line = next(x for x in doc["금액"] if x["이름"] == part)
sym = str(line["단가"]).removesuffix(".계")
return next((v for v in doc["변수"] if dim.dc.md.var_id(v) == sym), {})
def _eq(good: bool) -> str:
for row in m01.loaded()[1].numbers["EQ"].values():
if bool(machine.hourly(row["키"]).get("ok")) is good:
return row["키"]
pytest.skip("EQ 견본 없음")
# ── ① ② 꼴 · 두 단가기준 ──────────────────────────────────────────────────
def test_doc_form_numbers_links_and_both_bases() -> None:
eq = _eq(True)
items = [
_item("인력", "보통인부", 100000, "노무비", "LB000002", "0.5"),
_item("인력", "작업반장", 150000, "노무비", None, "0.1"),
_item("기계", "굴착기", 30000, "경비", eq, "2"),
]
mids = {"중간": [{"이름": "Qd", "식": "600"}, {"이름": "Q", "식": "Qd / 8"}]}
marks = ["참고 · 금액 줄 못 읽음 · 보인 값 얼림", "추정 · 단가 후보 여럿"]
doc = _doc(items, mids, {"표지": marks, "갈래": "EST", "파일": "견본.xls"})
root = dim.derive(doc)
assert root["꼴"] == "수식문서" and root["키"] == "SK-000001"
assert root["가져온"]["표지"] == marks and root["가져온"]["갈래"] == "EST" # 까닭 그대로
labor = _var(root, "보통인부")["연결"]
assert labor == {"갈래": "요소", "키": "LB000002", "엑셀": Decimal(100000)}
assert _var(root, "작업반장")["연결"] == {"갈래": "숫자", "값": Decimal(150000)} # 이음 없음
mach = _var(root, "굴착기")["연결"]
assert mach["갈래"] == "단가산출" and mach["칸"] == "경비" and mach["엑셀"] == Decimal(30000)
qd = next(v for v in root["변수"] if v["열쇠"] == "Qd") # 얼린 변수 = 숫자 · 식 = 식 줄
assert qd["연결"] == {"갈래": "숫자", "값": Decimal(600)}
assert [x["식"] for x in root["식"]] == [f"{dim.dc.md.var_id(qd)} ÷ 8"]
excel = dim.run(root, doc)
assert excel["계"] == Decimal(50000 + 15000 + 60000) # 엑셀 단가 그대로
ours = dim.run(root, _ours(doc))
lb = Decimal(str(m01.loaded()[1].get("LB000002")["값"]))
hour = machine.hourly(eq)["경비"]
assert ours["노무비"] == int(lb * Decimal("0.5") * 10) / Decimal(10) + 15000
assert float(ours["경비"]) == pytest.approx(hour * 2, abs=1) # 기계 = 시간당 그 비목
def test_machine_without_hourly_keeps_number_with_note() -> None:
eq = _eq(False)
doc = _doc([_item("기계", "리퍼", 5000, "경비", eq)])
var = _var(dim.derive(doc), "리퍼")
assert var["연결"] == {"갈래": "숫자", "값": Decimal(5000)} and eq in var["설명"]
assert dim.run(dim.derive(doc), _ours(doc))["계"] == 5000
def test_sk_money_is_doc_calc_and_equals_old_engine(world) -> None:
"""⑤ SK 금액 = 문서 셈(수식셈 표지) = 옛 엔진 셈 — 엑셀 · 우리 둘 다 · 덧줄 율 · 합밖."""
items = [
_item("인력", "보통인부", 100000, "노무비", "LB000002", "0.33"),
_item("재료", "경유", 1700, "재료비", "MO000002", "3") | {"합밖": True},
]
extra = {"덧줄": [{"이름": "공구손료", "식": "노무비 * 0.03", "비목": "경비"}]}
doc = _doc(items, extra)
for basis in ("엑셀", "우리"):
mine = imp._with(doc, 단가기준=basis)
new = imp._money(imp._run(mine, imp._whole()))
token = dsk.OLD.set(True)
try:
old = imp._money(imp._run(mine, imp._whole()))
finally:
dsk.OLD.reset(token)
assert new == old, basis
got = imp.to_price(doc, "우리")
assert got["전"]["계"] == 33000 + 990 and got["후"]["계"] != got["전"]["계"]
# ── ③ 산근 속 산근 · 실무 STmate · EST ──────────────────────────────────────
def _hp(world, tag: str):
path_ = golden.FILES.get(tag)
if path_ is None or not path_.exists():
pytest.skip(f"실무 엑셀 없음 {tag}")
folder = world["personal"]
hs._dir(folder).mkdir(parents=True, exist_ok=True)
(hs._dir(folder) / f"{KEY}.json").write_text(f'{{"키": "{KEY}", "판": {hs.BOOK}}}', "utf-8")
fh.keep(folder, KEY, path_.name, path_.read_bytes())
return folder
def _read(key: str) -> dict:
return store.read("sangeun", key)["문서"]
def test_stmate_nested_is_sibling_link(world) -> None:
folder = _hp(world, "울진대흥")
with store.at(folder):
made = fh.make(folder, KEY, 2200) # D02200 안에 D00075 콘크리트믹서사용
doc = _read(made["이름"])
root = dim.derive(doc)
assert root["가져온"]["갈래"] == "STmate" and root["가져온"]["코드"] == "D02200"
inner = _var(root, "콘크리트믹서사용")["연결"]
assert inner["갈래"] == "단가산출" and inner["키"].startswith("SK-")
assert inner["엑셀"]["노무비"] == 6216 # 엑셀 얼린 값 기억
assert dim.run(root, doc)["계"] == 67383 == made["검산"]["엑셀"]["계"]
assert dim.run(root, _ours(doc))["셈됨"] is True
sub = _read(inner["키"]) # 안쪽 단가 줄 하나 두 배 → 바깥이 따라감(엑셀 기준도)
one = next(x for x in sub[imp.KEY]["로직"]["호표"] if "단가" in x)
one["단가"] = one["단가"] * 2
store.write("sangeun", inner["키"], store.read("sangeun", inner["키"])["판"], sub)
now = dim.run(dim.derive(_read(inner["키"])), _read(inner["키"]))["계"]
assert now != 7528
assert dim.run(root, doc)["계"] == pytest.approx(67383 - 7528 + now, abs=1)
def test_est_by_part_and_frozen_vars(world) -> None:
folder = _hp(world, "북부청임도")
with store.at(folder):
made = fh.make(
folder, KEY, 67
) # 단산68 세 줄(노 · 재 · 경) = 묶음 한 줄(812-1 · 원파일 길)
doc = _read(made["이름"])
root = dim.derive(doc)
parts = [
(x["비목"], _var(root, x["이름"])["연결"].get("칸"))
for x in root["금액"]
if (_var(root, x["이름"]).get("연결") or {}).get("키", "").startswith("SK-")
]
assert parts == [("묶음", None)]
assert not any(x["식"].strip().replace(".", "").isdigit() for x in root["식"])
excel = dim.run(root, doc)
assert float(excel["계"]) == pytest.approx(float(made["검산"]["엑셀"]["계"]), abs=1)
assert dim.run(root, _ours(doc))["셈됨"] is True
# ── ④ 소요량 · 계수 고른 칸 ─────────────────────────────────────────────────
def test_qty_pick_becomes_table_link(world) -> None:
folder = _hp(world, "북부청임도")
found = None
for no in range(1, 200):
try:
out = fh.emit(folder, KEY, no)
except store.StoreError:
continue
if "야면석채집및운반(계곡내)" in out["로직"]["이름"]:
found = out
break
if found is None:
pytest.skip("야면석채집및운반(계곡내) 블록 없음")
row = found["로직"]
doc = _doc(row["호표"], {k: row[k] for k in ("중간", "덧줄", "단계", "끝전") if row.get(k)})
doc[imp.KEY]["로직"] |= {k: row[k] for k in ("이름", "결과단위")}
at = next(i for i, x in enumerate(row["호표"]) if x["이름"] == "보통인부")
cell = next(c for c in qty.view(doc)["줄"] if c["자리"] == f"호표:{at}:0")["후보"][0]
kept = qty.pick(doc, f"호표:{at}:0", {k: cell[k] for k in ("표", "줄", "칸")})["문서"]
root = dim.derive(kept)
pick = next(v for v in root["변수"] if v["열쇠"].startswith(dim.PICK))
assert pick["연결"]["갈래"] == "표" and pick["연결"]["표"] == cell["표"]
assert pick["연결"]["칸"] == cell["칸"] and pick["연결"][dim.XL] == Decimal(str(cell["값"]))
line = next(x for x in root["금액"] if x["이름"] == "보통인부")
assert dim.dc.md.var_id(pick) in line["수량"]
before = dim.run(dim.derive(doc), doc)["계"]
assert dim.run(root, kept)["계"] == before # 엑셀 그대로 금액 같음
assert dim.run(root, _ours(kept))["셈됨"] is True # 표 칸 = 같은 값
# ── ⑤ 수식 길 · 옮기기 ───────────────────────────────────────────────────
def test_formula_route_edit_then_old_edit_409_and_regen(world) -> None:
folder = world["personal"]
doc = _doc([_item("인력", "보통인부", 100000, "노무비", "LB000002", "0.5")])
with store.at(folder):
key = imp.create(doc[imp.KEY]["로직"], {"엑셀금액": {"계": 50000}}, "sub2임시 견본")["이름"]
read = path.read(key)
assert read["셈"]["계"] == 50000 and read["사본"]["단가기준"] == "엑셀"
edit = copy.deepcopy(read["문서"])
edit["금액"][0]["수량"] = "1"
pre = path.preview(key, {"문서": edit})
assert pre["전"]["계"] == 50000 and pre["후"]["계"] == 100000
path.save(key, {"문서": edit, "판": read["판"]})
again = path.read(key)
assert again["사본"]["따로"] is True and again["셈"]["계"] == 100000
sk = _read(key)
moved = imp.link(sk, sd_id(sk), None)["문서"] # 이음 풂 = 옛 칸 고침 → 409
with pytest.raises(store.StoreError) as why:
store.write("sangeun", key, store.read("sangeun", key)["판"], moved)
assert why.value.status == 409
ours = imp.to_price(sk, "우리")["문서"] # 단가기준만 바꿈 = 됨(문서 그대로 · 연결 값)
store.write("sangeun", key, store.read("sangeun", key)["판"], ours)
lb = Decimal(str(m01.loaded()[1].get("LB000002")["값"]))
assert path.read(key)["셈"]["노무비"] == int(lb * 10) / Decimal(10)
def sd_id(sk: dict) -> str:
return imp.sd.line_ids(imp.row_of(sk))[0]
def test_migrate_imported_same(world) -> None:
folder = world["personal"]
doc = _doc([_item("인력", "보통인부", 100000, "노무비", "LB000002", "0.5")])
with store.at(folder):
key = imp.create(doc[imp.KEY]["로직"], {"엑셀금액": {"계": 50000}}, "sub2임시 옮김")["이름"]
file = folder / "sangeun" / f"{key}.json"
got = mig.check("sangeun", file)
assert got["상태"] in ("같음", "있음")
if got["상태"] == "같음":
assert got["수식"]["문서"]["키"] == key and got["전"] == got["후"]
# ── TS 짝(importView) 거울 ─────────────────────────────────────────────
_NODE = """
const fs = require("fs"), path = require("path");
const ts = require(path.join(process.cwd(), "config", "node_modules", "typescript"));
const at = path.join(process.cwd(), "common_util", "common_util_formula_doc_import.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(([d, b]) => box.exports.importView(d, b))));
"""
def test_ts_view_mirror() -> None:
eq = _eq(True)
items = [
_item("인력", "보통인부", 100000, "노무비", "LB000002", "0.5"),
_item("인력", "작업반장", 150000, "노무비", None, "0.1"),
_item("기계", "굴착기", 30000, "경비", eq, "2"),
]
root = dim.derive(_doc(items))
root["변수"].append(
{"열쇠": "S", "기호": "S", "연결": {"갈래": "단가산출", "키": "SK-000009", "엑셀": 5}}
)
plain = json.loads(json.dumps(root, ensure_ascii=False, default=float))
cases = [[plain, b] for b in ("엑셀", "우리")]
out = subprocess.run(
["node", "-e", _NODE],
input=json.dumps(cases, ensure_ascii=False),
capture_output=True,
text=True,
encoding="utf-8",
cwd=Path(__file__).resolve().parents[2],
check=True,
)
ts_ = json.loads(out.stdout)
py = [json.loads(json.dumps(dim.view(plain, b), ensure_ascii=False)) for b in ("엑셀", "우리")]
assert ts_ == py
assert _var(ts_[0], "보통인부")["연결"] == {"갈래": "숫자", "값": 100000.0}
assert ts_[0]["변수"][-1]["연결"]["갈래"] == "단가산출" # 형제 SK = 늘 연결
def test_empty_block_is_excel_zero_not_dash() -> None:
"""줄 없는 엑셀 산근 = 0 원(옛 엔진 · 엑셀과 같음) · 표지 · 우리 단가로도 0."""
doc = _doc([])
root = dim.derive(doc)
assert dim.EMPTY in root["가져온"]["표지"]
assert dim.run(root, doc)["계"] == 0 == dim.run(root, _ours(doc))["계"]
assert imp.check(doc)["셈"]["계"] == 0