222 lines
10 KiB
Python
222 lines
10 KiB
Python
from test_est_rea_parity import run
|
|
|
|
|
|
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"]
|