Files
Aislo/resources/tester/test_est_excel_sheet.py
T
eomsangdonandClaude Opus 5.5 5e6c2988fc feat(est-test): 엑셀형 산출서 편집 동작과 조달청 표준공사코드 적용
- 엑셀형 산출서: 범위 고르기, 행·열 삽입/삭제(수식 참조·병합·수량 연결 이동, 지운 칸 #REF!), 잘라내기·복사·붙여넣기(상대 참조), 내용지우기, 굵게·기울임·밑줄·정렬, 병합·분리, 대각선, 다시 계산, 품명·규격·단위로 지정, 원본 .xls 병합 정보 읽기
- 조달청 표준공사코드 적용(원본 TfrmCodeChange): 7종 탭, 코드변경·검사 색·변경취소·적용(참조 행 포함, 재계산 없음), 코드일괄변경(.estx), 자동변경, 다산코드정리, 사용자정의코드, 검색. 조달청 목록은 사용자가 고른 PPS_*.dat 를 /api/pps-table 로 읽음

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

135 lines
6.5 KiB
Python

"""EST 테스트 앱 엑셀형 수량산출서 계산·편집(원본 TFRMMULEXCEL, .xls BIFF 수식)."""
import json
import subprocess
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
MAIN = Path("C:/Dasansoft/Est Plus 2/Data/ProjectData/main.estx")
def node(script: str, *args: str):
return json.loads(
subprocess.check_output(["node", "-e", script, *args], cwd=ROOT).decode("utf-8")
)
def test_evaluate_and_directional_sum():
out = node("""
const X = require(process.cwd() + '/tools/est-test/native-excel-sheet.cjs');
const cells = { '0,0': { value: '품명' }, '1,0': { value: 2 }, '2,0': { value: 3 }, '3,0': {},
'1,1': { value: 0, formula: '(A2+A3)/2*4' }, '1,2': { value: 0, formula: 'SUM(A2:B2)' },
'4,4': { value: 0, formula: 'ROUND(10/3,2)+ROUNDUP(1.21,1)+ROUNDDOWN(1.29,1)' }, '5,5': { value: 0, formula: 'F6+1' } };
const errors = X.recalc(cells);
const down = X.sumFormula(cells, 3, 0, 'up');
let none = ''; try { X.sumFormula({ '0,0': { value: 'x' } }, 1, 0, 'up'); } catch (e) { none = e.message; }
process.stdout.write(JSON.stringify({ b2: cells['1,1'].value, c2: cells['1,2'].value, e5: cells['4,4'].value,
cycle: [cells['5,5'].value, errors.length], down, none }));
""")
assert out["b2"] == 10 and out["c2"] == 12
assert out["e5"] == 3.33 + 1.3 + 1.2
assert out["cycle"] == ["#오류", 1]
assert out["down"] == "SUM(A2:A3)"
assert out["none"] == "더할 셀이 없습니다."
def test_real_sheets_recompute_and_link_items():
if not MAIN.exists():
pytest.skip("installed EST main sample unavailable")
out = node(
"""
const fs = require('fs'), vm = require('vm');
const imp = require(process.cwd() + '/tools/est-test/import-estx.cjs'), ex = require(process.cwd() + '/tools/est-test/export-estx.cjs');
const p = imp.importEstx(fs.readFileSync(process.argv[1]));
const s = p.quantitySheets.find((x) => x.Code === 'N0000000136');
const formulas = p.quantitySheets.filter((x) => x.excel).flatMap((x) => Object.values(x.excel[0].cells)).filter((c) => c.formula);
const same = Buffer.compare(ex.exportEstx(p), fs.readFileSync(process.argv[1])) === 0;
const ctx = vm.createContext({ structuredClone });
for (const f of ['native-math.js', 'native-formula.js', 'native-text.js', 'native-excel-sheet.cjs', 'native-quantity.cjs'])
vm.runInContext(fs.readFileSync('tools/est-test/' + f, 'utf8'), ctx, { filename: f });
ctx.s = s;
const before = vm.runInContext('EstQuantity.calculate(s).items.map((i) => i.GN)', ctx);
vm.runInContext("s.excel[0].cells['6,2'].value = 40; EstExcelSheet.recalc(s.excel[0].cells)", ctx);
const after = vm.runInContext('EstQuantity.calculate(s).items.map((i) => i.GN)', ctx);
s.excelEdited = true;
let message = ''; try { ex.exportEstx(p); } catch (e) { message = e.message; }
process.stdout.write(JSON.stringify({ decoded: formulas.every((c) => typeof c.formula === 'string'), count: formulas.length,
links: Object.fromEntries(Object.entries(s.excelLinks).map(([k, l]) => [k, [l.row, l.col, l.inferred]])), before, after, same, message }));
""",
str(MAIN),
)
assert out["decoded"] is True and out["count"] == 221
assert out["links"] == {
"V000001": [32, 4, True],
"V000002": [32, 6, True],
"V000003": [32, 8, True],
}
assert out["before"][:3] == [17, 12, 58]
assert out["after"][:3] != out["before"][:3]
assert out["same"] is True
assert out["message"].startswith("엑셀형 수량산출서 편집은 ESTX")
def test_edit_rows_columns_clipboard_merge_and_style():
out = node(r"""
const S = require(process.cwd() + '/tools/est-test/native-excel-sheet.cjs');
const E = require(process.cwd() + '/tools/est-test/native-excel-edit.cjs');
const s = { cells: { '0,0': { value: 1 }, '1,0': { value: 2 }, '2,0': { value: 3 },
'3,0': { value: 0, formula: 'SUM(A1:A3)' }, '3,1': { value: 0, formula: 'A2*$A$1+LOG10(2)' } } };
S.recalc(s.cells);
const links = { X: { n: 0, row: 3, col: 0 } };
const r = {};
E.shift(s, 'row', 1, 1, links);
r.insert = [s.cells['4,0'].formula, s.cells['4,0'].value, s.cells['4,1'].formula, links.X.row];
E.shift(s, 'row', 2, -1, links);
r.remove = [s.cells['3,0'].formula, s.cells['3,0'].value, s.cells['3,1'].formula, s.cells['3,1'].value];
E.shift(s, 'col', 0, 1, links);
r.col = [s.cells['3,1'].formula, links.X.col];
const clip = E.copyRange(s, { r1: 3, c1: 1, r2: 3, c2: 1 });
E.pasteRange(s, clip, 5, 3);
r.paste = [s.cells['5,3'].formula, E.offsetFormula('SUM(A1:B2)+$C$3+A$1', 1, 1)];
E.merge(s, { r1: 0, c1: 0, r2: 1, c2: 2 });
E.merge(s, { r1: 1, c1: 1, r2: 2, c2: 2 });
r.merge = s.merges;
E.shift(s, 'row', 0, -1, {});
r.mergeShift = s.merges;
E.split(s, { r1: 1, c1: 1, r2: 1, c2: 1 });
r.split = s.merges.length;
E.style(s, { r1: 0, c1: 0, r2: 0, c2: 1 }, 'b');
E.style(s, { r1: 0, c1: 0, r2: 0, c2: 0 }, 'diag', 'left');
r.style = [s.cells['0,0'].style, s.cells['0,1'].style];
E.style(s, { r1: 0, c1: 0, r2: 0, c2: 1 }, 'b');
r.unstyle = [s.cells['0,0'].style, s.cells['0,1'].style ?? null];
E.clearRange(s, { r1: 0, c1: 0, r2: 0, c2: 0 }, true);
r.clear = s.cells['0,0'];
process.stdout.write(JSON.stringify(r));
""")
assert out["insert"] == ["SUM(A1:A4)", 6, "A3*$A$1+LOG10(2)", 4]
assert out["remove"] == ["SUM(A1:A3)", 4, "#REF!*$A$1+LOG10(2)", "#오류"]
assert out["col"] == ["SUM(B1:B3)", 1]
assert out["paste"] == ["SUM(D3:D5)", "SUM(B2:C3)+$C$3+B$1"]
assert out["merge"] == [{"r1": 1, "r2": 2, "c1": 1, "c2": 2}]
assert out["mergeShift"] == [{"r1": 0, "r2": 1, "c1": 1, "c2": 2}]
assert out["split"] == 0
assert out["style"] == [{"b": True, "diag": "left"}, {"b": True}]
assert out["unstyle"] == [{"diag": "left"}, None]
assert out["clear"] == {"value": "", "style": {"diag": "left"}}
def test_real_sheets_read_merged_cells():
if not MAIN.exists():
pytest.skip("installed EST main sample unavailable")
out = node(
"""
const { importEstx } = require(process.cwd() + '/tools/est-test/import-estx.cjs');
const p = importEstx(require('fs').readFileSync(process.argv[1]));
const s = p.quantitySheets.find((x) => x.Code === 'N0000000136');
process.stdout.write(JSON.stringify({ count: s.excel[0].merges.length, first: s.excel[0].merges[0] }));
""",
str(MAIN),
)
assert out["count"] == 14
assert out["first"] == {"r1": 2, "r2": 4, "c1": 1, "c2": 1}