diff --git a/addons/spreadsheet/static/src/helpers/constants.js b/addons/spreadsheet/static/src/helpers/constants.js
index cb766da96f2..2314683f497 100644
--- a/addons/spreadsheet/static/src/helpers/constants.js
+++ b/addons/spreadsheet/static/src/helpers/constants.js
@@ -4,14 +4,6 @@ import { _t } from "@web/core/l10n/translation";
export const DEFAULT_LINES_NUMBER = 20;
-export const FORMATS = {
- day: { out: "MM/DD/YYYY", display: "DD MMM YYYY", interval: "d" },
- week: { out: "ww/YYYY", display: "[W]w YYYY", interval: "w" },
- month: { out: "MM/YYYY", display: "MMMM YYYY", interval: "M" },
- quarter: { out: "Q/YYYY", display: "[Q]Q YYYY", interval: "Q" },
- year: { out: "YYYY", display: "YYYY", interval: "y" },
-};
-
export const HEADER_STYLE = { fillColor: "#E6F2F3" };
export const TOP_LEVEL_STYLE = { bold: true, fillColor: "#E6F2F3" };
export const MEASURE_STYLE = { fillColor: "#E6F2F3", textColor: "#756f6f" };
diff --git a/addons/spreadsheet/static/src/pivot/pivot_data_source.js b/addons/spreadsheet/static/src/pivot/pivot_data_source.js
index 8df62a395ce..307480437a5 100644
--- a/addons/spreadsheet/static/src/pivot/pivot_data_source.js
+++ b/addons/spreadsheet/static/src/pivot/pivot_data_source.js
@@ -141,11 +141,12 @@ export default class PivotDataSource extends OdooViewsDataSource {
/**
* @param {string} fieldName
* @param {string} value raw string value
+ * @param {object} locale
* @returns {string}
*/
- getGroupByDisplayLabel(fieldName, value) {
+ getGroupByDisplayLabel(fieldName, value, locale) {
this._assertDataIsLoaded();
- return this._model.getGroupByDisplayLabel(fieldName, value);
+ return this._model.getGroupByDisplayLabel(fieldName, value, locale);
}
/**
diff --git a/addons/spreadsheet/static/src/pivot/pivot_functions.js b/addons/spreadsheet/static/src/pivot/pivot_functions.js
index 52f37857a00..b695af0266f 100644
--- a/addons/spreadsheet/static/src/pivot/pivot_functions.js
+++ b/addons/spreadsheet/static/src/pivot/pivot_functions.js
@@ -98,7 +98,7 @@ functionRegistry
const args = domain.map(toString);
assertPivotsExists(pivotId, this.getters);
assertDomainLength(args);
- return this.getters.getDisplayedPivotHeaderValue(pivotId, args);
+ return this.getters.getDisplayedPivotHeaderValue(pivotId, args, this.locale);
},
computeFormat: function (pivotId, ...domain) {
pivotId = toString(pivotId.value);
@@ -121,10 +121,18 @@ functionRegistry
return "#,##0.00";
case "date":
case "datetime":
- if (aggregateOperator === "day") {
- return this.locale.dateFormat;
+ switch (aggregateOperator) {
+ case "day":
+ return this.locale.dateFormat;
+ case "month":
+ return "mmmm yyyy";
+ case "year":
+ return "0";
+ case "week":
+ case "quarter":
+ return undefined;
}
- return undefined;
+ break;
default:
return undefined;
}
diff --git a/addons/spreadsheet/static/src/pivot/pivot_helpers.js b/addons/spreadsheet/static/src/pivot/pivot_helpers.js
index 2236e1077c9..d0b48327bec 100644
--- a/addons/spreadsheet/static/src/pivot/pivot_helpers.js
+++ b/addons/spreadsheet/static/src/pivot/pivot_helpers.js
@@ -1,7 +1,6 @@
/** @odoo-module **/
import { _t } from "@web/legacy/js/services/core";
-import { FORMATS } from "../helpers/constants";
import { getOdooFunctions } from "../helpers/odoo_functions_helpers";
export const pivotFormulaRegex = /^=.*PIVOT/;
@@ -10,19 +9,6 @@ export const pivotFormulaRegex = /^=.*PIVOT/;
// Public
//--------------------------------------------------------------------------
-/**
- * Format a data
- *
- * @param {string} interval aggregate interval i.e. month, week, quarter, ...
- * @param {string} value
- */
-export function formatDate(interval, value) {
- const output = FORMATS[interval].display;
- const input = FORMATS[interval].out;
- const date = moment(value, input);
- return date.isValid() ? date.format(output) : _t("None");
-}
-
/**
* Parse a spreadsheet formula and detect the number of PIVOT functions that are
* present in the given formula.
diff --git a/addons/spreadsheet/static/src/pivot/pivot_model.js b/addons/spreadsheet/static/src/pivot/pivot_model.js
index 54da5cbc0da..72f3cc6cb4d 100644
--- a/addons/spreadsheet/static/src/pivot/pivot_model.js
+++ b/addons/spreadsheet/static/src/pivot/pivot_model.js
@@ -5,14 +5,11 @@ import { Domain } from "@web/core/domain";
import { sprintf } from "@web/core/utils/strings";
import { PivotModel } from "@web/views/pivot/pivot_model";
import { computeReportMeasures } from "@web/views/utils";
-import { session } from "@web/session";
-
-import { FORMATS } from "../helpers/constants";
import * as spreadsheet from "@odoo/o-spreadsheet";
-import { formatDate } from "./pivot_helpers";
import { PERIODS } from "@spreadsheet/pivot/pivot_helpers";
import { SpreadsheetPivotTable } from "@spreadsheet/pivot/pivot_table";
+import { pivotTimeAdapter } from "./pivot_time_adapters";
const { toString, toNumber, toBoolean } = spreadsheet.helpers;
const { DEFAULT_LOCALE } = spreadsheet.constants;
@@ -81,9 +78,10 @@ function throwUnsupportedFieldError(field) {
* the two group values are "42" and "won".
* @param {object} field
* @param {number | boolean | string} groupValue
+ * @param {"day" | "week" | "month" | "quarter" | "year" | undefined} aggregateOperator
* @returns {number | boolean | string}
*/
-export function parsePivotFormulaFieldValue(field, groupValue) {
+export function toNormalizedPivotValue(field, groupValue, aggregateOperator) {
const groupValueString =
typeof groupValue === "boolean"
? toString(groupValue).toLocaleLowerCase()
@@ -98,7 +96,10 @@ export function parsePivotFormulaFieldValue(field, groupValue) {
switch (field.type) {
case "datetime":
case "date":
- return toString(groupValueString);
+ return pivotTimeAdapter(aggregateOperator).normalizeFunctionValue(
+ groupValueString,
+ field
+ );
case "selection":
case "char":
case "text":
@@ -299,7 +300,7 @@ export class SpreadsheetPivotModel extends PivotModel {
* @param {string} groupValueString Value of the group by
* @returns {string}
*/
- getGroupByDisplayLabel(groupFieldString, groupValueString) {
+ getGroupByDisplayLabel(groupFieldString, groupValueString, locale = DEFAULT_LOCALE) {
if (groupValueString === NO_RECORD_AT_THIS_POSITION) {
return "";
}
@@ -311,13 +312,15 @@ export class SpreadsheetPivotModel extends PivotModel {
return this.parseGroupField(groupValueString).field.string;
}
const { field, aggregateOperator } = this.parseGroupField(groupFieldString);
- const value = parsePivotFormulaFieldValue(field, groupValueString);
+ const value = toNormalizedPivotValue(field, groupValueString, aggregateOperator);
const undef = _t("None");
if (this._isDateField(field)) {
+ // TODO include this parsing to the pivot time adapters and extend it to other time periods
if (value && aggregateOperator === "day") {
return toNumber(value, DEFAULT_LOCALE);
}
- return formatDate(aggregateOperator, value);
+ const adapter = pivotTimeAdapter(aggregateOperator);
+ return adapter.format(value, locale);
}
if (field.relation) {
const label = this.metadataRepository.getRecordDisplayName(field.relation, value);
@@ -455,39 +458,16 @@ export class SpreadsheetPivotModel extends PivotModel {
return groupBys.map((groupBy) => {
const { field, aggregateOperator } = this.parseGroupField(groupBy);
if (this._isDateField(field)) {
- const value = this._getGroupStartingDay(groupBy, group);
- if (!value) {
- return false;
- }
- const fOut = FORMATS[aggregateOperator]["out"];
- // eslint-disable-next-line no-undef
- const date = moment(value);
- return date.isValid() ? date.format(fOut) : false;
+ return pivotTimeAdapter(aggregateOperator).normalizeServerValue(
+ groupBy,
+ field,
+ group
+ );
}
return this._sanitizeValue(group[groupBy]);
});
}
- /**
- * When grouping by a time field, return
- * the group starting day (local to the timezone)
- * @param {string} groupBy
- * @param {object} readGroup
- * @returns {string | undefined}
- */
- _getGroupStartingDay(groupBy, readGroup) {
- if (!readGroup["__range"] || !readGroup["__range"][groupBy]) {
- return undefined;
- }
- const { field } = this.parseGroupField(groupBy);
- const sqlValue = readGroup["__range"][groupBy].from;
- if (this.metaData.fields[field.name].type === "date") {
- return sqlValue;
- }
- const userTz = session.user_context.tz || luxon.Settings.defaultZoneName;
- return luxon.DateTime.fromSQL(sqlValue, { zone: "utc" }).setZone(userTz).toISODate();
- }
-
/**
* Check if the given field is used as col group by
*/
@@ -555,12 +535,13 @@ export class SpreadsheetPivotModel extends PivotModel {
while (i < domain.length) {
const groupFieldString = domain[i];
const groupValue = domain[i + 1];
- const { field, isPositional } = this.parseGroupField(groupFieldString);
+ const { field, isPositional, aggregateOperator } =
+ this.parseGroupField(groupFieldString);
let value;
if (isPositional) {
value = this._parsePivotFormulaWithPosition(field, groupValue, cols, rows);
} else {
- value = parsePivotFormulaFieldValue(field, groupValue);
+ value = toNormalizedPivotValue(field, groupValue, aggregateOperator);
}
if (this._isCol(field)) {
cols.push(value);
diff --git a/addons/spreadsheet/static/src/pivot/pivot_time_adapters.js b/addons/spreadsheet/static/src/pivot/pivot_time_adapters.js
new file mode 100644
index 00000000000..30d2622093f
--- /dev/null
+++ b/addons/spreadsheet/static/src/pivot/pivot_time_adapters.js
@@ -0,0 +1,274 @@
+/** @odoo-module */
+
+import { helpers, constants } from "@odoo/o-spreadsheet";
+import { deserializeDate } from "@web/core/l10n/dates";
+import { _t } from "@web/core/l10n/translation";
+import { sprintf } from "@web/core/utils/strings";
+import { session } from "@web/session";
+const { toNumber, formatValue } = helpers;
+const { DEFAULT_LOCALE } = constants;
+
+const { DateTime } = luxon;
+
+/**
+ * @param {"day" | "week" | "month" | "quarter" | "month"} groupAggregate
+ * @returns {PivotTimeAdapter}
+ */
+export function pivotTimeAdapter(groupAggregate) {
+ return TIME_ADAPTERS[groupAggregate];
+}
+
+/**
+ * The Time Adapter: Managing Time Periods for Pivot Functions
+ *
+ * Overview:
+ * A time adapter is responsible for managing time periods associated with pivot functions.
+ * Each type of period (day, week, month, quarter, etc.) has its own dedicated adapter.
+ * The adapter's primary role is to normalize period values between spreadsheet functions,
+ * the server, and the datasource.
+ * By normalizing the period value, it can be stored consistently in the datasource.
+ *
+ * Normalization Process:
+ * When dealing with the server, the time adapter ensures that the received periods are
+ * normalized before being stored in the datasource.
+ * For example, if the server returns a day period as "2023-12-25 22:00:00," the time adapter
+ * transforms it into the normalized form "12/25/2023" for storage in the datasource.
+ *
+ * Similarly, when working with functions in the spreadsheet, the time adapter normalizes
+ * the provided period to facilitate accurate lookup of values in the datasource.
+ * For instance, if the spreadsheet function represents a day period as a number generated
+ * by the DATE function (DATE(2023, 12, 25)), the time adapter will normalize it accordingly.
+ *
+ * Example:
+ * To illustrate the normalization process, let's consider the day period:
+ *
+ * 1. The server returns a day period as "2023-12-25 22:00:00"
+ * 2. The time adapter normalizes this period to "12/25/2023" for storage in the datasource.
+ * 3. Meanwhile, the spreadsheet function represents the day period as a number obtained from
+ * the DATE function (DATE(2023, 12, 25)).
+ *
+ * By applying the appropriate normalization, the time adapter ensures that the periods from
+ * different sources are consistently represented and can be effectively utilized for lookup
+ * operations in the datasource.
+ *
+ * Implementation notes/tips:
+ * - Do not mix luxon and spreadsheet dates in the same function. Timezones are not handled the same way.
+ * Spreadsheet dates are naive dates (no timezone) while luxon dates are timezone aware dates.
+ * **Don't do this**: DateTime.fromJSDate(toJsDate(value)) (it will be interpreted as UTC)
+ *
+ * - spreadsheet formats and luxon formats are not the same but can be equivalent.
+ * For example: "MM/dd/yyyy" (luxon format) is equivalent to "mm/dd/yyyy" (spreadsheet format)
+ *
+ * Limitations:
+ * If a period value is provided as a **string** to a function, it will interpreted as being in the default locale.
+ * e.g. in `ODOO.PIVOT(1, "amount", "create_date", "1/5/2023")`, the day is interpreted as being the 5th of January 2023,
+ * even if the spreadsheet locale is set to French and such a date is usually interpreted as the 1st of May 2023.
+ * The reason is ODOO.PIVOT functions are currently generated without being aware of the spreadsheet locale.
+ *
+ * @typedef {Object} PivotTimeAdapter
+ * @property {(groupBy: string, field: string, readGroupResult: object) => string} normalizeServerValue
+ * @property {(value: string) => string} normalizeFunctionValue
+ * @property {(normalizedValue: string, step: number) => string} increment
+ * @property {(normalizedValue: string, locale: Object) => string} format
+ */
+
+/**
+ * @type {PivotTimeAdapter}
+ * Normalized value: "12/25/2023"
+ *
+ * Note: Those two format are equivalent:
+ * - "MM/dd/yyyy" (luxon format)
+ * - "mm/dd/yyyy" (spreadsheet format)
+ **/
+const dayAdapter = {
+ normalizeServerValue(groupBy, field, readGroupResult) {
+ const serverDayValue = getGroupStartingDay(field, groupBy, readGroupResult);
+ const date = deserializeDate(serverDayValue);
+ return date.toFormat("MM/dd/yyyy");
+ },
+ normalizeFunctionValue(value) {
+ const date = toNumber(value, DEFAULT_LOCALE);
+ return formatValue(date, { locale: DEFAULT_LOCALE, format: "mm/dd/yyyy" });
+ },
+ increment(normalizedValue, step) {
+ const date = DateTime.fromFormat(normalizedValue, "MM/dd/yyyy");
+ return date.plus({ days: step }).toFormat("MM/dd/yyyy");
+ },
+ format(normalizedValue, locale) {
+ const value = toNumber(normalizedValue, DEFAULT_LOCALE);
+ return formatValue(value, { locale, format: locale.dateFormat });
+ },
+};
+
+/**
+ * @type {PivotTimeAdapter}
+ * Normalized value: "2/2023" for week 2 of 2023
+ */
+const weekAdapter = {
+ normalizeServerValue(groupBy, field, readGroupResult) {
+ const weekValue = readGroupResult[groupBy];
+ const { week, year } = parseServerWeekHeader(weekValue);
+ return `${week}/${year}`;
+ },
+ normalizeFunctionValue(value) {
+ const [week, year] = value.split("/");
+ return `${Number(week)}/${Number(year)}`;
+ },
+ increment(normalizedValue, step) {
+ const [week, year] = normalizedValue.split("/");
+ const weekNumber = Number(week);
+ const yearNumber = Number(year);
+ const date = DateTime.fromObject({ weekNumber, weekYear: yearNumber });
+ const nextWeek = date.plus({ weeks: step });
+ return `${nextWeek.weekNumber}/${nextWeek.weekYear}`;
+ },
+ format(normalizedValue, locale) {
+ const [year, week] = normalizedValue.split("/");
+ return sprintf(_t("W%(week)s %(year)s"), { week, year });
+ },
+};
+
+/**
+ * @type {PivotTimeAdapter}
+ * normalized month value is a string formatted as "MM/yyyy" (luxon format)
+ * e.g. "01/2020" for January 2020
+ */
+const monthAdapter = {
+ normalizeServerValue(groupBy, field, readGroupResult) {
+ const firstOfTheMonth = getGroupStartingDay(field, groupBy, readGroupResult);
+ const date = deserializeDate(firstOfTheMonth);
+ return date.toFormat("MM/yyyy");
+ },
+ normalizeFunctionValue(value) {
+ const date = toNumber(value, DEFAULT_LOCALE);
+ return formatValue(date, { DEFAULT_LOCALE, format: "mm/yyyy" });
+ },
+ increment(normalizedValue, step) {
+ return DateTime.fromFormat(normalizedValue, "MM/yyyy")
+ .plus({ months: step })
+ .toFormat("MM/yyyy");
+ },
+ format(normalizedValue, locale) {
+ const value = toNumber(normalizedValue, DEFAULT_LOCALE);
+ return formatValue(value, { locale, format: "mmmm yyyy" });
+ },
+};
+
+/**
+ * @type {PivotTimeAdapter}
+ * normalized quarter value is "quarter/year"
+ * e.g. "1/2020" for Q1 2020
+ */
+const quarterAdapter = {
+ normalizeServerValue(groupBy, field, readGroupResult) {
+ const firstOfTheQuarter = getGroupStartingDay(field, groupBy, readGroupResult);
+ const date = deserializeDate(firstOfTheQuarter);
+ return `${date.quarter}/${date.year}`;
+ },
+ normalizeFunctionValue(value) {
+ const [quarter, year] = value.split("/");
+ return `${quarter}/${year}`;
+ },
+ increment(normalizedValue, step) {
+ const [quarter, year] = normalizedValue.split("/");
+ const date = DateTime.fromObject({ year: Number(year), month: Number(quarter) * 3 });
+ const nextQuarter = date.plus({ quarters: step });
+ return `${nextQuarter.quarter}/${nextQuarter.year}`;
+ },
+ format(normalizedValue, locale) {
+ const [quarter, year] = normalizedValue.split("/");
+ return sprintf(_t("Q%(quarter)s %(year)s"), { quarter, year });
+ },
+};
+/**
+ * @type {PivotTimeAdapter}
+ */
+const yearAdapter = {
+ normalizeServerValue(groupBy, field, readGroupResult) {
+ return Number(readGroupResult[groupBy]);
+ },
+ normalizeFunctionValue(value) {
+ return toNumber(value, DEFAULT_LOCALE);
+ },
+ increment(normalizedValue, step) {
+ return normalizedValue + step;
+ },
+ format(normalizedValue, locale) {
+ return formatValue(normalizedValue, { locale, format: "0" });
+ },
+};
+
+/**
+ * Decorate adapter functions to handle the empty value "false"
+ * @param {PivotTimeAdapter} adapter
+ */
+function falseHandlerDecorator(adapter) {
+ return {
+ normalizeServerValue(groupBy, field, readGroupResult) {
+ if (readGroupResult[groupBy] === false) {
+ return false;
+ }
+ return adapter.normalizeServerValue(groupBy, field, readGroupResult);
+ },
+ normalizeFunctionValue(value) {
+ if (value === false || value === "false") {
+ return false;
+ }
+ return adapter.normalizeFunctionValue(value);
+ },
+ increment(normalizedValue, step) {
+ if (normalizedValue === false) {
+ return false;
+ }
+ return adapter.increment(normalizedValue, step);
+ },
+ format(normalizedValue, locale) {
+ if (normalizedValue === false) {
+ return _t("None");
+ }
+ return adapter.format(normalizedValue, locale);
+ },
+ };
+}
+
+const TIME_ADAPTERS = {
+ day: falseHandlerDecorator(dayAdapter),
+ week: falseHandlerDecorator(weekAdapter),
+ month: falseHandlerDecorator(monthAdapter),
+ quarter: falseHandlerDecorator(quarterAdapter),
+ year: falseHandlerDecorator(yearAdapter),
+};
+
+/**
+ * When grouping by a time field, return
+ * the group starting day (local to the timezone)
+ * @param {object} field
+ * @param {string} groupBy
+ * @param {object} readGroup
+ * @returns {string | undefined}
+ */
+function getGroupStartingDay(field, groupBy, readGroup) {
+ if (!readGroup["__range"] || !readGroup["__range"][groupBy]) {
+ return undefined;
+ }
+ const sqlValue = readGroup["__range"][groupBy].from;
+ if (field.type === "date") {
+ return sqlValue;
+ }
+ const userTz = session.user_context.tz || luxon.Settings.defaultZoneName;
+ return DateTime.fromSQL(sqlValue, { zone: "utc" }).setZone(userTz).toISODate();
+}
+
+/**
+ * Parses a pivot week header value.
+ * @param {string} value
+ * @example
+ * parseServerWeekHeader("W1 2020") // { week: 1, year: 2020 }
+ */
+function parseServerWeekHeader(value) {
+ // Value is always formatted as "W1 2020", no matter the language.
+ // Parsing this formatted value is the only way to ensure we get the same
+ // locale aware week number as the one used in the server.
+ const [week, year] = value.split(" ");
+ return { week: Number(week.slice(1)), year: Number(year) };
+}
diff --git a/addons/spreadsheet/static/tests/pivots/model/pivot_plugin_test.js b/addons/spreadsheet/static/tests/pivots/model/pivot_plugin_test.js
index dfb19aa3013..60cbd179049 100644
--- a/addons/spreadsheet/static/tests/pivots/model/pivot_plugin_test.js
+++ b/addons/spreadsheet/static/tests/pivots/model/pivot_plugin_test.js
@@ -586,6 +586,20 @@ QUnit.module("spreadsheet > pivot plugin", {}, () => {
assert.equal(getCellValue(model, "C5"), "");
});
+ QUnit.test("PIVOT.HEADER grouped by date field without value", async function (assert) {
+ for (const interval of ["day", "week", "month", "quarter", "year"]) {
+ const { model } = await createSpreadsheetWithPivot({
+ arch: /* xml */ `
+
+
+
+ `,
+ });
+ setCellContent(model, "A1", `=ODOO.PIVOT.HEADER(1, "date:${interval}", "false")`);
+ assert.equal(getCellValue(model, "A1"), "None");
+ }
+ });
+
QUnit.test("PIVOT formulas are correctly formatted at evaluation", async function (assert) {
const { model } = await createSpreadsheetWithPivot({
arch: /* xml */ `
@@ -615,6 +629,19 @@ QUnit.module("spreadsheet > pivot plugin", {}, () => {
}
);
+ QUnit.test("PIVOT.HEADER day are correctly formatted at evaluation", async function (assert) {
+ const { model } = await createSpreadsheetWithPivot({
+ arch: /* xml */ `
+
+
+
+ `,
+ });
+ assert.strictEqual(getEvaluatedCell(model, "B1").format, "m/d/yyyy");
+ assert.strictEqual(getEvaluatedCell(model, "B1").value, 42474);
+ assert.strictEqual(getEvaluatedCell(model, "B1").formattedValue, "4/14/2016");
+ });
+
QUnit.test(
"PIVOT.HEADER formulas are correctly formatted at evaluation",
async function (assert) {
diff --git a/addons/spreadsheet/static/tests/pivots/pivot_helpers_test.js b/addons/spreadsheet/static/tests/pivots/pivot_helpers_test.js
index 386632dcae9..7cf42866ed3 100644
--- a/addons/spreadsheet/static/tests/pivots/pivot_helpers_test.js
+++ b/addons/spreadsheet/static/tests/pivots/pivot_helpers_test.js
@@ -1,7 +1,7 @@
/** @odoo-module */
import { getFirstPivotFunction, getNumberOfPivotFormulas } from "@spreadsheet/pivot/pivot_helpers";
import { getFirstListFunction, getNumberOfListFormulas } from "@spreadsheet/list/list_helpers";
-import { parsePivotFormulaFieldValue } from "@spreadsheet/pivot/pivot_model";
+import { toNormalizedPivotValue } from "@spreadsheet/pivot/pivot_model";
function stringArg(value) {
return { type: "STRING", value: `${value}` };
@@ -27,9 +27,7 @@ QUnit.module("spreadsheet > pivot_helpers", {}, () => {
QUnit.test("Extraction with two PIVOT formulas", async function (assert) {
const formula = `=ODOO.PIVOT("1", "test") + ODOO.PIVOT("2", "hello", "bla")`;
- let functionName;
- let args;
- ({ functionName, args } = getFirstPivotFunction(formula));
+ const { functionName, args } = getFirstPivotFunction(formula);
assert.strictEqual(functionName, "ODOO.PIVOT");
assert.strictEqual(args.length, 2);
assert.deepEqual(args[0], stringArg("1"));
@@ -59,22 +57,22 @@ QUnit.module("spreadsheet > pivot_helpers", {}, () => {
});
});
-QUnit.module("spreadsheet > parsePivotFormulaFieldValue", {}, () => {
+QUnit.module("spreadsheet > toNormalizedPivotValue", {}, () => {
QUnit.test("parse values of a selection, char or text field", (assert) => {
for (const fieldType of ["selection", "text", "char"]) {
const field = {
type: fieldType,
string: "A field",
};
- assert.strictEqual(parsePivotFormulaFieldValue(field, "won"), "won");
- assert.strictEqual(parsePivotFormulaFieldValue(field, "1"), "1");
- assert.strictEqual(parsePivotFormulaFieldValue(field, 1), "1");
- assert.strictEqual(parsePivotFormulaFieldValue(field, "11/2020"), "11/2020");
- assert.strictEqual(parsePivotFormulaFieldValue(field, "2020"), "2020");
- assert.strictEqual(parsePivotFormulaFieldValue(field, "01/11/2020"), "01/11/2020");
- assert.strictEqual(parsePivotFormulaFieldValue(field, "false"), false);
- assert.strictEqual(parsePivotFormulaFieldValue(field, false), false);
- assert.strictEqual(parsePivotFormulaFieldValue(field, "true"), "true");
+ assert.strictEqual(toNormalizedPivotValue(field, "won"), "won");
+ assert.strictEqual(toNormalizedPivotValue(field, "1"), "1");
+ assert.strictEqual(toNormalizedPivotValue(field, 1), "1");
+ assert.strictEqual(toNormalizedPivotValue(field, "11/2020"), "11/2020");
+ assert.strictEqual(toNormalizedPivotValue(field, "2020"), "2020");
+ assert.strictEqual(toNormalizedPivotValue(field, "01/11/2020"), "01/11/2020");
+ assert.strictEqual(toNormalizedPivotValue(field, "false"), false);
+ assert.strictEqual(toNormalizedPivotValue(field, false), false);
+ assert.strictEqual(toNormalizedPivotValue(field, "true"), "true");
}
});
@@ -84,16 +82,38 @@ QUnit.module("spreadsheet > parsePivotFormulaFieldValue", {}, () => {
type: fieldType,
string: "A field",
};
- assert.strictEqual(parsePivotFormulaFieldValue(field, "11/2020"), "11/2020");
- assert.strictEqual(parsePivotFormulaFieldValue(field, "2020"), "2020");
- assert.strictEqual(parsePivotFormulaFieldValue(field, "01/11/2020"), "01/11/2020");
- assert.strictEqual(parsePivotFormulaFieldValue(field, "1"), "1");
- assert.strictEqual(parsePivotFormulaFieldValue(field, 1), "1");
- assert.strictEqual(parsePivotFormulaFieldValue(field, "false"), false);
- assert.strictEqual(parsePivotFormulaFieldValue(field, false), false);
- assert.strictEqual(parsePivotFormulaFieldValue(field, "true"), "true"); // this should throw because it's not a valid date
- assert.strictEqual(parsePivotFormulaFieldValue(field, true), "true"); // this should throw because it's not a valid date
- assert.strictEqual(parsePivotFormulaFieldValue(field, "won"), "won"); // this should throw because it's not a valid date
+ // day
+ assert.strictEqual(toNormalizedPivotValue(field, "1/11/2020", "day"), "01/11/2020");
+ assert.strictEqual(toNormalizedPivotValue(field, "01/11/2020", "day"), "01/11/2020");
+ assert.strictEqual(toNormalizedPivotValue(field, "11/2020", "day"), "11/01/2020");
+ assert.strictEqual(toNormalizedPivotValue(field, "1", "day"), "12/31/1899");
+ assert.strictEqual(toNormalizedPivotValue(field, 1, "day"), "12/31/1899");
+ assert.strictEqual(toNormalizedPivotValue(field, "false", "day"), false);
+ assert.strictEqual(toNormalizedPivotValue(field, false, "day"), false);
+ // week
+ assert.strictEqual(toNormalizedPivotValue(field, "11/2020", "week"), "11/2020");
+ assert.strictEqual(toNormalizedPivotValue(field, "1/2020", "week"), "1/2020");
+ assert.strictEqual(toNormalizedPivotValue(field, "01/2020", "week"), "1/2020");
+ assert.strictEqual(toNormalizedPivotValue(field, "false", "week"), false);
+ assert.strictEqual(toNormalizedPivotValue(field, false, "week"), false);
+ // month
+ assert.strictEqual(toNormalizedPivotValue(field, "11/2020", "month"), "11/2020");
+ assert.strictEqual(toNormalizedPivotValue(field, "1/2020", "month"), "01/2020");
+ assert.strictEqual(toNormalizedPivotValue(field, "01/2020", "month"), "01/2020");
+ assert.strictEqual(toNormalizedPivotValue(field, "2/11/2020", "month"), "02/2020");
+ assert.strictEqual(toNormalizedPivotValue(field, "2/1/2020", "month"), "02/2020");
+ assert.strictEqual(toNormalizedPivotValue(field, 1, "month"), "12/1899");
+ assert.strictEqual(toNormalizedPivotValue(field, "false", "month"), false);
+ assert.strictEqual(toNormalizedPivotValue(field, false, "month"), false);
+ // year
+ assert.strictEqual(toNormalizedPivotValue(field, "2020", "year"), 2020);
+ assert.strictEqual(toNormalizedPivotValue(field, 2020, "year"), 2020);
+ assert.strictEqual(toNormalizedPivotValue(field, "false", "year"), false);
+ assert.strictEqual(toNormalizedPivotValue(field, false, "year"), false);
+
+ assert.throws(() => toNormalizedPivotValue(field, "true", "month"));
+ assert.throws(() => toNormalizedPivotValue(field, true, "month"));
+ assert.throws(() => toNormalizedPivotValue(field, "won", "month"));
}
});
@@ -102,16 +122,16 @@ QUnit.module("spreadsheet > parsePivotFormulaFieldValue", {}, () => {
type: "boolean",
string: "A field",
};
- assert.strictEqual(parsePivotFormulaFieldValue(field, "false"), false);
- assert.strictEqual(parsePivotFormulaFieldValue(field, false), false);
- assert.strictEqual(parsePivotFormulaFieldValue(field, "true"), true);
- assert.strictEqual(parsePivotFormulaFieldValue(field, true), true);
- assert.throws(() => parsePivotFormulaFieldValue(field, "11/2020"));
- assert.throws(() => parsePivotFormulaFieldValue(field, "2020"));
- assert.throws(() => parsePivotFormulaFieldValue(field, "01/11/2020"));
- assert.throws(() => parsePivotFormulaFieldValue(field, "1"));
- assert.throws(() => parsePivotFormulaFieldValue(field, 1));
- assert.throws(() => parsePivotFormulaFieldValue(field, "won"));
+ assert.strictEqual(toNormalizedPivotValue(field, "false"), false);
+ assert.strictEqual(toNormalizedPivotValue(field, false), false);
+ assert.strictEqual(toNormalizedPivotValue(field, "true"), true);
+ assert.strictEqual(toNormalizedPivotValue(field, true), true);
+ assert.throws(() => toNormalizedPivotValue(field, "11/2020"));
+ assert.throws(() => toNormalizedPivotValue(field, "2020"));
+ assert.throws(() => toNormalizedPivotValue(field, "01/11/2020"));
+ assert.throws(() => toNormalizedPivotValue(field, "1"));
+ assert.throws(() => toNormalizedPivotValue(field, 1));
+ assert.throws(() => toNormalizedPivotValue(field, "won"));
});
QUnit.test("parse values of numeric fields", (assert) => {
@@ -120,16 +140,16 @@ QUnit.module("spreadsheet > parsePivotFormulaFieldValue", {}, () => {
type: fieldType,
string: "A field",
};
- assert.strictEqual(parsePivotFormulaFieldValue(field, "2020"), 2020);
- assert.strictEqual(parsePivotFormulaFieldValue(field, "01/11/2020"), 43841); // a date is actually a number in a spreadsheet
- assert.strictEqual(parsePivotFormulaFieldValue(field, "1"), 1);
- assert.strictEqual(parsePivotFormulaFieldValue(field, 1), 1);
- assert.strictEqual(parsePivotFormulaFieldValue(field, "false"), false);
- assert.strictEqual(parsePivotFormulaFieldValue(field, false), false);
- assert.throws(() => parsePivotFormulaFieldValue(field, "true"));
- assert.throws(() => parsePivotFormulaFieldValue(field, true));
- assert.throws(() => parsePivotFormulaFieldValue(field, "won"));
- assert.throws(() => parsePivotFormulaFieldValue(field, "11/2020"));
+ assert.strictEqual(toNormalizedPivotValue(field, "2020"), 2020);
+ assert.strictEqual(toNormalizedPivotValue(field, "01/11/2020"), 43841); // a date is actually a number in a spreadsheet
+ assert.strictEqual(toNormalizedPivotValue(field, "11/2020"), 44136); // 1st of november 2020
+ assert.strictEqual(toNormalizedPivotValue(field, "1"), 1);
+ assert.strictEqual(toNormalizedPivotValue(field, 1), 1);
+ assert.strictEqual(toNormalizedPivotValue(field, "false"), false);
+ assert.strictEqual(toNormalizedPivotValue(field, false), false);
+ assert.throws(() => toNormalizedPivotValue(field, "true"));
+ assert.throws(() => toNormalizedPivotValue(field, true));
+ assert.throws(() => toNormalizedPivotValue(field, "won"));
}
});
@@ -139,16 +159,16 @@ QUnit.module("spreadsheet > parsePivotFormulaFieldValue", {}, () => {
type: fieldType,
string: "A field",
};
- assert.throws(() => parsePivotFormulaFieldValue(field, "false"));
- assert.throws(() => parsePivotFormulaFieldValue(field, false));
- assert.throws(() => parsePivotFormulaFieldValue(field, "true"));
- assert.throws(() => parsePivotFormulaFieldValue(field, true));
- assert.throws(() => parsePivotFormulaFieldValue(field, "11/2020"));
- assert.throws(() => parsePivotFormulaFieldValue(field, "2020"));
- assert.throws(() => parsePivotFormulaFieldValue(field, "01/11/2020"));
- assert.throws(() => parsePivotFormulaFieldValue(field, "1"));
- assert.throws(() => parsePivotFormulaFieldValue(field, 1));
- assert.throws(() => parsePivotFormulaFieldValue(field, "won"));
+ assert.throws(() => toNormalizedPivotValue(field, "false"));
+ assert.throws(() => toNormalizedPivotValue(field, false));
+ assert.throws(() => toNormalizedPivotValue(field, "true"));
+ assert.throws(() => toNormalizedPivotValue(field, true));
+ assert.throws(() => toNormalizedPivotValue(field, "11/2020"));
+ assert.throws(() => toNormalizedPivotValue(field, "2020"));
+ assert.throws(() => toNormalizedPivotValue(field, "01/11/2020"));
+ assert.throws(() => toNormalizedPivotValue(field, "1"));
+ assert.throws(() => toNormalizedPivotValue(field, 1));
+ assert.throws(() => toNormalizedPivotValue(field, "won"));
}
});
});