677 lines
30 KiB
Python
677 lines
30 KiB
Python
# Annual allocation and resource expansion regression cases.
|
||
|
||
from test_est_rea_parity import run
|
||
|
||
|
||
def test_estimate_classification_refreshes_only_selected_source_and_preserves_manual_mode():
|
||
result = run(
|
||
"math",
|
||
"""(()=>{
|
||
const p=EstNative.empty();p.options.SourceRefresh=false;p.options.AutoCalc=false;
|
||
p.tables.StuffcDat=[{Code:'M',Name:'material',Cost1:100,ValueIndex:0},{Code:'N',Name:'other',Cost1:800,ValueIndex:0}];
|
||
p.tables.EstMainDat=[{PkID:1,Code:'M',Name:'material',LinkIndex:5,GN:2,GU:100,GC:200,JC:0},
|
||
{PkID:2,Code:'N',Name:'other',LinkIndex:5,GN:1,GU:5,GC:5},{LinkIndex:54}];
|
||
const row=p.tables.EstMainDat[0],other=JSON.stringify(p.tables.EstMainDat[1]);
|
||
EstBusinessActions.setEstimateClassification(p,row,0,3);
|
||
const manual={JU:row.JU,GU:row.GU,GC:row.GC,JC:row.JC};
|
||
const auto=EstNative.calculate(p).tables.EstMainDat[0];
|
||
EstBusinessActions.setEstimateClassification(p,row,0,0);
|
||
const restored={GU:row.GU,JU:row.JU};
|
||
const missing={PkID:3,Code:'missing',LinkIndex:5,SCheck:0,GU:7};p.tables.EstMainDat.push(missing);
|
||
EstBusinessActions.setEstimateClassification(p,missing,3,3);
|
||
let rejected=0;try{EstBusinessActions.setEstimateClassification(p,missing,3,0)}catch{rejected++}
|
||
EstBusinessActions.setEstimateClassification(p,row,0,3);
|
||
p.tables.StuffcDat.push({...p.tables.StuffcDat[0]});try{EstBusinessActions.setEstimateClassification(p,row,0,0)}catch{rejected++}
|
||
const mismatch={PkID:4,Code:'N',LinkIndex:5,SCheck:0,GU:5};p.tables.EstMainDat.push(mismatch);
|
||
EstBusinessActions.setEstimateClassification(p,mismatch,4,3);
|
||
try{EstBusinessActions.setEstimateClassification(p,mismatch,4,0)}catch{rejected++}
|
||
p.tables.StuffcDat.push({Code:'L',Cost1:1000000000000,ValueIndex:0});
|
||
const large={PkID:5,Code:'L',LinkIndex:5,SCheck:0,GU:999999999900};p.tables.EstMainDat.push(large);
|
||
EstBusinessActions.setEstimateClassification(p,large,5,3);
|
||
try{EstBusinessActions.setEstimateClassification(p,large,5,0)}catch{rejected++}
|
||
return {manual,auto:{GC:auto.GC,JC:auto.JC},restored,rejected,missing,mismatch,large,other:JSON.stringify(p.tables.EstMainDat[1])===other,flag:p.options.SourceRefresh};
|
||
})()""",
|
||
)
|
||
assert result["manual"] == {"JU": 100, "GU": 0, "GC": 200, "JC": 0}
|
||
assert result["auto"] == {"GC": 0, "JC": 200}
|
||
assert result["restored"] == {"GU": 100, "JU": 0}
|
||
assert result["rejected"] == 4
|
||
assert result["missing"]["SCheck"] == 3 and result["missing"]["JU"] == 7
|
||
assert result["mismatch"]["SCheck"] == 3 and result["mismatch"]["JU"] == 5
|
||
assert result["large"]["SCheck"] == 3 and result["large"]["JU"] == 999999999900
|
||
assert result["other"] and not result["flag"]
|
||
|
||
|
||
def test_head_classification_preserves_other_groups_and_rejects_unsafe_restoration():
|
||
result = run(
|
||
"math",
|
||
"""(()=>{
|
||
const p=EstNative.empty();p.options.SourceRefresh=false;p.options.AutoCalc=false;
|
||
p.tables.StuffcDat=[{Code:'M',Name:'material',Cost1:100,ValueIndex:0},{Code:'N',Name:'other',Cost1:800,ValueIndex:0}];
|
||
p.tables.HeadMainDat=[{Index:1,Code:'H1',Name:'first'},{Index:0,Code:'M',LinkIndex:5,GN:2,GU:100,GC:200,JC:0},
|
||
{Index:2,Code:'H2',Name:'second'},{Index:0,Code:'N',LinkIndex:5,GN:1,GU:5,GC:5}];
|
||
const row=p.tables.HeadMainDat[1],other=JSON.stringify(p.tables.HeadMainDat[3]);
|
||
EstBusinessActions.setEstimateClassification(p,row,1,3,'HeadMainDat');
|
||
const otherPreserved=JSON.stringify(p.tables.HeadMainDat[3])===other;
|
||
const manual={JU:row.JU,GU:row.GU,GC:row.GC,JC:row.JC};
|
||
const auto=EstNative.calculate(p).tables.HeadMainDat[1];
|
||
EstBusinessActions.setEstimateClassification(p,row,1,0,'HeadMainDat');
|
||
const restored={GU:row.GU,JU:row.JU};
|
||
const mismatch=p.tables.HeadMainDat[3];EstBusinessActions.setEstimateClassification(p,mismatch,3,3,'HeadMainDat');
|
||
let rejected=false;try{EstBusinessActions.setEstimateClassification(p,mismatch,3,0,'HeadMainDat')}catch{rejected=true}
|
||
return {manual,auto:{GC:auto.GC,JC:auto.JC},restored,rejected,mismatch,otherPreserved,flag:p.options.SourceRefresh};
|
||
})()""",
|
||
)
|
||
assert result["manual"] == {"JU": 100, "GU": 0, "GC": 200, "JC": 0}
|
||
assert result["auto"] == {"GC": 0, "JC": 200}
|
||
assert result["restored"] == {"GU": 100, "JU": 0}
|
||
assert result["rejected"] and result["mismatch"]["JU"] == 5
|
||
assert result["otherPreserved"] and not result["flag"]
|
||
|
||
|
||
def test_annual_formulas_split_component_amounts_and_preserve_remainder():
|
||
result = run(
|
||
"math",
|
||
"""(()=>{
|
||
const p=EstNative.empty();p.options.SourceRefresh=false;
|
||
p.tables.EstMainDat=[{PkID:1,Code:'R',Name:'item',GN:1,NU:5,GU:3},{LinkIndex:54}];
|
||
p.annual=[{name:'1차',formula:'[기준]*0.5'},{name:'2차',formula:'[기준]-[1차]'}];
|
||
const q=EstNative.calculate(p),before=JSON.stringify(q);
|
||
return {output:EstBusiness.annual(q),preserved:before===JSON.stringify(q)};
|
||
})()""",
|
||
)
|
||
assert result["preserved"]
|
||
output = result["output"]
|
||
assert output["errors"] == []
|
||
first, second = [item["project"] for item in output["results"]]
|
||
assert first["tables"]["EstMainDat"][0]["GN"] == 0.5
|
||
assert second["tables"]["EstMainDat"][0]["GN"] == 0.5
|
||
assert [item["calculation"]["direct"]["TC"] for item in [first, second]] == [3, 5]
|
||
assert first["tables"]["EstMainDat"][0]["NC"] == 2
|
||
assert second["tables"]["EstMainDat"][0]["NC"] == 3
|
||
assert second["tables"]["EstMainDat"][0]["GC"] == 2
|
||
assert all(first["tables"]["EstMainDat"][0][key] == 0 for key in ["TU", "NU", "GU", "JU"])
|
||
|
||
|
||
def test_annual_fixed_amount_with_zero_quantity_and_duplicate_names():
|
||
result = run(
|
||
"math",
|
||
"""(()=>{
|
||
const p=EstNative.empty();p.options.SourceRefresh=false;
|
||
p.tables.EstMainDat=[{PkID:1,Code:'R',GN:0,NC:3,GC:7,TC:10,CalcIndex:1},{LinkIndex:54}];
|
||
p.annual=[{name:'1차',formula:'[기준]*0.5'},{name:'2차',formula:'[기준]-[1차]'}];
|
||
const output=EstBusiness.annual(EstNative.calculate(p));
|
||
p.annual[1].name='1차';
|
||
return {output,invalid:EstBusiness.annual(p)};
|
||
})()""",
|
||
)
|
||
assert result["output"]["errors"] == []
|
||
assert [
|
||
item["project"]["calculation"]["direct"]["TC"] for item in result["output"]["results"]
|
||
] == [4, 6]
|
||
assert result["invalid"]["results"] == []
|
||
assert len(result["invalid"]["errors"]) == 1
|
||
|
||
|
||
def test_annual_procurement_fee_is_allocated_and_kept_in_government_costs():
|
||
result = run(
|
||
"math",
|
||
"""(()=>{
|
||
const p=EstNative.empty();p.options.SourceRefresh=false;
|
||
p.id='123456789012345678901234';p.options.GangubExceptJodalRatio=true;
|
||
p.tables.EstMainDat=[
|
||
{PkID:1,Name:'labor',GN:1,NU:1000},
|
||
{LinkIndex:54},
|
||
{PkID:2,Name:'관급자재',Level:1},
|
||
{PkID:3,Name:'material',GN:1,GU:301},
|
||
{PkID:4,Name:'조달수수료',LinkIndex:57,GN:10,Unite:'%',OCheck:4,SCheck:3}];
|
||
p.tables.Wonga=[
|
||
{Gubun:'F',Name:'직접노무비',BaseField:'NC'},
|
||
{Gubun:'G',Name:'관급자재대'},
|
||
{Gubun:'S',Name:'산업안전보건관리비',VSusic:'G*0.1'},
|
||
{Gubun:'T',Name:'총공사비',LinkIndex:55,VSusic:'F+G+S'}];
|
||
p.annual=[{name:'1차',formula:'[기준]*0.5'},{name:'2차',formula:'[기준]-[1차]'}];
|
||
const q=EstNative.calculate(p),before=JSON.stringify(q),output=EstBusiness.annual(q);
|
||
return {q,output,preserved:before===JSON.stringify(q),
|
||
again:output.results.map(item=>EstNative.calculate(item.project))};
|
||
})()""",
|
||
)
|
||
assert result["preserved"]
|
||
assert result["output"]["errors"] == []
|
||
assert result["q"]["calculation"]["total"] == 1361
|
||
projects = [item["project"] for item in result["output"]["results"]]
|
||
for project in projects + result["again"]:
|
||
assert "id" not in project
|
||
fee = project["tables"]["EstMainDat"][4]
|
||
assert fee["LinkIndex"] == 99
|
||
assert fee["CalcIndex"] == 1
|
||
assert fee["GN"] == 10
|
||
assert fee["TC"] == fee["JC"] == 15
|
||
assert fee["TU"] == fee["JU"] == 0
|
||
government = project["tables"]["Wonga"][1]
|
||
assert government["NC"] == 15
|
||
assert project["tables"]["Wonga"][2]["Cost"] == 15
|
||
assert [project["calculation"]["total"] for project in projects] == [680, 681]
|
||
assert [project["calculation"]["total"] for project in result["again"]] == [680, 681]
|
||
|
||
|
||
def test_annual_excluded_fee_does_not_reduce_the_government_safety_base():
|
||
result = run(
|
||
"math",
|
||
"""(()=>{
|
||
const p=EstNative.empty();p.options.SourceRefresh=false;
|
||
p.options.GangubExceptJodalRatio=true;
|
||
p.tables.EstMainDat=[{LinkIndex:54},{Level:1,Name:'관급자재'},
|
||
{GN:1,GU:301},{LinkIndex:57,Name:'조달수수료',GN:10,CalcIndex:2}];
|
||
p.tables.Wonga=[{Gubun:'G',Name:'관급자재대'},
|
||
{Gubun:'S',Name:'산업안전보건관리비',VSusic:'G*0.1'},
|
||
{Gubun:'T',Name:'총공사비',VSusic:'G+S',LinkIndex:55}];
|
||
p.annual=[{name:'first',formula:'[기준]*0.5'}];
|
||
const q=EstNative.calculate(p);
|
||
return {q,output:EstBusiness.annual(q)};
|
||
})()""",
|
||
)
|
||
assert result["q"]["tables"]["Wonga"][0]["NC"] == 0
|
||
project = result["output"]["results"][0]["project"]
|
||
assert project["tables"]["EstMainDat"][3]["CalcIndex"] == 3
|
||
assert project["tables"]["EstMainDat"][3]["TC"] == 15
|
||
assert project["tables"]["Wonga"][0]["Cost"] == 150
|
||
assert project["tables"]["Wonga"][0]["NC"] == 0
|
||
assert project["tables"]["Wonga"][1]["Cost"] == 15
|
||
|
||
|
||
def test_resource_expansion_preserves_different_labor_and_material_bid_rates():
|
||
result = run(
|
||
"math",
|
||
r"""(()=>{
|
||
const p=EstNative.empty();
|
||
p.tables.StuffcDat=[{Code:'M',Name:'material',ValueIndex:4,InputTC:100}];
|
||
p.tables.LaborcDat=[{Code:'L',Name:'labor',ValueIndex:4,InputTC:100}];
|
||
p.tables.CostMainDat=[{Index:1,Code:'C'},
|
||
{Code:'M',LinkIndex:5,SCheck:-2,LineData:' \x0ematerial\x0e: \x03100\x03 = \x12100\x12'},
|
||
{Code:'L',LinkIndex:6,SCheck:-1,LineData:' \x0elabor\x0e: \x03100\x03 = \x12100\x12'}];
|
||
p.tables.CostTitleDat=[{Code:'C'}];
|
||
p.tables.EstMainDat=[{PkID:1,Code:'C',LinkIndex:104,GN:10},{LinkIndex:54}];
|
||
const bid=EstBusiness.bid(EstNative.calculate(p),[80,90,100,100]);
|
||
const direct=EstResources.calculate(bid);
|
||
p.tables.HeadMainDat=[{Index:1,Code:'H'},{Code:'C',LinkIndex:104,GN:1}];
|
||
p.tables.HeadTitleDat=[{Code:'H'}];p.tables.EstMainDat[0].LinkIndex=103;p.tables.EstMainDat[0].Code='H';
|
||
const nested=EstResources.calculate(EstBusiness.bid(EstNative.calculate(p),[80,90,100,100]));
|
||
return {direct,nested};
|
||
})()""",
|
||
)
|
||
for output in result.values():
|
||
assert output["errors"] == []
|
||
resources = {row["Code"]: row for row in output["rows"]}
|
||
assert resources["M"]["quantity"] == 9
|
||
assert resources["M"]["amount"] == 900
|
||
assert resources["L"]["quantity"] == 8
|
||
assert resources["L"]["amount"] == 800
|
||
|
||
|
||
def test_resource_expansion_selects_nested_head_component_and_all_conversions():
|
||
result = run(
|
||
"math",
|
||
r"""(()=>{
|
||
const p=EstNative.empty();
|
||
p.tables.StuffcDat=[{Code:'M',Name:'material',ValueIndex:4,InputTC:100}];
|
||
p.tables.LaborcDat=[{Code:'L',Name:'labor',ValueIndex:4,InputTC:100}];
|
||
p.tables.HeadMainDat=[{Index:1,Code:'H',Name:'head'},
|
||
{Code:'M',LinkIndex:5,GN:1},{Code:'L',LinkIndex:6,GN:1}];
|
||
p.tables.HeadTitleDat=[{Code:'H',Name:'head'}];
|
||
p.tables.CostMainDat=[{Index:1,Code:'C'},
|
||
{Code:'H',LinkIndex:103,LineData:' \x0ehead\x0e'},
|
||
{Code:'H',LinkIndex:103,SCheck:-1,LineData:' 노무비: \x03100\x03 = \x12100\x12'},
|
||
{Code:'H',LinkIndex:103,SCheck:-2,LineData:' 재료비: \x03100\x03 = \x12100\x12'}];
|
||
p.tables.CostTitleDat=[{Code:'C'}];
|
||
p.tables.EstMainDat=[{PkID:1,Code:'C',LinkIndex:104,GN:10},{LinkIndex:54}];
|
||
const selected=EstResources.calculate(p);
|
||
p.tables.HeadMainDat.push({LinkIndex:68,GN:0.5},{LinkIndex:68,GN:0.1});
|
||
p.tables.EstMainDat[0].Code='H';p.tables.EstMainDat[0].LinkIndex=103;
|
||
return {selected,converted:EstResources.calculate(p)};
|
||
})()""",
|
||
)
|
||
assert all(output["errors"] == [] for output in result.values())
|
||
assert len(result["selected"]["rows"]) == 2
|
||
assert all(
|
||
row["quantity"] == 10 and row["amount"] == 1000 for row in result["selected"]["rows"]
|
||
)
|
||
assert all(
|
||
row["quantity"] == 0.5 and row["amount"] == 50 for row in result["converted"]["rows"]
|
||
)
|
||
|
||
|
||
def test_resource_expansion_includes_money_only_rows_and_percentage_costs():
|
||
result = run(
|
||
"math",
|
||
"""(()=>{
|
||
const p=EstNative.empty();p.options.SourceRefresh=false;
|
||
p.tables.EstMainDat=[{PkID:1,Name:'fixed',GN:0,NC:3,GC:7,JC:2,TC:12,CalcIndex:1},{LinkIndex:54}];
|
||
const fixed=EstResources.calculate(p);
|
||
const q=EstNative.empty();q.tables.StuffcDat=[{Code:'M',Name:'material',ValueIndex:4,InputTC:100}];
|
||
q.tables.HeadMainDat=[{Index:1,Code:'H'},{Code:'M',LinkIndex:5,GN:1},
|
||
{LinkIndex:58,Size:'합계',SCheck:3,GN:10}];q.tables.HeadTitleDat=[{Code:'H'}];
|
||
q.tables.EstMainDat=[{Code:'H',LinkIndex:103,GN:2},{LinkIndex:54}];
|
||
return {fixed,percentage:EstResources.calculate(q)};
|
||
})()""",
|
||
)
|
||
assert all(output["errors"] == [] for output in result.values())
|
||
fixed = {row["Code"]: row for row in result["fixed"]["rows"]}
|
||
assert fixed["LZZZZZZZZZZ"]["amount"] == fixed["LZZZZZZZZZZ"]["quantity"] == 3
|
||
assert fixed["MZZZZZZZZZZ"]["amount"] == fixed["MZZZZZZZZZZ"]["quantity"] == 7
|
||
assert fixed["EZZZZZZZZZZ"]["amount"] == 2
|
||
assert fixed["EZZZZZZZZZZ"]["Unite"] == "제외금액"
|
||
assert sum(row["amount"] for row in result["percentage"]["rows"]) == 220
|
||
|
||
|
||
def test_cost_code_refresh_without_name_marker_still_tracks_the_actual_resource():
|
||
result = run(
|
||
"math",
|
||
r"""(()=>{
|
||
const p=EstNative.empty();
|
||
p.tables.StuffcDat=[{Code:'M',Name:'material',ValueIndex:4,InputTC:150}];
|
||
p.tables.CostMainDat=[{Index:1,Code:'C'},
|
||
{Code:'M',LinkIndex:5,SCheck:-2,LineData:' 재료비: \x03100\x03 ×2 = \x12200\x12'}];
|
||
p.tables.CostTitleDat=[{Code:'C'}];
|
||
p.tables.EstMainDat=[{Code:'C',LinkIndex:104,GN:1},{LinkIndex:54}];
|
||
return {calculated:EstNative.calculate(p),resources:EstResources.calculate(p)};
|
||
})()""",
|
||
)
|
||
assert result["calculated"]["calculation"]["errors"] == []
|
||
assert result["calculated"]["calculation"]["direct"]["TC"] == 300
|
||
assert result["resources"]["errors"] == []
|
||
assert result["resources"]["rows"][0]["Code"] == "M"
|
||
assert result["resources"]["rows"][0]["quantity"] == 2
|
||
assert result["resources"]["rows"][0]["amount"] == 300
|
||
|
||
|
||
def test_unlinked_text_price_does_not_inherit_the_previous_rows_source():
|
||
result = run(
|
||
"math",
|
||
r"""(()=>{
|
||
const p=EstNative.empty();
|
||
p.tables.StuffcDat=[{Code:'M',Name:'material',ValueIndex:4,InputTC:150}];
|
||
p.tables.CostMainDat=[{Index:1,Code:'C'},
|
||
{Code:'M',LinkIndex:5,SCheck:-2,LineData:' \x0ematerial\x0e: \x03100\x03 = \x12100\x12'},
|
||
{Code:'M',LinkIndex:0,SCheck:-2,LineData:' 직접입력: \x0350\x03 ×2 = \x12100\x12'}];
|
||
p.tables.CostTitleDat=[{Code:'C'}];
|
||
p.tables.EstMainDat=[{Code:'C',LinkIndex:104,GN:1},{LinkIndex:54}];
|
||
return {calculated:EstNative.calculate(p),resources:EstResources.calculate(p)};
|
||
})()""",
|
||
)
|
||
assert result["calculated"]["calculation"]["errors"] == []
|
||
assert result["calculated"]["calculation"]["direct"]["TC"] == 250
|
||
resources = {row["Code"]: row for row in result["resources"]["rows"]}
|
||
assert resources["M"]["quantity"] == 1
|
||
assert resources["M"]["amount"] == 150
|
||
assert resources["MZZZZZZZZZZ"]["amount"] == 100
|
||
|
||
|
||
def test_change_comparison_uses_original_file_values_without_overwriting_a_baseline():
|
||
result = run(
|
||
"math",
|
||
"""(()=>{
|
||
const p=EstNative.empty();p.tables.EstMainDat=[
|
||
{PkID:1,Code:'R',GN:3,TC:150,GN_:2,TC_:100},
|
||
{PkID:2,Code:'Z',GN:1,TC:10,GN_:0,TC_:0},
|
||
{PkID:3,Code:'N',GN:1,TC:20}];
|
||
const stored=EstBusiness.changes(p);
|
||
p.baseline={name:'explicit',tables:{EstMainDat:[{PkID:1,Code:'R',GN:4,TC:200}]}};
|
||
return {stored,explicit:EstBusiness.changes(p),unchanged:p.tables.EstMainDat[0].TC_};
|
||
})()""",
|
||
)
|
||
assert result["stored"][0]["originalGN"] == 2
|
||
assert result["stored"][0]["originalTC"] == 100
|
||
assert result["stored"][0]["difference"] == 50
|
||
assert result["stored"][0]["originalSource"] == "file"
|
||
assert not result["stored"][1]["NewItem"]
|
||
assert result["stored"][2]["NewItem"]
|
||
assert result["explicit"][0]["originalTC"] == 200
|
||
assert result["explicit"][0]["difference"] == -50
|
||
assert result["explicit"][1]["NewItem"]
|
||
assert result["unchanged"] == 100
|
||
|
||
|
||
def test_original_numeric_fields_reject_invalid_saved_values():
|
||
result = run(
|
||
"math",
|
||
"""(()=>{
|
||
const p=EstNative.empty();p.tables.EstMainDat=[{GN:1,GN_:'invalid'}];
|
||
try {EstModel.validate(p);return false;}catch(error){return error.message;}
|
||
})()""",
|
||
)
|
||
assert "GN_" in result
|
||
|
||
|
||
def test_baseline_keeps_current_inputs_once_and_preserves_working_binaries():
|
||
result = run(
|
||
"math",
|
||
"""(()=>{
|
||
const p=EstNative.empty();
|
||
p.tables.CostMainDat=[{Index:1,Code:'C',LineData:'current',LineData_:'previous'}];
|
||
p.tables.CadMainDat=[{Code:'D',Data:{binary:'QUJD'}}];
|
||
p.tables.StuffcDat=[{Code:'M',InputTC:100}];p.originalArchive='original bytes';
|
||
const q=EstBusiness.baseline(p),again=EstBusiness.baseline(q);
|
||
return {p,q,again};
|
||
})()""",
|
||
)
|
||
for item in [result["q"], result["again"]]:
|
||
assert item["originalArchive"] == "original bytes"
|
||
assert item["tables"]["CadMainDat"][0]["Data"]["binary"] == "QUJD"
|
||
assert "CadMainDat" not in item["baseline"]["tables"]
|
||
assert "StuffcDat" not in item["baseline"]["tables"]
|
||
assert item["baseline"]["tables"]["CostMainDat"][0]["LineData"] == "current"
|
||
assert "LineData_" not in item["baseline"]["tables"]["CostMainDat"][0]
|
||
assert result["p"]["tables"]["CostMainDat"][0]["LineData_"] == "previous"
|
||
|
||
|
||
def test_large_original_baseline_stays_within_the_save_request_limit():
|
||
import os
|
||
from pathlib import Path
|
||
import pytest
|
||
|
||
archive = Path(
|
||
os.environ.get("EST_REA_MAIN", "C:/Dasansoft/Est Plus 2/Data/ProjectData/main.estx")
|
||
)
|
||
if not archive.is_file():
|
||
pytest.skip("Original main.estx is not installed")
|
||
result = run("baseline-size", str(archive))
|
||
assert result["archivePreserved"]
|
||
assert result["bytes"] < 256 * 1024 * 1024
|
||
|
||
|
||
def test_unit_calc_group_conversion_preserves_percentages_subtotals_fixed_and_other_groups():
|
||
result = run(
|
||
"math",
|
||
"""(()=>{
|
||
const p = EstNative.empty();
|
||
p.options.SourceRefresh = true;
|
||
p.tables.LaborcDat = [
|
||
{ Code: "L001", Name: "보통인부", Unite: "인", Cost1: 100000, TC: 100000 }
|
||
];
|
||
p.tables.StuffcDat = [
|
||
{ Code: "M001", Name: "시멘트", Unite: "포", Cost1: 10000, TC: 10000 }
|
||
];
|
||
p.tables.HeadMainDat = [
|
||
// 대상 공종 H001
|
||
{ Index: 1, Code: "H001", Name: "기존공종", Size: "100", Unite: "m3", GN: 0, TC: 0 },
|
||
// 1) 일반 비목 행 (수량 환산 적용)
|
||
{ Index: 0, Code: "L001", LinkIndex: 6, Name: "보통인부", Size: "", Unite: "인", GN: 1.0, TU: 100000, NU: 100000, GU: 0, JU: 0, NC: 100000, GC: 0, JC: 0, TC: 100000 },
|
||
// 2) 고정금액 행 (CalcIndex = 1, 환산 제외)
|
||
{ Index: 0, Code: "FIX01", LinkIndex: 0, CalcIndex: 1, Name: "고정항목", Size: "", Unite: "식", GN: 1.0, TU: 5000, NU: 0, GU: 0, JU: 5000, NC: 0, GC: 0, JC: 5000, TC: 5000 },
|
||
// 3) 비율 행 (LinkIndex = 58, GN = 10(%), 환산 제외)
|
||
{ Index: 0, Code: "", LinkIndex: 58, Name: "공구손료", Size: "10%", Unite: "%", GN: 10.0, TU: 0, NU: 0, GU: 0, JU: 0, NC: 0, GC: 0, JC: 0, TC: 0 },
|
||
// 4) 소계 행 (LinkIndex = 51, 환산 제외)
|
||
{ Index: 0, Code: "", LinkIndex: 51, Name: "소계", Size: "", Unite: "", GN: 0, TU: 0, NU: 0, GU: 0, JU: 0, NC: 0, GC: 0, JC: 0, TC: 0 },
|
||
// 5) 낙찰률 행 (LinkIndex = 52, GN = 80(%), 환산 제외)
|
||
{ Index: 0, Code: "", LinkIndex: 52, Name: "낙찰적용", Size: "80%", Unite: "%", GN: 80.0, TU: 0, NU: 0, GU: 0, JU: 0, NC: 0, GC: 0, JC: 0, TC: 0 },
|
||
|
||
// 비대상 공종 H002 (보존 검증)
|
||
{ Index: 1, Code: "H002", Name: "다른공종", Size: "공통", Unite: "식", GN: 0, TC: 0 },
|
||
{ Index: 0, Code: "M001", LinkIndex: 5, Name: "시멘트", Size: "", Unite: "포", GN: 3.0, TU: 10000, NU: 0, GU: 10000, JU: 0, NC: 0, GC: 30000, JC: 0, TC: 30000 }
|
||
];
|
||
|
||
// 1) factor = 0.5 환산
|
||
const halfConverted = EstBusiness.convertGroupUnit(p, "H001", {
|
||
name: "환산공종",
|
||
size: "50",
|
||
factor: 0.5,
|
||
unit: "식"
|
||
});
|
||
|
||
// 2) factor = 2 환산 (halfConverted 기준 재환산 -> 원래 수량 1.0으로 복귀)
|
||
const doubleConverted = EstBusiness.convertGroupUnit(halfConverted, "H001", {
|
||
name: "환산공종2",
|
||
size: "100",
|
||
factor: 2.0,
|
||
unit: "식"
|
||
});
|
||
|
||
const h1Half = halfConverted.tables.HeadMainDat;
|
||
const h1Double = doubleConverted.tables.HeadMainDat;
|
||
|
||
return {
|
||
// factor 0.5 결과
|
||
halfR1GN: h1Half[1].GN,
|
||
halfFixedGN: h1Half[2].GN,
|
||
halfPercentGN: h1Half[3].GN,
|
||
halfSubtotalGN: h1Half[4].GN,
|
||
halfBidGN: h1Half[5].GN,
|
||
halfOtherGroupGN: h1Half[7].GN,
|
||
|
||
// factor 2.0 결과
|
||
doubleR1GN: h1Double[1].GN,
|
||
doubleFixedGN: h1Double[2].GN,
|
||
doublePercentGN: h1Double[3].GN,
|
||
doubleSubtotalGN: h1Double[4].GN,
|
||
doubleBidGN: h1Double[5].GN,
|
||
doubleOtherGroupGN: h1Double[7].GN,
|
||
|
||
// AutoCalc false 옵션 보존 검증
|
||
autoCalcFalsePreserved: (() => {
|
||
const p2 = structuredClone(p);
|
||
p2.options.AutoCalc = false;
|
||
const res = EstBusiness.convertGroupUnit(p2, "H001", { factor: 0.5 });
|
||
return res.options.AutoCalc === false;
|
||
})()
|
||
};
|
||
})()""",
|
||
)
|
||
# factor 0.5
|
||
assert result["halfR1GN"] == 0.5
|
||
assert result["halfFixedGN"] == 1.0 # 고정금액 행 GN 불변
|
||
assert result["halfPercentGN"] == 10.0 # 비율 행 GN 불변
|
||
assert result["halfSubtotalGN"] == 0 # 소계 행 불변
|
||
assert result["halfBidGN"] == 80.0 # 낙찰률 행 GN 불변
|
||
assert result["halfOtherGroupGN"] == 3.0 # 다른 공종 H002 행 GN 불변
|
||
|
||
# factor 2.0 (원상복귀)
|
||
assert result["doubleR1GN"] == 1.0
|
||
assert result["doubleFixedGN"] == 1.0
|
||
assert result["doublePercentGN"] == 10.0
|
||
assert result["doubleBidGN"] == 80.0
|
||
assert result["doubleOtherGroupGN"] == 3.0
|
||
|
||
# AutoCalc false 옵션 보존
|
||
assert result["autoCalcFalsePreserved"] is True
|
||
|
||
|
||
def test_haljung_calculation_apply_to_head_and_cost_groups():
|
||
result = run(
|
||
"math",
|
||
"""(()=>{
|
||
// 1) 일위대가 공종에 할증 적용: 노무비 1000원의 10% 할증을 노무비에 가산
|
||
const p = EstNative.empty();
|
||
p.tables.HeadMainDat = [
|
||
{ Index: 1, Code: "H01", Name: "일위공종1", TC: 0 },
|
||
{ Index: 0, LinkIndex: 6, Name: "보통인부", GN: 1, NU: 1000, NC: 1000, TU: 1000, TC: 1000 },
|
||
{ Index: 0, LinkIndex: 5, Name: "모래", GN: 2, GU: 500, GC: 1000, TU: 500, TC: 1000 },
|
||
{ Index: 0, LinkIndex: 51, Name: "소계", TC: 0 }
|
||
];
|
||
p.tables.HeadTitleDat = [{ Code: "H01" }];
|
||
|
||
// 노무비의 10% 할증을 노무비에 적용 (sourceKind=2, targetKind=2, rate=10)
|
||
const p1 = EstHalJung.apply(p, "HeadMainDat", "H01", {
|
||
name: "할 증",
|
||
sourceKind: 2,
|
||
targetKind: 2,
|
||
rate: 10
|
||
});
|
||
|
||
const hRows = p1.tables.HeadMainDat;
|
||
const haljungRow = hRows[3]; // 보통인부, 모래 뒤, 소계 앞
|
||
const subtotalRow = hRows[4];
|
||
const titleRow = hRows[0];
|
||
|
||
// 1-2) 특정 비목(노무비 sourceKind=2)에서 적용비목이 비목별(targetKind=0)일 때 원천 비목(노무비 NC)에 가산되는지 확인
|
||
const p1_target0 = EstHalJung.apply(p, "HeadMainDat", "H01", {
|
||
name: "노무비별할증",
|
||
sourceKind: 2,
|
||
targetKind: 0,
|
||
rate: 10
|
||
});
|
||
const haljungRow_t0 = p1_target0.tables.HeadMainDat[3];
|
||
|
||
// 2) 일위대가 공종에 합계의 5% 할증을 경비에 적용 (sourceKind=4, targetKind=3, rate=5)
|
||
p1.tables.HeadMainDat.push(
|
||
{ Index: 1, Code: "H02", Name: "일위공종2" },
|
||
{ Index: 0, LinkIndex: 5, Name: "재료A", GN: 1, GU: 2000, GC: 2000, TU: 2000, TC: 2000 },
|
||
{ Index: 0, LinkIndex: 51, Name: "소계" }
|
||
);
|
||
p1.tables.HeadTitleDat.push({ Code: "H02" });
|
||
|
||
const p2 = EstHalJung.apply(p1, "HeadMainDat", "H02", {
|
||
name: "야간할증",
|
||
sourceKind: 4,
|
||
targetKind: 3,
|
||
rate: 5
|
||
});
|
||
|
||
const h2Rows = p2.tables.HeadMainDat;
|
||
const cHaljungRow = h2Rows[7]; // H01 5행 뒤 H02(제목, 재료A, 야간할증, 소계)
|
||
|
||
// 3) 잘못된 입력 거부 (0 또는 유효하지 않은 rate, 존재하지 않는 공종, 다른 테이블)
|
||
let rejected = 0;
|
||
try { EstHalJung.apply(p, "HeadMainDat", "H01", { rate: 0 }); } catch { rejected++; }
|
||
try { EstHalJung.apply(p, "HeadMainDat", "NON_EXIST", { rate: 10 }); } catch { rejected++; }
|
||
try { EstHalJung.apply(p, "CostMainDat", "H01", { rate: 10 }); } catch { rejected++; }
|
||
|
||
return {
|
||
haljungRow: {
|
||
Name: haljungRow.Name,
|
||
Size: haljungRow.Size,
|
||
Unite: haljungRow.Unite,
|
||
GN: haljungRow.GN,
|
||
NC: haljungRow.NC,
|
||
GC: haljungRow.GC,
|
||
JC: haljungRow.JC,
|
||
TC: haljungRow.TC,
|
||
SCheck: haljungRow.SCheck,
|
||
OCheck: haljungRow.OCheck
|
||
},
|
||
subtotalNU: subtotalRow.NU,
|
||
subtotalTU: subtotalRow.TU,
|
||
titleTC: titleRow.TC,
|
||
titleNC: titleRow.NC,
|
||
haljungTarget0: {
|
||
NC: haljungRow_t0.NC,
|
||
GC: haljungRow_t0.GC,
|
||
JC: haljungRow_t0.JC,
|
||
TC: haljungRow_t0.TC,
|
||
SCheck: haljungRow_t0.SCheck
|
||
},
|
||
cHaljungRow: {
|
||
Name: cHaljungRow.Name,
|
||
Size: cHaljungRow.Size,
|
||
Unite: cHaljungRow.Unite,
|
||
JC: cHaljungRow.JC,
|
||
TC: cHaljungRow.TC
|
||
},
|
||
rejected
|
||
};
|
||
})()""",
|
||
)
|
||
|
||
# 1) 일위대가 노무비 1000원의 10% = 100원 노무비 가산 확인
|
||
assert result["haljungRow"]["Name"] == "할 증"
|
||
assert result["haljungRow"]["Size"] == "노무비의"
|
||
assert result["haljungRow"]["Unite"] == "%"
|
||
assert result["haljungRow"]["GN"] == 10
|
||
assert result["haljungRow"]["NC"] == 100
|
||
assert result["haljungRow"]["GC"] == 0
|
||
assert result["haljungRow"]["JC"] == 0
|
||
assert result["haljungRow"]["TC"] == 100
|
||
assert result["haljungRow"]["SCheck"] == 2 # 노무비에
|
||
assert result["haljungRow"]["OCheck"] == 2 # 노무비의
|
||
|
||
# 소계(LinkIndex 51)의 단가 성분(NU/TU) 및 공종 제목행(Index 1) 합계:
|
||
# 노무비 1000 + 100 = 1100, 재료비 1000 => 합계 2100
|
||
assert result["subtotalNU"] == 1100
|
||
assert result["subtotalTU"] == 2100
|
||
assert result["titleNC"] == 1100
|
||
assert result["titleTC"] == 2100
|
||
|
||
# 1-2) sourceKind=2(노무비) & targetKind=0(비목별) -> 노무비(NC)에 100원 반영 확인 (경비 JC로 안 감)
|
||
assert result["haljungTarget0"]["NC"] == 100
|
||
assert result["haljungTarget0"]["GC"] == 0
|
||
assert result["haljungTarget0"]["JC"] == 0
|
||
assert result["haljungTarget0"]["TC"] == 100
|
||
assert result["haljungTarget0"]["SCheck"] == 2
|
||
|
||
# 2) 단가산출 합계 2000원의 5% = 100원 경비 가산 확인
|
||
assert result["cHaljungRow"]["Name"] == "야간할증"
|
||
assert result["cHaljungRow"]["Size"] == "합계의"
|
||
assert result["cHaljungRow"]["JC"] == 100
|
||
assert result["cHaljungRow"]["TC"] == 100
|
||
|
||
# 3) 안전성 거부 3건 확인
|
||
assert result["rejected"] == 3
|
||
|
||
|
||
def test_vat_calculation_exclude_include_and_unit_rounding_for_materials():
|
||
result = run(
|
||
"math",
|
||
"""(()=>{
|
||
const p = EstNative.empty();
|
||
p.tables.StuffcDat = [
|
||
{ Code: "M01", Name: "시멘트", Cost1: 11000, Cost2: 12100, ValueIndex: 0, InputTC: 11000, TC: 11000 },
|
||
{ Code: "M02", Name: "철근", Cost1: 10000, ValueIndex: 4, InputTC: 10000, TC: 10000 },
|
||
{ Code: "M03", Name: "단단위테스트", Cost1: 12345, InputTC: 12345, TC: 12345 }
|
||
];
|
||
|
||
// 1) M01 선택 후 부가세 제외 (/1.1)
|
||
const p1 = EstVat.apply(p, [0], {
|
||
calcMethod: 1, // 부가세제외 (/1.1)
|
||
diget: 0, // 원단위
|
||
digetMethod: 1 // 절하
|
||
});
|
||
|
||
// 2) M02에 부가세 포함 (*1.1) 및 십원 단위(-1자리) 절하 적용
|
||
const p2 = EstVat.apply(p, [1], {
|
||
calcMethod: 0, // 부가세포함 (*1.1)
|
||
diget: 1, // 십원 단위 절하
|
||
digetMethod: 1
|
||
});
|
||
|
||
// 3) M03에 부가세 제외 (/1.1) 후 단단위 사사오입(digetMethod: 0) 및 절상(digetMethod: 2)
|
||
// 12345 / 1.1 = 11222.7272...
|
||
// diget=1 (십원 단위 사사오입): 11220
|
||
// diget=1 (십원 단위 절상): 11230
|
||
const p3Round = EstVat.apply(p, [2], { calcMethod: 1, diget: 1, digetMethod: 0 });
|
||
const p3Up = EstVat.apply(p, [2], { calcMethod: 1, diget: 1, digetMethod: 2 });
|
||
|
||
return {
|
||
m1Cost1: p1.tables.StuffcDat[0].Cost1,
|
||
m1Cost2: p1.tables.StuffcDat[0].Cost2,
|
||
m1TC: p1.tables.StuffcDat[0].TC,
|
||
m2Cost1: p2.tables.StuffcDat[1].Cost1,
|
||
m2TC: p2.tables.StuffcDat[1].TC,
|
||
m3RoundCost: p3Round.tables.StuffcDat[2].Cost1,
|
||
m3UpCost: p3Up.tables.StuffcDat[2].Cost1
|
||
};
|
||
})()""",
|
||
)
|
||
|
||
# M01: 11000 / 1.1 = 10000, 12100 / 1.1 = 11000
|
||
# ValueIndex=0(최저단가) -> 재계산 후 TC도 10000으로 보존됨
|
||
assert result["m1Cost1"] == 10000
|
||
assert result["m1Cost2"] == 11000
|
||
assert result["m1TC"] == 10000
|
||
|
||
# M02: 10000 * 1.1 = 11000, InputTC/TC도 11000 확인
|
||
assert result["m2Cost1"] == 11000
|
||
assert result["m2TC"] == 11000
|
||
|
||
# M03: 12345 / 1.1 = 11222.72... -> 십원 단위 사사오입(11220), 절상(11230)
|
||
assert result["m3RoundCost"] == 11220
|
||
assert result["m3UpCost"] == 11230
|
||
|
||
|
||
def test_vat_round_up_handles_small_remainders_and_exact_boundaries():
|
||
result = run(
|
||
"math",
|
||
"""[EstVat.convertPrice(100.1, 0, 1, 2),
|
||
EstVat.convertPrice(100, 0, 1, 2)]""",
|
||
)
|
||
assert result == [120, 110]
|