[IMP] spreadsheet: interpret more months reprensentation
This commit improves the way months are recognized in pivot functions. It also removes the use of `moment.js` which will be removed in the future (see task 3391739) Currently, months in a pivot function must follow a very specific format. It must be mm/yyyy `ODOO.PIVOT(1, "amount", "date:month", "01/2020")` Among other things: "1/2020" does not work, it must be prefixed by a leading zeros. Now, any date-like value would work: month: "1/2020" first of the month: "1/1/2020" any day in the month: "1/2/2020" using the DATE function: DATE(2020, 12, 1) using NOW() Future work: `PIVOT.HEADER` should return a the date as a number when the date is a month. But it's currenctly quite a mess. because we use the same method for a million things (`getGroupByDisplayLabel`). Task 3413243 closes odoo/odoo#129782 Related: odoo/enterprise#44684 Signed-off-by: Rémi Rahir (rar) <rar@odoo.com>
This commit is contained in:
committed by
Rmi Rahir (rar)
parent
b6f2087bf8
commit
5318e947bd
@@ -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" };
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) };
|
||||
}
|
||||
@@ -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 */ `
|
||||
<pivot>
|
||||
<field name="date" interval="${interval}" type="col"/>
|
||||
<field name="foo" type="measure"/>
|
||||
</pivot>`,
|
||||
});
|
||||
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 */ `
|
||||
<pivot>
|
||||
<field name="date" interval="day" type="col"/>
|
||||
<field name="foo" type="measure"/>
|
||||
</pivot>`,
|
||||
});
|
||||
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) {
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user