feat(est): link quantity to individual rows
This commit is contained in:
@@ -136,3 +136,27 @@ def test_new_quantity_code_does_not_reuse_existing_or_referenced_codes():
|
||||
EstQuantity.newCode([{Code:'Q2',formula:'1'}]), EstQuantity.newCode([])]""",
|
||||
)
|
||||
assert result == ["Q3", "Q1", "Q1"]
|
||||
|
||||
|
||||
def test_target_row_ids_support_blank_and_duplicate_codes_for_both_modes():
|
||||
result = run(
|
||||
"math",
|
||||
"""(()=>{
|
||||
const p=EstNative.empty();p.tables.EstMainDat=[
|
||||
{PkID:1,Code:'X',GN:1},{PkID:2,Code:'X',GN:2},{PkID:3,Code:'',GN:3}];
|
||||
p.quantities=[{Code:'A',formula:'7',TargetCode:'X',TargetKey:'id:2'},
|
||||
{Code:'B',formula:'4',TargetCode:'',TargetKey:'id:3'}];
|
||||
EstQuantity.applyTotals(p,EstQuantity.totals(p,'manual'));
|
||||
const manual=p.tables.EstMainDat.map(row=>row.GN);
|
||||
p.quantitySheets=[{Code:'Q',options:{},rows:[{LineData:'2+3 = \\x120\\x12',markers:['r1']}],
|
||||
items:[{Code:'r1',GN:0,TargetCode:'X',TargetKey:'id:1'}]}];
|
||||
p.tables.EstMainDat.reverse();EstQuantity.applyTotals(p,EstQuantity.totals(p,'Q'));
|
||||
const sheet=p.tables.EstMainDat.map(row=>[row.PkID,row.GN]);
|
||||
p.quantitySheets[0].items[0].TargetKey='id:missing';let error='';
|
||||
try{EstQuantity.totals(p,'Q')}catch(e){error=e.message}
|
||||
return {manual,sheet,error};
|
||||
})()""",
|
||||
)
|
||||
assert result["manual"] == [1, 7, 4]
|
||||
assert result["sheet"] == [[3, 4], [2, 7], [1, 5]]
|
||||
assert result["error"]
|
||||
|
||||
@@ -25,7 +25,7 @@ Ctrl+S는 입력 중인 셀도 확정한 뒤 저장합니다. 연속 저장 요
|
||||
|
||||
초기 자료는 기술정보 DB의 실제 REA 저장 샘플입니다. `REA 샘플 대조`에서 원본 저장값과 275개 계산 결과를 비교합니다. 기존 ESTX의 기본 수량 연결은 문서·항목 코드와 내역 코드가 유일한 경우 화면에 표시하며, 중복·다른 모드는 원문에 보존하고 경고합니다. 원본 연결 데이터 실자료 검증과 새 연결의 ESTX 쓰기는 아직 지원하지 않습니다. 테스트용 임의 요율을 원본 기본값으로 넣지 않습니다. 새 원가 서식의 요율은 사용자가 입력합니다.
|
||||
|
||||
수량산출서에서 내역에 수량을 반영할 때 연결 코드에 해당하는 상세 내역이 정확히 하나여야 합니다. 코드가 없거나 중복되면 다른 항목을 포함해 반영을 중단하고 오류를 표시합니다.
|
||||
수량산출서와 수식표에서 내역 행을 직접 선택해 연결할 수 있습니다. 내역 행 선택을 사용하면 코드가 없거나 중복되어도 해당 행에만 수량을 반영합니다. 코드를 직접 입력할 때는 해당 상세 내역이 정확히 하나여야 하며, 대상이 없거나 중복되면 다른 항목을 포함해 반영을 중단하고 오류를 표시합니다.
|
||||
|
||||
수량 화면에서 새 산출서를 만들고 이름과 산출 항목의 품명·규격을 편집할 수 있습니다. `산출 행 추가`로 결과 표지가 연결된 행과 항목을 함께 만든 뒤 수식을 입력하고, 연결 내역코드를 지정해 계산 수량을 내역에 반영합니다. 행 아래 삽입, 위·아래 이동과 산출 행·산출서 삭제도 가능하며, 이 구조 변경은 JSON 저장과 다시 열기를 지원합니다. 기존 연결은 내역코드를 비워 해제할 수 있습니다.
|
||||
|
||||
|
||||
@@ -545,11 +545,12 @@ const EstActions = (() => {
|
||||
if (node.matches("[data-extra]")) {
|
||||
const value = node.type === "number" ? Number(node.value) : node.value;
|
||||
if (node.type === "number" && !Number.isFinite(value)) throw Error("수치 입력 오류");
|
||||
EstApp.change(
|
||||
() =>
|
||||
(project[node.dataset.extra][Number(node.dataset.row)][node.dataset.field] = value),
|
||||
false,
|
||||
);
|
||||
EstApp.change(() => {
|
||||
const row = project[node.dataset.extra][Number(node.dataset.row)];
|
||||
row[node.dataset.field] = value;
|
||||
if (node.dataset.extra === "quantities" && node.dataset.field === "TargetCode")
|
||||
row.TargetKey = "";
|
||||
}, false);
|
||||
return;
|
||||
}
|
||||
if (node.matches("[data-table][data-field]")) {
|
||||
|
||||
@@ -28,7 +28,8 @@ const EstExtra = (() => {
|
||||
return [
|
||||
field("quantities", index, "Code", row.Code, "text"),
|
||||
field("quantities", index, "Name", row.Name, "text"),
|
||||
field("quantities", index, "TargetCode", row.TargetCode, "text"),
|
||||
field("quantities", index, "TargetCode", row.TargetCode, "text") +
|
||||
EstQuantity.targetSelect(project, row, "manual", index),
|
||||
field("quantities", index, "formula", row.formula, "text"),
|
||||
field("quantities", index, "digits", row.digits ?? 2),
|
||||
esc(row.Error || number(row.GN)),
|
||||
|
||||
@@ -96,11 +96,19 @@ const EstForms = (() => {
|
||||
};
|
||||
}
|
||||
if (id === 14) {
|
||||
const target = (item) => {
|
||||
if (!item.TargetKey) return item.TargetCode ?? item.LinkCode ?? "";
|
||||
const row = project.tables.EstMainDat.find(
|
||||
(entry) => entry.PkID != null && `id:${entry.PkID}` === item.TargetKey,
|
||||
);
|
||||
return row ? `${row.Name || "내역"} (${row.Code || item.TargetKey})` : item.TargetKey;
|
||||
};
|
||||
const manual = EstQuantity.manual(project.quantities || []),
|
||||
rows = manual.rows.map((row) => ({
|
||||
...row,
|
||||
SheetName: "수식표",
|
||||
formula: row.formula || "",
|
||||
TargetCode: target(row),
|
||||
})),
|
||||
errors = [...manual.errors];
|
||||
for (const sheet of project.quantitySheets || []) {
|
||||
@@ -112,7 +120,7 @@ const EstForms = (() => {
|
||||
...item,
|
||||
SheetName: sheet.Name || sheet.Code,
|
||||
formula: index < 0 ? "" : EstText.clean(calculated.rows[index].LineData),
|
||||
TargetCode: item.TargetCode ?? item.LinkCode ?? "",
|
||||
TargetCode: target(item),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,18 +151,34 @@ const EstQuantity = (() => {
|
||||
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 (!code && !item.TargetKey) continue;
|
||||
const targets = project.tables.EstMainDat.map((row, index) => ({ row, index })).filter(
|
||||
({ row }) =>
|
||||
!Number(row.Level) &&
|
||||
(item.TargetKey
|
||||
? row.PkID != null && `id:${row.PkID}` === item.TargetKey
|
||||
: row.Code === code),
|
||||
);
|
||||
if (!targets.length) throw Error(`연결 내역 없음: ${item.TargetKey || code}`);
|
||||
if (targets.length > 1) throw Error(`연결 내역 중복: ${item.TargetKey || code}`);
|
||||
const index = targets[0].index;
|
||||
quantities.set(index, (quantities.get(index) || 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);
|
||||
for (const [index, value] of quantities) project.tables.EstMainDat[index].GN = value;
|
||||
}
|
||||
function targetSelect(project, item, section, index) {
|
||||
const { esc } = EstApp;
|
||||
const rows = project.tables.EstMainDat.filter((row) => row.PkID != null && !Number(row.Level));
|
||||
return `<select data-quantity-select="${section}" data-index="${index}" aria-label="연결 내역 행"><option value="">내역 행 선택</option>${rows
|
||||
.map((row) => {
|
||||
const key = `id:${row.PkID}`;
|
||||
return `<option value="${esc(key)}" ${item.TargetKey === key ? "selected" : ""}>${esc(row.Name || "내역")} · ${esc(row.Code || "코드 없음")} · ${esc(row.PkID)}</option>`;
|
||||
})
|
||||
.join("")}</select>`;
|
||||
}
|
||||
function newCode(items) {
|
||||
const codes = new Set(items.map((row) => row.Code));
|
||||
@@ -186,7 +202,7 @@ const EstQuantity = (() => {
|
||||
{ esc, number, byId } = EstApp;
|
||||
byId("work-content").innerHTML =
|
||||
selector(state) +
|
||||
`<div class="business-tools"><label>산출서명 <input data-quantity-name value="${esc(sheet.Name)}"></label><button data-quantity-add>산출 행 추가</button><button data-quantity-apply>내역 수량 반영</button><button data-quantity-delete-sheet>산출서 삭제</button></div><table class="sheet"><thead><tr><th>산출코드</th><th>품명</th><th>규격</th><th>저장 수량</th><th>재계산 수량</th><th>연결 내역코드</th></tr></thead><tbody>${output.items.map((item, index) => `<tr><td>${esc(item.Code)}</td><td><input data-quantity-item="Name" data-index="${index}" value="${esc(item.Name || "")}" aria-label="품명"></td><td><input data-quantity-item="Size" data-index="${index}" value="${esc(item.Size || "")}" aria-label="규격"></td><td>${number(sheet.items[index].GN)}</td><td>${number(item.GN)}</td><td><input data-quantity-target="${index}" value="${esc(item.TargetCode ?? item.LinkCode ?? "")}" aria-label="연결 내역코드"></td></tr>`).join("")}</tbody></table><p class="business-note">${esc(output.errors.join(" / "))} 산출 행의 결과 표지로 수량 항목을 연결합니다. 신규 산출서와 행 변경은 JSON으로 저장합니다.</p><table class="sheet"><thead><tr><th>줄</th><th>산출근거 편집</th><th>작업</th></tr></thead><tbody>${sheet.rows.map((row, index) => `<tr><td>${index + 1}</td><td><textarea data-quantity-line="${index}" rows="1" style="min-width:750px" aria-label="산출근거 ${index + 1}">${esc(EstText.display(output.rows[index].LineData))}</textarea></td><td><button data-quantity-add data-index="${index}">아래 추가</button><button data-quantity-move="-1" data-index="${index}" ${index === 0 ? "disabled" : ""}>↑</button><button data-quantity-move="1" data-index="${index}" ${index === sheet.rows.length - 1 ? "disabled" : ""}>↓</button><button data-quantity-delete-row="${index}">삭제</button></td></tr>`).join("")}</tbody></table>`;
|
||||
`<div class="business-tools"><label>산출서명 <input data-quantity-name value="${esc(sheet.Name)}"></label><button data-quantity-add>산출 행 추가</button><button data-quantity-apply>내역 수량 반영</button><button data-quantity-delete-sheet>산출서 삭제</button></div><table class="sheet"><thead><tr><th>산출코드</th><th>품명</th><th>규격</th><th>저장 수량</th><th>재계산 수량</th><th>연결 내역코드</th></tr></thead><tbody>${output.items.map((item, index) => `<tr><td>${esc(item.Code)}</td><td><input data-quantity-item="Name" data-index="${index}" value="${esc(item.Name || "")}" aria-label="품명"></td><td><input data-quantity-item="Size" data-index="${index}" value="${esc(item.Size || "")}" aria-label="규격"></td><td>${number(sheet.items[index].GN)}</td><td>${number(item.GN)}</td><td><input data-quantity-target="${index}" value="${esc(item.TargetCode ?? item.LinkCode ?? "")}" aria-label="연결 내역코드">${targetSelect(state.project, item, "sheet", index)}</td></tr>`).join("")}</tbody></table><p class="business-note">${esc(output.errors.join(" / "))} 산출 행의 결과 표지로 수량 항목을 연결합니다. 신규 산출서와 행 변경은 JSON으로 저장합니다.</p><table class="sheet"><thead><tr><th>줄</th><th>산출근거 편집</th><th>작업</th></tr></thead><tbody>${sheet.rows.map((row, index) => `<tr><td>${index + 1}</td><td><textarea data-quantity-line="${index}" rows="1" style="min-width:750px" aria-label="산출근거 ${index + 1}">${esc(EstText.display(output.rows[index].LineData))}</textarea></td><td><button data-quantity-add data-index="${index}">아래 추가</button><button data-quantity-move="-1" data-index="${index}" ${index === 0 ? "disabled" : ""}>↑</button><button data-quantity-move="1" data-index="${index}" ${index === sheet.rows.length - 1 ? "disabled" : ""}>↓</button><button data-quantity-delete-row="${index}">삭제</button></td></tr>`).join("")}</tbody></table>`;
|
||||
}
|
||||
function install() {
|
||||
document.addEventListener("change", (event) => {
|
||||
@@ -197,6 +213,23 @@ const EstQuantity = (() => {
|
||||
EstApp.render();
|
||||
return;
|
||||
}
|
||||
if (node.dataset.quantitySelect !== undefined) {
|
||||
const item =
|
||||
node.dataset.quantitySelect === "manual"
|
||||
? state.project.quantities[Number(node.dataset.index)]
|
||||
: state.project.quantitySheets?.find((s) => s.Code === state.quantitySheet)?.items[
|
||||
Number(node.dataset.index)
|
||||
];
|
||||
if (!item) return;
|
||||
const row = state.project.tables.EstMainDat.find(
|
||||
(r) => r.PkID != null && `id:${r.PkID}` === node.value && !Number(r.Level),
|
||||
);
|
||||
EstApp.change(() => {
|
||||
item.TargetKey = row ? node.value : "";
|
||||
item.TargetCode = row?.Code || "";
|
||||
}, false);
|
||||
return;
|
||||
}
|
||||
const sheet = state.project.quantitySheets?.find((s) => s.Code === state.quantitySheet);
|
||||
if (!sheet) return;
|
||||
if (node.dataset.quantityName !== undefined)
|
||||
@@ -213,10 +246,11 @@ const EstQuantity = (() => {
|
||||
false,
|
||||
);
|
||||
if (node.dataset.quantityTarget !== undefined)
|
||||
EstApp.change(
|
||||
() => (sheet.items[Number(node.dataset.quantityTarget)].TargetCode = node.value),
|
||||
false,
|
||||
);
|
||||
EstApp.change(() => {
|
||||
const item = sheet.items[Number(node.dataset.quantityTarget)];
|
||||
item.TargetCode = node.value;
|
||||
item.TargetKey = "";
|
||||
}, false);
|
||||
});
|
||||
document.addEventListener("click", (event) => {
|
||||
const { state } = EstApp;
|
||||
@@ -298,6 +332,7 @@ const EstQuantity = (() => {
|
||||
manual,
|
||||
totals,
|
||||
applyTotals,
|
||||
targetSelect,
|
||||
newCode,
|
||||
selector,
|
||||
render,
|
||||
|
||||
Reference in New Issue
Block a user