19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:32:02 +01:00
parent 62d197ac8b
commit 184bb0e321
667 changed files with 691406 additions and 239886 deletions

View file

@ -0,0 +1,429 @@
import { beforeEach, describe, expect, test } from "@odoo/hoot";
import { registries } from "@odoo/o-spreadsheet";
import { setCellContent, setSelection, updatePivot } from "@spreadsheet/../tests/helpers/commands";
import { defineSpreadsheetModels } from "@spreadsheet/../tests/helpers/data";
import { getEvaluatedCell, getFormattedValueGrid } from "@spreadsheet/../tests/helpers/getters";
import { createSpreadsheetWithPivot } from "@spreadsheet/../tests/helpers/pivot";
import { doMenuAction } from "@spreadsheet/../tests/helpers/ui";
import { waitForDataLoaded } from "@spreadsheet/helpers/model";
import { Partner, Product } from "../../helpers/data";
const { cellMenuRegistry } = registries;
describe.current.tags("headless");
defineSpreadsheetModels();
beforeEach(() => {
Product._records.push(
{ id: 200, display_name: "chair", name: "chair" },
{ id: 201, display_name: "table", name: "table" }
);
Partner._records.push(
{ id: 200, foo: 12, bar: true, product_id: 200, probability: 100, currency_id: 1 },
{ id: 201, foo: 13, bar: false, product_id: 201, probability: 50, currency_id: 1 }
);
});
describe("Pivot custom groups", () => {
test("Can have custom groups in a pivot", async function () {
const { model, pivotId } = await createSpreadsheetWithPivot();
updatePivot(model, pivotId, {
columns: [{ fieldName: "GroupedProducts", order: "asc" }],
rows: [],
measures: [{ id: "probability:sum", fieldName: "probability", aggregator: "sum" }],
customFields: {
GroupedProducts: {
parentField: "product_id",
name: "GroupedProducts",
groups: [{ name: "A Group", values: [37, 41] }],
},
},
});
await waitForDataLoaded(model);
setCellContent(model, "A1", "=PIVOT(1)");
// prettier-ignore
expect(getFormattedValueGrid(model, "A1:E3")).toEqual({
A1:"Partner Pivot", B1: "A Group", C1: "chair", D1: "table", E1: "Total",
A2: "", B2: "Probability", C2: "Probability", D2: "Probability", E2: "Probability",
A3: "Total", B3: "131.00", C3: "100.00", D3: "50.00", E3: "281.00",
});
});
test("Can have custom groups on char field", async function () {
Partner._records = Partner._records.map((record, i) => ({
...record,
name: `Partner${i + 1}`,
}));
const { model, pivotId } = await createSpreadsheetWithPivot();
updatePivot(model, pivotId, {
columns: [],
rows: [{ fieldName: "GroupedNames", order: "asc" }],
measures: [{ id: "probability:min", fieldName: "probability", aggregator: "min" }],
customFields: {
GroupedNames: {
parentField: "name",
name: "GroupedNames",
groups: [{ name: "First Three", values: ["Partner1", "Partner2", "Partner3"] }],
},
},
});
await waitForDataLoaded(model);
setCellContent(model, "A1", "=PIVOT(1)");
// prettier-ignore
expect(getFormattedValueGrid(model, "A1:B7")).toEqual({
A1:"Partner Pivot", B1: "Total",
A2: "", B2: "Probability",
A3: "First Three", B3: "10.00",
A4: "Partner4", B4: "15.00",
A5: "Partner5", B5: "100.00",
A6: "Partner6", B6: "50.00",
A7: "Total", B7: "10.00",
});
});
test('Cannot have custom groups with "count_distinct" measure', async function () {
const { model, pivotId } = await createSpreadsheetWithPivot();
updatePivot(model, pivotId, {
columns: [{ fieldName: "GroupedProducts", order: "asc" }],
rows: [],
measures: [
{
id: "probability:count_distinct",
fieldName: "probability",
aggregator: "count_distinct",
},
],
customFields: {
GroupedProducts: {
parentField: "product_id",
name: "GroupedProducts",
groups: [{ name: "A Group", values: [37, 41] }],
},
},
});
await waitForDataLoaded(model);
setCellContent(model, "A1", "=PIVOT(1)");
const cell = getEvaluatedCell(model, "A1");
expect(cell.value).toEqual("#ERROR");
expect(cell.message).toEqual(
'Cannot use custom pivot groups with "Count Distinct" measure'
);
const pivot = model.getters.getPivot(pivotId);
expect(pivot.definition.measures[0].isValid).toBe(false);
});
test("Can have both the grouped field and the base field at the same time in the pivot", async function () {
const { model, pivotId } = await createSpreadsheetWithPivot();
updatePivot(model, pivotId, {
columns: [{ fieldName: "GroupedProducts", order: "asc" }],
rows: [{ fieldName: "product_id", order: "asc" }],
measures: [{ id: "probability:sum", fieldName: "probability", aggregator: "sum" }],
customFields: {
GroupedProducts: {
parentField: "product_id",
name: "GroupedProducts",
groups: [
{ name: "Group1", values: [37, 41] },
{ name: "Group2", values: [200, 201] },
],
},
},
});
await waitForDataLoaded(model);
setCellContent(model, "A1", "=PIVOT(1)");
// prettier-ignore
expect(getFormattedValueGrid(model, "A1:D7")).toEqual({
A1:"Partner Pivot", B1: "Group1", C1: "Group2", D1: "Total",
A2: "", B2: "Probability", C2: "Probability", D2: "Probability",
A3: "xphone", B3: "10.00", C3: "", D3: "10.00",
A4: "xpad", B4: "121.00", C4: "", D4: "121.00",
A5: "chair", B5: "", C5: "100.00", D5: "100.00",
A6: "table", B6: "", C6: "50.00", D6: "50.00",
A7: "Total", B7: "131.00", C7: "150.00", D7: "281.00",
});
});
test("Custom groups handle None values", async function () {
Partner._records.push({ id: 202, foo: 12, bar: true, product_id: false, probability: 10 });
const { model, pivotId } = await createSpreadsheetWithPivot();
updatePivot(model, pivotId, {
columns: [{ fieldName: "GroupedProducts", order: "asc" }],
rows: [],
measures: [{ id: "probability:count", fieldName: "probability", aggregator: "count" }],
customFields: {
GroupedProducts: {
parentField: "product_id",
name: "GroupedProducts",
groups: [{ name: "Group1", values: [37, 41, 200] }],
},
},
});
await waitForDataLoaded(model);
setCellContent(model, "A1", "=PIVOT(1)");
// prettier-ignore
expect(getFormattedValueGrid(model, "A1:E3")).toEqual({
A1:"Partner Pivot", B1: "Group1", C1: "table", D1: "None", E1: "Total",
A2: "", B2: "Probability", C2: "Probability", D2: "Probability", E2: "Probability",
A3: "Total", B3: "5", C3: "1", D3: "1", E3: "7",
});
updatePivot(model, pivotId, {
customFields: {
GroupedProducts: {
parentField: "product_id",
name: "GroupedProducts",
groups: [{ name: "Group1", values: [37, 41, 200, false] }], // Add false to the group
},
},
});
await waitForDataLoaded(model);
// prettier-ignore
expect(getFormattedValueGrid(model, "A1:D3")).toEqual({
A1:"Partner Pivot", B1: "Group1", C1: "table", D1: "Total",
A2: "", B2: "Probability", C2: "Probability", D2: "Probability",
A3: "Total", B3: "6", C3: "1", D3: "7",
});
});
test("Can sort custom groups alphabetically", async function () {
Partner._records.push({ id: 202, foo: 12, bar: true, product_id: false, probability: 10 });
const { model, pivotId } = await createSpreadsheetWithPivot();
updatePivot(model, pivotId, {
columns: [{ fieldName: "GroupedProducts", order: "asc" }],
rows: [],
measures: [{ id: "probability:max", fieldName: "probability", aggregator: "max" }],
customFields: {
GroupedProducts: {
parentField: "product_id",
name: "GroupedProducts",
groups: [{ name: "My Group", values: [37, 41] }],
},
},
});
await waitForDataLoaded(model);
setCellContent(model, "A1", "=PIVOT(1)");
// prettier-ignore
expect(getFormattedValueGrid(model, "A1:F3")).toEqual({
A1:"Partner Pivot", B1: "chair", C1: "My Group", D1: "table", E1: "None", F1: "Total",
A2: "", B2: "Probability", C2: "Probability", D2: "Probability", E2: "Probability", F2: "Probability",
A3: "Total", B3: "100.00", C3: "95.00", D3: "50.00", E3: "10.00", F3: "100.00",
});
updatePivot(model, pivotId, {
columns: [{ fieldName: "GroupedProducts", order: "desc" }],
});
await waitForDataLoaded(model);
// prettier-ignore
expect(getFormattedValueGrid(model, "A1:F3")).toEqual({
A1:"Partner Pivot", B1: "None", C1: "table", D1: "My Group", E1: "chair", F1: "Total",
A2: "", B2: "Probability", C2: "Probability", D2: "Probability", E2: "Probability", F2: "Probability",
A3: "Total", B3: "10.00", C3: "50.00", D3: "95.00", E3: "100.00", F3: "100.00",
});
});
test("Can have a group with all the non-grouped values", async function () {
const { model, pivotId } = await createSpreadsheetWithPivot();
updatePivot(model, pivotId, {
columns: [{ fieldName: "GroupedProducts", order: "asc" }],
rows: [],
measures: [{ id: "probability:sum", fieldName: "probability", aggregator: "sum" }],
customFields: {
GroupedProducts: {
parentField: "product_id",
name: "GroupedProducts",
groups: [
{ name: "Group1", values: [37, 41] },
{ name: "Others", values: [], isOtherGroup: true },
],
},
},
});
await waitForDataLoaded(model);
setCellContent(model, "A1", "=PIVOT(1)");
// prettier-ignore
expect(getFormattedValueGrid(model, "A1:D3")).toEqual({
A1:"Partner Pivot", B1: "Group1", C1: "Others", D1: "Total",
A2: "", B2: "Probability", C2: "Probability", D2: "Probability",
A3: "Total", B3: "131.00", C3: "150.00", D3: "281.00",
});
});
test("Others group is always sorted at the end", async function () {
Partner._records.push({ id: 202, foo: 12, bar: true, product_id: false, probability: 10 });
const { model, pivotId } = await createSpreadsheetWithPivot();
updatePivot(model, pivotId, {
columns: [{ fieldName: "GroupedProducts", order: "asc" }],
rows: [],
measures: [{ id: "probability:sum", fieldName: "probability", aggregator: "sum" }],
customFields: {
GroupedProducts: {
parentField: "product_id",
name: "GroupedProducts",
groups: [
{ name: "Group1", values: [37, 41] },
{ name: "Group2", values: [200, false] },
{ name: "Others", values: [], isOtherGroup: true },
],
},
},
});
await waitForDataLoaded(model);
setCellContent(model, "A1", "=PIVOT(1)");
// prettier-ignore
expect(getFormattedValueGrid(model, "A1:E3")).toEqual({
A1:"Partner Pivot", B1: "Group1", C1: "Group2", D1: "Others", E1: "Total",
A2: "", B2: "Probability", C2: "Probability", D2: "Probability", E2: "Probability",
A3: "Total", B3: "131.00", C3: "110.00", D3: "50.00", E3: "291.00",
});
updatePivot(model, pivotId, {
columns: [{ fieldName: "GroupedProducts", order: "desc" }],
});
await waitForDataLoaded(model);
// prettier-ignore
expect(getFormattedValueGrid(model, "A1:E3")).toEqual({
A1:"Partner Pivot", B1: "Group2", C1: "Group1", D1: "Others", E1: "Total",
A2: "", B2: "Probability", C2: "Probability", D2: "Probability", E2: "Probability",
A3: "Total", B3: "110.00", C3: "131.00", D3: "50.00", E3: "291.00",
});
});
});
describe("Pivot custom groups menu items", () => {
test("Can add custom groups from the menu items", async function () {
const { model, pivotId, env } = await createSpreadsheetWithPivot();
updatePivot(model, pivotId, {
columns: [{ fieldName: "product_id" }],
rows: [],
measures: [{ id: "probability:sum", fieldName: "probability", aggregator: "sum" }],
});
await waitForDataLoaded(model);
setSelection(model, "C1:E1"); // "xpad", "chair", "table" column headers
await doMenuAction(cellMenuRegistry, ["pivot_headers_group"], env);
const definition = model.getters.getPivotCoreDefinition(pivotId);
expect(definition.customFields).toEqual({
Product2: {
parentField: "product_id",
name: "Product2",
groups: [{ name: "Group", values: [41, 200, 201] }],
},
});
expect(definition.columns).toEqual([
{ fieldName: "Product2" },
{ fieldName: "product_id" },
]);
});
test("Grouping a mix of ungrouped an grouped values creates a new group and removes the old one", async function () {
const { model, pivotId, env } = await createSpreadsheetWithPivot();
updatePivot(model, pivotId, {
columns: [{ fieldName: "product_id" }],
rows: [],
measures: [{ id: "probability:sum", fieldName: "probability", aggregator: "sum" }],
customFields: {
Product2: {
parentField: "product_id",
name: "Product2",
groups: [{ name: "Group", values: [41, 200, 201] }],
},
},
});
await waitForDataLoaded(model);
setSelection(model, "B1:C1"); // "xphone", "xpad" column headers
await doMenuAction(cellMenuRegistry, ["pivot_headers_group"], env);
const definition = model.getters.getPivotCoreDefinition(pivotId);
expect(definition.customFields).toEqual({
Product2: {
parentField: "product_id",
name: "Product2",
groups: [{ name: "Group", values: [37, 41] }],
},
});
expect(definition.columns).toEqual([
{ fieldName: "Product2" },
{ fieldName: "product_id" },
]);
});
test("Can merge existing group with other values with menu items", async function () {
const { model, pivotId, env } = await createSpreadsheetWithPivot();
updatePivot(model, pivotId, {
columns: [{ fieldName: "Product2", order: "asc" }],
rows: [],
measures: [{ id: "probability:sum", fieldName: "probability", aggregator: "sum" }],
customFields: {
Product2: {
parentField: "product_id",
name: "Product2",
groups: [{ name: "aaGroup", values: [200, 201] }],
},
},
});
await waitForDataLoaded(model);
setSelection(model, "B1:C1"); // "aaGroup", "xPad" column headers
await doMenuAction(cellMenuRegistry, ["pivot_headers_group"], env);
const definition = model.getters.getPivotCoreDefinition(pivotId);
expect(definition.customFields).toEqual({
Product2: {
parentField: "product_id",
name: "Product2",
groups: [{ name: "aaGroup", values: [200, 201, 41] }],
},
});
});
test("Can remove existing groups with menu items", async function () {
const { model, pivotId, env } = await createSpreadsheetWithPivot();
updatePivot(model, pivotId, {
columns: [{ fieldName: "Product2", order: "asc" }, { fieldName: "product_id" }],
rows: [],
measures: [{ id: "probability:sum", fieldName: "probability", aggregator: "sum" }],
customFields: {
Product2: {
parentField: "product_id",
name: "Product2",
groups: [
{ name: "MyGroup", values: [200, 201] },
{ name: "MyGroup2", values: [37, 41] },
],
},
},
});
await waitForDataLoaded(model);
setSelection(model, "B1"); // "MyGroup" column headers
await doMenuAction(cellMenuRegistry, ["pivot_headers_ungroup"], env);
await waitForDataLoaded(model);
let definition = model.getters.getPivotCoreDefinition(pivotId);
expect(definition.customFields).toEqual({
Product2: {
parentField: "product_id",
name: "Product2",
groups: [{ name: "MyGroup2", values: [37, 41] }],
},
});
setSelection(model, "C2"); // "xpad" column headers
await doMenuAction(cellMenuRegistry, ["pivot_headers_ungroup"], env);
await waitForDataLoaded(model);
definition = model.getters.getPivotCoreDefinition(pivotId);
expect(definition.customFields).toEqual({});
expect(definition.columns).toEqual([{ fieldName: "product_id" }]);
});
});

View file

