Files
Aislo/resources/tester/test_est_wonga_tools.py
T
eomsangdonandClaude Opus 5.5 cc1a6bcbde feat(est-test): 원가계산서 산출근거 약어로 표시
- 원본 popupSNameShow: 산출근거 식의 기호를 비목 약어로, 숫자를 요율 글자로 바꿔 화면·인쇄에 표시하고 편집 잠금, 저장 값은 그대로

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

169 lines
7.5 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
def test_abbreviate_formula_like_original_sname_show():
result = run_js(
r"""(() => {
const rows = source.tables.Wonga, a = EstWongaTools.abbreviate(rows, source.options);
const by = (g) => a[rows.findIndex((r) => r.Gubun === g)];
const nf = EstWongaTools.abbreviate([{ Index: 1, NumStr: '1', Name: '직 접 노 무 비' },
{ NumStr: '2', VSusic: '1 × 0.05', Youl: '5%' }], { VerFormat: false });
return { five: by('5'), c: by('C'), e: by('E'), empty: a[0], nf: nf[1], same: rows.every((r, i) => r.VSusic === source.tables.Wonga[i].VSusic) };
})()"""
)
assert result["five"] == "직.노 × 16.5%"
assert result["c"] == "( 산출경비 ~ 18 )"
assert result["e"] == "순.공 × 8%"
assert result["empty"] == ""
assert result["nf"] == "직접노무비 × 5%"
assert result["same"] is True