Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte
179 lines
7.2 KiB
Python
179 lines
7.2 KiB
Python
"""PLAN 302-4 인쇄 미리보기 원가계산 — B09 18장 = 기번3 내역서 시트 차례 · 이름 · 방향 ·
|
|
쪽 표 = 화면 표 짜기와 같은 줄 · 합계(링크 · 강조 · 고칠 칸 · 단가연결 칸만 뺌).
|
|
|
|
차례 = 실무 xlsx(openpyxl · 목차 · 안내 · 숨김 시트 빼고) · 견본 = 임시 폴더 프로젝트(145-2 한 바퀴 꼴)
|
|
→ 서버 셈 payload → `print_cost_entry.ts` 를 vite `--ssr` 로 번들해 node 로 대조.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import warnings
|
|
from pathlib import Path
|
|
|
|
import openpyxl
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
for path in (ROOT, ROOT / "resources" / "tester"):
|
|
if str(path) not in sys.path:
|
|
sys.path.insert(0, str(path))
|
|
|
|
import test_145_2_b09_labor_calc as t145 # noqa: E402
|
|
|
|
from B09_Estimation import B09_Estimation_Prices as prices # noqa: E402
|
|
from B09_Estimation.B09_Estimation_Labor_Source import daega_inputs # noqa: E402
|
|
from B09_Estimation.B09_Estimation_Linked import linked_resource_keys # noqa: E402
|
|
from B09_Estimation.B09_Estimation_Rates import rate_candidates, rate_table # noqa: E402
|
|
from B09_Estimation.B09_Estimation_Router import compute_twice # noqa: E402
|
|
|
|
COST = ROOT / "B09_Estimation" / "draft" / "B09_Draft_Print_Cost.ts"
|
|
TABS = ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs.ts"
|
|
ENTRY = ROOT / "resources" / "tester" / "print_cost" / "print_cost_entry.ts"
|
|
VITE = ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js"
|
|
XLSX = (
|
|
ROOT
|
|
/ "resources/knowledge/original/실무문서/2024년 간선임도(기번3-울진.대흥)(공통)"
|
|
/ "1. 설계원본/01 내역자료/2024년 간선임도사업(기번3).xlsx"
|
|
)
|
|
#: 내역서 앞 시트(차례 밖)
|
|
FRONT = ("〓 목 차 〓", "※※안내※※")
|
|
|
|
ENTRY_RE = re.compile(r'\["(\w+)", "([^"]+)", "(landscape|portrait)"\]')
|
|
|
|
|
|
def _cost_sheets() -> list[tuple[str, str, str]]:
|
|
src = COST.read_text("utf-8")
|
|
body = src[src.index("COST_SHEETS") : src.index("];", src.index("COST_SHEETS"))]
|
|
return ENTRY_RE.findall(body)
|
|
|
|
|
|
def _practical() -> list[tuple[str, str]]:
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter("ignore") # 인쇄 영역 정의 이름 경고
|
|
wb = openpyxl.load_workbook(XLSX)
|
|
return [
|
|
(ws.title, ws.page_setup.orientation or "landscape")
|
|
for ws in wb.worksheets
|
|
if ws.sheet_state == "visible" and ws.title not in FRONT
|
|
]
|
|
|
|
|
|
def test_차례_이름_방향_기번3_내역서():
|
|
ours = _cost_sheets()
|
|
assert len(ours) == 18
|
|
assert [(name, o) for _id, name, o in ours] == _practical()
|
|
assert all(len(name) <= 31 for _id, name, _o in ours) # 엑셀 시트 이름 한도
|
|
# 탭 이름 = 화면 탭 이름(B09 TABS 앞 18장 · 같은 차례)
|
|
tabs = re.findall(r'^ \["(\w+)", "([^"]+)"\],$', TABS.read_text("utf-8"), re.MULTILINE)
|
|
assert [(i, n) for i, n, _o in ours] == tabs[:18]
|
|
assert not {"schedule_sum", "schedule_calc"} & {i for i, _n, _o in ours} # 공기 초안 뺌
|
|
|
|
|
|
def _payload(root: Path) -> dict:
|
|
"""145-2 한 바퀴 견본 — 관 둘(대가 · 산근 · 중기 · 사전조합) · 설계내역 한 줄 → 라우터 꼴 payload."""
|
|
t145._snap(root, "pipe@100.0", "SS000901", t145.LINES, t145._values(t145.STONE))
|
|
promoted = [
|
|
{"로직": "GF000119", "승격": True, "수량": {"손": 1}},
|
|
{"참조": {"층": "system", "주인": None, "키": "SC-T"}, "수량": {"손": 4}},
|
|
]
|
|
t145._snap(root, "pipe@400.0", "SS000902", promoted, t145._values(t145.STONE), t145.SC)
|
|
inputs = prices.build_inputs(None, linked_resource_keys(root))
|
|
inputs["daega"] = daega_inputs(root)
|
|
inputs["hopyo"] = {}
|
|
inputs["bill"] = {
|
|
"items": [
|
|
{"key": "st:revet", "section": "2.1", "name": "돌기슭막이", "spec": "", "unit": "m",
|
|
"qty": 10, "ref": {"tab": "dg_list", "key": "revet"}, "note": "",
|
|
"owners": {"pipe@100.0": 4, "pipe@400.0": 6}},
|
|
]
|
|
} # fmt: skip
|
|
sheets, summary = compute_twice(root, inputs, {})
|
|
return {
|
|
"inputs": inputs,
|
|
"user": {},
|
|
"book": None,
|
|
"stale": None,
|
|
"sheets": sheets,
|
|
"rate_candidates": rate_candidates(),
|
|
"rate_table": rate_table(),
|
|
"notes": [],
|
|
"summary": summary,
|
|
}
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def pages(tmp_path_factory) -> list[dict]:
|
|
if shutil.which("node") is None:
|
|
pytest.skip("node 가 없음")
|
|
work = tmp_path_factory.mktemp("p302_4")
|
|
root = t145._project(work / "proj", [(100, "SS000901"), (400, "SS000902")])
|
|
data = work / "payload.json"
|
|
data.write_text(json.dumps(_payload(root), ensure_ascii=False, default=str), "utf-8")
|
|
with tempfile.TemporaryDirectory(prefix="print_cost_test_") as outdir:
|
|
entry_rel = "../" + str(ENTRY.relative_to(ROOT)).replace("\\", "/")
|
|
build = subprocess.run(
|
|
[
|
|
"node",
|
|
str(VITE),
|
|
"build",
|
|
"--configLoader",
|
|
"runner",
|
|
"--ssr",
|
|
entry_rel,
|
|
"--outDir",
|
|
outdir,
|
|
],
|
|
cwd=str(ROOT),
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
errors="replace",
|
|
timeout=180,
|
|
)
|
|
assert build.returncode == 0, f"vite ssr 번들 실패:\n{build.stdout}\n{build.stderr}"
|
|
run = subprocess.run(
|
|
["node", str(Path(outdir) / "print_cost_entry.js"), str(data)],
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
errors="replace",
|
|
timeout=60,
|
|
)
|
|
assert run.stdout.strip(), f"시험 진입이 아무 결과도 안 냄:\n{run.stderr}"
|
|
return json.loads(run.stdout)
|
|
|
|
|
|
def test_쪽_차례_이름_방향(pages):
|
|
assert [(p["id"], p["name"], p["orientation"]) for p in pages] == _cost_sheets()
|
|
assert all(p["caption"] == p["name"] for p in pages)
|
|
|
|
|
|
def test_쪽_표_화면과_같은_줄_합계(pages):
|
|
for p in pages:
|
|
assert p["sameRows"] and p["rows"] == p["screenRows"], p["id"]
|
|
assert p["total"] == p["screenTotal"], p["id"]
|
|
assert p["columns"] == [k for k in p["screenColumns"] if k != "connect"], p["id"]
|
|
assert p["interactive"] == [], p["id"] # 누름 · 고침 · 강조 뺌
|
|
by = {p["id"]: p for p in pages}
|
|
# 견본이 표를 채움 — 내역 · 대가 · 산근 · 중기 · 집계
|
|
for tab in ("bill", "dg_list", "dg", "sg_list", "sg", "machine", "sum_labor", "cost", "total"):
|
|
assert by[tab]["rows"] > 0, tab
|
|
|
|
|
|
def test_머리_여러_줄_레벨_병합_꼴(pages):
|
|
by = {p["id"]: p for p in pages}
|
|
bill = by["bill"]
|
|
assert bill["groups"] > 0 # 머리 두 줄(재료비 · 노무비 · 경비 묶음)
|
|
assert "connect" not in bill["columns"] # 단가연결 메뉴 칸 뺌
|
|
assert bill["level"] == {"key": "name", "tree": False} # 들여쓰기만 · 나무 아이콘 뺌
|
|
assert max(v for v in bill["levels"] if v) >= 2
|
|
assert {c for c in bill["rowClass"] if c} <= {"is-title"} # 손 줄 · 고른 줄 표시 뺌
|
|
assert "is-title" in bill["rowClass"]
|
|
assert by["total"]["mergeSame"] == ["part"] # 총괄 공종 칸 세로 병합
|