@ -1,962 +0,0 @@
/** @odoo-module */
import {
getCell,
getCellContent,
getCellFormula,
getCellFormattedValue,
getCellValue,
} from "@spreadsheet/../tests/utils/getters";
import { createSpreadsheetWithPivot } from "@spreadsheet/../tests/utils/pivot";
import CommandResult from "@spreadsheet/o_spreadsheet/cancelled_reason";
import { addGlobalFilter, setCellContent } from "@spreadsheet/../tests/utils/commands";
import {
createModelWithDataSource,
waitForDataSourcesLoaded,
} from "@spreadsheet/../tests/utils/model";
import { makeDeferred, nextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
import { session } from "@web/session";
import { RPCError } from "@web/core/network/rpc_service";
import { getBasicServerData } from "../../utils/data";
QUnit.module("spreadsheet > pivot plugin", {}, () => {
QUnit.test("can select a Pivot from cell formula", async function (assert) {
const { model } = await createSpreadsheetWithPivot({
arch: /* xml */ `
<pivot>
<field name="product_id" type="col"/>
<field name="foo" type="row"/>
<field name="probability" type="measure"/>
</pivot>`,
});
const sheetId = model.getters.getActiveSheetId();
const pivotId = model.getters.getPivotIdFromPosition(sheetId, 2, 2);
model.dispatch("SELECT_PIVOT", { pivotId });
const selectedPivotId = model.getters.getSelectedPivotId();
assert.strictEqual(selectedPivotId, "1");
});
QUnit.test(
"can select a Pivot from cell formula with '-' before the formula",
async function (assert) {
const { model } = await createSpreadsheetWithPivot({
arch: /* xml */ `
<pivot>
<field name="product_id" type="col"/>
<field name="foo" type="row"/>
<field name="probability" type="measure"/>
</pivot>`,
});
model.dispatch("SET_VALUE", {
xc: "C3",
text: `=-PIVOT("1","probability","bar","false","foo","2")`,
});
const sheetId = model.getters.getActiveSheetId();
const pivotId = model.getters.getPivotIdFromPosition(sheetId, 2, 2);
model.dispatch("SELECT_PIVOT", { pivotId });
const selectedPivotId = model.getters.getSelectedPivotId();
assert.strictEqual(selectedPivotId, "1");
}
);
QUnit.test(
"can select a Pivot from cell formula with other numerical values",
async function (assert) {
const { model } = await createSpreadsheetWithPivot({
arch: /* xml */ `
<pivot>
<field name="product_id" type="col"/>
<field name="foo" type="row"/>
<field name="probability" type="measure"/>
</pivot>`,
});
model.dispatch("SET_VALUE", {
xc: "C3",
text: `=3*PIVOT("1","probability","bar","false","foo","2")+2`,
});
const sheetId = model.getters.getActiveSheetId();
const pivotId = model.getters.getPivotIdFromPosition(sheetId, 2, 2);
model.dispatch("SELECT_PIVOT", { pivotId });
const selectedPivotId = model.getters.getSelectedPivotId();
assert.strictEqual(selectedPivotId, "1");
}
);
QUnit.test(
"can select a Pivot from cell formula where pivot is in a function call",
async function (assert) {
const { model } = await createSpreadsheetWithPivot({
arch: /* xml */ `
<pivot>
<field name="product_id" type="col"/>
<field name="foo" type="row"/>
<field name="probability" type="measure"/>
</pivot>`,
});
model.dispatch("SET_VALUE", {
xc: "C3",
text: `=SUM(PIVOT("1","probability","bar","false","foo","2"),PIVOT("1","probability","bar","false","foo","2"))`,
});
const sheetId = model.getters.getActiveSheetId();
const pivotId = model.getters.getPivotIdFromPosition(sheetId, 2, 2);
model.dispatch("SELECT_PIVOT", { pivotId });
const selectedPivotId = model.getters.getSelectedPivotId();
assert.strictEqual(selectedPivotId, "1");
}
);
QUnit.test(
"can select a Pivot from cell formula where the id is a reference",
async function (assert) {
const { model } = await createSpreadsheetWithPivot();
setCellContent(model, "C3", `=ODOO.PIVOT(G10,"probability","bar","false","foo","2")+2`);
setCellContent(model, "G10", "1");
const sheetId = model.getters.getActiveSheetId();
const pivotId = model.getters.getPivotIdFromPosition(sheetId, 2, 2);
model.dispatch("SELECT_PIVOT", { pivotId });
const selectedPivotId = model.getters.getSelectedPivotId();
assert.strictEqual(selectedPivotId, "1");
}
);
QUnit.test(
"can select a Pivot from cell formula (Mix of test scenarios above)",
async function (assert) {
const { model } = await createSpreadsheetWithPivot({
arch: /*xml*/ `
<pivot>
<field name="product_id" type="col"/>
<field name="foo" type="row"/>
<field name="probability" type="measure"/>
</pivot>`,
});
model.dispatch("SET_VALUE", {
xc: "C3",
text: `=3*SUM(PIVOT("1","probability","bar","false","foo","2"),PIVOT("1","probability","bar","false","foo","2"))+2*PIVOT("1","probability","bar","false","foo","2")`,
});
const sheetId = model.getters.getActiveSheetId();
const pivotId = model.getters.getPivotIdFromPosition(sheetId, 2, 2);
model.dispatch("SELECT_PIVOT", { pivotId });
const selectedPivotId = model.getters.getSelectedPivotId();
assert.strictEqual(selectedPivotId, "1");
}
);
QUnit.test("Can remove a pivot with undo after editing a cell", async function (assert) {
const { model } = await createSpreadsheetWithPivot();
assert.ok(getCellContent(model, "B1").startsWith("=ODOO.PIVOT.HEADER"));
setCellContent(model, "G10", "should be undoable");
model.dispatch("REQUEST_UNDO");
assert.equal(getCellContent(model, "G10"), "");
// 2 REQUEST_UNDO because of the AUTORESIZE feature
model.dispatch("REQUEST_UNDO");
model.dispatch("REQUEST_UNDO");
assert.equal(getCellContent(model, "B1"), "");
assert.equal(model.getters.getPivotIds().length, 0);
});
QUnit.test("rename pivot with empty name is refused", async (assert) => {
const { model } = await createSpreadsheetWithPivot();
const result = model.dispatch("RENAME_ODOO_PIVOT", {
pivotId: "1",
name: "",
});
assert.deepEqual(result.reasons, [CommandResult.EmptyName]);
});
QUnit.test("rename pivot with incorrect id is refused", async (assert) => {
const { model } = await createSpreadsheetWithPivot();
const result = model.dispatch("RENAME_ODOO_PIVOT", {
pivotId: "invalid",
name: "name",
});
assert.deepEqual(result.reasons, [CommandResult.PivotIdNotFound]);
});
QUnit.test("Undo/Redo for RENAME_ODOO_PIVOT", async function (assert) {
const { model } = await createSpreadsheetWithPivot();
assert.equal(model.getters.getPivotName("1"), "Partner Pivot");
model.dispatch("RENAME_ODOO_PIVOT", { pivotId: "1", name: "test" });
assert.equal(model.getters.getPivotName("1"), "test");
model.dispatch("REQUEST_UNDO");
assert.equal(model.getters.getPivotName("1"), "Partner Pivot");
model.dispatch("REQUEST_REDO");
assert.equal(model.getters.getPivotName("1"), "test");
});
QUnit.test("Can delete pivot", async function (assert) {
const { model } = await createSpreadsheetWithPivot();
model.dispatch("REMOVE_PIVOT", { pivotId: "1" });
assert.strictEqual(model.getters.getPivotIds().length, 0);
const B4 = getCell(model, "B4");
assert.equal(B4.evaluated.error.message, `There is no pivot with id "1"`);
assert.equal(B4.evaluated.value, `#ERROR`);
});
QUnit.test("Can undo/redo a delete pivot", async function (assert) {
const { model } = await createSpreadsheetWithPivot();
const value = getCell(model, "B4").evaluated.value;
model.dispatch("REMOVE_PIVOT", { pivotId: "1" });
model.dispatch("REQUEST_UNDO");
assert.strictEqual(model.getters.getPivotIds().length, 1);
let B4 = getCell(model, "B4");
assert.equal(B4.evaluated.error, undefined);
assert.equal(B4.evaluated.value, value);
model.dispatch("REQUEST_REDO");
assert.strictEqual(model.getters.getPivotIds().length, 0);
B4 = getCell(model, "B4");
assert.equal(B4.evaluated.error.message, `There is no pivot with id "1"`);
assert.equal(B4.evaluated.value, `#ERROR`);
});
QUnit.test("Format header displays an error for non-existing field", async function (assert) {
const { model } = await createSpreadsheetWithPivot();
setCellContent(model, "G10", `=ODOO.PIVOT.HEADER("1", "measure", "non-existing")`);
setCellContent(model, "G11", `=ODOO.PIVOT.HEADER("1", "non-existing", "bla")`);
await nextTick();
assert.equal(getCellValue(model, "G10"), "#ERROR");
assert.equal(getCellValue(model, "G11"), "#ERROR");
assert.equal(
getCell(model, "G10").evaluated.error.message,
"Field non-existing does not exist"
);
assert.equal(
getCell(model, "G11").evaluated.error.message,
"Field non-existing does not exist"
);
});
QUnit.test(
"user context is combined with pivot context to fetch data",
async function (assert) {
const context = {
allowed_company_ids: [15],
tz: "bx",
lang: "FR",
uid: 4,
};
const testSession = {
uid: 4,
user_companies: {
allowed_companies: {
15: { id: 15, name: "Hermit" },
16: { id: 16, name: "Craft" },
},
current_company: 15,
},
user_context: context,
};
const spreadsheetData = {
sheets: [
{
id: "sheet1",
cells: {
A1: { content: `=ODOO.PIVOT(1, "probability")` },
},
},
],
pivots: {
1: {
id: 1,
colGroupBys: ["foo"],
domain: [],
measures: [{ field: "probability", operator: "avg" }],
model: "partner",
rowGroupBys: ["bar"],
context: {
allowed_company_ids: [16],
default_stage_id: 9,
search_default_stage_id: 90,
tz: "nz",
lang: "EN",
uid: 40,
},
},
},
};
const expectedFetchContext = {
allowed_company_ids: [15],
default_stage_id: 9,
search_default_stage_id: 90,
tz: "bx",
lang: "FR",
uid: 4,
};
patchWithCleanup(session, testSession);
const model = await createModelWithDataSource({
spreadsheetData,
mockRPC: function (route, { model, method, kwargs }) {
if (model !== "partner") {
return;
}
switch (method) {
case "read_group":
assert.step("read_group");
assert.deepEqual(kwargs.context, expectedFetchContext, "read_group");
break;
}
},
});
await waitForDataSourcesLoaded(model);
assert.verifySteps(["read_group", "read_group", "read_group", "read_group"]);
}
);
QUnit.test("Context is purged from PivotView related keys", async function (assert) {
const spreadsheetData = {
sheets: [
{
id: "sheet1",
cells: {
A1: { content: `=ODOO.PIVOT(1, "probability")` },
},
},
],
pivots: {
1: {
id: 1,
colGroupBys: ["foo"],
rowGroupBys: ["bar"],
domain: [],
measures: [{ field: "probability", operator: "avg" }],
model: "partner",
context: {
pivot_measures: ["__count"],
// inverse row and col group bys
pivot_row_groupby: ["test"],
pivot_column_groupby: ["check"],
dummyKey: "true",
},
},
},
};
const model = await createModelWithDataSource({
spreadsheetData,
mockRPC: function (route, { model, method, kwargs }) {
if (model === "partner" && method === "read_group") {
assert.step(`pop`);
assert.notOk(
["pivot_measures", "pivot_row_groupby", "pivot_column_groupby"].some(
(val) => val in (kwargs.context || {})
),
"The context should not contain pivot related keys"
);
}
},
});
await waitForDataSourcesLoaded(model);
assert.verifySteps(["pop", "pop", "pop", "pop"]);
});
QUnit.test("fetch metadata only once per model", async function (assert) {
const spreadsheetData = {
sheets: [
{
id: "sheet1",
cells: {
A1: { content: `=ODOO.PIVOT(1, "probability")` },
A2: { content: `=ODOO.PIVOT(2, "probability")` },
},
},
],
pivots: {
1: {
id: 1,
colGroupBys: ["foo"],
domain: [],
measures: [{ field: "probability", operator: "avg" }],
model: "partner",
rowGroupBys: ["bar"],
context: {},
},
2: {
id: 2,
colGroupBys: ["bar"],
domain: [],
measures: [{ field: "probability", operator: "max" }],
model: "partner",
rowGroupBys: ["foo"],
context: {},
},
},
};
const model = await createModelWithDataSource({
spreadsheetData,
mockRPC: function (route, { model, method, kwargs }) {
if (model === "partner" && method === "fields_get") {
assert.step(`${model}/${method}`);
} else if (model === "ir.model" && method === "search_read") {
assert.step(`${model}/${method}`);
}
},
});
await waitForDataSourcesLoaded(model);
assert.verifySteps(["partner/fields_get"]);
});
QUnit.test("don't fetch pivot data if no formula use it", async function (assert) {
const spreadsheetData = {
sheets: [
{
id: "sheet1",
},
{
id: "sheet2",
cells: {
A1: { content: `=ODOO.PIVOT("1", "probability")` },
},
},
],
pivots: {
1: {
id: 1,
colGroupBys: ["foo"],
domain: [],
measures: [{ field: "probability", operator: "avg" }],
model: "partner",
rowGroupBys: ["bar"],
},
},
};
const model = await createModelWithDataSource({
spreadsheetData,
mockRPC: function (route, { model, method, kwargs }) {
if (!["partner", "ir.model"].includes(model)) {
return;
}
assert.step(`${model}/${method}`);
},
});
assert.verifySteps([]);
model.dispatch("ACTIVATE_SHEET", { sheetIdFrom: "sheet1", sheetIdTo: "sheet2" });
assert.equal(getCellValue(model, "A1"), "Loading...");
await nextTick();
assert.verifySteps([
"partner/fields_get",
"partner/read_group",
"partner/read_group",
"partner/read_group",
"partner/read_group",
]);
assert.equal(getCellValue(model, "A1"), 131);
});
QUnit.test("evaluates only once when two pivots are loading", async function (assert) {
const spreadsheetData = {
sheets: [{ id: "sheet1" }],
pivots: {
1: {
id: 1,
colGroupBys: ["foo"],
domain: [],
measures: [{ field: "probability", operator: "avg" }],
model: "partner",
rowGroupBys: ["bar"],
},
2: {
id: 2,
colGroupBys: ["foo"],
domain: [],
measures: [{ field: "probability", operator: "avg" }],
model: "partner",
rowGroupBys: ["bar"],
},
},
};
const model = await createModelWithDataSource({
spreadsheetData,
});
model.config.dataSources.addEventListener("data-source-updated", () =>
assert.step("data-source-notified")
);
setCellContent(model, "A1", '=ODOO.PIVOT("1", "probability")');
setCellContent(model, "A2", '=ODOO.PIVOT("2", "probability")');
assert.equal(getCellValue(model, "A1"), "Loading...");
assert.equal(getCellValue(model, "A2"), "Loading...");
await nextTick();
assert.equal(getCellValue(model, "A1"), 131);
assert.equal(getCellValue(model, "A2"), 131);
assert.verifySteps(["data-source-notified"], "evaluation after both pivots are loaded");
});
QUnit.test("concurrently load the same pivot twice", async function (assert) {
const spreadsheetData = {
sheets: [{ id: "sheet1" }],
pivots: {
1: {
id: 1,
colGroupBys: ["foo"],
domain: [],
measures: [{ field: "probability", operator: "avg" }],
model: "partner",
rowGroupBys: ["bar"],
},
},
};
const model = await createModelWithDataSource({
spreadsheetData,
});
// the data loads first here, when we insert the first pivot function
setCellContent(model, "A1", '=ODOO.PIVOT("1", "probability")');
assert.equal(getCellValue(model, "A1"), "Loading...");
// concurrently reload the same pivot
model.dispatch("REFRESH_PIVOT", { id: 1 });
await nextTick();
assert.equal(getCellValue(model, "A1"), 131);
});
QUnit.test("display loading while data is not fully available", async function (assert) {
const metadataPromise = makeDeferred();
const dataPromise = makeDeferred();
const spreadsheetData = {
sheets: [
{
id: "sheet1",
cells: {
A1: { content: `=ODOO.PIVOT.HEADER(1, "measure", "probability")` },
A2: { content: `=ODOO.PIVOT.HEADER(1, "product_id", 37)` },
A3: { content: `=ODOO.PIVOT(1, "probability")` },
},
},
],
pivots: {
1: {
id: 1,
colGroupBys: ["product_id"],
domain: [],
measures: [{ field: "probability", operator: "avg" }],
model: "partner",
rowGroupBys: [],
},
},
};
const model = await createModelWithDataSource({
spreadsheetData,
mockRPC: async function (route, args, performRPC) {
const { model, method, kwargs } = args;
const result = await performRPC(route, args);
if (model === "partner" && method === "fields_get") {
assert.step(`${model}/${method}`);
await metadataPromise;
}
if (
model === "partner" &&
method === "read_group" &&
kwargs.groupby[0] === "product_id"
) {
assert.step(`${model}/${method}`);
await dataPromise;
}
if (model === "product" && method === "name_get") {
assert.ok(false, "should not be called because data is put in cache");
}
return result;
},
});
assert.strictEqual(getCellValue(model, "A1"), "Loading...");
assert.strictEqual(getCellValue(model, "A2"), "Loading...");
assert.strictEqual(getCellValue(model, "A3"), "Loading...");
metadataPromise.resolve();
await nextTick();
setCellContent(model, "A10", "1"); // trigger a new evaluation (might also be caused by other async formulas resolving)
assert.strictEqual(getCellValue(model, "A1"), "Loading...");
assert.strictEqual(getCellValue(model, "A2"), "Loading...");
assert.strictEqual(getCellValue(model, "A3"), "Loading...");
dataPromise.resolve();
await nextTick();
setCellContent(model, "A10", "2");
assert.strictEqual(getCellValue(model, "A1"), "Probability");
assert.strictEqual(getCellValue(model, "A2"), "xphone");
assert.strictEqual(getCellValue(model, "A3"), 131);
assert.verifySteps(["partner/fields_get", "partner/read_group"]);
});
QUnit.test("pivot grouped by char field which represents numbers", async function (assert) {
const serverData = getBasicServerData();
serverData.models.partner.records = [
{ id: 1, name: "111", probability: 11 },
{ id: 2, name: "000111", probability: 15 },
];
const { model } = await createSpreadsheetWithPivot({
serverData,
arch: /*xml*/ `
<pivot>
<field name="name" type="row"/>
<field name="probability" type="measure"/>
</pivot>`,
});
const A3 = getCell(model, "A3");
const A4 = getCell(model, "A4");
assert.strictEqual(A3.content, '=ODOO.PIVOT.HEADER(1,"name","000111")');
assert.strictEqual(A4.content, '=ODOO.PIVOT.HEADER(1,"name",111)');
assert.strictEqual(A3.evaluated.value, "000111");
assert.strictEqual(A4.evaluated.value, "111");
const B3 = getCell(model, "B3");
const B4 = getCell(model, "B4");
assert.strictEqual(B3.content, '=ODOO.PIVOT(1,"probability","name","000111")');
assert.strictEqual(B4.content, '=ODOO.PIVOT(1,"probability","name",111)');
assert.strictEqual(B3.evaluated.value, 15);
assert.strictEqual(B4.evaluated.value, 11);
});
QUnit.test("relational PIVOT.HEADER with missing id", async function (assert) {
assert.expect(1);
const { model } = await createSpreadsheetWithPivot({
arch: /*xml*/ `
<pivot>
<field name="product_id" type="col"/>
<field name="bar" type="row"/>
<field name="probability" type="measure"/>
</pivot>`,
});
const sheetId = model.getters.getActiveSheetId();
model.dispatch("UPDATE_CELL", {
col: 4,
row: 9,
content: `=ODOO.PIVOT.HEADER("1", "product_id", "1111111")`,
sheetId,
});
await waitForDataSourcesLoaded(model);
assert.equal(
getCell(model, "E10").evaluated.error.message,
"Unable to fetch the label of 1111111 of model product"
);
});
QUnit.test("relational PIVOT.HEADER with undefined id", async function (assert) {
assert.expect(2);
const { model } = await createSpreadsheetWithPivot({
arch: /*xml*/ `
<pivot>
<field name="foo" type="col"/>
<field name="product_id" type="row"/>
<field name="probability" type="measure"/>
</pivot>`,
});
setCellContent(model, "F10", `=ODOO.PIVOT.HEADER("1", "product_id", A25)`);
assert.equal(getCell(model, "A25"), null, "the cell should be empty");
await waitForDataSourcesLoaded(model);
assert.equal(getCellValue(model, "F10"), "None");
});
QUnit.test("Verify pivot measures are correctly computed :)", async function (assert) {
assert.expect(4);
const { model } = await createSpreadsheetWithPivot();
assert.equal(getCellValue(model, "B4"), 11);
assert.equal(getCellValue(model, "C3"), 15);
assert.equal(getCellValue(model, "D4"), 10);
assert.equal(getCellValue(model, "E4"), 95);
});
QUnit.test("can import/export sorted pivot", async (assert) => {
const spreadsheetData = {
pivots: {
1: {
id: "1",
colGroupBys: ["foo"],
domain: [],
measures: [{ field: "probability" }],
model: "partner",
rowGroupBys: ["bar"],
sortedColumn: {
measure: "probability",
order: "asc",
groupId: [[], [1]],
},
name: "A pivot",
context: {},
fieldMatching: {},
},
},
};
const model = await createModelWithDataSource({ spreadsheetData });
assert.deepEqual(model.getters.getPivotDefinition(1).sortedColumn, {
measure: "probability",
order: "asc",
groupId: [[], [1]],
});
assert.deepEqual(model.exportData().pivots, spreadsheetData.pivots);
});
QUnit.test("Can group by many2many field ", async (assert) => {
const { model } = await createSpreadsheetWithPivot({
arch: /* xml */ `
<pivot>
<field name="foo" type="col"/>
<field name="tag_ids" type="row"/>
<field name="probability" type="measure"/>
</pivot>`,
});
assert.equal(getCellFormula(model, "A3"), '=ODOO.PIVOT.HEADER(1,"tag_ids","false")');
assert.equal(getCellFormula(model, "A4"), '=ODOO.PIVOT.HEADER(1,"tag_ids",42)');
assert.equal(getCellFormula(model, "A5"), '=ODOO.PIVOT.HEADER(1,"tag_ids",67)');
assert.equal(
getCellFormula(model, "B3"),
'=ODOO.PIVOT(1,"probability","tag_ids","false","foo",1)'
);
assert.equal(
getCellFormula(model, "B4"),
'=ODOO.PIVOT(1,"probability","tag_ids",42,"foo",1)'
);
assert.equal(
getCellFormula(model, "B5"),
'=ODOO.PIVOT(1,"probability","tag_ids",67,"foo",1)'
);
assert.equal(
getCellFormula(model, "C3"),
'=ODOO.PIVOT(1,"probability","tag_ids","false","foo",2)'
);
assert.equal(
getCellFormula(model, "C4"),
'=ODOO.PIVOT(1,"probability","tag_ids",42,"foo",2)'
);
assert.equal(
getCellFormula(model, "C5"),
'=ODOO.PIVOT(1,"probability","tag_ids",67,"foo",2)'
);
assert.equal(getCellValue(model, "A3"), "None");
assert.equal(getCellValue(model, "A4"), "isCool");
assert.equal(getCellValue(model, "A5"), "Growing");
assert.equal(getCellValue(model, "B3"), "");
assert.equal(getCellValue(model, "B4"), "11");
assert.equal(getCellValue(model, "B5"), "11");
assert.equal(getCellValue(model, "C3"), "");
assert.equal(getCellValue(model, "C4"), "15");
assert.equal(getCellValue(model, "C5"), "");
});
QUnit.test("PIVOT formulas are correctly formatted at evaluation", async function (assert) {
const { model } = await createSpreadsheetWithPivot({
arch: /* xml */ `
<pivot>
<field name="product_id" type="col"/>
<field name="name" type="row"/>
<field name="foo" type="measure"/>
<field name="probability" type="measure"/>
</pivot>`,
});
assert.strictEqual(getCell(model, "B3").evaluated.format, "0");
assert.strictEqual(getCell(model, "C3").evaluated.format, "#,##0.00");
});
QUnit.test(
"PIVOT formulas with monetary measure are correctly formatted at evaluation",
async function (assert) {
const { model } = await createSpreadsheetWithPivot({
arch: /* xml */ `
<pivot>
<field name="product_id" type="col"/>
<field name="name" type="row"/>
<field name="pognon" type="measure"/>
</pivot>`,
});
assert.strictEqual(getCell(model, "B3").evaluated.format, "#,##0.00[$€]");
}
);
QUnit.test(
"PIVOT.HEADER formulas are correctly formatted at evaluation",
async function (assert) {
const { model } = await createSpreadsheetWithPivot({
arch: /* xml */ `
<pivot>
<field name="date" interval="day" type="col"/>
<field name="probability" type="row"/>
<field name="foo" type="measure"/>
</pivot>`,
});
assert.strictEqual(getCell(model, "A3").evaluated.format, "#,##0.00");
assert.strictEqual(getCell(model, "B1").evaluated.format, "mm/dd/yyyy");
assert.strictEqual(getCell(model, "B2").evaluated.format, undefined);
}
);
QUnit.test("can edit pivot domain", async (assert) => {
const { model } = await createSpreadsheetWithPivot();
const [pivotId] = model.getters.getPivotIds();
assert.deepEqual(model.getters.getPivotDefinition(pivotId).domain, []);
assert.strictEqual(getCellValue(model, "B4"), 11);
model.dispatch("UPDATE_ODOO_PIVOT_DOMAIN", {
pivotId,
domain: [["foo", "in", [55]]],
});
assert.deepEqual(model.getters.getPivotDefinition(pivotId).domain, [["foo", "in", [55]]]);
await waitForDataSourcesLoaded(model);
assert.strictEqual(getCellValue(model, "B4"), "");
model.dispatch("REQUEST_UNDO");
await waitForDataSourcesLoaded(model);
assert.deepEqual(model.getters.getPivotDefinition(pivotId).domain, []);
await waitForDataSourcesLoaded(model);
assert.strictEqual(getCellValue(model, "B4"), 11);
model.dispatch("REQUEST_REDO");
assert.deepEqual(model.getters.getPivotDefinition(pivotId).domain, [["foo", "in", [55]]]);
await waitForDataSourcesLoaded(model);
assert.strictEqual(getCellValue(model, "B4"), "");
});
QUnit.test("edited domain is exported", async (assert) => {
const { model } = await createSpreadsheetWithPivot();
const [pivotId] = model.getters.getPivotIds();
model.dispatch("UPDATE_ODOO_PIVOT_DOMAIN", {
pivotId,
domain: [["foo", "in", [55]]],
});
assert.deepEqual(model.exportData().pivots["1"].domain, [["foo", "in", [55]]]);
});
QUnit.test("field matching is removed when filter is deleted", async function (assert) {
const { model } = await createSpreadsheetWithPivot();
await addGlobalFilter(
model,
{
filter: {
id: "42",
type: "relation",
label: "test",
defaultValue: [41],
modelName: undefined,
rangeType: undefined,
},
},
{
pivot: { 1: { chain: "product_id", type: "many2one" } },
}
);
const [filter] = model.getters.getGlobalFilters();
const matching = {
chain: "product_id",
type: "many2one",
};
assert.deepEqual(model.getters.getPivotFieldMatching("1", filter.id), matching);
assert.deepEqual(model.getters.getPivotDataSource("1").getComputedDomain(), [
["product_id", "in", [41]],
]);
model.dispatch("REMOVE_GLOBAL_FILTER", {
id: filter.id,
});
assert.deepEqual(
model.getters.getPivotFieldMatching("1", filter.id),
undefined,
"it should have removed the pivot and its fieldMatching and datasource altogether"
);
assert.deepEqual(model.getters.getPivotDataSource("1").getComputedDomain(), []);
model.dispatch("REQUEST_UNDO");
assert.deepEqual(model.getters.getPivotFieldMatching("1", filter.id), matching);
assert.deepEqual(model.getters.getPivotDataSource("1").getComputedDomain(), [
["product_id", "in", [41]],
]);
model.dispatch("REQUEST_REDO");
assert.deepEqual(model.getters.getPivotFieldMatching("1", filter.id), undefined);
assert.deepEqual(model.getters.getPivotDataSource("1").getComputedDomain(), []);
});
QUnit.test(
"Load pivot spreadsheet with models that cannot be accessed",
async function (assert) {
let hasAccessRights = true;
const { model } = await createSpreadsheetWithPivot({
mockRPC: async function (route, args) {
if (
args.model === "partner" &&
args.method === "read_group" &&
!hasAccessRights
) {
const error = new RPCError();
error.data = { message: "ya done!" };
throw error;
}
},
});
const headerCell = getCell(model, "A3");
const cell = getCell(model, "C3");
await waitForDataSourcesLoaded(model);
assert.equal(headerCell.evaluated.value, "No");
assert.equal(cell.evaluated.value, 15);
hasAccessRights = false;
model.dispatch("REFRESH_PIVOT", { id: "1" });
await waitForDataSourcesLoaded(model);
assert.equal(headerCell.evaluated.value, "#ERROR");
assert.equal(headerCell.evaluated.error.message, "ya done!");
assert.equal(cell.evaluated.value, "#ERROR");
assert.equal(cell.evaluated.error.message, "ya done!");
}
);
QUnit.test("date are between two years are correctly grouped by weeks", async (assert) => {
const serverData = getBasicServerData();
serverData.models.partner.records= [
{ active: true, id: 5, foo: 11, bar: true, product_id: 37, date: "2024-01-03" },
{ active: true, id: 6, foo: 12, bar: true, product_id: 41, date: "2024-12-30" },
{ active: true, id: 7, foo: 13, bar: true, product_id: 37, date: "2024-12-31" },
{ active: true, id: 8, foo: 14, bar: true, product_id: 37, date: "2025-01-01" }
];
const { model } = await createSpreadsheetWithPivot({
serverData,
arch: /*xml*/ `
<pivot string="Partners">
<field name="date:year" type="col"/>
<field name="date:week" type="col"/>
<field name="foo" type="measure"/>
</pivot>`,
});
assert.equal(getCellFormattedValue(model,"B1"),"2024");
assert.equal(getCellFormattedValue(model,"B2"),"W1 2024");
assert.equal(getCellFormattedValue(model,"B4"),"11");
assert.equal(getCellFormattedValue(model,"C2"),"W1 2025");
assert.equal(getCellFormattedValue(model,"C4"),"25");
assert.equal(getCellFormattedValue(model,"D1"),"2025");
assert.equal(getCellFormattedValue(model,"D2"),"W1 2025");
assert.equal(getCellFormattedValue(model,"D4"),"14");
});
QUnit.test("date are between two years are correctly grouped by weeks and days", async (assert) => {
const serverData = getBasicServerData();
serverData.models.partner.records= [
{ active: true, id: 5, foo: 11, bar: true, product_id: 37, date: "2024-01-03" },
{ active: true, id: 6, foo: 12, bar: true, product_id: 41, date: "2024-12-30" },
{ active: true, id: 7, foo: 13, bar: true, product_id: 37, date: "2024-12-31" },
{ active: true, id: 8, foo: 14, bar: true, product_id: 37, date: "2025-01-01" }
];
const { model } = await createSpreadsheetWithPivot({
serverData,
arch: /*xml*/ `
<pivot string="Partners">
<field name="date:year" type="col"/>
<field name="date:week" type="col"/>
<field name="date:day" type="col"/>
<field name="foo" type="measure"/>
</pivot>`,
});
assert.equal(getCellFormattedValue(model,"B1"),"2024");
assert.equal(getCellFormattedValue(model,"B2"),"W1 2024");
assert.equal(getCellFormattedValue(model,"B3"),"01/03/2024");
assert.equal(getCellFormattedValue(model,"B5"),"11");
assert.equal(getCellFormattedValue(model,"C2"),"W1 2025");
assert.equal(getCellFormattedValue(model,"C3"),"12/30/2024");
assert.equal(getCellFormattedValue(model,"C5"),"12");
assert.equal(getCellFormattedValue(model,"D3"),"12/31/2024");
assert.equal(getCellFormattedValue(model,"D5"),"13");
assert.equal(getCellFormattedValue(model,"E1"),"2025");
assert.equal(getCellFormattedValue(model,"E2"),"W1 2025");
assert.equal(getCellFormattedValue(model,"E3"),"01/01/2025");
assert.equal(getCellFormattedValue(model,"E5"),"14");
});
});

View file

@ -0,0 +1,377 @@
import { describe, expect, test } from "@odoo/hoot";
import {
defineSpreadsheetActions,
defineSpreadsheetModels,
} from "@spreadsheet/../tests/helpers/data";
import { setCellContent, updatePivot } from "@spreadsheet/../tests/helpers/commands";
import { getCellValue, getEvaluatedCell } from "@spreadsheet/../tests/helpers/getters";
import { createSpreadsheetWithPivot } from "@spreadsheet/../tests/helpers/pivot";
import { createModelWithDataSource } from "@spreadsheet/../tests/helpers/model";
import { waitForDataLoaded } from "@spreadsheet/helpers/model";
describe.current.tags("headless");
defineSpreadsheetModels();
defineSpreadsheetActions();
test("Can have positional args in pivot formula", async function () {
const { model } = await createSpreadsheetWithPivot();
// Columns
setCellContent(model, "H1", `=PIVOT.VALUE(1,"probability:avg","#foo", 1)`);
setCellContent(model, "H2", `=PIVOT.VALUE(1,"probability:avg","#foo", 2)`);
setCellContent(model, "H3", `=PIVOT.VALUE(1,"probability:avg","#foo", 3)`);
setCellContent(model, "H4", `=PIVOT.VALUE(1,"probability:avg","#foo", 4)`);
setCellContent(model, "H5", `=PIVOT.VALUE(1,"probability:avg","#foo", 5)`);
expect(getCellValue(model, "H1")).toBe(11);
expect(getCellValue(model, "H2")).toBe(15);
expect(getCellValue(model, "H3")).toBe(10);
expect(getCellValue(model, "H4")).toBe(95);
expect(getCellValue(model, "H5")).toBe("");
// Rows
setCellContent(model, "I1", `=PIVOT.VALUE(1,"probability:avg","#bar", 1)`);
setCellContent(model, "I2", `=PIVOT.VALUE(1,"probability:avg","#bar", 2)`);
setCellContent(model, "I3", `=PIVOT.VALUE(1,"probability:avg","#bar", 3)`);
expect(getCellValue(model, "I1")).toBe(15);
expect(getCellValue(model, "I2")).toBe(116);
expect(getCellValue(model, "I3")).toBe("");
});
test("Can have positional args in pivot headers formula", async function () {
const { model } = await createSpreadsheetWithPivot();
// Columns
setCellContent(model, "H1", `=PIVOT.HEADER(1,"#foo",1)`);
setCellContent(model, "H2", `=PIVOT.HEADER(1,"#foo",2)`);
setCellContent(model, "H3", `=PIVOT.HEADER(1,"#foo",3)`);
setCellContent(model, "H4", `=PIVOT.HEADER(1,"#foo",4)`);
setCellContent(model, "H5", `=PIVOT.HEADER(1,"#foo",5)`);
setCellContent(model, "H6", `=PIVOT.HEADER(1,"#foo",5, "measure", "probability:avg")`);
expect(getCellValue(model, "H1")).toBe(1);
expect(getCellValue(model, "H2")).toBe(2);
expect(getCellValue(model, "H3")).toBe(12);
expect(getCellValue(model, "H4")).toBe(17);
expect(getCellValue(model, "H5")).toBe("");
expect(getCellValue(model, "H6")).toBe("Probability");
// Rows
setCellContent(model, "I1", `=PIVOT.HEADER(1,"#bar",1)`);
setCellContent(model, "I2", `=PIVOT.HEADER(1,"#bar",2)`);
setCellContent(model, "I3", `=PIVOT.HEADER(1,"#bar",3)`);
setCellContent(model, "I4", `=PIVOT.HEADER(1,"#bar",3, "measure", "probability:avg")`);
expect(getCellValue(model, "I1")).toBe("No");
expect(getCellValue(model, "I2")).toBe("Yes");
expect(getCellValue(model, "I3")).toBe("");
expect(getCellValue(model, "I4")).toBe("Probability");
});
test("pivot positional with two levels of group bys in rows", async () => {
const { model } = await createSpreadsheetWithPivot({
arch: /*xml*/ `
<pivot>
<field name="bar" type="row"/>
<field name="product_id" type="row"/>
<field name="foo" type="col"/>
<field name="probability" type="measure"/>
</pivot>`,
});
// Rows Headers
setCellContent(model, "H1", `=PIVOT.HEADER(1,"bar","false","#product_id",1)`);
setCellContent(model, "H2", `=PIVOT.HEADER(1,"bar","true","#product_id",1)`);
setCellContent(model, "H3", `=PIVOT.HEADER(1,"#bar",1,"#product_id",1)`);
setCellContent(model, "H4", `=PIVOT.HEADER(1,"#bar",2,"#product_id",1)`);
setCellContent(model, "H5", `=PIVOT.HEADER(1,"#bar",3,"#product_id",1)`);
expect(getCellValue(model, "H1")).toBe("xpad");
expect(getCellValue(model, "H2")).toBe("xphone");
expect(getCellValue(model, "H3")).toBe("xpad");
expect(getCellValue(model, "H4")).toBe("xphone");
expect(getCellValue(model, "H5")).toBe("");
// Cells
setCellContent(
model,
"H1",
`=PIVOT.VALUE(1,"probability:avg","#bar",1,"#product_id",1,"#foo",2)`
);
setCellContent(
model,
"H2",
`=PIVOT.VALUE(1,"probability:avg","#bar",1,"#product_id",2,"#foo",2)`
);
expect(getCellValue(model, "H1")).toBe(15);
expect(getCellValue(model, "H2")).toBe("");
});
test("Positional argument without a number should crash", async () => {
const { model } = await createSpreadsheetWithPivot();
setCellContent(model, "A10", `=PIVOT.HEADER(1,"#bar","this is not a number")`);
expect(getCellValue(model, "A10")).toBe("#ERROR");
expect(getEvaluatedCell(model, "A10").message).toBe(
"The function PIVOT.HEADER expects a number value, but 'this is not a number' is a string, and cannot be coerced to a number."
);
});
test("sort first pivot column (ascending)", async () => {
const spreadsheetData = {
pivots: {
1: {
type: "ODOO",
columns: [{ fieldName: "foo" }],
rows: [{ fieldName: "bar" }],
domain: [],
measures: [{ id: "probability:sum", fieldName: "probability", aggregator: "sum" }],
model: "partner",
sortedColumn: {
domain: [{ field: "foo", type: "integer", value: 1 }],
measure: "probability:sum",
order: "asc",
},
},
},
};
const { model } = await createModelWithDataSource({ spreadsheetData });
setCellContent(model, "A1", `=PIVOT.HEADER(1,"#bar",1)`);
setCellContent(model, "A2", `=PIVOT.HEADER(1,"#bar",2)`);
setCellContent(model, "B1", `=PIVOT.VALUE(1,"probability:sum","#bar",1,"#foo",1)`);
setCellContent(model, "B2", `=PIVOT.VALUE(1,"probability:sum","#bar",2,"#foo",1)`);
setCellContent(model, "C1", `=PIVOT.VALUE(1,"probability:sum","#bar",1,"#foo",2)`);
setCellContent(model, "C2", `=PIVOT.VALUE(1,"probability:sum","#bar",2,"#foo",2)`);
setCellContent(model, "D1", `=PIVOT.VALUE(1,"probability:sum","#bar",1)`);
setCellContent(model, "D2", `=PIVOT.VALUE(1,"probability:sum","#bar",2)`);
await waitForDataLoaded(model);
expect(getCellValue(model, "A1")).toBe("No");
expect(getCellValue(model, "A2")).toBe("Yes");
expect(getCellValue(model, "B1")).toBe("");
expect(getCellValue(model, "B2")).toBe(11);
expect(getCellValue(model, "C1")).toBe(15);
expect(getCellValue(model, "C2")).toBe("");
expect(getCellValue(model, "D1")).toBe(15);
expect(getCellValue(model, "D2")).toBe(116);
});
test("sort first pivot column (descending)", async () => {
const spreadsheetData = {
pivots: {
1: {
type: "ODOO",
columns: [{ fieldName: "foo" }],
rows: [{ fieldName: "bar" }],
domain: [],
measures: [{ id: "probability:sum", fieldName: "probability", aggregator: "sum" }],
model: "partner",
sortedColumn: {
domain: [{ field: "foo", type: "integer", value: 1 }],
measure: "probability:sum",
order: "desc",
},
},
},
};
const { model } = await createModelWithDataSource({ spreadsheetData });
setCellContent(model, "A1", `=PIVOT.HEADER(1,"#bar",1)`);
setCellContent(model, "A2", `=PIVOT.HEADER(1,"#bar",2)`);
setCellContent(model, "B1", `=PIVOT.VALUE(1,"probability:sum","#bar",1,"#foo",1)`);
setCellContent(model, "B2", `=PIVOT.VALUE(1,"probability:sum","#bar",2,"#foo",1)`);
setCellContent(model, "C1", `=PIVOT.VALUE(1,"probability:sum","#bar",1,"#foo",2)`);
setCellContent(model, "C2", `=PIVOT.VALUE(1,"probability:sum","#bar",2,"#foo",2)`);
setCellContent(model, "D1", `=PIVOT.VALUE(1,"probability:sum","#bar",1)`);
setCellContent(model, "D2", `=PIVOT.VALUE(1,"probability:sum","#bar",2)`);
await waitForDataLoaded(model);
expect(getCellValue(model, "A1")).toBe("Yes");
expect(getCellValue(model, "A2")).toBe("No");
expect(getCellValue(model, "B1")).toBe(11);
expect(getCellValue(model, "B2")).toBe("");
expect(getCellValue(model, "C1")).toBe("");
expect(getCellValue(model, "C2")).toBe(15);
expect(getCellValue(model, "D1")).toBe(116);
expect(getCellValue(model, "D2")).toBe(15);
});
test("sort second pivot column (ascending)", async () => {
const spreadsheetData = {
pivots: {
1: {
type: "ODOO",
columns: [{ fieldName: "foo" }],
domain: [],
measures: [{ id: "probability:sum", fieldName: "probability", aggregator: "sum" }],
model: "partner",
rows: [{ fieldName: "bar" }],
name: "Partners by Foo",
sortedColumn: {
domain: [{ field: "foo", type: "integer", value: 2 }],
measure: "probability:sum",
order: "asc",
},
},
},
};
const { model } = await createModelWithDataSource({ spreadsheetData });
setCellContent(model, "A1", `=PIVOT.HEADER(1,"#bar",1)`);
setCellContent(model, "A2", `=PIVOT.HEADER(1,"#bar",2)`);
setCellContent(model, "B1", `=PIVOT.VALUE(1,"probability:sum","#bar",1,"#foo",1)`);
setCellContent(model, "B2", `=PIVOT.VALUE(1,"probability:sum","#bar",2,"#foo",1)`);
setCellContent(model, "C1", `=PIVOT.VALUE(1,"probability:sum","#bar",1,"#foo",2)`);
setCellContent(model, "C2", `=PIVOT.VALUE(1,"probability:sum","#bar",2,"#foo",2)`);
setCellContent(model, "D1", `=PIVOT.VALUE(1,"probability:sum","#bar",1)`);
setCellContent(model, "D2", `=PIVOT.VALUE(1,"probability:sum","#bar",2)`);
await waitForDataLoaded(model);
expect(getCellValue(model, "A1")).toBe("Yes");
expect(getCellValue(model, "A2")).toBe("No");
expect(getCellValue(model, "B1")).toBe(11);
expect(getCellValue(model, "B2")).toBe("");
expect(getCellValue(model, "C1")).toBe("");
expect(getCellValue(model, "C2")).toBe(15);
expect(getCellValue(model, "D1")).toBe(116);
expect(getCellValue(model, "D2")).toBe(15);
});
test("sort second pivot column (descending)", async () => {
const spreadsheetData = {
pivots: {
1: {
type: "ODOO",
columns: [{ fieldName: "foo" }],
domain: [],
measures: [{ id: "probability:sum", fieldName: "probability", aggregator: "sum" }],
model: "partner",
rows: [{ fieldName: "bar" }],
name: "Partners by Foo",
sortedColumn: {
domain: [{ field: "foo", type: "integer", value: 2 }],
measure: "probability:sum",
order: "desc",
},
},
},
};
const { model } = await createModelWithDataSource({ spreadsheetData });
setCellContent(model, "A1", `=PIVOT.HEADER(1,"#bar",1)`);
setCellContent(model, "A2", `=PIVOT.HEADER(1,"#bar",2)`);
setCellContent(model, "B1", `=PIVOT.VALUE(1,"probability:sum","#bar",1,"#foo",1)`);
setCellContent(model, "B2", `=PIVOT.VALUE(1,"probability:sum","#bar",2,"#foo",1)`);
setCellContent(model, "C1", `=PIVOT.VALUE(1,"probability:sum","#bar",1,"#foo",2)`);
setCellContent(model, "C2", `=PIVOT.VALUE(1,"probability:sum","#bar",2,"#foo",2)`);
setCellContent(model, "D1", `=PIVOT.VALUE(1,"probability:sum","#bar",1)`);
setCellContent(model, "D2", `=PIVOT.VALUE(1,"probability:sum","#bar",2)`);
await waitForDataLoaded(model);
expect(getCellValue(model, "A1")).toBe("No");
expect(getCellValue(model, "A2")).toBe("Yes");
expect(getCellValue(model, "B1")).toBe("");
expect(getCellValue(model, "B2")).toBe(11);
expect(getCellValue(model, "C1")).toBe(15);
expect(getCellValue(model, "C2")).toBe("");
expect(getCellValue(model, "D1")).toBe(15);
expect(getCellValue(model, "D2")).toBe(116);
});
test("sort second pivot measure (ascending)", async () => {
const spreadsheetData = {
pivots: {
1: {
type: "ODOO",
rows: [{ fieldName: "product_id" }],
columns: [],
domain: [],
measures: [
{ id: "probability:sum", fieldName: "probability", aggregator: "sum" },
{ id: "foo:sum", fieldName: "foo", aggregator: "sum" },
],
model: "partner",
sortedColumn: {
domain: [],
measure: "foo:sum",
order: "asc",
},
},
},
};
const { model } = await createModelWithDataSource({ spreadsheetData });
setCellContent(model, "A10", `=PIVOT.HEADER(1,"#product_id",1)`);
setCellContent(model, "A11", `=PIVOT.HEADER(1,"#product_id",2)`);
setCellContent(model, "B10", `=PIVOT.VALUE(1,"probability:sum","#product_id",1)`);
setCellContent(model, "B11", `=PIVOT.VALUE(1,"probability:sum","#product_id",2)`);
setCellContent(model, "C10", `=PIVOT.VALUE(1,"foo:sum","#product_id",1)`);
setCellContent(model, "C11", `=PIVOT.VALUE(1,"foo:sum","#product_id",2)`);
await waitForDataLoaded(model);
expect(getCellValue(model, "A10")).toBe("xphone");
expect(getCellValue(model, "A11")).toBe("xpad");
expect(getCellValue(model, "B10")).toBe(10);
expect(getCellValue(model, "B11")).toBe(121);
expect(getCellValue(model, "C10")).toBe(12);
expect(getCellValue(model, "C11")).toBe(20);
});
test("sort second pivot measure (descending)", async () => {
const spreadsheetData = {
pivots: {
1: {
type: "ODOO",
columns: [],
domain: [],
measures: [
{ id: "probability:sum", fieldName: "probability", aggregator: "sum" },
{ id: "foo:sum", fieldName: "foo", aggregator: "sum" },
],
model: "partner",
rows: [{ fieldName: "product_id" }],
sortedColumn: {
domain: [],
measure: "foo:sum",
order: "desc",
},
},
},
};
const { model } = await createModelWithDataSource({ spreadsheetData });
setCellContent(model, "A10", `=PIVOT.HEADER(1,"#product_id",1)`);
setCellContent(model, "A11", `=PIVOT.HEADER(1,"#product_id",2)`);
setCellContent(model, "B10", `=PIVOT.VALUE(1,"probability:sum","#product_id",1)`);
setCellContent(model, "B11", `=PIVOT.VALUE(1,"probability:sum","#product_id",2)`);
setCellContent(model, "C10", `=PIVOT.VALUE(1,"foo:sum","#product_id",1)`);
setCellContent(model, "C11", `=PIVOT.VALUE(1,"foo:sum","#product_id",2)`);
await waitForDataLoaded(model);
expect(getCellValue(model, "A10")).toBe("xpad");
expect(getCellValue(model, "A11")).toBe("xphone");
expect(getCellValue(model, "B10")).toBe(121);
expect(getCellValue(model, "B11")).toBe(10);
expect(getCellValue(model, "C10")).toBe(20);
expect(getCellValue(model, "C11")).toBe(12);
});
test("Formatting a pivot positional preserves the interval", async () => {
const { model } = await createSpreadsheetWithPivot({
arch: /*xml*/ `
<pivot>
<field name="date:day" type="row"/>
<field name="probability" type="measure"/>
</pivot>`,
});
setCellContent(model, "A1", `=PIVOT.HEADER(1,"#date:day",1)`);
expect(getEvaluatedCell(model, "A1").formattedValue).toBe("14 Apr 2016");
});
test("pivot positional formula with collapsed pivot", async () => {
const { model } = await createSpreadsheetWithPivot({
arch: /*xml*/ `
<pivot>
<field name="date" interval="year" type="row"/>
<field name="date" interval="month" type="row"/>
<field name="probability" type="measure"/>
</pivot>`,
});
const pivotId = model.getters.getPivotIds()[0];
updatePivot(model, pivotId, {
collapsedDomains: {
ROW: [[{ field: "date:year", value: 2016, type: "date" }]],
COL: [],
},
});
setCellContent(model, "H1", `=PIVOT.HEADER(1,"#date:year",1,"#date:month",1)`);
expect(getEvaluatedCell(model, "H1").formattedValue).toBe("April 2016");
setCellContent(model, "H2", `=PIVOT.HEADER(1,"#date:year",1,"#date:month",2)`);
expect(getEvaluatedCell(model, "H2").formattedValue).toBe("October 2016");
setCellContent(model, "H3", `=PIVOT.HEADER(1,"#date:year",1,"#date:month",3)`);
expect(getEvaluatedCell(model, "H3").formattedValue).toBe("December 2016");
});

View file

@ -1,343 +0,0 @@
/** @odoo-module */
import { setCellContent } from "@spreadsheet/../tests/utils/commands";
import { getCell, getCellValue } from "@spreadsheet/../tests/utils/getters";
import { createSpreadsheetWithPivot } from "@spreadsheet/../tests/utils/pivot";
import {
createModelWithDataSource,
waitForDataSourcesLoaded,
} from "@spreadsheet/../tests/utils/model";
QUnit.module("spreadsheet > positional pivot formula", {}, () => {
QUnit.test("Can have positional args in pivot formula", async function (assert) {
const { model } = await createSpreadsheetWithPivot();
// Columns
setCellContent(model, "H1", `=ODOO.PIVOT(1,"probability","#foo", 1)`);
setCellContent(model, "H2", `=ODOO.PIVOT(1,"probability","#foo", 2)`);
setCellContent(model, "H3", `=ODOO.PIVOT(1,"probability","#foo", 3)`);
setCellContent(model, "H4", `=ODOO.PIVOT(1,"probability","#foo", 4)`);
setCellContent(model, "H5", `=ODOO.PIVOT(1,"probability","#foo", 5)`);
assert.strictEqual(getCellValue(model, "H1"), 11);
assert.strictEqual(getCellValue(model, "H2"), 15);
assert.strictEqual(getCellValue(model, "H3"), 10);
assert.strictEqual(getCellValue(model, "H4"), 95);
assert.strictEqual(getCellValue(model, "H5"), "");
// Rows
setCellContent(model, "I1", `=ODOO.PIVOT(1,"probability","#bar", 1)`);
setCellContent(model, "I2", `=ODOO.PIVOT(1,"probability","#bar", 2)`);
setCellContent(model, "I3", `=ODOO.PIVOT(1,"probability","#bar", 3)`);
assert.strictEqual(getCellValue(model, "I1"), 15);
assert.strictEqual(getCellValue(model, "I2"), 116);
assert.strictEqual(getCellValue(model, "I3"), "");
});
QUnit.test("Can have positional args in pivot headers formula", async function (assert) {
const { model } = await createSpreadsheetWithPivot();
// Columns
setCellContent(model, "H1", `=ODOO.PIVOT.HEADER(1,"#foo",1)`);
setCellContent(model, "H2", `=ODOO.PIVOT.HEADER(1,"#foo",2)`);
setCellContent(model, "H3", `=ODOO.PIVOT.HEADER(1,"#foo",3)`);
setCellContent(model, "H4", `=ODOO.PIVOT.HEADER(1,"#foo",4)`);
setCellContent(model, "H5", `=ODOO.PIVOT.HEADER(1,"#foo",5)`);
setCellContent(model, "H6", `=ODOO.PIVOT.HEADER(1,"#foo",5, "measure", "probability")`);
assert.strictEqual(getCellValue(model, "H1"), 1);
assert.strictEqual(getCellValue(model, "H2"), 2);
assert.strictEqual(getCellValue(model, "H3"), 12);
assert.strictEqual(getCellValue(model, "H4"), 17);
assert.strictEqual(getCellValue(model, "H5"), "");
assert.strictEqual(getCellValue(model, "H6"), "Probability");
// Rows
setCellContent(model, "I1", `=ODOO.PIVOT.HEADER(1,"#bar",1)`);
setCellContent(model, "I2", `=ODOO.PIVOT.HEADER(1,"#bar",2)`);
setCellContent(model, "I3", `=ODOO.PIVOT.HEADER(1,"#bar",3)`);
setCellContent(model, "I4", `=ODOO.PIVOT.HEADER(1,"#bar",3, "measure", "probability")`);
assert.strictEqual(getCellValue(model, "I1"), "No");
assert.strictEqual(getCellValue(model, "I2"), "Yes");
assert.strictEqual(getCellValue(model, "I3"), "");
assert.strictEqual(getCellValue(model, "I4"), "Probability");
});
QUnit.test("pivot positional with two levels of group bys in rows", async (assert) => {
const { model } = await createSpreadsheetWithPivot({
arch: /*xml*/ `
<pivot>
<field name="bar" type="row"/>
<field name="product_id" type="row"/>
<field name="foo" type="col"/>
<field name="probability" type="measure"/>
</pivot>`,
});
// Rows Headers
setCellContent(model, "H1", `=ODOO.PIVOT.HEADER(1,"bar","false","#product_id",1)`);
setCellContent(model, "H2", `=ODOO.PIVOT.HEADER(1,"bar","true","#product_id",1)`);
setCellContent(model, "H3", `=ODOO.PIVOT.HEADER(1,"#bar",1,"#product_id",1)`);
setCellContent(model, "H4", `=ODOO.PIVOT.HEADER(1,"#bar",2,"#product_id",1)`);
setCellContent(model, "H5", `=ODOO.PIVOT.HEADER(1,"#bar",3,"#product_id",1)`);
assert.strictEqual(getCellValue(model, "H1"), "xpad");
assert.strictEqual(getCellValue(model, "H2"), "xphone");
assert.strictEqual(getCellValue(model, "H3"), "xpad");
assert.strictEqual(getCellValue(model, "H4"), "xphone");
assert.strictEqual(getCellValue(model, "H5"), "");
// Cells
setCellContent(
model,
"H1",
`=ODOO.PIVOT(1,"probability","#bar",1,"#product_id",1,"#foo",2)`
);
setCellContent(
model,
"H2",
`=ODOO.PIVOT(1,"probability","#bar",1,"#product_id",2,"#foo",2)`
);
assert.strictEqual(getCellValue(model, "H1"), 15);
assert.strictEqual(getCellValue(model, "H2"), "");
});
QUnit.test("Positional argument without a number should crash", async (assert) => {
const { model } = await createSpreadsheetWithPivot();
setCellContent(model, "A10", `=ODOO.PIVOT.HEADER(1,"#bar","this is not a number")`);
assert.strictEqual(getCellValue(model, "A10"), "#ERROR");
assert.strictEqual(
getCell(model, "A10").evaluated.error.message,
"The function ODOO.PIVOT.HEADER expects a number value, but 'this is not a number' is a string, and cannot be coerced to a number."
);
});
QUnit.test("sort first pivot column (ascending)", async (assert) => {
const spreadsheetData = {
pivots: {
1: {
colGroupBys: ["foo"],
rowGroupBys: ["bar"],
domain: [],
id: "1",
measures: [{ field: "probability" }],
model: "partner",
sortedColumn: {
groupId: [[], [1]],
measure: "probability",
order: "asc",
},
},
},
};
const model = await createModelWithDataSource({ spreadsheetData });
setCellContent(model, "A1", `=ODOO.PIVOT.HEADER(1,"#bar",1)`);
setCellContent(model, "A2", `=ODOO.PIVOT.HEADER(1,"#bar",2)`);
setCellContent(model, "B1", `=ODOO.PIVOT(1,"probability","#bar",1,"#foo",1)`);
setCellContent(model, "B2", `=ODOO.PIVOT(1,"probability","#bar",2,"#foo",1)`);
setCellContent(model, "C1", `=ODOO.PIVOT(1,"probability","#bar",1,"#foo",2)`);
setCellContent(model, "C2", `=ODOO.PIVOT(1,"probability","#bar",2,"#foo",2)`);
setCellContent(model, "D1", `=ODOO.PIVOT(1,"probability","#bar",1)`);
setCellContent(model, "D2", `=ODOO.PIVOT(1,"probability","#bar",2)`);
await waitForDataSourcesLoaded(model);
assert.strictEqual(getCellValue(model, "A1"), "No");
assert.strictEqual(getCellValue(model, "A2"), "Yes");
assert.strictEqual(getCellValue(model, "B1"), "");
assert.strictEqual(getCellValue(model, "B2"), 11);
assert.strictEqual(getCellValue(model, "C1"), 15);
assert.strictEqual(getCellValue(model, "C2"), "");
assert.strictEqual(getCellValue(model, "D1"), 15);
assert.strictEqual(getCellValue(model, "D2"), 116);
});
QUnit.test("sort first pivot column (descending)", async (assert) => {
const spreadsheetData = {
pivots: {
1: {
colGroupBys: ["foo"],
rowGroupBys: ["bar"],
domain: [],
id: "1",
measures: [{ field: "probability" }],
model: "partner",
sortedColumn: {
groupId: [[], [1]],
measure: "probability",
order: "desc",
},
},
},
};
const model = await createModelWithDataSource({ spreadsheetData });
setCellContent(model, "A1", `=ODOO.PIVOT.HEADER(1,"#bar",1)`);
setCellContent(model, "A2", `=ODOO.PIVOT.HEADER(1,"#bar",2)`);
setCellContent(model, "B1", `=ODOO.PIVOT(1,"probability","#bar",1,"#foo",1)`);
setCellContent(model, "B2", `=ODOO.PIVOT(1,"probability","#bar",2,"#foo",1)`);
setCellContent(model, "C1", `=ODOO.PIVOT(1,"probability","#bar",1,"#foo",2)`);
setCellContent(model, "C2", `=ODOO.PIVOT(1,"probability","#bar",2,"#foo",2)`);
setCellContent(model, "D1", `=ODOO.PIVOT(1,"probability","#bar",1)`);
setCellContent(model, "D2", `=ODOO.PIVOT(1,"probability","#bar",2)`);
await waitForDataSourcesLoaded(model);
assert.strictEqual(getCellValue(model, "A1"), "Yes");
assert.strictEqual(getCellValue(model, "A2"), "No");
assert.strictEqual(getCellValue(model, "B1"), 11);
assert.strictEqual(getCellValue(model, "B2"), "");
assert.strictEqual(getCellValue(model, "C1"), "");
assert.strictEqual(getCellValue(model, "C2"), 15);
assert.strictEqual(getCellValue(model, "D1"), 116);
assert.strictEqual(getCellValue(model, "D2"), 15);
});
QUnit.test("sort second pivot column (ascending)", async (assert) => {
const spreadsheetData = {
pivots: {
1: {
colGroupBys: ["foo"],
domain: [],
id: "1",
measures: [{ field: "probability" }],
model: "partner",
rowGroupBys: ["bar"],
name: "Partners by Foo",
sortedColumn: {
groupId: [[], [2]],
measure: "probability",
order: "asc",
},
},
},
};
const model = await createModelWithDataSource({ spreadsheetData });
setCellContent(model, "A1", `=ODOO.PIVOT.HEADER(1,"#bar",1)`);
setCellContent(model, "A2", `=ODOO.PIVOT.HEADER(1,"#bar",2)`);
setCellContent(model, "B1", `=ODOO.PIVOT(1,"probability","#bar",1,"#foo",1)`);
setCellContent(model, "B2", `=ODOO.PIVOT(1,"probability","#bar",2,"#foo",1)`);
setCellContent(model, "C1", `=ODOO.PIVOT(1,"probability","#bar",1,"#foo",2)`);
setCellContent(model, "C2", `=ODOO.PIVOT(1,"probability","#bar",2,"#foo",2)`);
setCellContent(model, "D1", `=ODOO.PIVOT(1,"probability","#bar",1)`);
setCellContent(model, "D2", `=ODOO.PIVOT(1,"probability","#bar",2)`);
await waitForDataSourcesLoaded(model);
assert.strictEqual(getCellValue(model, "A1"), "Yes");
assert.strictEqual(getCellValue(model, "A2"), "No");
assert.strictEqual(getCellValue(model, "B1"), 11);
assert.strictEqual(getCellValue(model, "B2"), "");
assert.strictEqual(getCellValue(model, "C1"), "");
assert.strictEqual(getCellValue(model, "C2"), 15);
assert.strictEqual(getCellValue(model, "D1"), 116);
assert.strictEqual(getCellValue(model, "D2"), 15);
});
QUnit.test("sort second pivot column (descending)", async (assert) => {
const spreadsheetData = {
pivots: {
1: {
colGroupBys: ["foo"],
domain: [],
id: "1",
measures: [{ field: "probability" }],
model: "partner",
rowGroupBys: ["bar"],
name: "Partners by Foo",
sortedColumn: {
groupId: [[], [2]],
measure: "probability",
order: "desc",
},
},
},
};
const model = await createModelWithDataSource({ spreadsheetData });
setCellContent(model, "A1", `=ODOO.PIVOT.HEADER(1,"#bar",1)`);
setCellContent(model, "A2", `=ODOO.PIVOT.HEADER(1,"#bar",2)`);
setCellContent(model, "B1", `=ODOO.PIVOT(1,"probability","#bar",1,"#foo",1)`);
setCellContent(model, "B2", `=ODOO.PIVOT(1,"probability","#bar",2,"#foo",1)`);
setCellContent(model, "C1", `=ODOO.PIVOT(1,"probability","#bar",1,"#foo",2)`);
setCellContent(model, "C2", `=ODOO.PIVOT(1,"probability","#bar",2,"#foo",2)`);
setCellContent(model, "D1", `=ODOO.PIVOT(1,"probability","#bar",1)`);
setCellContent(model, "D2", `=ODOO.PIVOT(1,"probability","#bar",2)`);
await waitForDataSourcesLoaded(model);
assert.strictEqual(getCellValue(model, "A1"), "No");
assert.strictEqual(getCellValue(model, "A2"), "Yes");
assert.strictEqual(getCellValue(model, "B1"), "");
assert.strictEqual(getCellValue(model, "B2"), 11);
assert.strictEqual(getCellValue(model, "C1"), 15);
assert.strictEqual(getCellValue(model, "C2"), "");
assert.strictEqual(getCellValue(model, "D1"), 15);
assert.strictEqual(getCellValue(model, "D2"), 116);
});
QUnit.test("sort second pivot measure (ascending)", async (assert) => {
const spreadsheetData = {
pivots: {
1: {
rowGroupBys: ["product_id"],
colGroupBys: [],
domain: [],
id: "1",
measures: [{ field: "probability" }, { field: "foo" }],
model: "partner",
sortedColumn: {
groupId: [[], []],
measure: "foo",
order: "asc",
},
},
},
};
const model = await createModelWithDataSource({ spreadsheetData });
setCellContent(model, "A10", `=ODOO.PIVOT.HEADER(1,"#product_id",1)`);
setCellContent(model, "A11", `=ODOO.PIVOT.HEADER(1,"#product_id",2)`);
setCellContent(model, "B10", `=ODOO.PIVOT(1,"probability","#product_id",1)`);
setCellContent(model, "B11", `=ODOO.PIVOT(1,"probability","#product_id",2)`);
setCellContent(model, "C10", `=ODOO.PIVOT(1,"foo","#product_id",1)`);
setCellContent(model, "C11", `=ODOO.PIVOT(1,"foo","#product_id",2)`);
await waitForDataSourcesLoaded(model);
assert.strictEqual(getCellValue(model, "A10"), "xphone");
assert.strictEqual(getCellValue(model, "A11"), "xpad");
assert.strictEqual(getCellValue(model, "B10"), 10);
assert.strictEqual(getCellValue(model, "B11"), 121);
assert.strictEqual(getCellValue(model, "C10"), 12);
assert.strictEqual(getCellValue(model, "C11"), 20);
});
QUnit.test("sort second pivot measure (descending)", async (assert) => {
const spreadsheetData = {
pivots: {
1: {
colGroupBys: [],
domain: [],
id: "1",
measures: [{ field: "probability" }, { field: "foo" }],
model: "partner",
rowGroupBys: ["product_id"],
sortedColumn: {
groupId: [[], []],
measure: "foo",
order: "desc",
},
},
},
};
const model = await createModelWithDataSource({ spreadsheetData });
setCellContent(model, "A10", `=ODOO.PIVOT.HEADER(1,"#product_id",1)`);
setCellContent(model, "A11", `=ODOO.PIVOT.HEADER(1,"#product_id",2)`);
setCellContent(model, "B10", `=ODOO.PIVOT(1,"probability","#product_id",1)`);
setCellContent(model, "B11", `=ODOO.PIVOT(1,"probability","#product_id",2)`);
setCellContent(model, "C10", `=ODOO.PIVOT(1,"foo","#product_id",1)`);
setCellContent(model, "C11", `=ODOO.PIVOT(1,"foo","#product_id",2)`);
await waitForDataSourcesLoaded(model);
assert.strictEqual(getCellValue(model, "A10"), "xpad");
assert.strictEqual(getCellValue(model, "A11"), "xphone");
assert.strictEqual(getCellValue(model, "B10"), 121);
assert.strictEqual(getCellValue(model, "B11"), 10);
assert.strictEqual(getCellValue(model, "C10"), 20);
assert.strictEqual(getCellValue(model, "C11"), 12);
});
QUnit.test("Formatting a pivot positional preserves the interval", async (assert) => {
const { model } = await createSpreadsheetWithPivot({
arch: /*xml*/ `
<pivot>
<field name="date:day" type="row"/>
<field name="probability" type="measure"/>
</pivot>`,
});
setCellContent(model, "A1", `=ODOO.PIVOT.HEADER(1,"#date:day",1)`);
assert.strictEqual(getCell(model, "A1").formattedValue, "04/14/2016");
});
});

View file

@ -0,0 +1,417 @@
import { animationFrame } from "@odoo/hoot-mock";
import { describe, expect, test, beforeEach } from "@odoo/hoot";
import {
defineSpreadsheetActions,
defineSpreadsheetModels,
} from "@spreadsheet/../tests/helpers/data";
import { setCellContent, updatePivot } from "@spreadsheet/../tests/helpers/commands";
import {
getEvaluatedCell,
getEvaluatedFormatGrid,
getEvaluatedGrid,
} from "@spreadsheet/../tests/helpers/getters";
import { createSpreadsheetWithPivot } from "@spreadsheet/../tests/helpers/pivot";
let model;
describe.current.tags("headless");
defineSpreadsheetModels();
defineSpreadsheetActions();
beforeEach(async () => {
({ model } = await createSpreadsheetWithPivot({
arch: /* xml */ `
<pivot>
<field name="product_id" type="col"/>
<field name="foo" type="row"/>
<field name="probability" type="measure"/>
</pivot>`,
}));
model.dispatch("CREATE_SHEET", { sheetId: "42" });
});
test("full PIVOT() values", async function () {
setCellContent(model, "A1", `=PIVOT("1")`, "42");
// prettier-ignore
expect(getEvaluatedGrid(model, "A1:D7", "42")).toEqual([
["Partner Pivot", "xphone", "xpad", "Total"],
["", "Probability", "Probability", "Probability"],
[1, "", 11, 11],
[2, "", 15, 15],
[12, 10, "", 10],
[17, "", 95, 95],
["Total", 10, 121, 131],
]);
});
test("full PIVOT() formats", async function () {
setCellContent(model, "A1", `=PIVOT("1")`, "42");
// prettier-ignore
expect(getEvaluatedFormatGrid(model, "A1:D7", "42")).toEqual([
[undefined, "@* ", "@* ", undefined],
[undefined, undefined, undefined, undefined],
["0* ", "#,##0.00", "#,##0.00", "#,##0.00"],
["0* ", "#,##0.00", "#,##0.00", "#,##0.00"],
["0* ", "#,##0.00", "#,##0.00", "#,##0.00"],
["0* ", "#,##0.00", "#,##0.00", "#,##0.00"],
[undefined, "#,##0.00", "#,##0.00", "#,##0.00"],
]);
});
test("PIVOT(row_count=1)", async function () {
setCellContent(model, "A1", `=PIVOT("1", 1)`, "42");
// prettier-ignore
expect(getEvaluatedGrid(model, "A1:D4", "42")).toEqual([
["Partner Pivot", "xphone", "xpad", "Total"],
["", "Probability", "Probability", "Probability"],
[1, "", 11, 11],
[null, null, null, null],
]);
});
test("PIVOT(row_count=0)", async function () {
setCellContent(model, "A1", `=PIVOT("1", 0)`, "42");
// prettier-ignore
expect(getEvaluatedGrid(model, "A1:D3", "42")).toEqual([
["Partner Pivot", "xphone", "xpad", "Total"],
["", "Probability", "Probability", "Probability"],
[null, null, null, null],
]);
});
test("PIVOT(negative row_count)", async function () {
setCellContent(model, "A1", `=PIVOT("1", -1)`, "42");
expect(getEvaluatedCell(model, "A1", "42").value).toBe("#ERROR");
expect(getEvaluatedCell(model, "A1", "42").message).toBe(
"The number of rows must be positive."
);
});
test("PIVOT(include_column_titles=FALSE)", async function () {
setCellContent(model, "A1", `=PIVOT("1",,,FALSE,,FALSE)`, "42");
// prettier-ignore
expect(getEvaluatedGrid(model, "A1:D5", "42")).toEqual([
[1, "", 11, 11],
[2, "", 15, 15],
[12, 10, "", 10],
[17, "", 95, 95],
["Total", 10, 121, 131],
]);
});
test("PIVOT(include_total=FALSE) with no groupbys applied", async function () {
const { model } = await createSpreadsheetWithPivot({
arch: /* xml */ `
<pivot>
<field name="probability" type="measure"/>
</pivot>`,
});
model.dispatch("CREATE_SHEET", { sheetId: "42" });
setCellContent(model, "A1", `=PIVOT("1",,FALSE)`, "42");
// prettier-ignore
expect(getEvaluatedGrid(model, "A1:B3", "42")).toEqual([
["Partner Pivot", "Total"],
["", "Probability"],
["Total", 131],
]);
});
test("PIVOT(include_total=FALSE) with multiple measures and no groupbys applied", async function () {
const { model } = await createSpreadsheetWithPivot({
arch: /* xml */ `
<pivot>
<field name="probability" type="measure"/>
<field name="foo" type="measure"/>
</pivot>`,
});
model.dispatch("CREATE_SHEET", { sheetId: "42" });
setCellContent(model, "A1", `=PIVOT("1",,FALSE)`, "42");
// prettier-ignore
expect(getEvaluatedGrid(model, "A1:C3", "42")).toEqual([
["Partner Pivot", "Total", ""],
["", "Probability", "Foo"],
["Total", 131, 32],
]);
});
test("PIVOT(include_total=FALSE) with only row groupby applied", async function () {
const { model } = await createSpreadsheetWithPivot({
arch: /* xml */ `
<pivot>
<field name="foo" type="row"/>
<field name="probability" type="measure"/>
</pivot>`,
});
model.dispatch("CREATE_SHEET", { sheetId: "42" });
setCellContent(model, "A1", `=PIVOT("1",,FALSE)`, "42");
// prettier-ignore
expect(getEvaluatedGrid(model, "A1:C7", "42")).toEqual([
["Partner Pivot", "Total", null],
["", "Probability", null],
[1, 11, null],
[2, 15, null],
[12, 10, null],
[17, 95, null],
[null, null, null],
]);
});
test("sorted PIVOT(include_total=FALSE) with only row groupby applied", async function () {
const { model } = await createSpreadsheetWithPivot({
arch: /* xml */ `
<pivot>
<field name="foo" type="row"/>
<field name="probability" type="measure"/>
</pivot>`,
});
const [pivotId] = model.getters.getPivotIds();
updatePivot(model, pivotId, {
sortedColumn: {
domain: [],
order: "desc",
measure: "probability:avg",
},
});
model.dispatch("CREATE_SHEET", { sheetId: "42" });
setCellContent(model, "A1", `=PIVOT("1",,FALSE)`, "42");
// prettier-ignore
expect(getEvaluatedGrid(model, "A1:C7", "42")).toEqual([
["Partner Pivot", "Total", null],
["", "Probability", null],
[17, 95, null],
[2, 15, null],
[1, 11, null],
[12, 10, null],
[null, null, null],
]);
});
test("PIVOT(include_total=FALSE) with multiple measures and only row groupby applied", async function () {
const { model } = await createSpreadsheetWithPivot({
arch: /* xml */ `
<pivot>
<field name="product_id" type="row"/>
<field name="probability" type="measure"/>
<field name="foo" type="measure"/>
</pivot>`,
});
model.dispatch("CREATE_SHEET", { sheetId: "42" });
setCellContent(model, "A1", `=PIVOT("1",,FALSE)`, "42");
// prettier-ignore
expect(getEvaluatedGrid(model, "A1:D5", "42")).toEqual([
["Partner Pivot", "Total", "", null],
["", "Probability", "Foo", null],
["xphone", 10, 12, null],
["xpad", 121, 20, null],
[null, null, null, null],
]);
});
test("PIVOT(include_total=FALSE) with only col groupby applied", async function () {
const { model } = await createSpreadsheetWithPivot({
arch: /* xml */ `
<pivot>
<field name="product_id" type="col"/>
<field name="probability" type="measure"/>
</pivot>`,
});
model.dispatch("CREATE_SHEET", { sheetId: "42" });
setCellContent(model, "A1", `=PIVOT("1",,FALSE)`, "42");
// prettier-ignore
expect(getEvaluatedGrid(model, "A1:D4", "42")).toEqual([
["Partner Pivot", "xphone", "xpad", null],
["", "Probability", "Probability", null],
["Total", 10, 121, null],
[null, null, null, null],
]);
});
test("PIVOT(include_total=FALSE, include_column_titles=FALSE)", async function () {
setCellContent(model, "A1", `=PIVOT("1",,FALSE,FALSE,,FALSE)`, "42");
// prettier-ignore
expect(getEvaluatedGrid(model, "A1:D5", "42")).toEqual([
[1, "", 11, null],
[2, "", 15, null],
[12, 10, "", null],
[17, "", 95, null],
[null, null, null, null],
]);
});
test("PIVOT(row_count=1, include_total=FALSE, include_column_titles=FALSE)", async function () {
setCellContent(model, "A1", `=PIVOT("1",1,FALSE,FALSE,,FALSE)`, "42");
// prettier-ignore
expect(getEvaluatedGrid(model, "A1:D2", "42")).toEqual([
[1, "", 11, null],
[null, null, null, null],
]);
});
test("PIVOT(row_count=0, include_total=FALSE, include_column_titles=FALSE)", async function () {
setCellContent(model, "A1", `=PIVOT("1",0,FALSE,FALSE,,FALSE)`, "42");
// prettier-ignore
expect(getEvaluatedGrid(model, "A1:D1", "42")).toEqual([
["Partner Pivot", null, null, null],
]);
});
test("PIVOT(row_count=0, include_total=TRUE, include_column_titles=FALSE)", async function () {
setCellContent(model, "A1", `=PIVOT("1",0,TRUE,FALSE,,FALSE)`, "42");
// prettier-ignore
expect(getEvaluatedGrid(model, "A1:D1", "42")).toEqual([
["Partner Pivot", null, null, null],
]);
});
test("PIVOT with multiple row groups", async function () {
const { model } = await createSpreadsheetWithPivot({
arch: /* xml */ `
<pivot>
<field name="product_id" type="col"/>
<field name="foo" type="row"/>
<field name="id" type="row"/>
<field name="probability" type="measure"/>
</pivot>`,
});
const firstSheetId = model.getters.getActiveSheetId();
// values in the first sheet from the individual pivot functions
// prettier-ignore
expect(getEvaluatedGrid(model, "A1:D11", firstSheetId)).toEqual([
["Partner Pivot", "xphone", "xpad", "Total"],
["", "Probability", "Probability", "Probability"],
[1, "", 11, 11],
["Steven", "", 11, 11],
[2, "", 15, 15],
["Zara", "", 15, 15],
[12, 10, "", 10],
["Raoul", 10, "", 10],
[17, "", 95, 95],
["Taylor", "", 95, 95],
["Total", 10, 121, 131],
]);
model.dispatch("CREATE_SHEET", { sheetId: "42" });
setCellContent(model, "A1", `=PIVOT("1")`, "42");
// values from the PIVOT function
// prettier-ignore
expect(getEvaluatedGrid(model, "A1:D11", "42")).toEqual([
["Partner Pivot", "xphone", "xpad", "Total"],
["", "Probability", "Probability", "Probability"],
[1, "", 11, 11],
["Steven", "", 11, 11],
[2, "", 15, 15],
["Zara", "", 15, 15],
[12, 10, "", 10],
["Raoul", 10, "", 10],
[17, "", 95, 95],
["Taylor", "", 95, 95],
["Total", 10, 121, 131],
]);
setCellContent(model, "A1", `=PIVOT("1",,FALSE)`, "42");
// values from the PIVOT function without any group totals
// prettier-ignore
expect(getEvaluatedGrid(model, "A1:D11", "42")).toEqual([
["Partner Pivot", "xphone", "xpad", null],
["", "Probability", "Probability", null],
[1, "", "", null], // group header but without total values
["Steven", "", 11, null],
[2, "", "", null], // group header but without total values
["Zara", "", 15, null],
[12, "", "", null], // group header but without total values
["Raoul", 10, "", null],
[17, "", "", null], // group header but without total values
["Taylor", "", 95, null],
[null, null, null, null],
]);
});
test("edit pivot groups", async function () {
setCellContent(model, "A1", `=PIVOT("1")`, "42");
const originalGrid = getEvaluatedGrid(model, "A1:D7", "42");
// prettier-ignore
expect(originalGrid).toEqual([
["Partner Pivot", "xphone", "xpad", "Total"],
["", "Probability", "Probability", "Probability"],
[1, "", 11, 11],
[2, "", 15, 15],
[12, 10, "", 10],
[17, "", 95, 95],
["Total", 10, 121, 131],
]);
const [pivotId] = model.getters.getPivotIds();
model.dispatch("UPDATE_PIVOT", {
pivotId,
pivot: {
...model.getters.getPivotCoreDefinition(pivotId),
columns: [],
rows: [],
},
});
await animationFrame();
// prettier-ignore
expect(getEvaluatedGrid(model, "A1:B3", "42")).toEqual([
["Partner Pivot", "Total"],
["", "Probability"],
["Total", 131],
]);
model.dispatch("REQUEST_UNDO");
await animationFrame();
expect(getEvaluatedGrid(model, "A1:D7", "42")).toEqual(originalGrid);
});
test("Renaming the pivot reevaluates the PIVOT function", async function () {
const pivotId = model.getters.getPivotIds()[0];
setCellContent(model, "A1", `=PIVOT("1")`, "42");
expect(getEvaluatedCell(model, "A1", "42").value).toBe("Partner Pivot");
model.dispatch("RENAME_PIVOT", {
pivotId,
name: "New Name",
});
expect(getEvaluatedCell(model, "A1", "42").value).toBe("New Name");
});
test("can hide a measure", async function () {
const { model } = await createSpreadsheetWithPivot({
arch: /* xml */ `
<pivot>
<field name="probability" type="measure"/>
<field name="foo" type="measure"/>
</pivot>`,
});
setCellContent(model, "A10", '=PIVOT("1")');
// prettier-ignore
expect(getEvaluatedGrid(model, "A10:C12")).toEqual([
["Partner Pivot", "Total", "",],
["", "Probability", "Foo"],
["Total", 131, 32],
]);
const [pivotId] = model.getters.getPivotIds();
const definition = model.getters.getPivotCoreDefinition(pivotId);
updatePivot(model, pivotId, {
measures: [{ ...definition.measures[0], isHidden: true }, definition.measures[1]],
});
await animationFrame();
// prettier-ignore
expect(getEvaluatedGrid(model, "A10:C12")).toEqual([
["Partner Pivot", "Total", null],
["", "Foo", null],
["Total", 32, null],
]);
});
test("can have a dimension with a field of a relational field", async function () {
const { model, pivotId } = await createSpreadsheetWithPivot();
updatePivot(model, pivotId, {
rows: [{ fieldName: "product_id.display_name", order: "asc" }],
columns: [],
});
await animationFrame();
// prettier-ignore
expect(getEvaluatedGrid(model, "A1:B5")).toEqual([
["Partner Pivot", "Total"],
["", "Probability"],
["xpad", 121],
["xphone", 10],
["Total", 131],
]);
});

View file

@ -0,0 +1,36 @@
import { describe, expect, test } from "@odoo/hoot";
import { defineSpreadsheetModels } from "@spreadsheet/../tests/helpers/data";
import { addGlobalFilterWithoutReload } from "@spreadsheet/../tests/helpers/commands";
import { createSpreadsheetWithPivot } from "@spreadsheet/../tests/helpers/pivot";
describe.current.tags("headless");
defineSpreadsheetModels();
test("getFiltersMatchingPivotArgs should returns correct value for each filter", async function () {
const { model, pivotId } = await createSpreadsheetWithPivot({
arch: /* xml */ `
<pivot>
<field name="product_id" type="col"/>
<field name="foo" type="row"/>
<field name="probability" type="measure"/>
</pivot>`,
});
addGlobalFilterWithoutReload(
model,
{
type: "text",
label: "Text",
id: "1",
},
{
pivot: {
[pivotId]: { chain: "foo", type: "char" },
},
}
);
const filters = model.getters.getFiltersMatchingPivotArgs(pivotId, [
{ field: "foo", type: "char", value: "hello" },
]);
expect(filters).toEqual([{ filterId: "1", value: { operator: "ilike", strings: ["hello"] } }]);
});

View file

@ -0,0 +1,273 @@
import { beforeEach, describe, expect, test } from "@odoo/hoot";
import { getFirstListFunction, getNumberOfListFormulas } from "@spreadsheet/list/list_helpers";
import { constants, tokenize, helpers } from "@odoo/o-spreadsheet";
import { allowTranslations } from "@web/../tests/web_test_helpers";
const {
getFirstPivotFunction,
getNumberOfPivotFunctions,
pivotTimeAdapter,
toNormalizedPivotValue,
toNumber,
} = helpers;
const { DEFAULT_LOCALE } = constants;
function stringArg(value, tokenIndex) {
return {
type: "STRING",
value: `${value}`,
tokenStartIndex: tokenIndex,
tokenEndIndex: tokenIndex,
};
}
beforeEach(() => {
allowTranslations();
});
describe.current.tags("headless");
test("Basic formula extractor", async function () {
const formula = `=PIVOT.VALUE("1", "test") + ODOO.LIST("2", "hello", "bla")`;
const tokens = tokenize(formula);
let functionName;
let args;
({ functionName, args } = getFirstPivotFunction(tokens));
expect(functionName).toBe("PIVOT.VALUE");
expect(args.length).toBe(2);
expect(args[0]).toEqual(stringArg("1", 3));
expect(args[1]).toEqual(stringArg("test", 6));
({ functionName, args } = getFirstListFunction(tokens));
expect(functionName).toBe("ODOO.LIST");
expect(args.length).toBe(3);
expect(args[0]).toEqual(stringArg("2", 13));
expect(args[1]).toEqual(stringArg("hello", 16));
expect(args[2]).toEqual(stringArg("bla", 19));
});
test("Extraction with two PIVOT formulas", async function () {
const formula = `=PIVOT.VALUE("1", "test") + PIVOT.VALUE("2", "hello", "bla")`;
const tokens = tokenize(formula);
const { functionName, args } = getFirstPivotFunction(tokens);
expect(functionName).toBe("PIVOT.VALUE");
expect(args.length).toBe(2);
expect(args[0]).toEqual(stringArg("1", 3));
expect(args[1]).toEqual(stringArg("test", 6));
expect(getFirstListFunction(tokens)).toBe(undefined);
});
test("Number of formulas", async function () {
const formula = `=PIVOT.VALUE("1", "test") + PIVOT.VALUE("2", "hello", "bla") + ODOO.LIST("1", "bla")`;
expect(getNumberOfPivotFunctions(tokenize(formula))).toBe(2);
expect(getNumberOfListFormulas(tokenize(formula))).toBe(1);
expect(getNumberOfPivotFunctions(tokenize("=1+1"))).toBe(0);
expect(getNumberOfListFormulas(tokenize("=1+1"))).toBe(0);
expect(getNumberOfPivotFunctions(tokenize("=bla"))).toBe(0);
expect(getNumberOfListFormulas(tokenize("=bla"))).toBe(0);
});
test("getFirstPivotFunction does not crash when given crap", async function () {
expect(getFirstListFunction(tokenize("=SUM(A1)"))).toBe(undefined);
expect(getFirstPivotFunction(tokenize("=SUM(A1)"))).toBe(undefined);
expect(getFirstListFunction(tokenize("=1+1"))).toBe(undefined);
expect(getFirstPivotFunction(tokenize("=1+1"))).toBe(undefined);
expect(getFirstListFunction(tokenize("=bla"))).toBe(undefined);
expect(getFirstPivotFunction(tokenize("=bla"))).toBe(undefined);
expect(getFirstListFunction(tokenize("bla"))).toBe(undefined);
expect(getFirstPivotFunction(tokenize("bla"))).toBe(undefined);
});
describe("toNormalizedPivotValue", () => {
test("parse values of a selection, char or text field", () => {
for (const fieldType of ["selection", "text", "char"]) {
const dimension = {
type: fieldType,
displayName: "A field",
name: "my_field_name",
};
expect(toNormalizedPivotValue(dimension, "won")).toBe("won");
expect(toNormalizedPivotValue(dimension, "1")).toBe("1");
expect(toNormalizedPivotValue(dimension, 1)).toBe("1");
expect(toNormalizedPivotValue(dimension, "11/2020")).toBe("11/2020");
expect(toNormalizedPivotValue(dimension, "2020")).toBe("2020");
expect(toNormalizedPivotValue(dimension, "01/11/2020")).toBe("01/11/2020");
expect(toNormalizedPivotValue(dimension, "false")).toBe(false);
expect(toNormalizedPivotValue(dimension, false)).toBe(false);
expect(toNormalizedPivotValue(dimension, "true")).toBe("true");
}
});
test("parse values of time fields", () => {
for (const fieldType of ["date", "datetime"]) {
const dimension = {
type: fieldType,
displayName: "A field",
name: "my_field_name",
};
dimension.granularity = "day";
expect(toNormalizedPivotValue(dimension, "1/11/2020")).toBe(43841);
expect(toNormalizedPivotValue(dimension, "01/11/2020")).toBe(43841);
expect(toNormalizedPivotValue(dimension, "11/2020")).toBe(44136);
expect(toNormalizedPivotValue(dimension, "1")).toBe(1);
expect(toNormalizedPivotValue(dimension, 1)).toBe(1);
expect(toNormalizedPivotValue(dimension, "false")).toBe(false);
expect(toNormalizedPivotValue(dimension, false)).toBe(false);
dimension.granularity = "week";
expect(toNormalizedPivotValue(dimension, "11/2020")).toBe("11/2020");
expect(toNormalizedPivotValue(dimension, "1/2020")).toBe("1/2020");
expect(toNormalizedPivotValue(dimension, "01/2020")).toBe("1/2020");
expect(toNormalizedPivotValue(dimension, "false")).toBe(false);
expect(toNormalizedPivotValue(dimension, false)).toBe(false);
dimension.granularity = "month";
expect(toNormalizedPivotValue(dimension, "11/2020")).toBe("11/2020");
expect(toNormalizedPivotValue(dimension, "1/2020")).toBe("01/2020");
expect(toNormalizedPivotValue(dimension, "01/2020")).toBe("01/2020");
expect(toNormalizedPivotValue(dimension, "2/11/2020")).toBe("02/2020");
expect(toNormalizedPivotValue(dimension, "2/1/2020")).toBe("02/2020");
expect(toNormalizedPivotValue(dimension, 1)).toBe("12/1899");
expect(toNormalizedPivotValue(dimension, "false")).toBe(false);
expect(toNormalizedPivotValue(dimension, false)).toBe(false);
expect(() => toNormalizedPivotValue(dimension, "true")).toThrow();
expect(() => toNormalizedPivotValue(dimension, true)).toThrow();
expect(() => toNormalizedPivotValue(dimension, "won")).toThrow();
dimension.granularity = "quarter";
// special quarter syntax:
expect(toNormalizedPivotValue(dimension, "1/2020")).toBe("1/2020");
expect(toNormalizedPivotValue(dimension, "2/2020")).toBe("2/2020");
expect(toNormalizedPivotValue(dimension, "3/2020")).toBe("3/2020");
expect(toNormalizedPivotValue(dimension, "4/2020")).toBe("4/2020");
// falls back on regular date parsing:
expect(toNormalizedPivotValue(dimension, "5/2020")).toBe("2/2020");
expect(toNormalizedPivotValue(dimension, "01/01/2020")).toBe("1/2020");
expect(toNormalizedPivotValue(dimension, toNumber("01/01/2020", DEFAULT_LOCALE))).toBe(
"1/2020"
);
expect(() => toNormalizedPivotValue(dimension, "hello")).toThrow();
dimension.granularity = "year";
expect(toNormalizedPivotValue(dimension, "2020")).toBe(2020);
expect(toNormalizedPivotValue(dimension, 2020)).toBe(2020);
expect(toNormalizedPivotValue(dimension, "false")).toBe(false);
expect(toNormalizedPivotValue(dimension, false)).toBe(false);
}
});
test("parse values of boolean field", () => {
const dimension = {
type: "boolean",
displayName: "A field",
name: "my_field_name",
};
expect(toNormalizedPivotValue(dimension, "false")).toBe(false);
expect(toNormalizedPivotValue(dimension, false)).toBe(false);
expect(toNormalizedPivotValue(dimension, "true")).toBe(true);
expect(toNormalizedPivotValue(dimension, true)).toBe(true);
expect(() => toNormalizedPivotValue(dimension, "11/2020")).toThrow();
expect(() => toNormalizedPivotValue(dimension, "2020")).toThrow();
expect(() => toNormalizedPivotValue(dimension, "01/11/2020")).toThrow();
expect(() => toNormalizedPivotValue(dimension, "1")).toThrow();
expect(() => toNormalizedPivotValue(dimension, 1)).toThrow();
expect(() => toNormalizedPivotValue(dimension, "won")).toThrow();
});
test("parse values of numeric fields", () => {
for (const fieldType of ["float", "integer", "monetary", "many2one", "many2many"]) {
const dimension = {
type: fieldType,
displayName: "A field",
name: "my_field_name",
};
expect(toNormalizedPivotValue(dimension, "2020")).toBe(2020);
expect(toNormalizedPivotValue(dimension, "01/11/2020")).toBe(43841); // a date is actually a number in a spreadsheet
expect(toNormalizedPivotValue(dimension, "11/2020")).toBe(44136); // 1st of november 2020
expect(toNormalizedPivotValue(dimension, "1")).toBe(1);
expect(toNormalizedPivotValue(dimension, 1)).toBe(1);
expect(toNormalizedPivotValue(dimension, "false")).toBe(false);
expect(toNormalizedPivotValue(dimension, false)).toBe(false);
expect(() => toNormalizedPivotValue(dimension, "true")).toThrow();
expect(() => toNormalizedPivotValue(dimension, true)).toThrow();
expect(() => toNormalizedPivotValue(dimension, "won")).toThrow();
}
});
test("parse values of unsupported fields", () => {
for (const fieldType of ["one2many", "binary", "html"]) {
const dimension = {
type: fieldType,
displayName: "A field",
name: "my_field_name",
};
expect(() => toNormalizedPivotValue(dimension, "false")).toThrow();
expect(() => toNormalizedPivotValue(dimension, false)).toThrow();
expect(() => toNormalizedPivotValue(dimension, "true")).toThrow();
expect(() => toNormalizedPivotValue(dimension, true)).toThrow();
expect(() => toNormalizedPivotValue(dimension, "11/2020")).toThrow();
expect(() => toNormalizedPivotValue(dimension, "2020")).toThrow();
expect(() => toNormalizedPivotValue(dimension, "01/11/2020")).toThrow();
expect(() => toNormalizedPivotValue(dimension, "1")).toThrow();
expect(() => toNormalizedPivotValue(dimension, 1)).toThrow();
expect(() => toNormalizedPivotValue(dimension, "won")).toThrow();
}
});
});
describe("pivot time adapters formatted value", () => {
test("Day adapter", () => {
const adapter = pivotTimeAdapter("day");
expect(adapter.toValueAndFormat("11/12/2020", DEFAULT_LOCALE)).toEqual({
value: 44147,
format: "dd mmm yyyy",
});
expect(adapter.toValueAndFormat("01/11/2020", DEFAULT_LOCALE)).toEqual({
value: 43841,
format: "dd mmm yyyy",
});
expect(adapter.toValueAndFormat("12/05/2020", DEFAULT_LOCALE)).toEqual({
value: 44170,
format: "dd mmm yyyy",
});
});
test("Week adapter", () => {
const adapter = pivotTimeAdapter("week");
expect(adapter.toValueAndFormat("5/2024", DEFAULT_LOCALE)).toEqual({ value: "W5 2024" });
expect(adapter.toValueAndFormat("51/2020", DEFAULT_LOCALE)).toEqual({
value: "W51 2020",
});
});
test("Month adapter", () => {
const adapter = pivotTimeAdapter("month");
expect(adapter.toValueAndFormat("12/2020", DEFAULT_LOCALE)).toEqual({
value: 44166,
format: "mmmm yyyy",
});
expect(adapter.toValueAndFormat("02/2020", DEFAULT_LOCALE)).toEqual({
value: 43862,
format: "mmmm yyyy",
});
});
test("Quarter adapter", () => {
const adapter = pivotTimeAdapter("quarter");
expect(adapter.toValueAndFormat("1/2022", DEFAULT_LOCALE)).toEqual({ value: "Q1 2022" });
expect(adapter.toValueAndFormat("3/1998", DEFAULT_LOCALE)).toEqual({ value: "Q3 1998" });
});
test("Year adapter", () => {
const adapter = pivotTimeAdapter("year");
expect(adapter.toValueAndFormat("2020", DEFAULT_LOCALE)).toEqual({
value: 2020,
format: "0",
});
expect(adapter.toValueAndFormat("1997", DEFAULT_LOCALE)).toEqual({
value: 1997,
format: "0",
});
});
});

View file

@ -1,154 +0,0 @@
/** @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";
function stringArg(value) {
return { type: "STRING", value: `${value}` };
}
QUnit.module("spreadsheet > pivot_helpers", {}, () => {
QUnit.test("Basic formula extractor", async function (assert) {
const formula = `=ODOO.PIVOT("1", "test") + ODOO.LIST("2", "hello", "bla")`;
let functionName;
let args;
({ functionName, args } = getFirstPivotFunction(formula));
assert.strictEqual(functionName, "ODOO.PIVOT");
assert.strictEqual(args.length, 2);
assert.deepEqual(args[0], stringArg("1"));
assert.deepEqual(args[1], stringArg("test"));
({ functionName, args } = getFirstListFunction(formula));
assert.strictEqual(functionName, "ODOO.LIST");
assert.strictEqual(args.length, 3);
assert.deepEqual(args[0], stringArg("2"));
assert.deepEqual(args[1], stringArg("hello"));
assert.deepEqual(args[2], stringArg("bla"));
});
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));
assert.strictEqual(functionName, "ODOO.PIVOT");
assert.strictEqual(args.length, 2);
assert.deepEqual(args[0], stringArg("1"));
assert.deepEqual(args[1], stringArg("test"));
assert.strictEqual(getFirstListFunction(formula), undefined);
});
QUnit.test("Number of formulas", async function (assert) {
const formula = `=ODOO.PIVOT("1", "test") + ODOO.PIVOT("2", "hello", "bla") + ODOO.LIST("1", "bla")`;
assert.strictEqual(getNumberOfPivotFormulas(formula), 2);
assert.strictEqual(getNumberOfListFormulas(formula), 1);
assert.strictEqual(getNumberOfPivotFormulas("=1+1"), 0);
assert.strictEqual(getNumberOfListFormulas("=1+1"), 0);
assert.strictEqual(getNumberOfPivotFormulas("=bla"), 0);
assert.strictEqual(getNumberOfListFormulas("=bla"), 0);
});
QUnit.test("getFirstPivotFunction does not crash when given crap", async function (assert) {
assert.strictEqual(getFirstListFunction("=SUM(A1)"), undefined);
assert.strictEqual(getFirstPivotFunction("=SUM(A1)"), undefined);
assert.strictEqual(getFirstListFunction("=1+1"), undefined);
assert.strictEqual(getFirstPivotFunction("=1+1"), undefined);
assert.strictEqual(getFirstListFunction("=bla"), undefined);
assert.strictEqual(getFirstPivotFunction("=bla"), undefined);
assert.strictEqual(getFirstListFunction("bla"), undefined);
assert.strictEqual(getFirstPivotFunction("bla"), undefined);
});
});
QUnit.module("spreadsheet > parsePivotFormulaFieldValue", {}, () => {
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");
}
});
QUnit.test("parse values of time fields", (assert) => {
for (const fieldType of ["date", "datetime"]) {
const field = {
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
}
});
QUnit.test("parse values of boolean field", (assert) => {
const field = {
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"));
});
QUnit.test("parse values of numeric fields", (assert) => {
for (const fieldType of ["float", "integer", "monetary", "many2one", "many2many"]) {
const field = {
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"));
}
});
QUnit.test("parse values of unsupported fields", (assert) => {
for (const fieldType of ["one2many", "binary", "html"]) {
const field = {
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"));
}
});
});

View file

@ -0,0 +1,435 @@
import { animationFrame, Deferred } from "@odoo/hoot-mock";
import { describe, expect, test } from "@odoo/hoot";
import {
defineSpreadsheetActions,
defineSpreadsheetModels,
getPyEnv,
Partner,
Product,
} from "@spreadsheet/../tests/helpers/data";
import { selectCell, setCellContent, updatePivot } from "@spreadsheet/../tests/helpers/commands";
import { doMenuAction, getActionMenu } from "@spreadsheet/../tests/helpers/ui";
import { createSpreadsheetWithPivot } from "@spreadsheet/../tests/helpers/pivot";
import { waitForDataLoaded } from "@spreadsheet/helpers/model";
import * as spreadsheet from "@odoo/o-spreadsheet";
import { getCell, getCellFormula, getCellValue } from "@spreadsheet/../tests/helpers/getters";
import { mockService, onRpc } from "@web/../tests/web_test_helpers";
const { cellMenuRegistry } = spreadsheet.registries;
onRpc("ir.model", "display_name_for", (args) => {
const models = args.args[0];
const pyEnv = getPyEnv();
const records = pyEnv["ir.model"].filter((record) => models.includes(record.model));
return records.map((record) => ({
model: record.model,
display_name: record.name,
}));
});
describe.current.tags("headless");
defineSpreadsheetModels();
defineSpreadsheetActions();
const basicListAction = {
type: "ir.actions.act_window",
name: "Partner",
res_model: "partner",
views: [
[false, "list"],
[false, "form"],
],
target: "current",
domain: [],
context: {},
};
test("Can open see records on headers col", async function () {
const fakeActionService = {
doAction: (actionRequest, options = {}) => {
expect.step("doAction");
expect(actionRequest).toEqual({
...basicListAction,
domain: [["foo", "=", 1]],
});
expect(options.viewType).toBe("list");
},
};
mockService("action", fakeActionService);
const { env, model } = await createSpreadsheetWithPivot();
selectCell(model, "B1");
await animationFrame();
await doMenuAction(cellMenuRegistry, ["pivot_see_records"], env);
expect.verifySteps(["doAction"]);
});
test("Can open see records on headers row", async function () {
const fakeActionService = {
doAction: (actionRequest, options = {}) => {
expect.step("doAction");
expect(actionRequest).toEqual({
...basicListAction,
domain: [["bar", "=", false]],
});
expect(options.viewType).toBe("list");
},
};
mockService("action", fakeActionService);
const { env, model } = await createSpreadsheetWithPivot();
selectCell(model, "A3");
await animationFrame();
await doMenuAction(cellMenuRegistry, ["pivot_see_records"], env);
expect.verifySteps(["doAction"]);
});
test("Can open see records on measure headers", async function () {
const fakeActionService = {
doAction: (actionRequest, options = {}) => {
expect.step("doAction");
expect(actionRequest).toEqual({
...basicListAction,
domain: [["foo", "=", 1]],
});
expect(options.viewType).toBe("list");
},
};
mockService("action", fakeActionService);
const { env, model } = await createSpreadsheetWithPivot();
selectCell(model, "B2");
await animationFrame();
await doMenuAction(cellMenuRegistry, ["pivot_see_records"], env);
expect.verifySteps(["doAction"]);
});
test("Domain with granularity quarter_number are correctly computer", async function () {
const fakeActionService = {
doAction: (actionRequest) => {
expect.step("doAction");
expect.step(actionRequest.domain);
},
};
mockService("action", fakeActionService);
const { env, model, pivotId } = await createSpreadsheetWithPivot();
updatePivot(model, pivotId, {
rows: [{ fieldName: "date", granularity: "quarter_number", order: "asc" }],
});
await animationFrame();
setCellContent(model, "A1", `=PIVOT.HEADER(1,"date:quarter_number",2)`);
selectCell(model, "A1");
await doMenuAction(cellMenuRegistry, ["pivot_see_records"], env);
expect.verifySteps(["doAction", [[`date.quarter_number`, "=", 2]]]);
});
test("Domain with granularity iso_week_number are correctly computer", async function () {
const fakeActionService = {
doAction: (actionRequest) => {
expect.step("doAction");
expect.step(actionRequest.domain);
},
};
mockService("action", fakeActionService);
const { env, model, pivotId } = await createSpreadsheetWithPivot();
updatePivot(model, pivotId, {
rows: [{ fieldName: "date", granularity: "iso_week_number", order: "asc" }],
});
await animationFrame();
setCellContent(model, "A1", `=PIVOT.HEADER(1,"date:iso_week_number",15)`);
selectCell(model, "A1");
await doMenuAction(cellMenuRegistry, ["pivot_see_records"], env);
expect.verifySteps(["doAction", [[`date.iso_week_number`, "=", 15]]]);
});
test("Domain with granularity month_number are correctly computer", async function () {
const fakeActionService = {
doAction: (actionRequest) => {
expect.step("doAction");
expect.step(actionRequest.domain);
},
};
mockService("action", fakeActionService);
const { env, model, pivotId } = await createSpreadsheetWithPivot();
updatePivot(model, pivotId, {
rows: [{ fieldName: "date", granularity: "month_number", order: "asc" }],
});
await animationFrame();
setCellContent(model, "A1", `=PIVOT.HEADER(1,"date:month_number",4)`);
selectCell(model, "A1");
await doMenuAction(cellMenuRegistry, ["pivot_see_records"], env);
expect.verifySteps(["doAction", [[`date.month_number`, "=", 4]]]);
});
test("Domain with granularity day_of_month are correctly computer", async function () {
const fakeActionService = {
doAction: (actionRequest) => {
expect.step("doAction");
expect.step(actionRequest.domain);
},
};
mockService("action", fakeActionService);
const { env, model, pivotId } = await createSpreadsheetWithPivot();
updatePivot(model, pivotId, {
rows: [{ fieldName: "date", granularity: "day_of_month", order: "asc" }],
});
await animationFrame();
setCellContent(model, "A1", `=PIVOT.HEADER(1,"date:day_of_month",11)`);
selectCell(model, "A1");
await doMenuAction(cellMenuRegistry, ["pivot_see_records"], env);
expect.verifySteps(["doAction", [[`date.day_of_month`, "=", 11]]]);
});
test("Cannot open see records on the main PIVOT cell", async function () {
const { env, model } = await createSpreadsheetWithPivot();
model.dispatch("CREATE_SHEET", { sheetId: "42" });
setCellContent(model, "A1", `=PIVOT("1")`, "42");
selectCell(model, "A1", "42");
const action = await getActionMenu(cellMenuRegistry, ["pivot_see_records"], env);
expect(action.isVisible(env)).toBe(false);
});
test("Cannot open see records on the empty PIVOT cell below the main cell", async function () {
const { env, model } = await createSpreadsheetWithPivot();
model.dispatch("CREATE_SHEET", { sheetId: "42" });
setCellContent(model, "A1", `=PIVOT("1")`, "42");
selectCell(model, "A2", "42"); // A2 is always empty. It's the cell next to measure headers.
const action = await getActionMenu(cellMenuRegistry, ["pivot_see_records"], env);
expect(action.isVisible(env)).toBe(false);
});
test("Can see records on PIVOT cells", async function () {
const actions = [];
const fakeActionService = {
doAction: (actionRequest, options = {}) => {
expect.step("doAction");
actions.push(actionRequest);
},
};
mockService("action", fakeActionService);
const { env, model } = await createSpreadsheetWithPivot({ pivotType: "static" });
const firstSheetId = model.getters.getActiveSheetId();
async function checkCells(cells) {
// Let's check that clicking on a cell opens the same action on the first sheet
// where the pivot is made of individual regular pivot formulas and on the second
// sheet where the pivot is made of a single PIVOT formula.
for (const [xc, formula] of Object.entries(cells)) {
// let's check the cell formula is what we expect
expect(getCell(model, xc, firstSheetId)?.content).toBe(formula, {
message: `${xc} on the first sheet is ${formula}`,
});
// action on the first sheet, on regular pivot formula
selectCell(model, xc, firstSheetId);
await doMenuAction(cellMenuRegistry, ["pivot_see_records"], env);
// action on the second sheet, on PIVOT
selectCell(model, xc, "42");
await doMenuAction(cellMenuRegistry, ["pivot_see_records"], env);
setCellContent(model, "G7", `=${xc}`);
selectCell(model, "G7", "42");
await doMenuAction(cellMenuRegistry, ["pivot_see_records"], env);
expect(actions[0]).toEqual(actions[1], { message: "both actions are the same" });
expect(actions[0]).toEqual(actions[2], { message: "all actions are the same" });
expect.verifySteps(["doAction", "doAction", "doAction"]);
actions.length = 0;
}
}
model.dispatch("CREATE_SHEET", { sheetId: "42" });
setCellContent(model, "A1", `=PIVOT("1")`, "42");
// here is what the cells look like
const header_cells = {
// B1 is a column header
B1: '=PIVOT.HEADER(1,"foo",1)',
// B2 is a measure header
B2: '=PIVOT.HEADER(1,"foo",1,"measure","probability:avg")',
// A3 is a row header
A3: '=PIVOT.HEADER(1,"bar",FALSE)',
// A5 is a total header
A5: "=PIVOT.HEADER(1)",
};
const data_cells = {
// B3 is an empty value
B3: '=PIVOT.VALUE(1,"probability:avg","bar",FALSE,"foo",1)',
// B4 is an non-empty value
B4: '=PIVOT.VALUE(1,"probability:avg","bar",TRUE,"foo",1)',
// B5 is a column group total value
B5: '=PIVOT.VALUE(1,"probability:avg","foo",1)',
// F3 is a row group total value
F3: '=PIVOT.VALUE(1,"probability:avg","bar",FALSE)',
// F5 is the total
F5: '=PIVOT.VALUE(1,"probability:avg")',
};
await checkCells({ ...header_cells, ...data_cells });
// same but without the column headers
// set the function in A3 such as the data cells matches the ones in the first sheet
setCellContent(model, "A3", `=PIVOT("1",,,FALSE,,FALSE)`, "42");
await checkCells(data_cells);
});
test("Cannot see records of pivot formula without value", async function () {
const { env, model } = await createSpreadsheetWithPivot({ pivotType: "static" });
expect(getCellFormula(model, "B3")).toBe(
`=PIVOT.VALUE(1,"probability:avg","bar",FALSE,"foo",1)`
);
expect(getCellValue(model, "B3")).toBe("", { message: "B3 is empty" });
selectCell(model, "B3");
const action = await getActionMenu(cellMenuRegistry, ["pivot_see_records"], env);
expect(action.isVisible(env)).toBe(false);
});
test("Cannot see records of spreadsheet pivot", async function () {
const { model, env } = await createSpreadsheetWithPivot();
setCellContent(model, "A11", "A");
setCellContent(model, "A12", "1");
setCellContent(model, "B11", "B");
setCellContent(model, "B12", "2");
model.dispatch("ADD_PIVOT", {
pivotId: "2",
pivot: {
type: "SPREADSHEET",
columns: [{ fieldName: "A", order: "asc" }],
rows: [],
measures: [{ id: "B:sum", fieldName: "B", aggregator: "sum" }],
name: "Pivot2",
dataSet: {
sheetId: model.getters.getActiveSheetId(),
zone: { top: 10, bottom: 11, left: 0, right: 1 },
},
},
});
setCellContent(model, "A13", `=PIVOT("2")`);
expect(getCellValue(model, "B15")).toBe(2);
selectCell(model, "B15");
const action = await getActionMenu(cellMenuRegistry, ["pivot_see_records"], env);
expect(action.isVisible(env)).toBe(false);
});
test("See records is not visible on an empty cell", async function () {
const { env, model } = await createSpreadsheetWithPivot();
expect(getCell(model, "A21")).toBe(undefined);
selectCell(model, "A21");
const action = cellMenuRegistry.getAll().find((item) => item.id === "pivot_see_records");
expect(action.isVisible(env)).toBe(false);
});
test("Cannot see records of out of range positional pivot formula with calculated field", async function () {
const { env, model, pivotId } = await createSpreadsheetWithPivot();
updatePivot(model, pivotId, {
measures: [
{
id: "calculated",
fieldName: "calculated",
aggregator: "sum",
computedBy: {
formula: "=0",
sheetId: model.getters.getActiveSheetId(),
},
},
],
});
await waitForDataLoaded(model);
setCellContent(model, "A1", `=PIVOT.VALUE(1,"calculated","bar",FALSE,"#foo",22)`);
selectCell(model, "A1");
const action = await getActionMenu(cellMenuRegistry, ["pivot_see_records"], env);
expect(!!action.isVisible(env)).toBe(false);
});
test("See records is not visible if the pivot is not loaded, even if the cell has a value", async function () {
let deferred = undefined;
const { env, model } = await createSpreadsheetWithPivot({
arch: /*xml*/ `
<pivot>
<field name="probability" type="measure"/>
</pivot>
`,
mockRPC: async function (route, args) {
if (deferred && args.method === "read_group" && args.model === "partner") {
await deferred;
}
},
});
setCellContent(model, "A1", '=IFERROR(PIVOT.VALUE("1","probability"), 42)');
deferred = new Deferred();
model.dispatch("REFRESH_ALL_DATA_SOURCES");
const action = cellMenuRegistry.getAll().find((item) => item.id === "pivot_see_records");
expect(action.isVisible(env)).toBe(false);
deferred.resolve();
await animationFrame();
expect(action.isVisible(env)).toBe(true);
});
test("See records with custom pivot groups", async function () {
Product._records.push({ id: 200, display_name: "chair", name: "chair" });
Partner._records.push({ id: 200, bar: false, product_id: 200, probability: 100 });
let doActionReceivedDomain = undefined;
mockService("action", {
doAction: (actionRequest, options = {}) => {
expect.step("doAction");
doActionReceivedDomain = actionRequest.domain;
expect(options.viewType).toBe("list");
},
});
const { env, model, pivotId } = await createSpreadsheetWithPivot();
updatePivot(model, pivotId, {
columns: [{ fieldName: "GroupedProducts", order: "asc" }],
rows: [{ fieldName: "bar", order: "asc" }],
measures: [{ id: "probability:sum", fieldName: "probability", aggregator: "sum" }],
customFields: {
GroupedProducts: {
parentField: "product_id",
name: "GroupedProducts",
groups: [
{ name: "Group1", values: [37, 41] },
{ name: "Others", values: [], isOtherGroup: true },
],
},
},
});
await waitForDataLoaded(model);
selectCell(model, "B1"); // "Group1" group column header
await doMenuAction(cellMenuRegistry, ["pivot_see_records"], env);
expect.verifySteps(["doAction"]);
expect(doActionReceivedDomain).toEqual(["|", ["product_id", "=", 37], ["product_id", "=", 41]]);
selectCell(model, "B2"); // "Probability" measure header
await doMenuAction(cellMenuRegistry, ["pivot_see_records"], env);
expect.verifySteps(["doAction"]);
expect(doActionReceivedDomain).toEqual(["|", ["product_id", "=", 37], ["product_id", "=", 41]]);
selectCell(model, "B4"); // Pivot value for "Group1" group and bar = true
await doMenuAction(cellMenuRegistry, ["pivot_see_records"], env);
expect.verifySteps(["doAction"]);
expect(doActionReceivedDomain).toEqual([
"|",
"&",
["product_id", "=", 37],
["bar", "=", true],
"&",
["product_id", "=", 41],
["bar", "=", true],
]);
selectCell(model, "C1"); // "Others" group column header
await doMenuAction(cellMenuRegistry, ["pivot_see_records"], env);
expect.verifySteps(["doAction"]);
expect(doActionReceivedDomain).toEqual([["product_id", "=", 200]]);
selectCell(model, "C3"); // Pivot value for "Others" group and bar = false
await doMenuAction(cellMenuRegistry, ["pivot_see_records"], env);
expect.verifySteps(["doAction"]);
expect(doActionReceivedDomain).toEqual(["&", ["product_id", "=", 200], ["bar", "=", false]]);
});

