- 원본 엑셀 변환 내역서 탭처럼 시트·행 범위·항목별 열 문자로 남의 Excel 내역을 읽음 - 미리보기, 순공사비 위 추가·바꾸기, 단위·수량 없는 줄은 공종 제목 - 수식 셀은 저장된 계산값 사용, 합계단가만 있는 줄은 비목 배분 없이 거부 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
135 lines
5.4 KiB
Python
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
|