Files

677 lines
30 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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]