feat(est): exchange Excel quantity links
This commit is contained in:
@@ -213,6 +213,29 @@ def test_excel_exports_manual_quantities_and_schedule():
|
||||
)
|
||||
|
||||
|
||||
def test_excel_quantity_links_export_and_import_reject_invalid_targets():
|
||||
import openpyxl
|
||||
|
||||
link = {"target": 5, "file": "source.xlsx", "sheet": "Data", "cell": "B2"}
|
||||
data = export_workbook({"name": "project", "tables": {}, "excelQuantityLinks": [link]})
|
||||
workbook = openpyxl.load_workbook(io.BytesIO(data), data_only=False)
|
||||
assert list(workbook["ExcelQuantityLinks"].values)[1] == (5, "source.xlsx", "Data", "B2")
|
||||
workbook.close()
|
||||
result = run(
|
||||
"math",
|
||||
"""(()=>{
|
||||
const p=EstNative.empty();p.tables.EstMainDat=[{PkID:5,Code:'X',GN:1}];
|
||||
const link={target:5,file:'source.xlsx',sheet:'Data',cell:'B2'};
|
||||
const q=EstExchange.apply(p,{ExcelQuantityLinks:[link]});let rejected=0;
|
||||
for(const rows of [[{...link,target:9}],[link,link],[{...link,cell:'XFE1'}]]){
|
||||
try{EstExchange.apply(p,{ExcelQuantityLinks:rows,EstMainDat:[{PkID:5,Code:'X',GN:7}]})}catch{rejected++}
|
||||
}
|
||||
return {links:q.excelQuantityLinks,rejected,original:p.tables.EstMainDat[0].GN};
|
||||
})()""",
|
||||
)
|
||||
assert result == {"links": [link], "rejected": 3, "original": 1}
|
||||
|
||||
|
||||
def test_excel_imports_manual_quantities_and_schedule_and_rejects_invalid():
|
||||
result = run(
|
||||
"math",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
## Codex · EST 독립 테스트 앱 (2026-11-04)
|
||||
- [x] Excel 수량 연결을 전체 자료 Excel에 포함 → 재가져오기와 잘못된 내역 연결의 전체 적용 차단 확인.
|
||||
- 자체 검증: Tailscale HTTP Edge에서 실제 Excel 내보내기·재가져오기와 잘못된 내역 연결 전체 차단 확인. 수량·Excel 시험 25개 통과, openpyxl 경고 8건.
|
||||
- [x] Excel 셀과 내역 행 연결을 JSON에 보관하고 다시 선택한 파일로 일괄 갱신 → 중복·누락·오류 시 전체 변경 차단.
|
||||
- 자체 검증: Tailscale HTTP Edge에서 2행 연결·JSON 재열기·일괄 갱신·실행 취소·연결 해제 확인. 한 셀 오류 시 두 행 모두 유지, ESTX 저장에서 연결 누락 차단. 수량·첨부·독립 서버 시험 25개 통과.
|
||||
- [x] Excel 숫자 셀을 선택한 내역 행의 수량으로 반영 → 파일·시트·셀 오류 차단, 계산·저장·취소 확인.
|
||||
|
||||
@@ -55,6 +55,7 @@ Ctrl+S는 입력 중인 셀도 확정한 뒤 저장합니다. 연속 저장 요
|
||||
수량산출서와 수식표에서 내역 행을 직접 선택해 연결할 수 있습니다. 내역 행 선택을 사용하면 코드가 없거나 중복되어도 해당 행에만 수량을 반영합니다. 코드를 직접 입력할 때는 해당 상세 내역이 정확히 하나여야 하며, 대상이 없거나 중복되면 다른 항목을 포함해 반영을 중단하고 오류를 표시합니다.
|
||||
|
||||
수량 화면의 `Excel 셀 수량 연결`은 `.xlsx` 파일과 시트·셀·내역 행을 연결하고 저장된 숫자 값을 반영합니다. 연결 정보는 작업 JSON에 저장되며, `Excel 연결 관리`에서 같은 이름의 파일을 다시 선택해 해당 파일의 연결을 일괄 갱신하거나 연결을 해제할 수 있습니다. 어느 연결에 오류가 있어도 갱신 전체를 중단합니다. 수식 셀은 파일에 저장된 계산값을 사용하며, 저장된 값이 없거나 숫자가 아니면 반영하지 않습니다. Excel 파일은 자동 감시하지 않습니다. 웹 Excel 연결은 원본 ESTX에 저장할 수 없어 JSON으로 보관합니다.
|
||||
전체 자료 Excel의 `ExcelQuantityLinks` 시트에도 연결 정보가 포함됩니다. 연결된 원본 Excel 파일은 별도로 보관해야 합니다.
|
||||
|
||||
수량 화면에서 새 산출서를 만들고 이름과 산출 항목의 품명·규격을 편집할 수 있습니다. `산출 행 추가`로 결과 표지가 연결된 행과 항목을 함께 만든 뒤 수식을 입력하고, 연결 내역코드를 지정해 계산 수량을 내역에 반영합니다. 행 아래 삽입, 위·아래 이동과 산출 행·산출서 삭제도 가능하며, 이 구조 변경은 JSON 저장과 다시 열기를 지원합니다. 기존 연결은 내역코드를 비워 해제할 수 있습니다.
|
||||
|
||||
|
||||
@@ -95,6 +95,7 @@ function workbook(project) {
|
||||
project.schedule,
|
||||
["id", "SourceKey", "Code", "Name", "start", "end", "weight", "progress"],
|
||||
],
|
||||
["ExcelQuantityLinks", project.excelQuantityLinks, ["target", "file", "sheet", "cell"]],
|
||||
])
|
||||
if (Array.isArray(rows) && rows.length)
|
||||
sheets.push({
|
||||
|
||||
@@ -38,6 +38,7 @@ const EstExchange = (() => {
|
||||
![
|
||||
"Quantities",
|
||||
"Schedule",
|
||||
"ExcelQuantityLinks",
|
||||
"QDocuments",
|
||||
"QLines",
|
||||
"QItems",
|
||||
@@ -289,6 +290,7 @@ const EstExchange = (() => {
|
||||
"ProgressInputs",
|
||||
"Escalation",
|
||||
"EscalationSettings",
|
||||
"ExcelQuantityLinks",
|
||||
].includes(table)
|
||||
)
|
||||
continue;
|
||||
@@ -450,6 +452,20 @@ const EstExchange = (() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Object.hasOwn(tables, "ExcelQuantityLinks")) {
|
||||
next.excelQuantityLinks = tables.ExcelQuantityLinks.map((row) => ({
|
||||
target: row.target,
|
||||
file: row.file,
|
||||
sheet: row.sheet,
|
||||
cell: row.cell,
|
||||
}));
|
||||
EstModel.validate(next);
|
||||
for (const link of next.excelQuantityLinks) {
|
||||
const rows = next.tables.EstMainDat.filter((row) => row.PkID === link.target);
|
||||
if (rows.length !== 1 || !EstQuantity.excelTarget(rows[0]))
|
||||
throw Error(`Excel 연결 내역이 없거나 중복되었거나 상세 행이 아닙니다: ${link.target}`);
|
||||
}
|
||||
}
|
||||
if (Object.hasOwn(tables, "EscalationSettings")) {
|
||||
const rows = tables.EscalationSettings;
|
||||
if (
|
||||
|
||||
@@ -535,6 +535,7 @@ const EstQuantity = (() => {
|
||||
allTotals,
|
||||
applyTotals,
|
||||
excelCell,
|
||||
excelTarget,
|
||||
validExcelAddress,
|
||||
excelUpdates,
|
||||
targetSelect,
|
||||
|
||||
Reference in New Issue
Block a user