314 lines
13 KiB
Python
314 lines
13 KiB
Python
from test_est_rea_parity import run
|
|
|
|
|
|
def test_standard_changes_include_new_deleted_and_changed_link_types():
|
|
result = run(
|
|
"math",
|
|
"""(()=>{
|
|
const p=EstNative.empty();p.tables.EstMainDat=[
|
|
{PkID:1,Code:'A',Name:'changed',LinkIndex:21,GN:2,TC:200},
|
|
{PkID:2,Code:'B',Name:'changed link',LinkIndex:5,GN:1,TC:50},
|
|
{PkID:4,Code:'D',Name:'new',LinkIndex:21,GN:1,TC:40}];
|
|
p.baseline={tables:{EstMainDat:[
|
|
{PkID:1,Code:'A',Name:'changed',LinkIndex:21,GN:1,TC:100},
|
|
{PkID:2,Code:'B',Name:'changed link',LinkIndex:21,GN:1,TC:50},
|
|
{PkID:3,Code:'C',Name:'deleted',LinkIndex:21,GN:3,TC:300},
|
|
{PkID:5,Code:'E',Name:'other',LinkIndex:5,GN:1,TC:10}]}};
|
|
return {rows:EstStandard.changes(p),print:EstForms.data('standard-change',p)};
|
|
})()""",
|
|
)
|
|
rows = {row["Code"]: row for row in result["rows"]}
|
|
assert set(rows) == {"A", "B", "C", "D"}
|
|
assert rows["A"]["difference"] == 100
|
|
assert rows["B"]["status"] == "변경"
|
|
assert rows["C"]["status"] == "삭제" and rows["C"]["index"] is None
|
|
assert rows["D"]["status"] == "신규"
|
|
assert result["print"]["rows"] == result["rows"]
|
|
|
|
|
|
def test_standard_usage_reports_applied_and_source_prices_without_mutation():
|
|
result = run(
|
|
"math",
|
|
"""(()=>{
|
|
const p=EstNative.empty();p.tables.SilJukDat=[{Code:'A',Name:'source',ValueIndex:4,InputTC:120},
|
|
{Code:'B',ValueIndex:4,InputTC:50},{Code:'B',ValueIndex:4,InputTC:60}];
|
|
p.tables.EstMainDat=[{Code:'A',Name:'work',LinkIndex:21,GN:2,TU:100,TC:200},
|
|
{Code:'B',Name:'duplicate',LinkIndex:21,GN:1,TU:45,TC:45},
|
|
{Code:'C',Name:'missing',LinkIndex:21,GN:1,TU:20,TC:20},
|
|
{Code:'A',Name:'material',LinkIndex:5,GN:1,TU:10}];
|
|
const before=JSON.stringify(p), rows=EstStandard.usage(p), print=EstForms.data('standard-usage',p);
|
|
return {rows,print,same:before===JSON.stringify(p)};
|
|
})()""",
|
|
)
|
|
assert result["same"]
|
|
assert [row["status"] for row in result["rows"]] == ["연결됨", "코드 중복", "연결 없음"]
|
|
assert result["rows"][0]["sourcePrice"] == 120
|
|
assert result["rows"][0]["appliedPrice"] == 100
|
|
assert result["rows"][0]["difference"] == -20
|
|
assert result["rows"][1]["sourcePrice"] is None
|
|
assert result["print"]["rows"] == result["rows"]
|
|
|
|
|
|
def test_standard_apply_row_and_all_prices():
|
|
result = run(
|
|
"math",
|
|
"""(()=>{
|
|
const p = EstNative.empty();
|
|
p.options.SourceRefresh = false;
|
|
p.options.AutoCalc = false;
|
|
p.tables.SilJukDat = [
|
|
{ Code: 'A', Name: 'standard A', ValueIndex: 4, InputTC: 150, InputGC: 50, InputNC: 70, InputJC: 30, InputSC: 0 },
|
|
{ Code: 'B', Name: 'standard B', ValueIndex: 4, InputTC: 300, InputSC: 300 },
|
|
{ Code: 'D', Name: 'dup', ValueIndex: 4, InputTC: 50 },
|
|
{ Code: 'D', Name: 'dup', ValueIndex: 4, InputTC: 60 },
|
|
];
|
|
p.tables.EstMainDat = [
|
|
{ Code: 'A', Name: 'item A', LinkIndex: 21, GN: 2, TU: 100, GU: 30, NU: 50, JU: 20, GC: 60, NC: 100, JC: 40, TC: 200, SCheck: 1 },
|
|
{ Code: 'B', Name: 'item B', LinkIndex: 21, GN: 3, TU: 200, SU: 200, SC: 600, TC: 600 },
|
|
{ Code: 'C', Name: 'item C', LinkIndex: 21, GN: 1, TU: 50, SU: 50, SC: 50, TC: 50 },
|
|
{ Code: 'D', Name: 'item D', LinkIndex: 21, GN: 1, TU: 40, SU: 40, SC: 40, TC: 40 },
|
|
{ Code: 'OTHER', Name: 'other row', LinkIndex: 5, GN: 5, TU: 10, GC: 50, TC: 50 },
|
|
];
|
|
const initialUsage = EstStandard.usage(p);
|
|
const otherBefore = JSON.stringify(p.tables.EstMainDat[4]);
|
|
|
|
// 행 0(SCheck=1: 재료비 몰아주기 설정)에 단가 적용:
|
|
// SCheck=1이 보존되어 GU=150, NU=0, JU=0, SU=0 및 GN=2 -> GC=300, TC=300
|
|
EstStandard.applyRowPrice(p, 0);
|
|
const afterRow0 = {
|
|
row0_TU: p.tables.EstMainDat[0].TU,
|
|
row0_GU: p.tables.EstMainDat[0].GU,
|
|
row0_NU: p.tables.EstMainDat[0].NU,
|
|
row0_JU: p.tables.EstMainDat[0].JU,
|
|
row0_GC: p.tables.EstMainDat[0].GC,
|
|
row0_TC: p.tables.EstMainDat[0].TC,
|
|
row0_SCheck: p.tables.EstMainDat[0].SCheck,
|
|
otherPreserved: JSON.stringify(p.tables.EstMainDat[4]) === otherBefore,
|
|
sourceRefresh: p.options.SourceRefresh,
|
|
autoCalc: p.options.AutoCalc,
|
|
};
|
|
|
|
// SCheck를 0으로 초기화한 뒤 다시 적용하면 원천의 재료/노무/경비 비목 분할(50/70/30)이 유지됨
|
|
p.tables.EstMainDat[0].SCheck = 0;
|
|
EstStandard.applyRowPrice(p, 0);
|
|
const afterRow0_split = {
|
|
row0_TU: p.tables.EstMainDat[0].TU,
|
|
row0_GU: p.tables.EstMainDat[0].GU,
|
|
row0_NU: p.tables.EstMainDat[0].NU,
|
|
row0_JU: p.tables.EstMainDat[0].JU,
|
|
row0_GC: p.tables.EstMainDat[0].GC,
|
|
row0_NC: p.tables.EstMainDat[0].NC,
|
|
row0_JC: p.tables.EstMainDat[0].JC,
|
|
row0_TC: p.tables.EstMainDat[0].TC,
|
|
};
|
|
|
|
let errMissing = false;
|
|
try { EstStandard.applyRowPrice(p, 2); } catch { errMissing = true; }
|
|
let errDup = false;
|
|
try { EstStandard.applyRowPrice(p, 3); } catch { errDup = true; }
|
|
|
|
// 남은 대상인 행 1(수량 3)에 대해 applyAllPrices 적용
|
|
const appliedCount = EstStandard.applyAllPrices(p);
|
|
const afterAll = {
|
|
row1_TU: p.tables.EstMainDat[1].TU,
|
|
row1_SU: p.tables.EstMainDat[1].SU,
|
|
row1_SC: p.tables.EstMainDat[1].SC,
|
|
row1_TC: p.tables.EstMainDat[1].TC,
|
|
row2_TU: p.tables.EstMainDat[2].TU,
|
|
row3_TU: p.tables.EstMainDat[3].TU,
|
|
otherPreserved: JSON.stringify(p.tables.EstMainDat[4]) === otherBefore,
|
|
};
|
|
const finalUsage = EstStandard.usage(p);
|
|
|
|
// AutoCalc=true 전환 후 전체 재계산: 금액이 수량(GN)을 반영해 갱신됨
|
|
p.options.AutoCalc = true;
|
|
const calculated = EstNative.calculate(p);
|
|
const afterRecalc = {
|
|
row0_GC: calculated.tables.EstMainDat[0].GC,
|
|
row0_NC: calculated.tables.EstMainDat[0].NC,
|
|
row0_JC: calculated.tables.EstMainDat[0].JC,
|
|
row0_TC: calculated.tables.EstMainDat[0].TC,
|
|
row1_SC: calculated.tables.EstMainDat[1].SC,
|
|
row1_TC: calculated.tables.EstMainDat[1].TC,
|
|
};
|
|
|
|
return { initialUsage, afterRow0, afterRow0_split, errMissing, errDup, appliedCount, afterAll, finalUsage, afterRecalc };
|
|
})()""",
|
|
)
|
|
assert result["initialUsage"][0]["canApply"] is True
|
|
assert result["initialUsage"][1]["canApply"] is True
|
|
assert result["initialUsage"][2]["canApply"] is False
|
|
assert result["initialUsage"][3]["canApply"] is False
|
|
assert result["afterRow0"] == {
|
|
"row0_TU": 150,
|
|
"row0_GU": 150,
|
|
"row0_NU": 0,
|
|
"row0_JU": 0,
|
|
"row0_GC": 60,
|
|
"row0_TC": 200,
|
|
"row0_SCheck": 1,
|
|
"otherPreserved": True,
|
|
"sourceRefresh": False,
|
|
"autoCalc": False,
|
|
}
|
|
assert result["afterRow0_split"] == {
|
|
"row0_TU": 150,
|
|
"row0_GU": 50,
|
|
"row0_NU": 70,
|
|
"row0_JU": 30,
|
|
"row0_GC": 60,
|
|
"row0_NC": 100,
|
|
"row0_JC": 40,
|
|
"row0_TC": 200,
|
|
}
|
|
assert result["errMissing"] is True
|
|
assert result["errDup"] is True
|
|
assert result["appliedCount"] == 1
|
|
assert result["afterAll"] == {
|
|
"row1_TU": 300,
|
|
"row1_SU": 300,
|
|
"row1_SC": 600,
|
|
"row1_TC": 600,
|
|
"row2_TU": 50,
|
|
"row3_TU": 40,
|
|
"otherPreserved": True,
|
|
}
|
|
assert result["afterRecalc"] == {
|
|
"row0_GC": 100,
|
|
"row0_NC": 140,
|
|
"row0_JC": 60,
|
|
"row0_TC": 300,
|
|
"row1_SC": 900,
|
|
"row1_TC": 900,
|
|
}
|
|
assert result["finalUsage"][0]["canApply"] is False
|
|
assert result["finalUsage"][1]["canApply"] is False
|
|
|
|
|
|
def test_edge_standard_usage_apply_price_and_undo_when_server_running():
|
|
from playwright.sync_api import sync_playwright
|
|
|
|
with sync_playwright() as p:
|
|
browser = p.chromium.launch(channel="msedge", headless=True)
|
|
page = browser.new_page()
|
|
try:
|
|
page.goto("http://100.65.141.15:5175", timeout=5000)
|
|
page.wait_for_function("() => Boolean(EstApp.state.project)")
|
|
except Exception:
|
|
browser.close()
|
|
import pytest
|
|
|
|
pytest.skip("EST 5175 server not reachable")
|
|
|
|
# 1. 테스트 프로젝트 데이터 세팅 (표준시장단가 및 내역 행 주입)
|
|
page.evaluate(
|
|
"""(() => {
|
|
const p = EstNative.empty();
|
|
p.options.SourceRefresh = false;
|
|
p.tables.SilJukDat = [
|
|
{ Code: 'S001', Name: '표준토공', Size: '일반', Unite: 'm3', ValueIndex: 4, InputTC: 15000 },
|
|
{ Code: 'S002', Name: '표준포장', Size: 't=10cm', Unite: 'm2', ValueIndex: 4, InputTC: 32000 },
|
|
];
|
|
p.tables.EstMainDat = [
|
|
{ PkID: 101, Code: 'S001', Name: '표준토공', Size: '일반', Unite: 'm3', LinkIndex: 21, GN: 10, TU: 12000, SU: 12000, SC: 120000, TC: 120000 },
|
|
{ PkID: 102, Code: 'S002', Name: '표준포장', Size: 't=10cm', Unite: 'm2', LinkIndex: 21, GN: 5, TU: 30000, SU: 30000, SC: 150000, TC: 150000 },
|
|
];
|
|
EstApp.setProject(p);
|
|
EstApp.state.view = 'standard';
|
|
EstApp.render();
|
|
})()"""
|
|
)
|
|
|
|
# 2. 표준시장단가 사용 내역 화면 확인
|
|
assert page.locator("#view-title:has-text('표준시장단가 사용 내역')").is_visible()
|
|
btn_apply_all = page.locator("button[data-standard-apply-all]")
|
|
assert btn_apply_all.is_visible()
|
|
assert "기초단가 일괄 적용 (2건)" in btn_apply_all.text_content()
|
|
|
|
# 3. 개별 단가 적용 버튼 클릭 (1행 S001)
|
|
btn_row_apply = page.locator("button[data-standard-apply-row='0']")
|
|
assert btn_row_apply.is_visible()
|
|
btn_row_apply.click()
|
|
|
|
# 1행이 15,000으로 갱신되었는지 확인
|
|
tu_row0 = page.evaluate("EstApp.state.project.tables.EstMainDat[0].TU")
|
|
tc_row0 = page.evaluate("EstApp.state.project.tables.EstMainDat[0].TC")
|
|
assert tu_row0 == 15000
|
|
assert tc_row0 == 150000
|
|
|
|
# 버튼 라벨이 (1건)으로 변경되었는지 확인
|
|
assert "기초단가 일괄 적용 (1건)" in btn_apply_all.text_content()
|
|
|
|
# 4. 일괄 적용 버튼 클릭 (남은 1건 S002 적용)
|
|
btn_apply_all.click()
|
|
tu_row1 = page.evaluate("EstApp.state.project.tables.EstMainDat[1].TU")
|
|
tc_row1 = page.evaluate("EstApp.state.project.tables.EstMainDat[1].TC")
|
|
assert tu_row1 == 32000
|
|
assert tc_row1 == 160000
|
|
|
|
# 이제 일괄 적용 버튼이 disabled 상태인지 확인
|
|
assert btn_apply_all.is_disabled()
|
|
|
|
# 5. Undo(실행 취소) 검증
|
|
page.locator(".toolbar button[data-action='undo']").click()
|
|
# 1단계 취소 시 2행은 원래대로 30,000 복귀
|
|
assert page.evaluate("EstApp.state.project.tables.EstMainDat[1].TU") == 30000
|
|
|
|
page.locator(".toolbar button[data-action='undo']").click()
|
|
# 2단계 취소 시 1행도 원래대로 12,000 복귀
|
|
assert page.evaluate("EstApp.state.project.tables.EstMainDat[0].TU") == 12000
|
|
|
|
browser.close()
|
|
|
|
|
|
def test_standard_manual_mode_autocalc_false_and_recalc_switch():
|
|
result = run(
|
|
"math",
|
|
"""(()=>{
|
|
const p = EstNative.empty();
|
|
p.options.SourceRefresh = false;
|
|
p.options.AutoCalc = false; // 수동계산 모드
|
|
p.tables.SilJukDat = [
|
|
{ Code: 'S10', Name: '표준단가 10', ValueIndex: 4, InputTC: 20000, InputSC: 20000 },
|
|
];
|
|
p.tables.EstMainDat = [
|
|
{ Level: 1, Code: '', Name: '공종1', TC: 50000 },
|
|
{ Code: 'S10', Name: '품목 10', LinkIndex: 21, GN: 5, TU: 10000, SU: 10000, SC: 50000, TC: 50000 },
|
|
];
|
|
|
|
// 1. 수동계산 상태에서 applyRowPrice 실행
|
|
// 해당 행(인덱스 1)의 단가(TU, SU)는 20,000으로 갱신되나,
|
|
// AutoCalc=false이므로 자체 금액(SC=50000, TC=50000) 및 상위 공종 행(Level 1, TC=50000) 금액이 모두 보존됨
|
|
EstStandard.applyRowPrice(p, 1);
|
|
const afterManual = {
|
|
row1_TU: p.tables.EstMainDat[1].TU,
|
|
row1_SC: p.tables.EstMainDat[1].SC,
|
|
row1_TC: p.tables.EstMainDat[1].TC,
|
|
row0_Level1_TC: p.tables.EstMainDat[0].TC,
|
|
};
|
|
|
|
// 2. 전체 재계산(AutoCalc=true 전환 및 EstNative.calculate 실행)
|
|
p.options.AutoCalc = true;
|
|
const calculated = EstNative.calculate(p);
|
|
const afterRecalc = {
|
|
row1_TU: calculated.tables.EstMainDat[1].TU,
|
|
row1_SC: calculated.tables.EstMainDat[1].SC,
|
|
row1_TC: calculated.tables.EstMainDat[1].TC,
|
|
row0_Level1_TC: calculated.tables.EstMainDat[0].TC,
|
|
};
|
|
|
|
return { afterManual, afterRecalc };
|
|
})()""",
|
|
)
|
|
assert result["afterManual"]["row1_TU"] == 20000
|
|
assert result["afterManual"]["row1_SC"] == 50000
|
|
assert result["afterManual"]["row1_TC"] == 50000
|
|
# 수동 상태에서는 자체 금액 및 상위 공종(Level 1) 금액 모두 보존됨
|
|
assert result["afterManual"]["row0_Level1_TC"] == 50000
|
|
|
|
# 전체 재계산 전환 후 수량(GN=5)을 반영해 자체 금액 및 상위 공종 정상 합산됨
|
|
assert result["afterRecalc"]["row1_TU"] == 20000
|
|
assert result["afterRecalc"]["row1_SC"] == 100000
|
|
assert result["afterRecalc"]["row1_TC"] == 100000
|
|
assert result["afterRecalc"]["row0_Level1_TC"] == 100000
|