from test_est_rea_parity import run def test_basic_prices_changes_for_material_and_labor(): result = run( "math", """(()=>{ const p = EstNative.empty(); p.tables.EstMainDat = [ { PkID: 1, Code: 'M1', Name: 'changed mat', LinkIndex: 5, GN: 2, TC: 200 }, { PkID: 2, Code: 'L1', Name: 'changed lab', LinkIndex: 6, GN: 1, TC: 50 }, { PkID: 4, Code: 'M2', Name: 'new mat', LinkIndex: 5, GN: 1, TC: 40 }, { PkID: 5, Code: 'L2', Name: 'new lab', LinkIndex: 6, GN: 1, TC: 80 }, ]; p.baseline = { tables: { EstMainDat: [ { PkID: 1, Code: 'M1', Name: 'changed mat', LinkIndex: 5, GN: 1, TC: 100 }, { PkID: 2, Code: 'L1', Name: 'changed lab', LinkIndex: 6, GN: 1, TC: 30 }, { PkID: 3, Code: 'M3', Name: 'deleted mat', LinkIndex: 5, GN: 3, TC: 300 }, { PkID: 6, Code: 'L3', Name: 'deleted lab', LinkIndex: 6, GN: 2, TC: 120 }, ] } }; return { matChanges: EstBasicPrices.changes(p, 'material'), labChanges: EstBasicPrices.changes(p, 'labor'), matPrint: EstForms.data('material-change', p), labPrint: EstForms.data('labor-change', p), }; })()""", ) mat_rows = {row["Code"]: row for row in result["matChanges"]} assert set(mat_rows) == {"M1", "M2", "M3"} assert mat_rows["M1"]["difference"] == 100 assert mat_rows["M3"]["status"] == "삭제" assert mat_rows["M2"]["status"] == "신규" assert result["matPrint"]["rows"] == result["matChanges"] lab_rows = {row["Code"]: row for row in result["labChanges"]} assert set(lab_rows) == {"L1", "L2", "L3"} assert lab_rows["L1"]["difference"] == 20 assert lab_rows["L3"]["status"] == "삭제" assert lab_rows["L2"]["status"] == "신규" assert result["labPrint"]["rows"] == result["labChanges"] def test_basic_prices_usage_reports_applied_and_source_prices_without_mutation(): result = run( "math", """(()=>{ const p = EstNative.empty(); p.tables.StuffcDat = [ { Code: 'M1', Name: 'mat 1', ValueIndex: 0, Cost1: 1200 }, { Code: 'M2', Name: 'mat 2', ValueIndex: 0, Cost1: 500 }, { Code: 'M2', Name: 'mat 2 dup', ValueIndex: 0, Cost1: 600 }, ]; p.tables.LaborcDat = [ { Code: 'L1', Name: 'lab 1', ValueIndex: 0, Cost1: 200000 }, ]; p.tables.EstMainDat = [ { Code: 'M1', Name: 'mat 1', LinkIndex: 5, GN: 2, TU: 1000, GU: 1000, GC: 2000, TC: 2000 }, { Code: 'M2', Name: 'dup mat', LinkIndex: 5, GN: 1, TU: 450, GU: 450, GC: 450, TC: 450 }, { Code: 'M3', Name: 'missing mat', LinkIndex: 5, GN: 1, TU: 200, GU: 200, GC: 200, TC: 200 }, { Code: 'L1', Name: 'lab 1', LinkIndex: 6, GN: 1, TU: 180000, NU: 180000, NC: 180000, TC: 180000 }, ]; const before = JSON.stringify(p); const matUsage = EstBasicPrices.usage(p, 'material'); const labUsage = EstBasicPrices.usage(p, 'labor'); const matPrint = EstForms.data('material-usage', p); const labPrint = EstForms.data('labor-usage', p); return { matUsage, labUsage, matPrint, labPrint, same: before === JSON.stringify(p) }; })()""", ) assert result["same"] assert [row["status"] for row in result["matUsage"]] == [ "연결됨", "코드 중복", "연결 없음", ] assert result["matUsage"][0]["sourcePrice"] == 1200 assert result["matUsage"][0]["appliedPrice"] == 1000 assert result["matUsage"][0]["difference"] == -200 assert result["matUsage"][1]["sourcePrice"] is None assert result["matPrint"]["rows"] == result["matUsage"] assert len(result["labUsage"]) == 1 assert result["labUsage"][0]["status"] == "연결됨" assert result["labUsage"][0]["sourcePrice"] == 200000 assert result["labUsage"][0]["appliedPrice"] == 180000 assert result["labUsage"][0]["difference"] == -20000 assert result["labPrint"]["rows"] == result["labUsage"] def test_basic_prices_apply_row_and_all_prices_with_autocalc_modes(): result = run( "math", """(()=>{ const p = EstNative.empty(); p.options.SourceRefresh = false; p.options.AutoCalc = false; // 수동계산 모드 p.tables.StuffcDat = [ { Code: 'M1', Name: 'mat 1', ValueIndex: 0, Cost1: 1500 }, { Code: 'M2', Name: 'mat 2', ValueIndex: 0, Cost1: 3000 }, ]; p.tables.LaborcDat = [ { Code: 'L1', Name: 'lab 1', ValueIndex: 0, Cost1: 220000 }, ]; p.tables.EstMainDat = [ { Code: 'M1', Name: 'item mat 1', LinkIndex: 5, GN: 2, TU: 1000, GU: 1000, GC: 2000, TC: 2000, SCheck: 2 }, // SCheck=2: 노무비 몰아주기 { Code: 'M2', Name: 'item mat 2', LinkIndex: 5, GN: 3, TU: 2000, GU: 2000, GC: 6000, TC: 6000 }, { Code: 'L1', Name: 'item lab 1', LinkIndex: 6, GN: 2, TU: 200000, NU: 200000, NC: 400000, TC: 400000 }, ]; // 1. 수동계산 상태에서 자재 행 0 단가 적용 (SCheck=2 보존 검증) // AutoCalc=false이므로 단가는 NU=1500, TU=1500으로 갱신되나 기존 금액(GC=2000, TC=2000) 보존 EstBasicPrices.applyRowPrice(p, 0, 'material'); const afterRow0 = { TU: p.tables.EstMainDat[0].TU, GU: p.tables.EstMainDat[0].GU, NU: p.tables.EstMainDat[0].NU, GC: p.tables.EstMainDat[0].GC, NC: p.tables.EstMainDat[0].NC, TC: p.tables.EstMainDat[0].TC, SCheck: p.tables.EstMainDat[0].SCheck, }; // 2. 자재 일괄 적용: 남은 행 1 적용 (단가 3000 갱신, 수동계산 금액 GC=6000 보존) const matCount = EstBasicPrices.applyAllPrices(p, 'material'); const afterMatAll = { TU: p.tables.EstMainDat[1].TU, GU: p.tables.EstMainDat[1].GU, GC: p.tables.EstMainDat[1].GC, TC: p.tables.EstMainDat[1].TC, }; // 3. 노임 단가 적용: 행 2 적용 (단가 220000 갱신, 금액 NC=400000 보존) const labCount = EstBasicPrices.applyAllPrices(p, 'labor'); const afterLab = { TU: p.tables.EstMainDat[2].TU, NU: p.tables.EstMainDat[2].NU, NC: p.tables.EstMainDat[2].NC, TC: p.tables.EstMainDat[2].TC, }; // 4. AutoCalc=true로 전환 후 전체 재계산: GN(수량)을 반영한 금액으로 일괄 전파 p.options.AutoCalc = true; const calculated = EstNative.calculate(p); const afterRecalc = { row0_NC: calculated.tables.EstMainDat[0].NC, row0_TC: calculated.tables.EstMainDat[0].TC, row1_GC: calculated.tables.EstMainDat[1].GC, row1_TC: calculated.tables.EstMainDat[1].TC, row2_NC: calculated.tables.EstMainDat[2].NC, row2_TC: calculated.tables.EstMainDat[2].TC, }; return { afterRow0, matCount, afterMatAll, labCount, afterLab, afterRecalc }; })()""", ) # 행 0: SCheck=2 보존되어 NU=1500, GU=0, 금액은 수동계산 유지(GC=2000, NC=0, TC=2000) assert result["afterRow0"]["TU"] == 1500 assert result["afterRow0"]["NU"] == 1500 assert result["afterRow0"]["GU"] == 0 assert result["afterRow0"]["GC"] == 2000 assert result["afterRow0"]["TC"] == 2000 assert result["afterRow0"]["SCheck"] == 2 # 행 1: 단가 3000, 수동 금액 6000 보존 assert result["matCount"] == 1 assert result["afterMatAll"]["TU"] == 3000 assert result["afterMatAll"]["GU"] == 3000 assert result["afterMatAll"]["GC"] == 6000 assert result["afterMatAll"]["TC"] == 6000 # 행 2: 단가 220000, 수동 금액 400000 보존 assert result["labCount"] == 1 assert result["afterLab"]["TU"] == 220000 assert result["afterLab"]["NU"] == 220000 assert result["afterLab"]["NC"] == 400000 assert result["afterLab"]["TC"] == 400000 # 전체 재계산 후: GN(수량) 반영 정상 산출 # 행 0: GN=2 -> NC=3000, TC=3000 assert result["afterRecalc"]["row0_NC"] == 3000 assert result["afterRecalc"]["row0_TC"] == 3000 # 행 1: GN=3 -> GC=9000, TC=9000 assert result["afterRecalc"]["row1_GC"] == 9000 assert result["afterRecalc"]["row1_TC"] == 9000 # 행 2: GN=2 -> NC=440000, TC=440000 assert result["afterRecalc"]["row2_NC"] == 440000 assert result["afterRecalc"]["row2_TC"] == 440000 def test_edge_basic_prices_material_and_labor_workflow_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.StuffcDat = [ { Code: 'M01', Name: '시멘트 포대', Size: '40kg', Unite: '포', ValueIndex: 0, Cost1: 6000 }, ]; p.tables.LaborcDat = [ { Code: 'L01', Name: '특별인부', Unite: '인', ValueIndex: 0, Cost1: 180000 }, ]; p.tables.EstMainDat = [ { PkID: 301, Code: 'M01', Name: '시멘트 포대', Size: '40kg', Unite: '포', LinkIndex: 5, GN: 10, TU: 5000, GU: 5000, GC: 50000, TC: 50000 }, { PkID: 302, Code: 'L01', Name: '특별인부', Unite: '인', LinkIndex: 6, GN: 5, TU: 160000, NU: 160000, NC: 800000, TC: 800000 }, ]; EstApp.setProject(p); EstApp.state.view = 'material-usage'; EstApp.render(); })()""" ) # 2. 자재단가 사용 내역 화면 확인 assert page.locator("#view-title:has-text('자재단가 사용 내역')").is_visible() btn_mat_apply = page.locator("button[data-basic-apply-row='0']") assert btn_mat_apply.is_visible() btn_mat_apply.click() # 자재 단가 6,000으로 적용 확인 assert page.evaluate("EstApp.state.project.tables.EstMainDat[0].TU") == 6000 assert page.evaluate("EstApp.state.project.tables.EstMainDat[0].GU") == 6000 # 3. 노임단가 사용 내역 화면으로 전환 page.evaluate( """(() => { EstApp.state.view = 'labor-usage'; EstApp.render(); })()""" ) assert page.locator("#view-title:has-text('노임단가 사용 내역')").is_visible() btn_lab_apply = page.locator("button[data-basic-apply-all='labor']") assert btn_lab_apply.is_visible() btn_lab_apply.click() # 노임 단가 180,000으로 일괄 적용 확인 assert page.evaluate("EstApp.state.project.tables.EstMainDat[1].TU") == 180000 assert page.evaluate("EstApp.state.project.tables.EstMainDat[1].NU") == 180000 # 4. Undo(실행 취소) 검증 page.locator(".toolbar button[data-action='undo']").click() assert page.evaluate("EstApp.state.project.tables.EstMainDat[1].TU") == 160000 page.locator(".toolbar button[data-action='undo']").click() assert page.evaluate("EstApp.state.project.tables.EstMainDat[0].TU") == 5000 # 5. 기계경비 사용 내역 화면 전환 및 개별/일괄 적용, Undo 검증 page.evaluate( """(() => { EstApp.navigate('machine-usage'); })()""" ) assert page.locator("#view-title:has-text('기계경비 사용 내역')").is_visible() browser.close() def test_machine_price_usage_changes_and_safety_rejection(): result = run( "math", """(()=>{ const p = structuredClone(project); // 신규 항목(TC_ null) 추가하여 비교 검증 p.tables.MachTitleDat.push({ Code: 'E_NEW_999', Name: '신규 기계', Size: '10Ton', Unite: 'hr', TC: 50000, NC: 30000, GC: 10000, JC: 10000, TC_: null, NC_: null, GC_: null, JC_: null }); const usage = EstMachine.usage(p); const changes = EstMachine.changes(p); const newItemUsage = usage.find(u => u.Code === 'E_NEW_999'); const newItemChange = changes.find(c => c.Code === 'E_NEW_999'); const excUsage = usage.find(u => u.Code === 'E0000000000'); // 파괴적인 당초값 덮어쓰기 적용 차단 검증 let blockedSingle = 0, singleErrMsg = ''; try { EstMachine.applyRowPrice(p, 0); } catch (e) { blockedSingle++; singleErrMsg = e.message; } let blockedAll = 0, allErrMsg = ''; try { EstMachine.applyAllPrices(p); } catch (e) { blockedAll++; allErrMsg = e.message; } // 호표 단가 및 단가산출 금액 불변 보존 확인 const t0Unchanged = (p.tables.MachTitleDat[0].TC === 103047); const costRow0 = p.tables.CostMainDat.find(r => r.LinkIndex === 105 && r.Code === 'E0000000000' && r.TC > 0); const costUnchanged = (costRow0.TC === 1682.7); return { usageCount: usage.length, changesCount: changes.length, newItemUsage, newItemChange, excUsage, blockedSingle, singleErrMsg, blockedAll, allErrMsg, t0Unchanged, costUnchanged }; })()""", ) assert result["usageCount"] == 9 assert result["changesCount"] == 9 # 신규 항목 TC_ null 안전 처리 검증 assert result["newItemUsage"]["originalPrice"] is None assert result["newItemUsage"]["difference"] is None assert result["newItemUsage"]["status"] == "신규(당초없음)" assert result["newItemUsage"]["canApply"] is False assert result["newItemChange"]["status"] == "신규" # 기존 항목 당초/현재 비교 검증 assert result["excUsage"]["appliedPrice"] == 103047 assert result["excUsage"]["originalPrice"] == 62207 assert result["excUsage"]["difference"] == 40840 assert result["excUsage"]["status"] == "당초차이" assert result["excUsage"]["canApply"] is False # 파괴적인 apply 차단 및 데이터 보존 검증 assert result["blockedSingle"] == 1 assert "덮어쓰기 적용은 지원하지 않습니다" in result["singleErrMsg"] assert result["blockedAll"] == 1 assert "덮어쓰기 적용은 지원하지 않습니다" in result["allErrMsg"] assert result["t0Unchanged"] is True assert result["costUnchanged"] is True