diff --git a/resources/tester/helper_stmate_smt_opts.cjs b/resources/tester/helper_stmate_smt_opts.cjs new file mode 100644 index 000000000..390c2721b --- /dev/null +++ b/resources/tester/helper_stmate_smt_opts.cjs @@ -0,0 +1,20 @@ +// STMATE 재현 앱 구성 조립 설정 — stdin {cases:[{rows, table, ini, code, sys}]} → 각 결과(M·L·S)와 설정 +globalThis.window = globalThis; +require("../../tools/stmate-test/calc.js"); +require("../../tools/stmate-test/smt_eval.js"); +require("../../tools/stmate-test/smt_engine.js"); +let buf = ""; +process.stdin.setEncoding("utf8"); +process.stdin.on("data", (d) => (buf += d)); +process.stdin.on("end", () => { + const { cases } = JSON.parse(buf); + const out = cases.map((c) => { + const ctx = { dbfData: { start_ini: c.ini || {}, costn_rows: [] }, state: { sysConfig: c.sys || {} } }; + const opts = window.STMATE_ENGINE.compositionOptions(ctx, c.code || "X00001", c.row || {}); + const lookup = (code) => c.table[code] || null; + const ev = window.STMATE_SMT_EVAL.evaluateComposition(c.rows, lookup, opts); + const smt = window.STMATE_ENGINE.smtOptions(ctx, "D00001"); + return { M: ev.M, L: ev.L, S: ev.S, opts: { ...opts, labor: null }, smtLine: smt.lineDigits }; + }); + process.stdout.write(JSON.stringify(out)); +}); diff --git a/resources/tester/helper_stmate_smt_tools.cjs b/resources/tester/helper_stmate_smt_tools.cjs new file mode 100644 index 000000000..b96bf2cbe --- /dev/null +++ b/resources/tester/helper_stmate_smt_tools.cjs @@ -0,0 +1,11 @@ +// STMATE 재현 앱 계산 도구(수량 계산기 · 실적→일식 비목율) — stdin [{fn, args}] → 결과 배열 +const X = require("../../tools/stmate-test/calc_xrate.js"); +const A = require("../../tools/stmate-test/calc_a2w.js"); +let buf = ""; +process.stdin.setEncoding("utf8"); +process.stdin.on("data", (d) => (buf += d)); +process.stdin.on("end", () => { + const fns = { ...X, ...A }; + const out = JSON.parse(buf).map(({ fn, args }) => fns[fn](...args)); + process.stdout.write(JSON.stringify(out)); +}); diff --git a/resources/tester/test_stmate_smt_opts.py b/resources/tester/test_stmate_smt_opts.py new file mode 100644 index 000000000..8df0bd972 --- /dev/null +++ b/resources/tester/test_stmate_smt_opts.py @@ -0,0 +1,94 @@ +"""STMATE 재현 앱 구성 조립 설정 — 소수 자리(KCD·BPN·ACD·JCD·ECD) · 외산장비 환율($ 구간·€·¥). + +근거: 22번 §3.4(0·1·2 자리) · REA 14 TWM_SET_SYS 안내문 + KCD 중기사용료 금액 · BPN 일위대가표 금액 · ACD 단가산출서 금액 · JCD 「X」의 중기노임 단가 · ECD 「X」의 $장비 단가 + ₩/$(10만$이상은 ed_EXH) · ₩/€ · ₩/100¥ · 단가의 조사시점(ARENM) = 환율 칸 1/2. +실무 7건 값(KCD·ACD·BPN = 1 · ECD = 0 · JCD 0/1 · EXLNM = EXHNM)은 test_stmate_ilwi_eval 이 1원 대조 · 여기는 손셈. +""" + +import json +import subprocess +from pathlib import Path + +HELPER = Path(__file__).with_name("helper_stmate_smt_opts.cjs") + + +def run(*cases): + out = subprocess.check_output( + ["node", str(HELPER)], + input=json.dumps({"cases": list(cases)}, ensure_ascii=False), + text=True, + encoding="utf-8", + ) + return json.loads(out) + + +def comp(rows, table, ini=None, code="X00001", row=None): + return {"rows": rows, "table": table, "ini": ini or {}, "code": code, "row": row or {}} + + +M1 = {"M00001": {"M": 333, "L": 0, "S": 0, "unit": "kg"}} +ROWS = [{"CODE": "M00001", "QTY1": 0.33}] # 333 × 0.33 = 109.89 + + +def test_line_digits_by_kind(): + a, b, c, d = run( + comp(ROWS, M1, {"KCDNM": "1"}), + comp(ROWS, M1, {"KCDNM": "0"}), + comp(ROWS, M1, {"BPNNM": "2"}, code="B00001"), + comp(ROWS, M1, {"KCDNM": "0", "BPNNM": "1"}, code="B00001"), + ) + # 줄 금액 109.89 → 1 자리 109.8 · 0 자리 109 · 결과는 원 미만 절사 + assert a["opts"]["lineDigits"] == 1 and b["opts"]["lineDigits"] == 0 + assert c["opts"]["lineDigits"] == 2 and d["opts"]["lineDigits"] == 1 + assert a["M"] == b["M"] == 109 + + +def test_line_digits_change_sum(): + rows = [{"CODE": "M00001", "QTY1": 0.33}] * 3 # 109.89 × 3 + one, zero, two = run( + comp(rows, M1, {"KCDNM": "1"}), comp(rows, M1, {"KCDNM": "0"}), comp(rows, M1, {"KCDNM": "2"}) + ) + assert (one["M"], zero["M"], two["M"]) == (329, 327, 329) # 329.4 · 327 · 329.67 + + +def test_acd_digits_for_smt(): + a, b = run(comp([], {}, {"ACDNM": "0"}), comp([], {}, {})) + assert a["smtLine"] == 0 and b["smtLine"] == 1 + + +def test_wage_digits_jcd(): + table = {"L00001": {"M": 0, "L": 100001, "S": 0, "unit": "인"}} + rows = [{"CODE": "L00001", "QTY1": 10}] + ini = {"CONLD": "L00001", "RXXNM": "1/8", "KCDNM": "2"} + j0, j1 = run(comp(rows, table, {**ini, "JCDNM": "0"}), comp(rows, table, {**ini, "JCDNM": "1"})) + # 100001 / 8 = 12500.125 → JCD 0 = 12500 · JCD 1 = 12500.1 → × 10 + assert j0["L"] == 125000 and j1["L"] == 125001 + + +def test_foreign_units_and_usd_band(): + table = { + "S00001": {"M": 0, "L": 0, "S": 49594, "unit": "$"}, + "S00002": {"M": 0, "L": 0, "S": 150000, "unit": "$"}, + "S00003": {"M": 0, "L": 0, "S": 1000, "unit": "€"}, + "S00004": {"M": 0, "L": 0, "S": 1000, "unit": "¥"}, + } + ini = {"EXLNM1": "1470", "EXHNM1": "1500", "EXEUR1": "1423", "EXYEN1": "915"} + ini |= {"EXLNM2": "1000", "EXHNM2": "1000", "EXEUR2": "1000", "EXYEN2": "1000"} + one = lambda c: [{"CODE": c, "QTY1": 1}] # noqa: E731 + r = run( + comp(one("S00001"), table, ini), + comp(one("S00002"), table, ini), + comp(one("S00003"), table, ini), + comp(one("S00004"), table, ini), + comp(one("S00001"), table, {**ini, "ARENM": "2"}), + ) + # 단가(천원) = 가격 × 환율 ÷ 1000 · 10만$ 이상 = EXHNM · ¥ 는 100¥당 + assert [x["S"] for x in r] == [72903, 225000, 1423, 9, 49594] + + +def test_usd_real_case_ulljin(): + # 울진 X00127 트럭트랙터 — S00346 49594$ × 1470 ÷ 1000 × 0.2529 = 18437(저장 SAMT1) + table = {"S00346": {"M": 0, "L": 0, "S": 49594, "unit": "$"}} + (r,) = run(comp([{"CODE": "S00346", "QTY1": 0.2529}], table, {"EXLNM1": "1470", "EXHNM1": "1470"})) + assert r["S"] == 18437 diff --git a/resources/tester/test_stmate_smt_tools.py b/resources/tester/test_stmate_smt_tools.py new file mode 100644 index 000000000..432a282d2 --- /dev/null +++ b/resources/tester/test_stmate_smt_tools.py @@ -0,0 +1,66 @@ +"""STMATE 재현 앱 계산 도구 — 노무·장비 수량 계산기(wM_Edit_XRate) · 실적→일식 비목율(wM_Conv_A2W). + +근거: REA 30 labor_equipment_quantity_rules.json · REA 27 conversion_component_rate_rules.json — 손셈 대조. +수량 계산기 반올림(MROUNDUD) 방향은 미확정 — 반올림으로 둔 값은 경계가 아닌 수로만 봄. +""" + +import json +import subprocess +from pathlib import Path + +HELPER = Path(__file__).with_name("helper_stmate_smt_tools.cjs") + + +def call(*calls): + payload = [{"fn": f, "args": list(a)} for f, *a in calls] + out = subprocess.check_output( + ["node", str(HELPER)], input=json.dumps(payload), text=True, encoding="utf-8" + ) + return json.loads(out) + + +def test_xrate_daily_and_applied_quantity(): + lab, eq, zero = call( + ("compute", {"qty1": 120, "cond": "0.9*1.1", "hrat": 80, "lqty": 3, "labor": True, "point": 4}), + ("compute", {"qty1": 120, "cond": "", "hrat": 100, "lqty": 1, "labor": False, "auto": True}), + ("compute", {"qty1": 0, "cond": "", "hrat": 100, "lqty": 2, "labor": True, "point": 3}), + ) + # 120 × 0.99 × 80% = 95.04 · 노무 3 × 1 ÷ 95.04 = 0.031565… → 4 자리 0.0316 + assert lab["qday"] == 95.04 and lab["cqty"] == 0.0316 and abs(lab["cond"] - 0.99) < 1e-12 + # 장비 1 × 8 ÷ 120 = 0.0666… · 자동 자리 = 「120」 3자리 + 1 = 4 → 0.0667 + assert eq["point"] == 4 and eq["cqty"] == 0.0667 + assert zero["qday"] == 0 and zero["cqty"] == 0 + + +def test_xrate_auto_point_and_condition_chars(): + p1, p2, p3, bad, paren, power = call( + ("autoPoint", 5.5), + ("autoPoint", 123456.0), + ("autoPoint", 1234), + ("condFactor", "0.9 * a"), + ("condFactor", "(1+2)"), + ("condFactor", "2^3/4"), + ) + assert (p1, p2, p3) == (2, 6, 5) + assert not bad["ok"] and not paren["ok"] and bad["value"] == 1 + assert power == {"ok": True, "value": 2} + + +def test_a2w_editor_row_and_bulk(): + e1, e2, e3, r1, r2 = call( + ("normalizeEditor", 70, 50, "L"), + ("normalizeEditor", 70, 50, "M"), + ("normalizeEditor", 130, 0, "L"), + ("normalizeRow", 70, 50, 10), + ("normalizeRow", 70, 50, 9), + ) + assert e1 == {"L": 70, "M": 30, "E": 0} and e2 == {"L": 50, "M": 50, "E": 0} + assert e3 == {"L": 100, "M": 0, "E": 0} + assert r1 == {"L": 50, "M": 50, "E": 0} and r2 == {"L": 70, "M": 30, "E": 0} + rows = [{"sel": True, "rateL": 0}, {"sel": True, "rateL": 80}, {"sel": False, "L": 5}] + plain, indiv = call( + ("bulkApply", rows, {"L": 40, "M": 30}, {}), + ("bulkApply", rows, {"L": 40, "M": 30}, {"individual": True}), + ) + assert [(r["L"], r["M"], r["E"]) for r in plain] == [(40, 30, 30), (40, 30, 30), (0, 0, 0)] + assert [(r["L"], r["M"], r["E"]) for r in indiv] == [(40, 30, 30), (80, 20, 0), (0, 0, 0)]