View file

@ -1,155 +0,0 @@
/** @odoo-module */
import { makeDeferred, nextTick } from "@web/../tests/helpers/utils";
import { selectCell } from "@spreadsheet/../tests/utils/commands";
import { createSpreadsheetWithPivot } from "@spreadsheet/../tests/utils/pivot";
import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended";
import { registry } from "@web/core/registry";
import { setCellContent } from "../utils/commands";
import { getCell } from "../utils/getters";
const { cellMenuRegistry } = spreadsheet.registries;
QUnit.module("spreadsheet > see pivot records");
const basicListAction = {
type: "ir.actions.act_window",
name: "Partner",
res_model: "partner",
view_mode: "list",
views: [
[false, "list"],
[false, "form"],
],
target: "current",
domain: [],
};
QUnit.test("Can open see records on headers col", async function (assert) {
const fakeActionService = {
dependencies: [],
start: (env) => ({
doAction: (actionRequest, options = {}) => {
assert.step("doAction");
assert.deepEqual(actionRequest, {
...basicListAction,
domain: [["foo", "=", 1]],
});
},
}),
};
registry.category("services").add("action", fakeActionService);
const { env, model } = await createSpreadsheetWithPivot();
selectCell(model, "B1");
await nextTick();
const root = cellMenuRegistry.getAll().find((item) => item.id === "pivot_see_records");
await root.action(env);
assert.verifySteps(["doAction"]);
});
QUnit.test("Can open see records on headers row", async function (assert) {
const fakeActionService = {
dependencies: [],
start: (env) => ({
doAction: (actionRequest, options = {}) => {
assert.step("doAction");
assert.deepEqual(actionRequest, {
...basicListAction,
domain: [["bar", "=", false]],
});
},
}),
};
registry.category("services").add("action", fakeActionService);
const { env, model } = await createSpreadsheetWithPivot();
selectCell(model, "A3");
await nextTick();
const root = cellMenuRegistry.getAll().find((item) => item.id === "pivot_see_records");
await root.action(env);
assert.verifySteps(["doAction"]);
});
QUnit.test("Can open see records on measure headers", async function (assert) {
const fakeActionService = {
dependencies: [],
start: (env) => ({
doAction: (actionRequest, options = {}) => {
assert.step("doAction");
assert.deepEqual(actionRequest, {
...basicListAction,
domain: [["foo", "=", 1]],
});
},
}),
};
registry.category("services").add("action", fakeActionService);
const { env, model } = await createSpreadsheetWithPivot();
selectCell(model, "B2");
await nextTick();
const root = cellMenuRegistry.getAll().find((item) => item.id === "pivot_see_records");
await root.action(env);
assert.verifySteps(["doAction"]);
});
QUnit.test(
"See records is not visible if the pivot is not loaded, even if the cell has a value",
async function (assert) {
let deferred = undefined;
const { env, model } = await createSpreadsheetWithPivot({
arch: /*xml*/ `
<pivot>
<field name="probability" type="measure"/>
</pivot>
`,
mockRPC: async function (route, args) {
if (deferred && args.method === "read_group" && args.model === "partner") {
await deferred;
}
},
});
setCellContent(model, "A1", '=IFERROR(ODOO.PIVOT("1","probability"), 42)');
deferred = makeDeferred();
model.dispatch("REFRESH_ALL_DATA_SOURCES");
const action = cellMenuRegistry.getAll().find((item) => item.id === "pivot_see_records");
assert.strictEqual(action.isVisible(env), false);
deferred.resolve();
await nextTick();
assert.strictEqual(action.isVisible(env), true);
}
);
QUnit.test("See records is not visible if the formula has an weird IF", async function (assert) {
let deferred = undefined;
const { env, model } = await createSpreadsheetWithPivot({
arch: /*xml*/ `
<pivot>
<field name="probability" type="measure"/>
</pivot>
`,
mockRPC: async function (route, args) {
if (deferred && args.method === "read_group" && args.model === "partner") {
await deferred;
}
},
});
setCellContent(
model,
"A1",
'=if(false, ODOO.PIVOT("1","probability","user_id",2,"partner_id", "#Error"), "test")'
);
deferred = makeDeferred();
model.dispatch("REFRESH_ALL_DATA_SOURCES");
const action = cellMenuRegistry.getAll().find((item) => item.id === "pivot_see_records");
assert.strictEqual(action.isVisible(env), false);
deferred.resolve();
await nextTick();
assert.strictEqual(action.isVisible(env), false);
});
QUnit.test("See records is not visible on an empty cell", async function (assert) {
const { env, model } = await createSpreadsheetWithPivot();
assert.strictEqual(getCell(model, "A21"), undefined);
selectCell(model, "A21");
const action = cellMenuRegistry.getAll().find((item) => item.id === "pivot_see_records");
assert.strictEqual(action.isVisible(env), false);
});