from test_est_rea_parity import run def test_new_progress_name_avoids_existing_names_with_whitespace(): result = run( "math", """[EstBusiness.newProgressName([{name:'2회 기성'},{name:' 1회 기성 '}]), EstBusiness.newProgressName([{name:'2회 기성'}]),EstBusiness.newProgressName([])]""", ) assert result == ["3회 기성", "1회 기성", "1회 기성"] def test_progress_cannot_lock_when_previous_result_is_missing(): result = run( "math", """(()=>{ const p=EstNative.empty(); p.tables.EstMainDat=[{PkID:1,Code:'R',GN:10,GU:100},{LinkIndex:54}]; p.progress=[{id:'one',locked:true,inputs:{}},{id:'two',inputs:{'1':{Gum:2}}}]; const calculated=EstProgress.calculate(p,'two'); let rejected=false; try { EstProgress.lock(p,'two'); } catch { rejected=true; } return {errors:calculated.errors,rejected}; })()""", ) assert any("전회 기성 결과가 없습니다" in error for error in result["errors"]) assert result["rejected"] def test_progress_cannot_lock_when_previous_costs_are_missing(): result = run( "math", """(()=>{ const p=EstNative.empty(); p.tables.EstMainDat=[{PkID:1,Code:'R',GN:10,GU:100},{LinkIndex:54}]; p.tables.Wonga=[{Gubun:'D',BaseField:'TC'},{LinkIndex:55,VSusic:'D'}]; p.progress=[{id:'one',locked:true,inputs:{},result:{rows:[]}}, {id:'two',inputs:{'1':{Gum:2}}}]; const calculated=EstProgress.calculate(p,'two'); let rejected=false; try { EstProgress.lock(p,'two'); } catch { rejected=true; } return {errors:calculated.errors,rejected}; })()""", ) assert any("전회 기성 원가 결과가 없습니다" in error for error in result["errors"]) assert result["rejected"] def test_progress_rejects_ambiguous_inputs_without_applying_them_to_multiple_rows(): result = run( "math", """(()=>{ const p=EstNative.empty();p.options.SourceRefresh=false; p.tables.EstMainDat=[{Code:'A',GN:10,GU:100},{Code:'A',GN:20,GU:100}, {PkID:3,Code:'B',GN:10,GU:100},{LinkIndex:54}]; p.progress=[{id:'one',inputs:{A:{Gum:2},'3':{Gum:3}}}]; const out=EstProgress.calculate(EstNative.calculate(p),'one');let rejected=false; try{EstProgress.lock(EstNative.calculate(p),'one')}catch{rejected=true} return {out,rejected}; })()""", ) assert result["rejected"] and result["out"]["errors"] assert [row["GumTC"] for row in result["out"]["rows"][:3]] == [0, 0, 300] def test_progress_amount_input_preserves_exact_money_and_component_total(): result = run( "math", """(()=>{ const p=EstNative.empty();p.options.SourceRefresh=false; p.tables.EstMainDat=[{PkID:1,Code:'R',GN:3,NU:33,GU:33,JU:34},{LinkIndex:54}]; p.tables.Wonga=[{Gubun:'D',BaseField:'TC'},{Gubun:'T',LinkIndex:55,VSusic:'D'}]; p.progress=[{id:'one',inputs:{'1':{Gum:100,GumMode:'amount',Sa:90,SaMode:'amount'}},approved:true}]; return EstProgress.calculate(EstNative.calculate(p),'one'); })()""", ) assert result["errors"] == [] row = result["rows"][0] assert row["GumTC"] == 100 assert sum(row["Gum" + key] for key in ["NC", "GC", "JC", "SC"]) == 100 assert row["TotalTC"] == 90 assert result["total"] == 90 def test_progress_fixed_amount_and_zero_quantity_ratio(): result = run( "math", """(()=>{ const p=EstNative.empty();p.tables.EstMainDat=[ {PkID:1,Code:'R',GN:0,GC:1000,TC:1000,CalcIndex:1},{LinkIndex:54}]; p.progress=[{id:'one',inputs:{'1':{Gum:25,GumMode:'ratio',Sa:333,SaMode:'amount'}}}]; return EstProgress.calculate(p,'one'); })()""", ) assert result["errors"] == [] assert result["rows"][0]["GumGN"] == 0 assert result["rows"][0]["GumTC"] == 250 assert result["rows"][0]["SaTC"] == 333 def test_progress_ratio_quantity_precision_and_bankers_percentage(): result = run( "math", """(()=>{ const p=EstNative.empty();p.tables.EstMainDat=[ {PkID:1,Code:'R',GN:10,NU:10,NC:100,TC:100}, {PkID:2,Name:'percentage',GN:5,LinkIndex:67,SCheck:2,Size:'노무',NC:5,TC:5}, {LinkIndex:54}]; p.progress=[{id:'one',inputs:{'1':{Gum:25,GumMode:'ratio',Sa:5}}}]; return EstProgress.calculate(p,'one'); })()""", ) assert result["errors"] == [] assert result["rows"][0]["GumGN"] == 3 assert result["rows"][1]["GumTC"] == 2 assert result["rows"][1]["SaTC"] == 2 # Round(2.5) = 2 in c9261c. def test_locked_progress_uses_its_contract_and_server_rebuilds_its_totals(): 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:'material',BaseField:'GC'}, {Gubun:'V',Name:'부가세',Youl:'10%',VSusic:'D*0.1'}, {Gubun:'T',Name:'total',LinkIndex:55,VSusic:'D+V'}]; p.progress=[{id:'one',inputs:{'1':{Gum:2}}}]; const q=EstProgress.lock(EstNative.calculate(p),'one'); q.tables.EstMainDat[0].GU=200; q.progress[0].result.rows[0].TotalTC=99999; EstProgress.rebuild(q); q.progress.push({id:'two',inputs:{'1':{Gum:3}}}); const second=EstProgress.calculate(q,'two'); const unlocked=EstProgress.unlock(q,'one'); return {q,second,unlocked}; })()""", ) assert result["q"]["progress"][0]["result"]["rows"][0]["TotalTC"] == 200 second = result["second"] assert second["errors"] == [] assert second["rows"][0]["JunTC"] == 200 assert second["rows"][0]["GumTC"] == 600 assert second["rows"][0]["TotalTC"] == 800 assert second["costs"][-1]["TotalCost"] == 880 assert all(not row["locked"] for row in result["unlocked"]["progress"]) assert all("result" not in row for row in result["unlocked"]["progress"]) def test_progress_total_only_mode_and_invalid_saved_input_rejection(): result = run( "math", """(()=>{ const p=EstNative.empty();p.tables.EstMainDat=[{PkID:1,Code:'R',GN:10,TC:1000,CalcIndex:1},{LinkIndex:54}]; p.tables.Wonga=[{Gubun:'D',BaseField:'TC'},{Gubun:'T',LinkIndex:55,VSusic:'D'}]; p.progress=[{id:'one',separateComponents:false,inputs:{'1':{Gum:25,GumMode:'ratio'}}}]; const output=EstProgress.calculate(p,'one'); p.progress[0].inputs['1'].Gum='invalid';let rejected=false; try{EstModel.validate(p);}catch{rejected=true;} return {output,rejected}; })()""", ) assert result["rejected"] assert result["output"]["errors"] == [] assert result["output"]["rows"][0]["GumTC"] == 250 assert result["output"]["total"] == 250 def test_progress_preserves_zero_wonga_total_and_falls_back_only_when_absent(): 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',BaseField:'TC'},{Gubun:'T',LinkIndex:55,VSusic:'0'}]; p.progress=[{id:'one',inputs:{'1':{Gum:2}}}]; const zero=EstProgress.calculate(EstNative.calculate(p),'one'); p.tables.Wonga=[];const absent=EstProgress.calculate(EstNative.calculate(p),'one'); return {zero,absent}; })()""", ) assert result["zero"]["errors"] == [] assert result["zero"]["rows"][0]["GumTC"] == 200 assert result["zero"]["total"] == 0 assert result["absent"]["total"] == 200 def test_progress_preserves_prior_costs_for_multiple_blank_codes(): result = run( "math", """(()=>{ const p=EstNative.empty();p.options.SourceRefresh=false; p.tables.EstMainDat=[{PkID:1,Code:'R',GN:10,GU:100,NU:50},{LinkIndex:54}]; p.tables.Wonga=[{Gubun:'',Name:'material',BaseField:'GC'}, {Gubun:'',Name:'labor',BaseField:'NC'}]; p.progress=[{id:'one',inputs:{'1':{Gum:2}}}]; const q=EstProgress.lock(EstNative.calculate(p),'one'); q.progress.push({id:'two',inputs:{'1':{Gum:3}}}); return EstProgress.calculate(q,'two'); })()""", ) assert result["errors"] == [] assert [row["JunCost"] for row in result["costs"]] == [200, 100] assert [row["TotalCost"] for row in result["costs"]] == [500, 250]