Files
Aislo/resources/tester/test_est_excel_map.py
T
eomsangdonandClaude Opus 5.5 4876ae1e75 feat(est-test): Excel 내역 열 맞춰 가져오기
- 원본 엑셀 변환 내역서 탭처럼 시트·행 범위·항목별 열 문자로 남의 Excel 내역을 읽음
- 미리보기, 순공사비 위 추가·바꾸기, 단위·수량 없는 줄은 공종 제목
- 수식 셀은 저장된 계산값 사용, 합계단가만 있는 줄은 비목 배분 없이 거부

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 17:05:13 +09:00

135 lines
5.4 KiB
Python

"""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