Files
Aislo/resources/tester/test_est_wonga_tools.py
T
eomsangdonandClaude Opus 5.5 d38d2a2893 feat(est-test): 엑셀형 수량산출서 계산·편집과 원가 행 자리수
- 엑셀형 산출서 BIFF 수식 해석·다시 계산·수식줄 편집·4방향 합·수량으로 지정(가져올 때 값이 같은 합계 셀 추정 연결), 셀 편집 시 ESTX 저장 거부
- 원가 행 계산을 원본 c9d278 끝부분대로: 소수 4자리 반올림 뒤 원 미만 절사, Diget>0 행 자리맞춤(0x11293e8)
- 원가계산서 「선택 행 자릿수 설정」을 원본 선택항목 절하하기 대화로(도급액 행 거부, 입력 행 원금액 기준)

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

151 lines
6.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""EST 테스트 앱 원가 보조 계산(원본 TfrmWongaDiget 기타경비 계산·산업안전보건관리비 비교)."""
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-wonga-tools.js",
]
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.source = JSON.parse(fs.readFileSync('tools/est-test/sample.json', 'utf8'));
process.stdout.write(JSON.stringify(vm.runInContext({json.dumps(code)}, ctx)));
"""
return json.loads(subprocess.check_output(["node", "-e", runner], cwd=ROOT).decode("utf-8"))
def test_anjun_reproduces_original_formula_pair_in_sample():
result = run_js(
"""(() => {
const calc = (p) => EstNative.calculate(EstModel.validate(p));
const p = calc(structuredClone(source));
const plain = (t) => String(t).replace(/\s+/g, '');
const row = p.tables.Wonga.find((r) => plain(r.Name).includes('산업안전') && Number(r.CalcIndex) !== 2);
const original = [row.VSusic, p.tables.Wonga.find((r) => plain(r.Name).includes('산업안전') && Number(r.CalcIndex) === 2).VSusic];
const before = p.tables.Wonga.length;
const out = EstWongaTools.anjun(p, { youl: 3.15, base: 5349000, addBase: false, compare: true });
const after = calc(p);
return { base: EstWongaTools.anjunBase(p.tables.Wonga), original, chosen: out.chosen, other: out.other,
rows: p.tables.Wonga.length === before, errors: after.calculation.errors.length };
})()"""
)
assert result["base"] == "(A + 4)"
assert result["chosen"] == "(A + 4) × 0.0315 × 1.2"
assert result["other"] == "((A + 4) + J/1.1) × 0.0315"
assert [x.replace(" ", "") for x in result["original"]] == [
result["chosen"].replace(" ", ""),
result["other"].replace(" ", ""),
]
assert result["rows"] is True and result["errors"] == 0
def test_gita_picks_smaller_base_and_validates():
result = run_js(
"""(() => {
const p = { options: {}, tables: { Wonga: [
{ Gubun: 'A', Name: '직 접 공 사 비', Cost: 1000 }, { Gubun: 'B', Name: '직접노무비', Cost: 300 },
{ Gubun: 'C', Name: '간접노무비', Cost: 50 }, { Gubun: 'D', Name: '기 타 경 비', Youl: '5%' } ] } };
const out = EstWongaTools.gita(p, { gcRatio: 0.4, baseRatio: 0.6, youl: 5.2, compare: true });
const row = p.tables.Wonga[3], cmp = p.tables.Wonga[4];
const errors = [];
for (const bad of [{ gcRatio: 2, baseRatio: 0.5, youl: 1 }, { gcRatio: 0.5, baseRatio: 0.5, youl: 11 }])
try { EstWongaTools.gita(p, bad); } catch (e) { errors.push(e.message.slice(0, 4)); }
let none = ''; try { EstWongaTools.anjun({ options: {}, tables: { Wonga: [] } }, { youl: 1 }); } catch (e) { none = e.message; }
return { values: out.values, vs: row.VSusic, youl: row.Youl, cmp: [cmp.VSusic, cmp.CalcIndex], errors, none,
stored: [p.options.GitaGCRatio, p.options.GitaBaseRatio] };
})()"""
)
assert result["values"] == [750, 600]
assert result["vs"] == "(A × 0.6) × 0.052"
assert result["youl"] == "5.2%"
assert result["cmp"] == ["((A × 0.4) + B + C) × 0.052", 2]
assert result["errors"] == ["1 이하", "기타경비"]
assert result["none"] == "산업안전보건관리비 비목이 없습니다."
assert result["stored"] == [0.4, 0.6]
def test_diget_round_follows_original_engine():
result = run_js(
r"""(() => {
const r = (Diget, DigetIndex, v) => EstWonga.digetRound({ Diget, DigetIndex }, v);
const row = (Diget, DigetIndex, LinkIndex, v) => EstWonga.rowCost({ Diget, DigetIndex, LinkIndex }, v);
return {
none: r(3, 0, 1660273), half: r(3, 1, 1660500), cut: r(3, 2, 1660999), up: r(3, 3, 1660001),
other: r(2, 7, 1234), neg: r(3, 2, -1660999), dec: r(-2, 1, 1.236),
nearly: row(0, 0, 0, 1660273.99996), keep55: row(3, 2, 55, 79603324), cut1: row(1, 2, 0, 1234.9),
p: (() => {
const p = EstNative.calculate(EstModel.validate(structuredClone(source)));
const plain = (t) => String(t).replace(/\s+/g, '');
return p.tables.Wonga.filter((w) => /산업안전|이윤|도급액/.test(plain(w.Name))).map((w) => [plain(w.Name), w.Cost]);
})(),
};
})()"""
)
assert [result[k] for k in ("none", "half", "cut", "up", "other", "neg")] == [
1660273,
1661000,
1660000,
1661000,
1200,
-1660000,
]
assert abs(result["dec"] - 1.24) < 1e-9
assert result["nearly"] == 1660274
assert result["keep55"] == 79603324
assert result["cut1"] == 1230
costs = [cost for _, cost in result["p"]]
assert {1660273, 1887446, 7620104, 79603000} <= set(costs)
def test_position_sets_row_digits_like_original():
result = run_js(
r"""(() => {
const calc = (p) => EstNative.calculate(EstModel.validate(p));
const p = calc(structuredClone(source));
const plain = (t) => String(t).replace(/\s+/g, '');
const rows = p.tables.Wonga;
const anjun = rows.findIndex((r) => plain(r.Name).includes('산업안전') && Number(r.CalcIndex) !== 2);
const total = rows.findIndex((r) => Number(r.LinkIndex) === 55);
EstWongaTools.position(p, [anjun], 2, 3);
const cut = calc(p).tables.Wonga[anjun].Cost;
EstWongaTools.position(p, [anjun], 0, 3);
const reset = calc(p).tables.Wonga[anjun].Cost;
let refused = '';
try { EstWongaTools.position(p, [total], 2, 3); } catch (e) { refused = e.message; }
const input = { options: {}, tables: { Wonga: [{ Name: '관급자재대', Cost: 1234567 }] } };
EstWongaTools.position(input, [0], 1, 2);
return { cut, reset, refused, input: input.tables.Wonga[0], last: input.options.WongaLastDiget };
})()"""
)
assert result["cut"] == 1660000
assert result["reset"] == 1660273
assert result["refused"].startswith("도급액 역산 자리수")
assert result["input"]["Cost"] == 1234600 and result["input"]["TC"] == 1234567
assert result["input"]["Diget"] == 2 and result["input"]["DigetIndex"] == 1
assert result["last"] == 2