From a007d6358a35407264151af9ae51026d71bee355 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 5 Nov 2026 13:22:24 +0900 Subject: [PATCH] fix(est): allocate annual component amounts --- resources/tester/test_est_rea_parity.py | 44 +++++++++++++++++++++++++ tools/est-test/README.md | 2 ++ tools/est-test/native-actions.js | 3 +- tools/est-test/native-business.js | 24 +++++++++++--- tools/est-test/native-extra.js | 4 +-- 5 files changed, 68 insertions(+), 9 deletions(-) diff --git a/resources/tester/test_est_rea_parity.py b/resources/tester/test_est_rea_parity.py index d144b2a47..dcc630b39 100644 --- a/resources/tester/test_est_rea_parity.py +++ b/resources/tester/test_est_rea_parity.py @@ -648,3 +648,47 @@ def test_text_total_without_result_marker_evaluates_its_expression(): assert result["rows"][0]["TC"] == 20 assert result["rows"][1]["TC"] == 20 assert result["rows"][2]["TC"] == 0 + + +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 diff --git a/tools/est-test/README.md b/tools/est-test/README.md index 2f2ba5101..22913d6a3 100644 --- a/tools/est-test/README.md +++ b/tools/est-test/README.md @@ -24,6 +24,8 @@ 단가 변경은 산근 소계(61), 비목 참조 비율(58), 선택 구간 합계(62)를 거쳐 내역으로 전달됩니다. 산출근거 편집기의 `구간지정`·`구간합계` 표지로 A~Z 묶음을 제어합니다. 소계 표지 1,433개를 추가한 대용량 원본 대조는 115,145개 일치·해석 오류 0이며, 원본 산근 낙찰 행(52)의 성분 계산과 연결 편집도 회귀 시험합니다. +연차공사의 `[기준]*0.5` 수식은 수량과 각 성분 금액에 각각 적용됩니다. 금액은 정수 절사하고, 다음 차수의 `[기준]-[1차]`로 절사 후 잔액을 배분합니다. 차수 사본은 단가를 비운 고정 금액으로 저장하여 다시 열어도 금액을 유지합니다. `[계약수량]`은 이전 작업 파일과의 호환 표기로 `[기준]`과 같은 역할을 합니다. + **전체 원본 동등성은 아직 확보하지 못했습니다.** 기성·물가변동·연차 등은 분석 분기와 합성 시나리오를 시험했으며 원본 업무 샘플 대조는 없습니다. 원본 화면 실행 비교는 못했습니다. 원본 페이지·도면 배치, CAD/VEC0650 편집, 원본 인쇄 서식, Excel/XML/JDL 변환, ESTX 수정 저장은 미구현입니다. 원본 대체품의 완료 판정으로 보지 않습니다. 검사: diff --git a/tools/est-test/native-actions.js b/tools/est-test/native-actions.js index 1c81a1810..3fa229736 100644 --- a/tools/est-test/native-actions.js +++ b/tools/est-test/native-actions.js @@ -393,8 +393,7 @@ const EstActions = (() => { }, false); } else if (name === "add-annual") EstApp.change( - () => - project.annual.push({ name: `${project.annual.length + 1}차`, formula: "[계약수량]" }), + () => project.annual.push({ name: `${project.annual.length + 1}차`, formula: "[기준]" }), false, ); else if (name === "open-annual") { diff --git a/tools/est-test/native-business.js b/tools/est-test/native-business.js index 2096a7365..3f52ee74d 100644 --- a/tools/est-test/native-business.js +++ b/tools/est-test/native-business.js @@ -336,11 +336,16 @@ const EstBusiness = (() => { function annual(project) { const errors = [], results = []; + const names = (project.annual || []).map((year) => String(year.name || "")); + if (names.some((name) => !name.trim()) || new Set(names).size !== names.length) + return { results, errors: ["차수 이름은 비어 있거나 중복될 수 없습니다."] }; for (const year of project.annual || []) { const next = copy(project); + next.name = `${project.name} · ${year.name}`; next.progress = []; next.annual = []; next.options.TotalFix = false; + next.options.SourceRefresh = false; for (let index = 0; index < next.tables.EstMainDat.length; index++) { const row = next.tables.EstMainDat[index]; if (num(row.LinkIndex) === 57) { @@ -351,11 +356,20 @@ const EstBusiness = (() => { } if (num(row.Level) > 0 || [54, 55, 58, 59, 62, 67].includes(num(row.LinkIndex))) continue; try { - row.GN = EstExpression.evaluate(year.formula || "[계약수량]", (name) => { - if (name === "계약수량") return num(project.tables.EstMainDat[index].GN); - const previous = results.find((r) => r.name === name); - return previous ? num(previous.project.tables.EstMainDat[index]?.GN) : undefined; - }); + // f0f9b8/f1098c resolve the same column formula against each current field. + const value = (field) => + EstExpression.evaluate(year.formula || "[기준]", (name) => { + if (["기준", "계약", "계약수량"].includes(name)) + return num(project.tables.EstMainDat[index][field]); + const previous = results.find((result) => result.name === name); + return previous ? num(previous.project.tables.EstMainDat[index]?.[field]) : undefined; + }); + row.GN = value("GN"); + components.forEach((key) => (row[key] = Math.trunc(value(key)))); + row.TC = sum(row); + units.forEach((key) => (row[key] = 0)); + row.TU = 0; + row.CalcIndex = num(row.CalcIndex) >= 2 ? 3 : 1; } catch (error) { errors.push(`${year.name}: ${error.message}`); } diff --git a/tools/est-test/native-extra.js b/tools/est-test/native-extra.js index d0db53d92..bb2fb6c79 100644 --- a/tools/est-test/native-extra.js +++ b/tools/est-test/native-extra.js @@ -222,7 +222,7 @@ const EstExtra = (() => { "연차공사", button("add-annual", "차수 추가"), table( - ["차수", "수량 배분 수식", "순공사비", "도급액", "작업", ""], + ["차수", "수량·금액 배분 수식", "순공사비", "도급액", "작업", ""], project.annual.map((year, index) => [ field("annual", index, "name", year.name, "text"), field("annual", index, "formula", year.formula, "text"), @@ -232,7 +232,7 @@ const EstExtra = (() => { button("delete-extra", "삭제", `data-section="annual" data-row="${index}"`), ]), ), - `수식 예: [계약수량]*0.5, [계약수량]-[1차]. ${esc(output.errors.join(" / "))}`, + `수식 예: [기준]*0.5, [기준]-[1차]. 같은 수식을 수량과 각 성분 금액에 따로 적용하고 금액은 원 미만 절사합니다. ${esc(output.errors.join(" / "))}`, ); } else if (view === "resources") { const output = EstResources.calculate(project);