"""EST 테스트 앱 Excel 내역 열 맞춰 가져오기(원본 TFRMEXCELTOEST 내역서 탭).""" import json import subprocess from pathlib import Path ROOT = Path(__file__).resolve().parents[2] FILES = [ "native-math.js", "native-formula.js", "native-machine.js", "native-rows.js", "native-text.js", "native-quantity.cjs", "native-wonga.js", "native-engine.js", "native-row-match.js", "native-escalation.js", "native-progress.js", "native-business.js", "native-review.js", "native-resources.js", "native-schedule.js", "native-model.js", "native-verify.js", "native-excel-map.js", ] TABLE = """const table = [ ['공 내 역 서'], ['번호','품명','규격','단위','수량','합계단가','재료단가','노무단가','경비단가','비고'], ['1','토공사'], ['1.1','터파기','백호 0.7㎥','㎥','1,200',null,'0','3500','1200','현장'], [], ['1.2','되메우기','','㎥',800], ['2','포장공'], ['2.1','아스콘포장','t=5cm','㎡',350.5,31000,21000,'7000','3000'], ]; const columns = {Pr:'a',Name:'B',Size:'C',Unite:'D',GN:'E',TU:'F',GU:'G',NU:'H',JU:'I',Bigo:'J'};""" def run_js(code: str): runner = f""" const fs = require('fs'), vm = require('vm'); const ctx = vm.createContext({{ structuredClone, console }}); for (const f of {json.dumps(FILES)}) vm.runInContext(fs.readFileSync('tools/est-test/' + f, 'utf8'), ctx, {{ filename: f }}); ctx.project = JSON.parse(fs.readFileSync('tools/est-test/sample.json', 'utf8')); process.stdout.write(JSON.stringify(vm.runInContext({json.dumps(code)}, ctx))); """ out = subprocess.check_output(["node", "-e", runner], cwd=ROOT) return json.loads(out.decode("utf-8")) def test_records_map_columns_parse_numbers_and_skip_blank_rows(): result = run_js( "(() => {" + TABLE + """ const list = EstExcelMap.records(table, 3, 20, columns); const errors = []; const t = (fn) => { try { fn(); errors.push('ok'); } catch (e) { errors.push(e.message.slice(0, 8)); } }; t(() => EstExcelMap.records(table, 3, 8, {GN:'E'})); t(() => EstExcelMap.records(table, 0, 8, columns)); t(() => EstExcelMap.records(table, 3, 8, {...columns, Name:'B1'})); t(() => EstExcelMap.records([[null,'x','', '', '1개']], 1, 1, columns)); return { lines: list.map((r) => r.line), first: list[1], errors, col: [EstExcelMap.columnIndex('AA'), EstExcelMap.columnName(26)] }; })()""" ) assert result["lines"] == [3, 4, 6, 7, 8] assert result["first"]["GN"] == 1200 and result["first"]["NU"] == 3500 assert result["first"]["TU"] is None and result["first"]["Pr"] == "1.1" assert result["col"] == [26, "AA"] assert [e[:4] for e in result["errors"]] == ["품명 열", "시작행을", "열 문자", "E1: "] def test_rows_make_headings_details_and_insert_above_net_cost(): result = run_js( "(() => {" + TABLE + """ const list = EstExcelMap.records(table, 3, 20, columns); const { rows, warnings } = EstExcelMap.rows(project, list); const est = project.tables.EstMainDat, boundary = est.findIndex((r) => Number(r.LinkIndex) === 54); const ids = new Set([...est.map((r) => r.PkID), ...rows.map((r) => r.PkID)]); EstExcelMap.insert(project, rows, 'append'); const done = EstNative.calculate(EstModel.validate(project)); const at = done.tables.EstMainDat.findIndex((r) => r.PkID === rows[1].PkID); const pave = done.tables.EstMainDat.find((r) => r.PkID === rows[4].PkID); return { kinds: rows.map((r) => [r.Name, Number(r.Level) || 0]), warnings, unique: ids.size === est.length, placed: at === boundary + 1, net: done.tables.EstMainDat[boundary + rows.length].LinkIndex, dig: [done.tables.EstMainDat[at].NC, done.tables.EstMainDat[at].JC], pave: [pave.TC, pave.Bigo ?? null], }; })()""" ) assert result["kinds"] == [ ["토공사", 1], ["터파기", 0], ["되메우기", 0], ["포장공", 1], ["아스콘포장", 0], ] assert result["warnings"] == [] assert result["unique"] is True assert result["placed"] is True assert result["net"] == 54 assert result["dig"] == [4200000, 1440000] assert result["pave"][0] == 350.5 * 31000 def test_total_price_without_split_is_rejected_and_replace_mode(): result = run_js( "(() => {" + TABLE + """ table[3][6] = null; table[3][7] = null; table[3][8] = null; table[3][5] = 4700; let message = ''; try { EstExcelMap.rows(project, EstExcelMap.records(table, 3, 20, columns)); } catch (e) { message = e.message; } table[3][5] = null; const { rows } = EstExcelMap.rows(project, EstExcelMap.records(table, 3, 4, columns)); const before = project.tables.EstMainDat.length, boundary = project.tables.EstMainDat.findIndex((r) => Number(r.LinkIndex) === 54); EstExcelMap.insert(project, rows, 'replace'); return { message, length: project.tables.EstMainDat.length === before - boundary + rows.length, first: project.tables.EstMainDat[0].Name, net: project.tables.EstMainDat[rows.length].LinkIndex }; })()""" ) assert result["message"].startswith("4행: 합계단가만 있습니다") assert result["length"] is True assert result["first"] == "토공사" assert result["net"] == 54 def test_shift_column_letters_like_original(): out = run_js( r"""(() => { const keys = EstExcelMap.fields.map(([k]) => k); const right = EstExcelMap.shift({ Code: 'A', Name: 'Q' }, 1); const left = EstExcelMap.shift({ Code: 'C' }, -1); let low = ''; try { EstExcelMap.shift({ Code: 'A' }, -1); } catch (e) { low = e.message; } return { right: keys.map((k) => right[k]).slice(0, 4), last: right[keys.at(-1)], left: left.Code, low }; })()""" ) assert out["right"] == ["B", "C", "D", "E"] assert out["last"] == "P" assert out["left"] == "B" assert out["low"] == '시작 부호는 "A" 이하로는 할수 없습니다.'