Files
Aislo/resources/tester/test_est_documents.py
T

240 lines
11 KiB
Python

from test_est_rea_parity import run
def test_custom_print_form_documents_are_saved_and_unknown_keys_rejected():
result = run(
"math",
"""(()=>{
const p=EstNative.empty();p.documents={'annual-items':'stage note','progress-costs':'progress note','standard-usage':'standard note'};
const valid=EstModel.validate(p).documents;let rejected=false;
const q=EstNative.empty();q.documents={'invalid-key':'bad'};try{EstModel.validate(q)}catch{rejected=true}
return {valid,rejected};
})()""",
)
assert result["valid"] == {
"annual-items": "stage note",
"progress-costs": "progress note",
"standard-usage": "standard note",
}
assert result["rejected"]
def test_schedule_refreshes_progress_from_latest_locked_round_without_losing_dates():
result = run(
"math",
"""(()=>{
const p=EstNative.empty();p.tables.EstMainDat=[{PkID:1,Code:'A',GN:10},{PkID:2,Code:'B',GN:0}];
p.schedule=[{id:'a',SourceKey:'id:1',Code:'A',Name:'work',start:'2026-01-01',end:'2026-02-01',weight:100,progress:10},
{id:'b',SourceKey:'id:2',Code:'B',Name:'zero',start:'',end:'',weight:0,progress:40},
{id:'c',Code:'X',Name:'manual',start:'',end:'',weight:10,progress:20}];
p.progress=[{id:'one',locked:true,result:{rows:[{PkID:1,Code:'A',GN:10,TotalGN:2}]}},
{id:'two',locked:true,result:{rows:[{PkID:1,Code:'A',GN:10,TotalGN:3},{PkID:2,Code:'B',GN:0,TotalGN:0}]}}];
const before=JSON.stringify(p), updated=EstSchedule.refreshProgress(p);let bad=0;
p.progress[1].result.rows[0].TotalGN=11;try{EstSchedule.refreshProgress(p)}catch{bad++}
p.progress=[];try{EstSchedule.refreshProgress(p)}catch{bad++}
return {updated,bad,unchanged:JSON.parse(before).schedule};
})()""",
)
assert [row["progress"] for row in result["updated"]] == [30, 40, 20]
assert result["updated"][0]["start"] == "2026-01-01"
assert result["updated"][0]["weight"] == 100
assert result["unchanged"][0]["progress"] == 10
assert result["bad"] == 2
def test_annual_print_contains_each_stage_and_calculation_errors():
result = run(
"math",
"""(()=>{
const p=EstNative.empty();p.options.SourceRefresh=false;
p.tables.EstMainDat=[{PkID:1,Code:'R',GN:3,GU:100},{LinkIndex:54}];
p.annual=[{name:'first',formula:'[기준]*0.25'},{name:'second',formula:'[기준]-[first]'}];
const q=EstNative.calculate(p), good=EstForms.data('annual-summary',q), items=EstForms.data('annual-items',q);
q.annual[0].formula='[unknown]';
return {good,items,bad:EstForms.data('annual-summary',q),badItems:EstForms.data('annual-items',q)};
})()""",
)
assert [row["directCost"] for row in result["good"]["rows"]] == [75, 225]
assert result["good"]["rows"][1]["formula"] == "[기준]-[first]"
assert result["good"]["errors"] == []
assert result["bad"]["errors"]
assert [row["stage"] for row in result["items"]["rows"]] == ["first", "second"]
assert [row["TC"] for row in result["items"]["rows"]] == [75, 225]
assert result["badItems"]["errors"]
def test_progress_and_escalation_print_use_selected_calculations():
result = run(
"math",
"""(()=>{
const p=EstNative.empty();p.options.SourceRefresh=false;p.options.EscDesignTotal=1000;
p.tables.EstMainDat=[{PkID:1,Code:'R',Name:'work',GN:10,GU:100,AGU:100,EscActual:3},{LinkIndex:54}];
p.progress=[{id:'one',name:'first',inputs:{'1':{Gum:2}}}];
const q=EstProgress.lock(EstNative.calculate(p),'one');
q.tables.EstMainDat[0].GU=200;
q.progress.push({id:'two',name:'second',inputs:{'1':{Gum:3}}});
return {locked:EstForms.data('progress-summary',q,{roundId:'one'}),
draft:EstForms.data('progress-summary',q,{roundId:'two'}),
escalation:EstForms.data('escalation-summary',p),
empty:EstForms.data('progress-summary',EstNative.empty())};
})()""",
)
assert result["locked"]["rows"][0]["GumTC"] == 200
assert result["locked"]["rows"][0]["GU"] == 100
assert result["draft"]["rows"][0]["GumTC"] == 600
assert result["draft"]["rows"][0]["TotalTC"] == 800
assert result["escalation"]["rows"][0]["EscExcluded"] == 3
assert result["escalation"]["rows"][0]["EscApplied"] == 7
assert result["empty"]["errors"]
def test_progress_cost_print_preserves_selected_round_costs():
result = run(
"math",
"""(()=>{
const p=EstNative.empty();p.options.SourceRefresh=false;
p.tables.EstMainDat=[{PkID:1,Code:'R',GN:10,GU:100},{LinkIndex:54}];
p.tables.Wonga=[{Gubun:'D',Name:'direct',BaseField:'GC'},{Gubun:'T',Name:'total',LinkIndex:55,VSusic:'D'}];
p.progress=[{id:'one',inputs:{'1':{Gum:2,Sa:1}},approved:true}];
const q=EstProgress.lock(EstNative.calculate(p),'one');
q.tables.EstMainDat[0].GU=200;q.progress.push({id:'two',inputs:{'1':{Gum:3}}});
return {one:EstForms.data('progress-costs',q,{roundId:'one'}),two:EstForms.data('progress-costs',q,{roundId:'two'})};
})()""",
)
assert result["one"]["rows"][-1]["ContractCost"] == 1000
assert result["one"]["rows"][-1]["GumCost"] == 200
assert result["one"]["rows"][-1]["SaCost"] == 100
assert result["two"]["rows"][-1]["JunCost"] == 100
assert result["two"]["rows"][-1]["GumCost"] == 600
assert result["two"]["rows"][-1]["TotalCost"] == 700
def test_escalation_cost_print_uses_computed_cost_breakdowns():
result = run(
"math",
"""(()=>{
const p=EstNative.empty();p.options.SourceRefresh=false;p.options.EscDesignTotal=1000;
p.tables.EstMainDat=[{PkID:1,Code:'R',GN:10,GU:80},{LinkIndex:54}];
p.tables.Wonga=[{Gubun:'D',Name:'direct',BaseField:'GC'},{Gubun:'T',Name:'total',LinkIndex:55,VSusic:'D'}];
const q=EstBusiness.baseline(EstNative.calculate(p));
Object.assign(q.tables.EstMainDat[0],{AGU:100,GU:120,EscActual:3});
return EstForms.data('escalation-costs',q);
})()""",
)
assert result["errors"] == []
assert result["rows"][-1]["ContractCost"] == 800
assert result["rows"][-1]["adjustedCost"] == 960
assert result["rows"][-1]["excludedCost"] == 240
def test_resource_report_separates_each_work_item_without_changing_aggregate():
result = run(
"math",
"""(()=>{
const p=EstNative.empty();p.options.SourceRefresh=false;
p.tables.StuffcDat=[{Code:'M',Name:'material',InputTC:100,ValueIndex:4}];
p.tables.EstMainDat=[{PkID:1,Code:'M',LinkIndex:5,GN:2,GU:100},{PkID:2,Code:'M',LinkIndex:5,GN:3,GU:100},{LinkIndex:54}];
return {all:EstResources.calculate(p),split:EstResources.calculate(p,true)};
})()""",
)
assert len(result["all"]["rows"]) == 1
assert len(result["split"]["rows"]) == 2
assert (
sum(r["quantity"] for r in result["split"]["rows"])
== result["all"]["rows"][0]["quantity"]
== 5
)
assert (
sum(r["amount"] for r in result["split"]["rows"])
== result["all"]["rows"][0]["amount"]
== 500
)
def test_schedule_dates_ranges_and_weighted_user_progress():
result = run(
"math",
"""(()=>{
const p=EstNative.empty();p.schedule=[
{id:'a',Code:'A',Name:'first',start:'2026-11-01',end:'2026-11-05',weight:100,progress:25},
{id:'b',Code:'B',Name:'second',start:'2026-11-06',end:'2026-11-10',weight:300,progress:50}];
const valid=EstSchedule.calculate(p),bad=[];
for(const change of [{start:'2026-02-30'},{progress:101},{weight:-1},{end:'2026-10-01'}]) {
const q=structuredClone(p);Object.assign(q.schedule[0],change);
try{EstModel.validate(q);bad.push(false)}catch{bad.push(true)}
}
return {valid,bad};
})()""",
)
assert result["valid"]["progress"] == 43.75
assert result["valid"]["rows"][0]["width"] == 50
assert result["valid"]["rows"][1]["left"] == 50
assert result["bad"] == [True] * 4
def test_schedule_import_and_update_weights_preserve_user_schedule():
result = run(
"math",
"""(()=>{
const p=EstNative.empty();
p.tables.EstMainDat=[{Code:'A',Name:'one',TC:100}, {Code:'B',Name:'two',TC:300},
{Code:'HEADER',Level:1,TC:999},{Code:'TOTAL',LinkIndex:54,TC:400}];
p.schedule=[{id:'old',Code:'A',Name:'custom',start:'2026-01-01',end:'2026-01-10',weight:1,progress:40},
{id:'manual',Code:'other',Name:'manual',start:'',end:'',weight:5,progress:20}];
let id=0;const added=EstSchedule.fromEstimate(p,()=>String(++id));p.schedule.push(...added);
p.tables.EstMainDat[0].TC=200;p.schedule=EstSchedule.refreshWeights(p);
let duplicate='';p.tables.EstMainDat.push({Code:'A',TC:20});
try{EstSchedule.fromEstimate(p,()=>String(++id))}catch(e){duplicate=e.message}
return {added,rows:p.schedule,duplicate};
})()""",
)
assert [row["Code"] for row in result["added"]] == ["B"]
assert [row["weight"] for row in result["rows"]] == [200, 5, 300]
assert result["rows"][0]["Name"] == "custom"
assert result["rows"][0]["start"] == "2026-01-01"
assert result["rows"][0]["progress"] == 40
assert result["duplicate"]
def test_schedule_imports_distinct_rows_with_repeated_or_empty_codes():
result = run(
"math",
"""(()=>{
const p=EstNative.empty();p.tables.EstMainDat=[
{PkID:1,Code:'X',Name:'one',TC:100},{PkID:2,Code:'X',Name:'two',TC:200},
{PkID:3,Code:null,Name:'three',TC:300}];
let id=0;p.schedule=EstSchedule.fromEstimate(p,()=>String(++id));
p.schedule[0].start='2026-01-01';p.schedule[0].progress=50;
p.tables.EstMainDat.reverse();p.tables.EstMainDat[0].TC=400;
const updated=EstSchedule.refreshWeights(p);
return {rows:p.schedule,updated,again:EstSchedule.fromEstimate(p,()=>String(++id))};
})()""",
)
assert [row["SourceKey"] for row in result["rows"]] == ["id:1", "id:2", "id:3"]
assert [row["weight"] for row in result["updated"]] == [100, 200, 400]
assert result["updated"][0]["start"] == "2026-01-01"
assert result["updated"][0]["progress"] == 50
assert result["again"] == []
def test_quantity_print_includes_manual_and_sheet_formulas_and_errors():
result = run(
"math",
"""(()=>{
const p=EstNative.empty();p.quantities=[
{Code:'A',Name:'first',formula:'2',TargetCode:'X'},
{Code:'B',Name:'second',formula:'[A]*3',TargetCode:'X'},
{Code:'C',Name:'bad',formula:'[missing]',TargetCode:'X'}];
p.quantitySheets=[{Code:'Q',Name:'sheet',options:{},rows:[
{LineData:'4+5 = \\x120\\x12',markers:['r1']}],
items:[{Code:'r1',Name:'sheet item',GN:0,TargetCode:'Y'}]}];
return EstForms.data(14,p);
})()""",
)
assert [row["GN"] for row in result["rows"]] == [2, 6, None, 9]
assert result["rows"][1]["formula"] == "[A]*3"
assert "4+5" in result["rows"][3]["formula"]
assert result["rows"][3]["TargetCode"] == "Y"
assert result["rows"][2]["Error"]
assert result["errors"]