diff --git a/resources/tester/test_est_escalation_workflow.py b/resources/tester/test_est_escalation_workflow.py index 36ed614c1..fc0dfaca0 100644 --- a/resources/tester/test_est_escalation_workflow.py +++ b/resources/tester/test_est_escalation_workflow.py @@ -1,6 +1,23 @@ from test_est_rea_parity import run +def test_progress_exclusions_preserve_new_rows_and_reject_ambiguous_keys(): + result = run( + "math", + """(()=>{ + const p=EstNative.empty();p.tables.EstMainDat=[{PkID:1,Code:'A',EscProgress:9}, + {PkID:2,Code:'B',EscProgress:7}]; + const before=JSON.stringify(p), saved={rows:[{PkID:1,Code:'A',TotalGN:3}]}; + const updates=EstBusiness.progressExclusions(p,saved);let blocked=0; + try{EstBusiness.progressExclusions(p,{rows:[{Code:'A',TotalGN:1},{Code:'A',TotalGN:2}]})}catch{blocked++} + const q=structuredClone(p);q.tables.EstMainDat=[{Code:'A'},{Code:'A'}]; + try{EstBusiness.progressExclusions(q,{rows:[{Code:'A',TotalGN:1}]})}catch{blocked++} + return {updates,blocked,same:before===JSON.stringify(p)}; + })()""", + ) + assert result == {"updates": [{"index": 0, "value": 3}], "blocked": 2, "same": True} + + def test_escalation_preserves_explicit_zero_total_and_falls_back_only_without_wonga(): result = run( "math", diff --git a/tools/est-test/native-business-actions.js b/tools/est-test/native-business-actions.js index 677087634..69eb997f7 100644 --- a/tools/est-test/native-business-actions.js +++ b/tools/est-test/native-business-actions.js @@ -142,17 +142,14 @@ const EstBusinessActions = (() => { choices: locked.map((round) => [round.id, round.name]), }, ], - (values) => + (values) => { + const result = locked.find((round) => round.id === values.id).result; + const updates = EstBusiness.progressExclusions(project, result); EstApp.change(() => { - const result = locked.find((round) => round.id === values.id).result; - const rows = new Map( - result.rows.map((row, index) => [EstBusiness.rowKey(row, index), row]), - ); - project.tables.EstMainDat.forEach( - (row, index) => - (row.EscProgress = Number(rows.get(EstBusiness.rowKey(row, index))?.TotalGN) || 0), - ); - }, false), + for (const { index, value } of updates) + project.tables.EstMainDat[index].EscProgress = value; + }, false); + }, ); } else if (name === "load-contract" || name === "load-bid") { purpose = name; diff --git a/tools/est-test/native-business.js b/tools/est-test/native-business.js index f1956775a..b6c999b48 100644 --- a/tools/est-test/native-business.js +++ b/tools/est-test/native-business.js @@ -4,6 +4,27 @@ const EstBusiness = (() => { const copy = (value) => structuredClone(value); const rowKey = (row, index) => String(row.PkID ?? (row.Code || `row:${index}`)); const special = (row) => [57, 58, 62, 67].includes(num(row.LinkIndex)); + function progressExclusions(project, result) { + const saved = new Map(); + result.rows.forEach((row, index) => { + const key = rowKey(row, index); + if (saved.has(key)) throw Error(`기성 내역 식별자가 중복됩니다: ${key}`); + const value = Number(row.TotalGN); + if (!Number.isFinite(value)) throw Error(`기성 누계수량 오류: ${key}`); + saved.set(key, value); + }); + const counts = new Map(); + project.tables.EstMainDat.forEach((row, index) => { + const key = rowKey(row, index); + counts.set(key, (counts.get(key) || 0) + 1); + }); + return project.tables.EstMainDat.flatMap((row, index) => { + const key = rowKey(row, index); + if (!saved.has(key)) return []; + if (counts.get(key) !== 1) throw Error(`현재 내역 식별자가 중복됩니다: ${key}`); + return [{ index, value: saved.get(key) }]; + }); + } function moveEstimateRows(rows, selection, direction) { const indices = [...selection].sort((a, b) => a - b); if (!indices.length) throw Error("이동할 내역 행을 선택하세요."); @@ -384,6 +405,7 @@ const EstBusiness = (() => { annual, newAnnualName, rowKey, + progressExclusions, moveEstimateRows, importContract, importBid,