Skip to content

Commit b893ca7

Browse files
committed
[FIX] xlsx: do not export dynamic tables to excel
Excel does not support array formulas inside tables. With this commit, if a table contains an array formula, it will not be exported to excel but be exported as individual cell styles instead. closes #8167 Task: 5214240 X-original-commit: f915568 Signed-off-by: Adrien Minne (adrm) <adrm@odoo.com> Signed-off-by: Rémi Rahir (rar) <rar@odoo.com>
1 parent 0b54431 commit b893ca7

File tree

2 files changed

+69
-10
lines changed

2 files changed

+69
-10
lines changed

packages/o-spreadsheet-engine/src/plugins/ui_core_views/dynamic_tables.ts

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
import { CellPosition, FilterId, TableId, UID, Zone } from "../..";
2-
import { deepEquals } from "../../helpers/misc";
2+
import { toXC } from "../../helpers/coordinates";
3+
import { getItemId } from "../../helpers/data_normalization";
4+
import { deepEquals, isObjectEmptyRecursive } from "../../helpers/misc";
35
import { createFilter } from "../../helpers/table_helpers";
46
import {
57
areZonesContinuous,
68
getZoneArea,
79
isInside,
810
overlap,
11+
positions,
912
positionToZone,
1013
toZone,
1114
union,
12-
zoneToXc,
1315
} from "../../helpers/zones";
1416
import { Command, invalidateEvaluationCommands } from "../../types/commands";
1517
import { CellErrorType } from "../../types/errors";
1618
import { PivotStyle } from "../../types/pivot";
1719
import { CoreTable, DynamicTable, Filter, Table, TableConfig } from "../../types/table";
18-
import { ExcelWorkbookData } from "../../types/workbook_data";
20+
import { ExcelTableData, ExcelWorkbookData } from "../../types/workbook_data";
1921

2022
import { CoreViewPlugin } from "../core_view_plugin";
2123

@@ -293,19 +295,51 @@ export class DynamicTablesPlugin extends CoreViewPlugin {
293295
return tableId + "_" + tableCol;
294296
}
295297

298+
/** Check if a table contains array formula, as we cannot have them in a table in Excel */
299+
private isTableExcelExportable(sheetId: UID, table: CoreTable): boolean {
300+
if (table.type === "dynamic") {
301+
return false;
302+
}
303+
304+
return !positions(table.range.zone).some((position) =>
305+
this.getters.getArrayFormulaSpreadingOn({ sheetId, ...position })
306+
);
307+
}
308+
296309
exportForExcel(data: ExcelWorkbookData) {
297310
for (const sheet of data.sheets) {
311+
const exportedTables: ExcelTableData[] = [];
298312
for (const tableData of sheet.tables) {
299313
const zone = toZone(tableData.range);
300314
const topLeft = { sheetId: sheet.id, col: zone.left, row: zone.top };
301315
const coreTable = this.getters.getCoreTable(topLeft);
302316
const table = this.getTable(topLeft);
303317

304-
if (coreTable?.type !== "dynamic" || !table) {
318+
if (!coreTable || !table || this.isTableExcelExportable(sheet.id, coreTable)) {
319+
exportedTables.push(tableData);
305320
continue;
306321
}
307-
tableData.range = zoneToXc(table.range.zone);
322+
sheet.styles = sheet.styles || {};
323+
sheet.borders = sheet.borders || {};
324+
325+
// If the table is not exportable to excel, we apply the cell styles to individual cells instead
326+
for (const position of positions(table.range.zone)) {
327+
const cellPosition = { sheetId: sheet.id, ...position };
328+
const style = this.getters.getCellComputedStyle(cellPosition);
329+
const border = this.getters.getCellComputedBorder(cellPosition);
330+
const xc = toXC(position.col, position.row);
331+
if (!isObjectEmptyRecursive(style)) {
332+
const styleId = getItemId(style, data.styles);
333+
sheet.styles[xc] = styleId;
334+
}
335+
336+
if (border) {
337+
const borderId = getItemId(border, data.borders);
338+
sheet.borders[xc] = borderId;
339+
}
340+
}
308341
}
342+
sheet.tables = exportedTables;
309343
}
310344
}
311345
}

tests/table/dynamic_table_plugin.test.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Model } from "../../src";
22
import { toZone } from "../../src/helpers";
3-
import { UID } from "../../src/types";
3+
import { BorderDescr, UID } from "../../src/types";
44
import {
55
copy,
66
createDynamicTable,
@@ -235,12 +235,37 @@ describe("Dynamic tables", () => {
235235
expect(getTables(model, sheetId)).toMatchObject([{ zone: "A1:B2" }]);
236236
});
237237

238-
test("Dynamic tables are transformed into static tables when exporting for excel", async () => {
239-
setCellContent(model, "A1", "=MUNIT(3)");
240-
createDynamicTable(model, "A1");
238+
test("Dynamic table is exported as individual cell style to Excel", async () => {
239+
setCellContent(model, "A1", "=MUNIT(2)");
240+
createDynamicTable(model, "A1", { styleId: "TableStyleLight8" });
241+
242+
const exported = await getExportedExcelData(model);
243+
const sheetData = exported.sheets[0];
244+
expect(sheetData.tables).toHaveLength(0);
245+
expect(exported.styles).toEqual({
246+
"1": { fillColor: "#000000", textColor: "#FFFFFF", bold: true },
247+
});
248+
const border: BorderDescr = { color: "#000000", style: "thin" };
249+
expect(exported.borders).toEqual({
250+
"1": { top: border, left: border, bottom: border },
251+
"2": { top: border, bottom: border, right: border },
252+
});
253+
expect(sheetData.borders).toEqual({ A1: 1, A2: 1, B1: 2, B2: 2 });
254+
expect(sheetData.styles).toEqual({ A1: 1, B1: 1 });
255+
});
256+
257+
test("Tables that contains an array formula are also exported as individual cell styles", async () => {
258+
setCellContent(model, "A2", "=MUNIT(2)");
259+
createTable(model, "A1:B3", { styleId: "TableStyleLight8" });
241260

242261
const exported = await getExportedExcelData(model);
243-
expect(exported.sheets[0].tables).toMatchObject([{ range: "A1:C3" }]);
262+
const sheetData = exported.sheets[0];
263+
expect(sheetData.tables).toHaveLength(0);
264+
expect(exported.styles).toEqual({
265+
"1": { fillColor: "#000000", textColor: "#FFFFFF", bold: true },
266+
});
267+
268+
expect(sheetData.styles).toEqual({ A1: 1, B1: 1 });
244269
});
245270
});
246271

0 commit comments

Comments
 (0)