Files
Aislo/resources/tester/test_est_basic_prices.py

491 lines
20 KiB
Python

from test_est_rea_parity import run
def test_machine_restore_group_uses_saved_baseline_without_changing_other_groups():
result = run(
"math",
"""(()=>{
const p=EstNative.empty();
p.tables.MachMainDat=[{PkID:1,Index:1,Code:'M1',Name:'장비1'},{PkID:2,Index:0,GN:2},
{PkID:3,Index:1,Code:'M2',Name:'장비2'},{PkID:4,Index:0,GN:3}];
p.tables.MachTitleDat=[{Code:'M1',Name:'장비1'},{Code:'M2',Name:'장비2'}];
const q=EstModel.validate(JSON.parse(JSON.stringify(EstBusiness.baseline(p))));
q.tables.MachMainDat[1].GN=9;q.tables.MachMainDat[3].GN=7;
const before=JSON.stringify(q), rows=EstMachine.restoreGroup(q,'M1');
const originalUntouched=JSON.stringify(q)===before;
let missing=false,duplicate=false,collision=false;
const legacy=structuredClone(q);delete legacy.baseline.tables.MachMainDat;
try { EstMachine.restoreGroup(legacy,'M1'); } catch { missing=true; }
const conflict=structuredClone(q);conflict.tables.MachMainDat[3].PkID=2;
try { EstMachine.restoreGroup(conflict,'M1'); } catch { collision=true; }
q.tables.MachMainDat.push({Index:1,Code:'M1'});
try { EstMachine.restoreGroup(q,'M1'); } catch { duplicate=true; }
return {restored:rows[1].GN,other:rows[3].GN,originalUntouched,
baseline:q.baseline.tables.MachMainDat[1].GN,missing,duplicate,collision};
})()""",
)
assert result == {
"restored": 2,
"other": 7,
"originalUntouched": True,
"baseline": 2,
"missing": True,
"duplicate": True,
"collision": True,
}
def test_machine_change_includes_new_changed_and_deleted_titles():
result = run(
"math",
"""(()=>{
const p=EstNative.empty();
p.tables.MachTitleDat=[{Code:'M1',Name:'장비1',TC:150},{Code:'M3',Name:'신규',TC:50}];
p.baseline={tables:{EstMainDat:[],MachTitleDat:[
{Code:'M1',Name:'장비1',TC:100},{Code:'M2',Name:'삭제',TC:200}]}};
return EstMachine.changes(p).map(({Code,originalTC,TC,difference,status,index})=>
({Code,originalTC,TC,difference,status,index}));
})()""",
)
assert result == [
{
"Code": "M1",
"originalTC": 100,
"TC": 150,
"difference": 50,
"status": "변경",
"index": 0,
},
{
"Code": "M3",
"originalTC": None,
"TC": 50,
"difference": None,
"status": "신규",
"index": 1,
},
{
"Code": "M2",
"originalTC": 200,
"TC": 0,
"difference": -200,
"status": "삭제",
"index": None,
},
]
def test_recover_deleted_machine_group_and_reject_id_collision():
result = run(
"math",
"""(()=>{
const p=EstNative.empty();
p.tables.MachMainDat=[{PkID:1,Index:1,Code:'M1',Name:'first'},
{PkID:2,Index:-2,GN:2,TU:100},{PkID:3,Index:1,Code:'M2',Name:'second'},
{PkID:4,Index:-2,GN:3,TU:100}];
p.tables.MachTitleDat=[{PkID:10,Code:'M1',Name:'first',TC:200},
{PkID:11,Code:'M2',Name:'second',TC:300}];
const q=EstBusiness.baseline(p);
q.tables.MachMainDat=q.tables.MachMainDat.slice(2);
q.tables.MachTitleDat=q.tables.MachTitleDat.slice(1);
const before=JSON.stringify(q), restored=EstMachine.recoverGroup(q,'M1');
const unchanged=JSON.stringify(q)===before;
const collision=structuredClone(q);collision.tables.MachMainDat[1].PkID=2;
let rejected=false;try { EstMachine.recoverGroup(collision,'M1'); } catch { rejected=true; }
q.tables.MachMainDat=restored.rows;q.tables.MachTitleDat=restored.titles;
return {codes:q.tables.MachTitleDat.map(x=>x.Code),quantity:q.tables.MachMainDat[1].GN,
original:EstMachine.usage(q).find(x=>x.Code==='M1').originalPrice,unchanged,rejected};
})()""",
)
assert result == {
"codes": ["M1", "M2"],
"quantity": 2,
"original": 200,
"unchanged": True,
"rejected": True,
}
def test_recovered_machine_group_recalculates_linked_estimate_after_reopen():
result = run(
"math",
"""(()=>{
const p=EstNative.empty();p.options.SourceRefresh=true;
p.tables.MachMainDat=[{PkID:1,Index:1,Code:'M1',Name:'machine'},
{PkID:2,Index:-2,Name:'fuel',GN:2,TU:100}];
p.tables.MachTitleDat=[{PkID:10,Code:'M1',Name:'machine'}];
p.tables.EstMainDat=[{PkID:20,Code:'M1',Name:'machine',LinkIndex:105,GN:2}];
const q=EstBusiness.baseline(EstNative.calculate(p));
q.tables.MachMainDat=[];q.tables.MachTitleDat=[];
const restored=EstMachine.recoverGroup(JSON.parse(JSON.stringify(q)),'M1');
q.tables.MachMainDat=restored.rows;q.tables.MachTitleDat=restored.titles;
const out=EstNative.calculate(EstModel.validate(JSON.parse(JSON.stringify(q))));
return {machine:out.tables.MachTitleDat[0].TC, estimate:out.tables.EstMainDat[0].TC,
errors:out.calculation.errors};
})()""",
)
assert result == {"machine": 200, "estimate": 400, "errors": []}
def test_basic_prices_changes_for_material_and_labor():
result = run(
"math",
"""(()=>{
const p = EstNative.empty();
p.tables.EstMainDat = [
{ PkID: 1, Code: 'M1', Name: 'changed mat', LinkIndex: 5, GN: 2, TC: 200 },
{ PkID: 2, Code: 'L1', Name: 'changed lab', LinkIndex: 6, GN: 1, TC: 50 },
{ PkID: 4, Code: 'M2', Name: 'new mat', LinkIndex: 5, GN: 1, TC: 40 },
{ PkID: 5, Code: 'L2', Name: 'new lab', LinkIndex: 6, GN: 1, TC: 80 },
];
p.baseline = {
tables: {
EstMainDat: [
{ PkID: 1, Code: 'M1', Name: 'changed mat', LinkIndex: 5, GN: 1, TC: 100 },
{ PkID: 2, Code: 'L1', Name: 'changed lab', LinkIndex: 6, GN: 1, TC: 30 },
{ PkID: 3, Code: 'M3', Name: 'deleted mat', LinkIndex: 5, GN: 3, TC: 300 },
{ PkID: 6, Code: 'L3', Name: 'deleted lab', LinkIndex: 6, GN: 2, TC: 120 },
]
}
};
return {
matChanges: EstBasicPrices.changes(p, 'material'),
labChanges: EstBasicPrices.changes(p, 'labor'),
matPrint: EstForms.data('material-change', p),
labPrint: EstForms.data('labor-change', p),
};
})()""",
)
mat_rows = {row["Code"]: row for row in result["matChanges"]}
assert set(mat_rows) == {"M1", "M2", "M3"}
assert mat_rows["M1"]["difference"] == 100
assert mat_rows["M3"]["status"] == "삭제"
assert mat_rows["M2"]["status"] == "신규"
assert result["matPrint"]["rows"] == result["matChanges"]
lab_rows = {row["Code"]: row for row in result["labChanges"]}
assert set(lab_rows) == {"L1", "L2", "L3"}
assert lab_rows["L1"]["difference"] == 20
assert lab_rows["L3"]["status"] == "삭제"
assert lab_rows["L2"]["status"] == "신규"
assert result["labPrint"]["rows"] == result["labChanges"]
def test_basic_prices_usage_reports_applied_and_source_prices_without_mutation():
result = run(
"math",
"""(()=>{
const p = EstNative.empty();
p.tables.StuffcDat = [
{ Code: 'M1', Name: 'mat 1', ValueIndex: 0, Cost1: 1200 },
{ Code: 'M2', Name: 'mat 2', ValueIndex: 0, Cost1: 500 },
{ Code: 'M2', Name: 'mat 2 dup', ValueIndex: 0, Cost1: 600 },
];
p.tables.LaborcDat = [
{ Code: 'L1', Name: 'lab 1', ValueIndex: 0, Cost1: 200000 },
];
p.tables.EstMainDat = [
{ Code: 'M1', Name: 'mat 1', LinkIndex: 5, GN: 2, TU: 1000, GU: 1000, GC: 2000, TC: 2000 },
{ Code: 'M2', Name: 'dup mat', LinkIndex: 5, GN: 1, TU: 450, GU: 450, GC: 450, TC: 450 },
{ Code: 'M3', Name: 'missing mat', LinkIndex: 5, GN: 1, TU: 200, GU: 200, GC: 200, TC: 200 },
{ Code: 'L1', Name: 'lab 1', LinkIndex: 6, GN: 1, TU: 180000, NU: 180000, NC: 180000, TC: 180000 },
];
const before = JSON.stringify(p);
const matUsage = EstBasicPrices.usage(p, 'material');
const labUsage = EstBasicPrices.usage(p, 'labor');
const matPrint = EstForms.data('material-usage', p);
const labPrint = EstForms.data('labor-usage', p);
return { matUsage, labUsage, matPrint, labPrint, same: before === JSON.stringify(p) };
})()""",
)
assert result["same"]
assert [row["status"] for row in result["matUsage"]] == [
"연결됨",
"코드 중복",
"연결 없음",
]
assert result["matUsage"][0]["sourcePrice"] == 1200
assert result["matUsage"][0]["appliedPrice"] == 1000
assert result["matUsage"][0]["difference"] == -200
assert result["matUsage"][1]["sourcePrice"] is None
assert result["matPrint"]["rows"] == result["matUsage"]
assert len(result["labUsage"]) == 1
assert result["labUsage"][0]["status"] == "연결됨"
assert result["labUsage"][0]["sourcePrice"] == 200000
assert result["labUsage"][0]["appliedPrice"] == 180000
assert result["labUsage"][0]["difference"] == -20000
assert result["labPrint"]["rows"] == result["labUsage"]
def test_basic_prices_apply_row_and_all_prices_with_autocalc_modes():
result = run(
"math",
"""(()=>{
const p = EstNative.empty();
p.options.SourceRefresh = false;
p.options.AutoCalc = false; // 수동계산 모드
p.tables.StuffcDat = [
{ Code: 'M1', Name: 'mat 1', ValueIndex: 0, Cost1: 1500 },
{ Code: 'M2', Name: 'mat 2', ValueIndex: 0, Cost1: 3000 },
];
p.tables.LaborcDat = [
{ Code: 'L1', Name: 'lab 1', ValueIndex: 0, Cost1: 220000 },
];
p.tables.EstMainDat = [
{ Code: 'M1', Name: 'item mat 1', LinkIndex: 5, GN: 2, TU: 1000, GU: 1000, GC: 2000, TC: 2000, SCheck: 2 }, // SCheck=2: 노무비 몰아주기
{ Code: 'M2', Name: 'item mat 2', LinkIndex: 5, GN: 3, TU: 2000, GU: 2000, GC: 6000, TC: 6000 },
{ Code: 'L1', Name: 'item lab 1', LinkIndex: 6, GN: 2, TU: 200000, NU: 200000, NC: 400000, TC: 400000 },
];
// 1. 수동계산 상태에서 자재 행 0 단가 적용 (SCheck=2 보존 검증)
// AutoCalc=false이므로 단가는 NU=1500, TU=1500으로 갱신되나 기존 금액(GC=2000, TC=2000) 보존
EstBasicPrices.applyRowPrice(p, 0, 'material');
const afterRow0 = {
TU: p.tables.EstMainDat[0].TU,
GU: p.tables.EstMainDat[0].GU,
NU: p.tables.EstMainDat[0].NU,
GC: p.tables.EstMainDat[0].GC,
NC: p.tables.EstMainDat[0].NC,
TC: p.tables.EstMainDat[0].TC,
SCheck: p.tables.EstMainDat[0].SCheck,
};
// 2. 자재 일괄 적용: 남은 행 1 적용 (단가 3000 갱신, 수동계산 금액 GC=6000 보존)
const matCount = EstBasicPrices.applyAllPrices(p, 'material');
const afterMatAll = {
TU: p.tables.EstMainDat[1].TU,
GU: p.tables.EstMainDat[1].GU,
GC: p.tables.EstMainDat[1].GC,
TC: p.tables.EstMainDat[1].TC,
};
// 3. 노임 단가 적용: 행 2 적용 (단가 220000 갱신, 금액 NC=400000 보존)
const labCount = EstBasicPrices.applyAllPrices(p, 'labor');
const afterLab = {
TU: p.tables.EstMainDat[2].TU,
NU: p.tables.EstMainDat[2].NU,
NC: p.tables.EstMainDat[2].NC,
TC: p.tables.EstMainDat[2].TC,
};
// 4. AutoCalc=true로 전환 후 전체 재계산: GN(수량)을 반영한 금액으로 일괄 전파
p.options.AutoCalc = true;
const calculated = EstNative.calculate(p);
const afterRecalc = {
row0_NC: calculated.tables.EstMainDat[0].NC,
row0_TC: calculated.tables.EstMainDat[0].TC,
row1_GC: calculated.tables.EstMainDat[1].GC,
row1_TC: calculated.tables.EstMainDat[1].TC,
row2_NC: calculated.tables.EstMainDat[2].NC,
row2_TC: calculated.tables.EstMainDat[2].TC,
};
return { afterRow0, matCount, afterMatAll, labCount, afterLab, afterRecalc };
})()""",
)
# 행 0: SCheck=2 보존되어 NU=1500, GU=0, 금액은 수동계산 유지(GC=2000, NC=0, TC=2000)
assert result["afterRow0"]["TU"] == 1500
assert result["afterRow0"]["NU"] == 1500
assert result["afterRow0"]["GU"] == 0
assert result["afterRow0"]["GC"] == 2000
assert result["afterRow0"]["TC"] == 2000
assert result["afterRow0"]["SCheck"] == 2
# 행 1: 단가 3000, 수동 금액 6000 보존
assert result["matCount"] == 1
assert result["afterMatAll"]["TU"] == 3000
assert result["afterMatAll"]["GU"] == 3000
assert result["afterMatAll"]["GC"] == 6000
assert result["afterMatAll"]["TC"] == 6000
# 행 2: 단가 220000, 수동 금액 400000 보존
assert result["labCount"] == 1
assert result["afterLab"]["TU"] == 220000
assert result["afterLab"]["NU"] == 220000
assert result["afterLab"]["NC"] == 400000
assert result["afterLab"]["TC"] == 400000
# 전체 재계산 후: GN(수량) 반영 정상 산출
# 행 0: GN=2 -> NC=3000, TC=3000
assert result["afterRecalc"]["row0_NC"] == 3000
assert result["afterRecalc"]["row0_TC"] == 3000
# 행 1: GN=3 -> GC=9000, TC=9000
assert result["afterRecalc"]["row1_GC"] == 9000
assert result["afterRecalc"]["row1_TC"] == 9000
# 행 2: GN=2 -> NC=440000, TC=440000
assert result["afterRecalc"]["row2_NC"] == 440000
assert result["afterRecalc"]["row2_TC"] == 440000
def test_edge_basic_prices_material_and_labor_workflow_when_server_running():
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(channel="msedge", headless=True)
page = browser.new_page()
try:
page.goto("http://100.65.141.15:5175", timeout=5000)
page.wait_for_function("() => Boolean(EstApp.state.project)")
except Exception:
browser.close()
import pytest
pytest.skip("EST 5175 server not reachable")
# 1. 자재단가 및 노임단가 테스트 데이터 세팅
page.evaluate(
"""(() => {
const p = EstNative.empty();
p.options.SourceRefresh = false;
p.tables.StuffcDat = [
{ Code: 'M01', Name: '시멘트 포대', Size: '40kg', Unite: '포', ValueIndex: 0, Cost1: 6000 },
];
p.tables.LaborcDat = [
{ Code: 'L01', Name: '특별인부', Unite: '인', ValueIndex: 0, Cost1: 180000 },
];
p.tables.EstMainDat = [
{ PkID: 301, Code: 'M01', Name: '시멘트 포대', Size: '40kg', Unite: '포', LinkIndex: 5, GN: 10, TU: 5000, GU: 5000, GC: 50000, TC: 50000 },
{ PkID: 302, Code: 'L01', Name: '특별인부', Unite: '인', LinkIndex: 6, GN: 5, TU: 160000, NU: 160000, NC: 800000, TC: 800000 },
];
EstApp.setProject(p);
EstApp.state.view = 'material-usage';
EstApp.render();
})()"""
)
# 2. 자재단가 사용 내역 화면 확인
assert page.locator("#view-title:has-text('자재단가 사용 내역')").is_visible()
btn_mat_apply = page.locator("button[data-basic-apply-row='0']")
assert btn_mat_apply.is_visible()
btn_mat_apply.click()
# 자재 단가 6,000으로 적용 확인
assert page.evaluate("EstApp.state.project.tables.EstMainDat[0].TU") == 6000
assert page.evaluate("EstApp.state.project.tables.EstMainDat[0].GU") == 6000
# 3. 노임단가 사용 내역 화면으로 전환
page.evaluate(
"""(() => {
EstApp.state.view = 'labor-usage';
EstApp.render();
})()"""
)
assert page.locator("#view-title:has-text('노임단가 사용 내역')").is_visible()
btn_lab_apply = page.locator("button[data-basic-apply-all='labor']")
assert btn_lab_apply.is_visible()
btn_lab_apply.click()
# 노임 단가 180,000으로 일괄 적용 확인
assert page.evaluate("EstApp.state.project.tables.EstMainDat[1].TU") == 180000
assert page.evaluate("EstApp.state.project.tables.EstMainDat[1].NU") == 180000
# 4. Undo(실행 취소) 검증
page.locator(".toolbar button[data-action='undo']").click()
assert page.evaluate("EstApp.state.project.tables.EstMainDat[1].TU") == 160000
page.locator(".toolbar button[data-action='undo']").click()
assert page.evaluate("EstApp.state.project.tables.EstMainDat[0].TU") == 5000
# 5. 기계경비 사용 내역 화면 전환 및 개별/일괄 적용, Undo 검증
page.evaluate(
"""(() => {
EstApp.navigate('machine-usage');
})()"""
)
assert page.locator("#view-title:has-text('기계경비 사용 내역')").is_visible()
browser.close()
def test_machine_price_usage_changes_and_safety_rejection():
result = run(
"math",
"""(()=>{
const p = structuredClone(project);
// 신규 항목(TC_ null) 추가하여 비교 검증
p.tables.MachTitleDat.push({
Code: 'E_NEW_999',
Name: '신규 기계',
Size: '10Ton',
Unite: 'hr',
TC: 50000,
NC: 30000,
GC: 10000,
JC: 10000,
TC_: null,
NC_: null,
GC_: null,
JC_: null
});
const usage = EstMachine.usage(p);
const changes = EstMachine.changes(p);
const newItemUsage = usage.find(u => u.Code === 'E_NEW_999');
const newItemChange = changes.find(c => c.Code === 'E_NEW_999');
const excUsage = usage.find(u => u.Code === 'E0000000000');
// 파괴적인 당초값 덮어쓰기 적용 차단 검증
let blockedSingle = 0, singleErrMsg = '';
try {
EstMachine.applyRowPrice(p, 0);
} catch (e) {
blockedSingle++;
singleErrMsg = e.message;
}
let blockedAll = 0, allErrMsg = '';
try {
EstMachine.applyAllPrices(p);
} catch (e) {
blockedAll++;
allErrMsg = e.message;
}
// 호표 단가 및 단가산출 금액 불변 보존 확인
const t0Unchanged = (p.tables.MachTitleDat[0].TC === 103047);
const costRow0 = p.tables.CostMainDat.find(r => r.LinkIndex === 105 && r.Code === 'E0000000000' && r.TC > 0);
const costUnchanged = (costRow0.TC === 1682.7);
return {
usageCount: usage.length,
changesCount: changes.length,
newItemUsage,
newItemChange,
excUsage,
blockedSingle,
singleErrMsg,
blockedAll,
allErrMsg,
t0Unchanged,
costUnchanged
};
})()""",
)
assert result["usageCount"] == 9
assert result["changesCount"] == 9
# 신규 항목 TC_ null 안전 처리 검증
assert result["newItemUsage"]["originalPrice"] is None
assert result["newItemUsage"]["difference"] is None
assert result["newItemUsage"]["status"] == "신규(당초없음)"
assert result["newItemUsage"]["canApply"] is False
assert result["newItemChange"]["status"] == "신규"
# 기존 항목 당초/현재 비교 검증
assert result["excUsage"]["appliedPrice"] == 103047
assert result["excUsage"]["originalPrice"] == 62207
assert result["excUsage"]["difference"] == 40840
assert result["excUsage"]["status"] == "당초차이"
assert result["excUsage"]["canApply"] is False
# 파괴적인 apply 차단 및 데이터 보존 검증
assert result["blockedSingle"] == 1
assert "덮어쓰기 적용은 지원하지 않습니다" in result["singleErrMsg"]
assert result["blockedAll"] == 1
assert "덮어쓰기 적용은 지원하지 않습니다" in result["allErrMsg"]
assert result["t0Unchanged"] is True
assert result["costUnchanged"] is True