- 원본 popupSNameShow: 산출근거 식의 기호를 비목 약어로, 숫자를 요율 글자로 바꿔 화면·인쇄에 표시하고 편집 잠금, 저장 값은 그대로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
169 lines
7.5 KiB
Python
169 lines
7.5 KiB
Python
"""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
|