Files
Aislo/resources/tester/test_810_2_native_doc.py
T
eomsangdonandClaude Opus 5.5 56a3e0538f feat(814-1 1·2): 올리기 파일 하나 · 확장자 파일 = 단가산출표 판 + 산출근거
- 같은 공사 엑셀 · 원파일 붙이기 걷음(keep · upload_native · same 지움) — 엑셀이든 .estx · .STC 든 하나만
- 원파일 = 새 HP · 산출근거 머리(코드 · 이름 · 규격 · 단위 · 저장 금액) 단가산출표 격자 · 일위대가 안 만듦(HeadMainDat 안 씀)
- 화면 = 일위대가 없으면 단가산출 목록 먼저 · 올림 안내 글 하나로
- 시험 810-2: 붙음 · EST 호표 → SC 기대 지움(요구 742-1 · 743-2 로 걷은 길) → 파일 하나 · 단가산출표 판 기대

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 14:14:21 +09:00

240 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
"""810-2 지킴 — 원파일 구조 줄 → 수식 문서(`M02_Doc_NativeEst` · `M02_Doc_FromNative`) · 원파일 우선.
(a) 전수 — EST 여섯 · STC 일곱 산근 블록 모두: 가져온 로직 → 수식 문서 셈(엑셀 단가 그대로) = 원파일
저장 금액(STmate 계약단가 = 낙찰률 버림 뒤) · 검산 줄 틀림은 모두 표지 줄(표지 없는 틀린 줄 0)
(b) 꼴 — 소계 = 합 밖 · 비목 칸 비움(문서 셈 금액만) · 기계 세 줄 = 묶음 한 줄 · 변수 = 이름(상수 아님)
· 분수 그림 공식 = 변수 식 · 수 하나 변수 = 숫자 연결 + 출처 「원파일」 · 품셈 조건 글 = 「추정」 표지
(c) 문서 셈 거울 — 비목 없는 합 밖 줄 파이썬 = TS
(d) 올림 길(814-1 파일 하나) — 원파일 = 새 HP(단가산출표 판 · 일위대가 없음 · 엑셀 HP 에 안 붙음) ·
가져온 식 = 원파일 · 엑셀 HP = 엑셀 길 그대로
원파일 · 실무 엑셀 없으면 그 시험만 skip(다른 PC).
"""
from __future__ import annotations
import math
import sys
from decimal import Decimal as D
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 helper_m02_hopyo_golden as golden # 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_FromNative as fn # noqa: E402
from M02_MasterTemplete import M02_Doc_Import as dim # noqa: E402
from M02_MasterTemplete import M02_Hopyo_Excel as hx # noqa: E402
from M02_MasterTemplete import M02_Hopyo_Store as hs # noqa: E402
from M02_MasterTemplete import M02_MasterTemplete_Store as store # noqa: E402
from M02_MasterTemplete import M02_Sangeun_Doc as sd # noqa: E402
from M02_MasterTemplete import M02_Sangeun_FromHopyo as fh # noqa: E402
from M02_MasterTemplete import M02_Sangeun_Imported as imp # noqa: E402
from resources.tester.test_m02_labor_ref import world # noqa: E402, F401 — 같은 임시 층
WHO = {"이름": "sub3임시"}
COST = (("L", "노무비"), ("M", "재료비"), ("S", "경비"), ("T", "계"))
def _file(path: Path) -> Path:
if not path.exists():
pytest.skip(f"원파일 없음 {path.name}")
return path
@lru_cache(maxsize=2)
def native(kind: str, tag: str) -> dict:
path = _file((EST if kind == "EST" else STC)[tag])
return fn.read(path.name, path.read_bytes())
@lru_cache(maxsize=1)
def whole():
return imp._whole()
def sk_of(out: dict) -> dict:
"""{로직, 원본} → 저장 없는 가져온 식 SK(만들기 몸과 같은 꼴 · 키 = 시험용)."""
row = out["로직"]
sk = {"키": "SK-999999", "이름": row["이름"], "단위": row["결과단위"].split("/", 1)[1]}
sk |= {"바탕": None, "입력값": {}, "덮개": {}, "뺀줄": [], "더한줄": []}
sk[imp.KEY] = {"로직": row, "원본": out["원본"], "단가기준": "엑셀"}
return sd.clean(sk, None, whole())
def doc_and_money(out: dict) -> tuple[dict, dict, dict]:
sk = sk_of(out)
doc = dim.derive(sk, None, whole())
return sk, doc, dim.run(doc, sk, whole())
# ── (a) 전수 ─────────────────────────────────────────────────────────────
def _sweep(saved: dict) -> dict:
stat = {"블록": 0, "같음": 0, "표지없는틀린줄": 0, "다름": []}
for no, blk in fn.numbers(saved).items():
out = fn.emit(saved, no)
sk, _, got = doc_and_money(out)
mine = {c: got.get(k) for c, k in COST}
nag = (out["원본"].get("계약") or {}).get("낙찰률")
if nag: # 계약단가(B09 몫) — 원파일 저장값은 낙찰률 뒤
for c in "LMS":
mine[c] = math.floor(round(float(mine[c]) * nag / 100, 6))
mine["T"] = mine["L"] + mine["M"] + mine["S"]
want = blk.get("저장") or {}
same = all(
mine[c] is not None and abs(float(mine[c]) - float(want.get(c) or 0)) < 0.5
for c, _ in COST
)
stat["블록"] += 1
stat["같음"] += same
if not same:
stat["다름"].append((no, blk.get("이름")))
marked = {int(m.split(":")[0]) for m in out["원본"]["표지"] if m.split(":")[0].isdigit()}
lines = imp.check(sk, whole())["줄"]
stat["표지없는틀린줄"] += sum(1 for x in lines if not x["같음"] and x["행"] not in marked)
return stat
@pytest.mark.parametrize("tag", list(EST))
def test_est_every_block_equals_saved(tag: str) -> None:
stat = _sweep(native("EST", tag))
assert stat["블록"] and not stat["다름"] and stat["같음"] == stat["블록"], stat["다름"][:5]
assert stat["표지없는틀린줄"] == 0
@pytest.mark.parametrize("tag", list(STC))
def test_stc_every_block_equals_saved(tag: str) -> None:
stat = _sweep(native("STC", tag))
assert stat["블록"] and not stat["다름"], stat["다름"][:5]
assert stat["표지없는틀린줄"] == 0
# ── (b) 꼴 ───────────────────────────────────────────────────────────────
def _block(no: int) -> tuple[dict, dict, dict]:
out = fn.emit(native("EST", "거창"), no)
_, doc, got = doc_and_money(out)
return out, doc, got
def test_subtotal_out_of_sum_no_cost() -> None:
out, doc, got = _block(7) # 지장목 제거 — 인부소계 822.5 · 기구손료 10 %
sub = next(x for x in doc["금액"] if x["이름"] == "인부소계")
assert sub["합밖"] and "비목" not in sub and sub["출처"] == dim.SUB
line = next(x for x in got["줄"] if x["이름"] == "인부소계")
assert line["금액"] == D("822.5") and line["비목"] == {} and line["합밖"]
rate = next(x for x in doc["금액"] if "인부소계" in str(x.get("단가")))
assert rate["비목"] == "경비" and not rate.get("합밖")
assert (got["노무비"], got["재료비"], got["경비"]) == (1437, 58, 210)
def test_machine_one_line_bundle() -> None:
out, doc, got = _block(1) # 측구터파기 토사 — 굴삭기 노 · 재 · 경 세 줄
rows = [x for x in out["로직"]["호표"] if (x.get("자원") or {}).get("갈래") == "기계"]
assert len(rows) == 1 and rows[0]["종류"] == "로직" and rows[0]["수량"] == "(1 / Q)"
assert rows[0]["이음"] and rows[0]["이음"].startswith("로직(GC") # 시간당 기계경비 로직
money = [x for x in doc["금액"] if x["이름"] == rows[0]["이름"]]
assert len(money) == 1 and money[0]["비목"] == "묶음"
line = next(x for x in got["줄"] if x["이름"] == rows[0]["이름"])
assert line["비목"] == {"노무비": D("2523.3"), "재료비": D("832.5"), "경비": D("1049.7")}
def test_variables_by_name_and_fraction_formula() -> None:
out, doc, _ = _block(1)
mids = {m["이름"]: m["식"] for m in out["로직"]["중간"]}
assert mids["Q"] == "반올림(((((((3600 * q) * K) * f) * E)) / (Cm)), 2)" or "Cm" in mids["Q"]
assert {"q", "K", "f", "E", "Cm"} <= set(mids)
by = {v["열쇠"]: v for v in doc["변수"]}
nums = [v for v in by.values() if (v.get("연결") or {}).get("갈래") == "숫자"]
assert nums and all(v.get("출처") == "원파일" for v in nums)
assert not any(x for x in out["원본"]["표지"] if "공식 글 못 읽음" in x)
def test_guess_mark_for_condition_text() -> None:
out, _, _ = _block(7)
assert out["원본"]["확정"]["추정"] >= 1
assert any("추정" in x and "품셈 조건" in x for x in out["원본"]["표지"])
# ── (c) 문서 셈 거울 ─────────────────────────────────────────────────────
def test_bare_out_of_sum_line_mirror() -> None:
import test_802_1_formula_doc as t
doc = t._doc(
[],
[],
[
t._man("갑", "2", "100", 기호="C_갑"),
{"이름": "소계", "단위": "식", "수량": "1", "단가": "C_갑 + 0.07", "합밖": True},
{"이름": "율", "단위": "식", "비목": "경비", "단가": "소계 × 10 ÷ 100"},
],
)
doc["금액"][1]["기호"] = "소계"
want = t.mc.calc(doc, t.Fake())
assert not want["표지"] and want["노무비"] == 200 and want["경비"] == 20
assert want["줄"][1]["금액"] == D("200") and want["줄"][1]["비목"] == {}
got = t._ts({"문서들": {"맨": doc}, "대상": ["맨"], "기록": {}, "글": [], "표": []})
t._same(t.ml._norm(want), got["답"]["맨"], "맨")
bad = dict(doc, 금액=[{**doc["금액"][1], "합밖": False}])
assert any(m["갈래"] == "금액 줄" for m in t.mc.calc(bad, t.Fake())["표지"])
# ── (d) 올림 길 ──────────────────────────────────────────────────────────
def _excel_hp(folder: Path, tag: str) -> str:
src = golden.FILES.get(tag)
if src is None or not src.exists():
pytest.skip(f"실무 엑셀 없음 {tag}")
data = src.read_bytes()
parsed = hx.read_book(src.name, data)
row = hs.save(folder, src.name, parsed, WHO, None, hs.fingerprint(data))
fh.keep(folder, row["키"], src.name, data)
return row["키"]
def test_upload_native_one_file_list_only(world) -> None:
"""814-1 — 파일 하나: 원파일은 같은 공사 엑셀 HP 에 안 붙고 새 HP(단가산출표 판 · 일위대가 없음)."""
folder = world["personal"]
est, stc = _file(EST["거창"]), _file(STC["봉화"])
with store.at(folder):
hp = _excel_hp(folder, "거창")
got = fh.upload_native(folder, est.name, est.read_bytes(), WHO)
assert got["원파일"] and got["키"] != hp and "붙음" not in got and got["호표수"] == 0
assert not fh.emit(folder, hp, 15)["원본"].get("원파일") # 엑셀 HP = 엑셀 길 그대로
again = fh.upload_native(folder, est.name, est.read_bytes(), WHO)
assert again["이어서"] and again["키"] == got["키"] and len(hs.rows(folder)) == 2
grid = hs.grid(folder, got["키"])
assert grid["보기"] == {"단가산출표": "s1"} and not grid["호표"]
assert len(grid["목록"]["단산"]) == len(fn.numbers(native("EST", "거창")))
made = fh.make(folder, got["키"], 15) # 전석 메쌓기 — 안쪽 산근 둘 + 굴삭기
sk = store.read("sangeun", made["이름"])["문서"]
assert sk["가져온"]["원본"]["원파일"] and made["검산"]["같음"]
assert len([x for x in sk["가져온"]["로직"]["호표"] if x.get("참조")]) == 2
ans = dim.run(dim.of(sk), sk)
assert (ans["노무비"], ans["재료비"], ans["경비"]) == (74880, 7029, 12306)
st = fh.upload_native(folder, stc.name, stc.read_bytes(), WHO)
assert st["원파일"] and st["호표수"] == 0
head = hs.grid(folder, st["키"])["책"]["시트"][0]["칸"]
assert head["A1"]["값"] == "코드" and str(head["A2"]["값"]).startswith("D")
made = fh.make(folder, st["키"], next(iter(fn.numbers(native("STC", "봉화")))))
assert made["검산"]["같음"]
with pytest.raises(store.StoreError):
fh.upload_native(folder, "깨진.estx", b"PKgarbage", WHO)
def test_router_branch_by_extension() -> None:
import inspect
from M02_MasterTemplete import M02_Hopyo_Router as router
src = inspect.getsource(router._upload)
assert "from_native.is_native(name)" in src and "upload_native" in src
assert fn.is_native("가.ESTX") and fn.is_native("나.stc") and not fn.is_native("다.xlsx")