fix(est): apply selected quantity calculations
This commit is contained in:
@@ -102,3 +102,28 @@ def test_insert_and_move_quantity_rows_keeps_item_results_linked():
|
|||||||
assert result["linked"]["errors"] == []
|
assert result["linked"]["errors"] == []
|
||||||
assert result["linked"]["items"][1]["GN"] == 6
|
assert result["linked"]["items"][1]["GN"] == 6
|
||||||
assert result["forward"]["errors"]
|
assert result["forward"]["errors"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_manual_and_selected_sheet_totals_validate_before_application():
|
||||||
|
result = run(
|
||||||
|
"math",
|
||||||
|
"""(()=>{
|
||||||
|
const p=EstNative.empty();
|
||||||
|
p.tables.EstMainDat=[{Code:'X',Level:0,GN:1,MulCode:{binary:'YWJj'}}];
|
||||||
|
p.quantities=[{Code:'A',formula:'2',TargetCode:'X'},
|
||||||
|
{Code:'B',formula:'[A]*3',TargetCode:'X'}];
|
||||||
|
const manual=EstQuantity.totals(p,'manual');EstQuantity.applyTotals(p,manual);
|
||||||
|
const a=structuredClone(p.tables.EstMainDat[0]);
|
||||||
|
p.quantitySheets=[{Code:'Q',options:{},rows:[{LineData:'4+5 = \\x120\\x12',markers:['r1']}],
|
||||||
|
items:[{Code:'r1',GN:0,TargetCode:'X'}]}];
|
||||||
|
const sheet=EstQuantity.totals(p,'Q');EstQuantity.applyTotals(p,sheet);
|
||||||
|
let invalid='';p.quantities.push({Code:'C',formula:'1',TargetCode:'missing'});
|
||||||
|
try{EstQuantity.totals(p,'manual')}catch(e){invalid=e.message}
|
||||||
|
return {a,b:p.tables.EstMainDat[0],invalid};
|
||||||
|
})()""",
|
||||||
|
)
|
||||||
|
assert result["a"]["GN"] == 8
|
||||||
|
assert result["a"]["MulCode"] == {"binary": "YWJj"}
|
||||||
|
assert result["b"]["GN"] == 9
|
||||||
|
assert result["b"]["MulCode"] == {"binary": "YWJj"}
|
||||||
|
assert result["invalid"]
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ Ctrl+S는 입력 중인 셀도 확정한 뒤 저장합니다. 연속 저장 요
|
|||||||
|
|
||||||
수량 화면에서 새 산출서를 만들고 이름과 산출 항목의 품명·규격을 편집할 수 있습니다. `산출 행 추가`로 결과 표지가 연결된 행과 항목을 함께 만든 뒤 수식을 입력하고, 연결 내역코드를 지정해 계산 수량을 내역에 반영합니다. 행 아래 삽입, 위·아래 이동과 산출 행·산출서 삭제도 가능하며, 이 구조 변경은 JSON 저장과 다시 열기를 지원합니다. 기존 연결은 내역코드를 비워 해제할 수 있습니다.
|
수량 화면에서 새 산출서를 만들고 이름과 산출 항목의 품명·규격을 편집할 수 있습니다. `산출 행 추가`로 결과 표지가 연결된 행과 항목을 함께 만든 뒤 수식을 입력하고, 연결 내역코드를 지정해 계산 수량을 내역에 반영합니다. 행 아래 삽입, 위·아래 이동과 산출 행·산출서 삭제도 가능하며, 이 구조 변경은 JSON 저장과 다시 열기를 지원합니다. 기존 연결은 내역코드를 비워 해제할 수 있습니다.
|
||||||
|
|
||||||
|
재계산 메뉴의 수량 반영도 현재 선택한 산출서 또는 직접 작성 수식표를 사용합니다. 반영 전에 모든 연결 대상과 수식을 검사하며, 기존 원본 연결 데이터는 그대로 보관합니다.
|
||||||
|
|
||||||
## 검증과 남은 범위
|
## 검증과 남은 범위
|
||||||
|
|
||||||
기계 성분·합계 48건, 일위 41건, 내역 37건, 산출 결과 118건과 호표 8건, 원가 수식 23건을 대조합니다. 원가 도급액 고정 반복 차이 `324 → -32 → 3 → 0`을 재현합니다. 단가 갱신 후 전체 재계산과 반복 계산의 안정성도 시험합니다.
|
기계 성분·합계 48건, 일위 41건, 내역 37건, 산출 결과 118건과 호표 8건, 원가 수식 23건을 대조합니다. 원가 도급액 고정 반복 차이 `324 → -32 → 3 → 0`을 재현합니다. 단가 갱신 후 전체 재계산과 반복 계산의 안정성도 시험합니다.
|
||||||
|
|||||||
@@ -410,28 +410,9 @@ const EstActions = (() => {
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
else if (name === "apply-quantity") {
|
else if (name === "apply-quantity") {
|
||||||
const values = Object.create(null),
|
const totals = EstQuantity.totals(project, state.quantitySheet);
|
||||||
totals = new Map();
|
|
||||||
for (const row of project.quantities) {
|
|
||||||
const value = EstMath.floating(
|
|
||||||
EstExpression.evaluate(row.formula, (key) => values[key]),
|
|
||||||
row.digits ?? 2,
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
values[row.Code] = value;
|
|
||||||
if (row.TargetCode) totals.set(row.TargetCode, (totals.get(row.TargetCode) || 0) + value);
|
|
||||||
}
|
|
||||||
if (!totals.size) throw Error("연결 내역코드를 입력하세요.");
|
|
||||||
if (confirm("연결된 내역 수량을 산출 결과로 바꾸시겠습니까?"))
|
if (confirm("연결된 내역 수량을 산출 결과로 바꾸시겠습니까?"))
|
||||||
EstApp.change(() => {
|
EstApp.change(() => EstQuantity.applyTotals(project, totals));
|
||||||
for (const row of project.tables.EstMainDat)
|
|
||||||
if (totals.has(row.Code)) {
|
|
||||||
row.GN = totals.get(row.Code);
|
|
||||||
row.MullangIndex = 1;
|
|
||||||
row.ExcelSheetIndex = 0;
|
|
||||||
row.MulCode = row.Code;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (name === "add-round") {
|
} else if (name === "add-round") {
|
||||||
const next = {
|
const next = {
|
||||||
id: uid(),
|
id: uid(),
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ const EstApp = (() => {
|
|||||||
state.selection.clear();
|
state.selection.clear();
|
||||||
state.group = "";
|
state.group = "";
|
||||||
state.page = 0;
|
state.page = 0;
|
||||||
|
state.quantitySheet = state.project.quantitySheets?.[0]?.Code || "manual";
|
||||||
state.roundId =
|
state.roundId =
|
||||||
state.project.progress.find((round) => round.id === state.roundId)?.id ||
|
state.project.progress.find((round) => round.id === state.roundId)?.id ||
|
||||||
state.project.progress[0]?.id;
|
state.project.progress[0]?.id;
|
||||||
|
|||||||
@@ -113,6 +113,44 @@ const EstQuantity = (() => {
|
|||||||
errors: result.errors,
|
errors: result.errors,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
function totals(project, selected) {
|
||||||
|
let items;
|
||||||
|
if (selected && selected !== "manual") {
|
||||||
|
const sheet = project.quantitySheets?.find((s) => s.Code === selected);
|
||||||
|
if (!sheet) throw Error("선택한 수량산출서가 없습니다.");
|
||||||
|
const output = calculate(sheet);
|
||||||
|
if (output.errors.length) throw Error(output.errors.join(" / "));
|
||||||
|
items = output.items;
|
||||||
|
} else {
|
||||||
|
const values = Object.create(null);
|
||||||
|
items = (project.quantities || []).map((row) => {
|
||||||
|
if (!row.Code || Object.hasOwn(values, row.Code))
|
||||||
|
throw Error(`산출코드가 비어 있거나 중복됩니다: ${row.Code || ""}`);
|
||||||
|
const GN = EstMath.floating(
|
||||||
|
EstExpression.evaluate(row.formula || "0", (key) => values[key]),
|
||||||
|
row.digits ?? 2,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
values[row.Code] = GN;
|
||||||
|
return { ...row, GN };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const quantities = new Map();
|
||||||
|
for (const item of items) {
|
||||||
|
const code = item.TargetCode ?? item.LinkCode;
|
||||||
|
if (!code) continue;
|
||||||
|
const targets = project.tables.EstMainDat.filter((r) => r.Code === code && !Number(r.Level));
|
||||||
|
if (!targets.length) throw Error(`연결 내역코드 없음: ${code}`);
|
||||||
|
if (targets.length > 1) throw Error(`연결 내역코드 중복: ${code}`);
|
||||||
|
quantities.set(code, (quantities.get(code) || 0) + Number(item.GN || 0));
|
||||||
|
}
|
||||||
|
if (!quantities.size) throw Error("연결할 내역코드를 먼저 입력하세요.");
|
||||||
|
return quantities;
|
||||||
|
}
|
||||||
|
function applyTotals(project, quantities) {
|
||||||
|
for (const row of project.tables.EstMainDat)
|
||||||
|
if (quantities.has(row.Code) && !Number(row.Level)) row.GN = quantities.get(row.Code);
|
||||||
|
}
|
||||||
function selector(state) {
|
function selector(state) {
|
||||||
const { esc } = EstApp;
|
const { esc } = EstApp;
|
||||||
return `<div class="business-tools"><label>수량산출서 <select id="quantity-sheet-choice"><option value="manual">수식표 직접 작성</option>${(state.project.quantitySheets || []).map((sheet) => `<option value="${esc(sheet.Code)}" ${state.quantitySheet === sheet.Code ? "selected" : ""}>${esc(sheet.Name || sheet.Code)}</option>`).join("")}</select></label><button data-quantity-new>새 산출서</button></div>`;
|
return `<div class="business-tools"><label>수량산출서 <select id="quantity-sheet-choice"><option value="manual">수식표 직접 작성</option>${(state.project.quantitySheets || []).map((sheet) => `<option value="${esc(sheet.Code)}" ${state.quantitySheet === sheet.Code ? "selected" : ""}>${esc(sheet.Name || sheet.Code)}</option>`).join("")}</select></label><button data-quantity-new>새 산출서</button></div>`;
|
||||||
@@ -224,30 +262,14 @@ const EstQuantity = (() => {
|
|||||||
}
|
}
|
||||||
if (!event.target.closest("[data-quantity-apply]")) return;
|
if (!event.target.closest("[data-quantity-apply]")) return;
|
||||||
try {
|
try {
|
||||||
const output = calculate(sheet);
|
const quantities = totals(state.project, state.quantitySheet);
|
||||||
if (output.errors.length) throw Error(output.errors.join(" / "));
|
EstApp.change(() => applyTotals(state.project, quantities));
|
||||||
const quantities = new Map();
|
|
||||||
for (const item of output.items)
|
|
||||||
if (item.TargetCode ?? item.LinkCode) {
|
|
||||||
const code = item.TargetCode ?? item.LinkCode;
|
|
||||||
const targets = state.project.tables.EstMainDat.filter(
|
|
||||||
(r) => r.Code === code && !Number(r.Level),
|
|
||||||
);
|
|
||||||
if (!targets.length) throw Error(`연결 내역코드 없음: ${code}`);
|
|
||||||
if (targets.length > 1) throw Error(`연결 내역코드 중복: ${code}`);
|
|
||||||
quantities.set(code, (quantities.get(code) || 0) + Number(item.GN || 0));
|
|
||||||
}
|
|
||||||
if (!quantities.size) throw Error("연결할 내역코드를 먼저 입력하세요.");
|
|
||||||
EstApp.change(() => {
|
|
||||||
for (const row of state.project.tables.EstMainDat)
|
|
||||||
if (quantities.has(row.Code) && !Number(row.Level)) row.GN = quantities.get(row.Code);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
EstApp.state.notice = error.message;
|
EstApp.state.notice = error.message;
|
||||||
EstApp.render();
|
EstApp.render();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return { entities, fromWpt, calculate, selector, render, install };
|
return { entities, fromWpt, calculate, totals, applyTotals, selector, render, install };
|
||||||
})();
|
})();
|
||||||
if (typeof module !== "undefined" && module.exports) module.exports = EstQuantity;
|
if (typeof module !== "undefined" && module.exports) module.exports = EstQuantity;
|
||||||
|
|||||||
Reference in New Issue
Block a user