odoo18/addons/spreadsheet/static/src/o_spreadsheet/migration.js

390 lines
12 KiB
JavaScript

/** @odoo-module */
import * as spreadsheet from "@odoo/o-spreadsheet";
import { OdooCorePlugin } from "@spreadsheet/plugins";
const { tokenize, parse, convertAstNodes, astToFormula } = spreadsheet;
const { corePluginRegistry, migrationStepRegistry } = spreadsheet.registries;
export const ODOO_VERSION = 12;
const MAP_V1 = {
PIVOT: "ODOO.PIVOT",
"PIVOT.HEADER": "ODOO.PIVOT.HEADER",
"PIVOT.POSITION": "ODOO.PIVOT.POSITION",
"FILTER.VALUE": "ODOO.FILTER.VALUE",
LIST: "ODOO.LIST",
"LIST.HEADER": "ODOO.LIST.HEADER",
};
const MAP_FN_NAMES_V10 = {
"ODOO.PIVOT": "PIVOT.VALUE",
"ODOO.PIVOT.HEADER": "PIVOT.HEADER",
"ODOO.PIVOT.TABLE": "PIVOT",
};
const dmyRegex = /^([0|1|2|3][1-9])\/(0[1-9]|1[0-2])\/(\d{4})$/i;
migrationStepRegistry.add("odoo_migration", {
versionFrom: "16.1",
migrate(data) {
return migrateOdooData(data);
},
});
function migrateOdooData(data) {
const version = data.odooVersion || 0;
if (version < 1) {
data = migrate0to1(data);
}
if (version < 2) {
data = migrate1to2(data);
}
if (version < 3) {
data = migrate2to3(data);
}
if (version < 4) {
data = migrate3to4(data);
}
if (version < 5) {
data = migrate4to5(data);
}
if (version < 6) {
data = migrate5to6(data);
}
if (version < 7) {
data = migrate6to7(data);
}
if (version < 8) {
data = migrate7to8(data);
}
if (version < 9) {
data = migrate8to9(data);
}
if (version < 10) {
data = migrate9to10(data);
}
if (version < 11) {
data = migrate10to11(data);
}
if (version < 12) {
data = migrate11to12(data);
}
return data;
}
function parseDimension(dimension) {
const [name, granularity] = dimension.split(":");
if (granularity) {
return { name, granularity };
}
return { name };
}
function renameFunctions(data, map) {
for (const sheet of data.sheets || []) {
for (const xc in sheet.cells || []) {
const cell = sheet.cells[xc];
if (cell.content && cell.content.startsWith("=")) {
const tokens = tokenize(cell.content);
for (const token of tokens) {
if (token.type === "SYMBOL" && token.value.toUpperCase() in map) {
token.value = map[token.value.toUpperCase()];
}
}
cell.content = tokensToString(tokens);
}
}
}
return data;
}
function tokensToString(tokens) {
return tokens.reduce((acc, token) => acc + token.value, "");
}
function migrate0to1(data) {
return renameFunctions(data, MAP_V1);
}
function migrate1to2(data) {
for (const sheet of data.sheets || []) {
for (const xc in sheet.cells || []) {
const cell = sheet.cells[xc];
if (cell.content && cell.content.startsWith("=")) {
try {
cell.content = migratePivotDaysParameters(cell.content);
} catch {
continue;
}
}
}
}
return data;
}
/**
* Migration of global filters
*/
function migrate2to3(data) {
if (data.globalFilters) {
for (const gf of data.globalFilters) {
if (gf.fields) {
gf.pivotFields = gf.fields;
delete gf.fields;
}
if (
gf.type === "date" &&
typeof gf.defaultValue === "object" &&
"year" in gf.defaultValue
) {
switch (gf.defaultValue.year) {
case "last_year":
gf.defaultValue.yearOffset = -1;
break;
case "antepenultimate_year":
gf.defaultValue.yearOffset = -2;
break;
case "this_year":
case undefined:
gf.defaultValue.yearOffset = 0;
break;
}
delete gf.defaultValue.year;
}
if (!gf.listFields) {
gf.listFields = {};
}
if (!gf.graphFields) {
gf.graphFields = {};
}
}
}
return data;
}
/**
* Migration of list/pivot names
*/
function migrate3to4(data) {
if (data.lists) {
for (const list of Object.values(data.lists)) {
list.name = list.name || list.model;
}
}
if (data.pivots) {
for (const pivot of Object.values(data.pivots)) {
pivot.name = pivot.name || pivot.model;
}
}
return data;
}
function migrate4to5(data) {
for (const filter of data.globalFilters || []) {
for (const [id, fm] of Object.entries(filter.pivotFields || {})) {
if (!(data.pivots && id in data.pivots)) {
delete filter.pivotFields[id];
continue;
}
if (!data.pivots[id].fieldMatching) {
data.pivots[id].fieldMatching = {};
}
data.pivots[id].fieldMatching[filter.id] = { chain: fm.field, type: fm.type };
if ("offset" in fm) {
data.pivots[id].fieldMatching[filter.id].offset = fm.offset;
}
}
delete filter.pivotFields;
for (const [id, fm] of Object.entries(filter.listFields || {})) {
if (!(data.lists && id in data.lists)) {
delete filter.listFields[id];
continue;
}
if (!data.lists[id].fieldMatching) {
data.lists[id].fieldMatching = {};
}
data.lists[id].fieldMatching[filter.id] = { chain: fm.field, type: fm.type };
if ("offset" in fm) {
data.lists[id].fieldMatching[filter.id].offset = fm.offset;
}
}
delete filter.listFields;
const findFigureFromId = (id) => {
for (const sheet of data.sheets) {
const fig = sheet.figures.find((f) => f.id === id);
if (fig) {
return fig;
}
}
return undefined;
};
for (const [id, fm] of Object.entries(filter.graphFields || {})) {
const figure = findFigureFromId(id);
if (!figure) {
delete filter.graphFields[id];
continue;
}
if (!figure.data.fieldMatching) {
figure.data.fieldMatching = {};
}
figure.data.fieldMatching[filter.id] = { chain: fm.field, type: fm.type };
if ("offset" in fm) {
figure.data.fieldMatching[filter.id].offset = fm.offset;
}
}
delete filter.graphFields;
}
return data;
}
/**
* Convert pivot formulas days parameters from day/month/year
* format to the standard spreadsheet month/day/year format.
* e.g. =PIVOT.HEADER(1,"create_date:day","30/07/2022") becomes =PIVOT.HEADER(1,"create_date:day","07/30/2022")
* @param {string} formulaString
* @returns {string}
*/
function migratePivotDaysParameters(formulaString) {
const ast = parse(formulaString);
const convertedAst = convertAstNodes(ast, "FUNCALL", (ast) => {
if (["ODOO.PIVOT", "ODOO.PIVOT.HEADER"].includes(ast.value.toUpperCase())) {
for (const subAst of ast.args) {
if (subAst.type === "STRING") {
const date = subAst.value.match(dmyRegex);
if (date) {
subAst.value = `${[date[2], date[1], date[3]].join("/")}`;
}
}
}
}
return ast;
});
return "=" + astToFormula(convertedAst);
}
function migrate5to6(data) {
if (!data.globalFilters?.length) {
return data;
}
for (const filter of data.globalFilters) {
if (filter.type === "date" && ["year", "quarter", "month"].includes(filter.rangeType)) {
if (filter.defaultsToCurrentPeriod) {
filter.defaultValue = `this_${filter.rangeType}`;
}
filter.rangeType = "fixedPeriod";
}
delete filter.defaultsToCurrentPeriod;
}
return data;
}
/**
* Migrate the pivot data to add the type, by default "ODOO". And replace the
* pivot with a new object that contains type and definition (the old pivot).
*/
function migrate6to7(data) {
if (data.pivots) {
for (const [id, definition] of Object.entries(data.pivots)) {
definition.measures = definition.measures.map((measure) => measure.field);
const fieldMatching = definition.fieldMatching;
delete definition.fieldMatching;
data.pivots[id] = {
type: "ODOO",
definition,
fieldMatching,
};
}
}
return data;
}
function migrate7to8(data) {
if (data.pivots) {
for (const [id, pivot] of Object.entries(data.pivots)) {
data.pivots[id] = {
type: pivot.type,
fieldMatching: pivot.fieldMatching,
...pivot.definition,
};
}
}
return data;
}
function migrate8to9(data) {
if (data.pivots) {
for (const id of Object.keys(data.pivots)) {
data.pivots[id].formulaId = id;
}
}
return data;
}
function migrate9to10(data) {
return renameFunctions(data, MAP_FN_NAMES_V10);
}
function migrate10to11(data) {
if (data.pivots) {
for (const pivot of Object.values(data.pivots)) {
pivot.measures = pivot.measures.map((measure) => ({
name: measure,
}));
pivot.columns = pivot.colGroupBys.map(parseDimension);
delete pivot.colGroupBys;
pivot.rows = pivot.rowGroupBys.map(parseDimension);
delete pivot.rowGroupBys;
}
}
return data;
}
function migrate11to12(data) {
// remove the calls to ODOO.PIVOT.POSITION and replace
// the previous argument to a relative position
for (const sheet of data.sheets || []) {
for (const xc in sheet.cells || []) {
const cell = sheet.cells[xc];
if (
cell.content &&
cell.content.startsWith("=") &&
cell.content.includes("ODOO.PIVOT.POSITION")
) {
const tokens = tokenize(cell.content);
/* given that odoo.pivot.position is automatically set, we know that:
1) it is always on the form of ODOO.PIVOT.POSITION(1, ...)
2) it is always preceded by a dimension of a pivot or header, inside another pivot formula
3) there is only one odoo.pivot.position per cell
4) odoo.pivot.position can only exist after the 3rd token and needs at least 7 tokens to be valid*/
for (let i = 2; i < tokens.length - 7; i++) {
const token = tokens[i];
if (
token.type === "SYMBOL" &&
token.value.toUpperCase() === "ODOO.PIVOT.POSITION"
) {
const order = tokens[i + 6];
tokens[i - 2].value = '"#' + tokens[i - 2].value.slice(1); // "dimension" becomes "#dimension"
tokens.splice(i, 7); // remove "ODOO.PIVOT.POSITION", "(", "1", ",", "dimension", ", ", order
// tokens[i-1] is the comma before odoo.pivot.position
tokens[i] = order;
cell.content = tokensToString(tokens);
}
}
}
}
}
return data;
}
export class OdooVersion extends OdooCorePlugin {
static getters = /** @type {const} */ ([]);
export(data) {
data.odooVersion = ODOO_VERSION;
}
}
corePluginRegistry.add("odooMigration", OdooVersion);