mirror of
https://github.com/bringout/oca-ocb-report.git
synced 2026-04-22 02:42:05 +02:00
19.0 vanilla
This commit is contained in:
parent
62d197ac8b
commit
184bb0e321
667 changed files with 691406 additions and 239886 deletions
|
|
@ -0,0 +1,98 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { createSpreadsheetWithChart } from "@spreadsheet/../tests/helpers/chart";
|
||||
import { createBasicChart } from "@spreadsheet/../tests/helpers/commands";
|
||||
import { defineSpreadsheetModels } from "@spreadsheet/../tests/helpers/data";
|
||||
import { makeMockEnv } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { Model } from "@odoo/o-spreadsheet";
|
||||
|
||||
const chartId = "uuid1";
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
defineSpreadsheetModels();
|
||||
|
||||
test("Links between charts and ir.menus are correctly imported/exported", async function () {
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { env } });
|
||||
createBasicChart(model, chartId);
|
||||
model.dispatch("LINK_ODOO_MENU_TO_CHART", {
|
||||
chartId,
|
||||
odooMenuId: 1,
|
||||
});
|
||||
const exportedData = model.exportData();
|
||||
expect(exportedData.chartOdooMenusReferences[chartId]).toBe(1, {
|
||||
message: "Link to odoo menu is exported",
|
||||
});
|
||||
const importedModel = new Model(exportedData, { custom: { env } });
|
||||
const chartMenu = importedModel.getters.getChartOdooMenu(chartId);
|
||||
expect(chartMenu.id).toBe(1, { message: "Link to odoo menu is imported" });
|
||||
});
|
||||
|
||||
test("Can undo-redo a LINK_ODOO_MENU_TO_CHART", async function () {
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { env } });
|
||||
createBasicChart(model, chartId);
|
||||
model.dispatch("LINK_ODOO_MENU_TO_CHART", {
|
||||
chartId,
|
||||
odooMenuId: 1,
|
||||
});
|
||||
expect(model.getters.getChartOdooMenu(chartId).id).toBe(1);
|
||||
model.dispatch("REQUEST_UNDO");
|
||||
expect(model.getters.getChartOdooMenu(chartId)).toBe(undefined);
|
||||
model.dispatch("REQUEST_REDO");
|
||||
expect(model.getters.getChartOdooMenu(chartId).id).toBe(1);
|
||||
});
|
||||
|
||||
test("link is removed when figure is deleted", async function () {
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { env } });
|
||||
createBasicChart(model, chartId);
|
||||
model.dispatch("LINK_ODOO_MENU_TO_CHART", {
|
||||
chartId,
|
||||
odooMenuId: 1,
|
||||
});
|
||||
expect(model.getters.getChartOdooMenu(chartId).id).toBe(1);
|
||||
model.dispatch("DELETE_FIGURE", {
|
||||
sheetId: model.getters.getActiveSheetId(),
|
||||
figureId: model.getters.getFigureIdFromChartId(chartId),
|
||||
});
|
||||
expect(model.getters.getChartOdooMenu(chartId)).toBe(undefined);
|
||||
});
|
||||
|
||||
test("Links of Odoo charts are duplicated when duplicating a sheet", async function () {
|
||||
const { model } = await createSpreadsheetWithChart({ type: "odoo_pie" });
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
const secondSheetId = "mySecondSheetId";
|
||||
const chartId = model.getters.getChartIds(sheetId)[0];
|
||||
model.dispatch("DUPLICATE_SHEET", {
|
||||
sheetId,
|
||||
sheetIdTo: secondSheetId,
|
||||
sheetNameTo: "Next Name",
|
||||
});
|
||||
const newChartId = model.getters.getChartIds(secondSheetId)[0];
|
||||
expect(model.getters.getChartOdooMenu(newChartId)).toEqual(
|
||||
model.getters.getChartOdooMenu(chartId)
|
||||
);
|
||||
});
|
||||
|
||||
test("Links of standard charts are duplicated when duplicating a sheet", async function () {
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { env } });
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
const secondSheetId = "mySecondSheetId";
|
||||
createBasicChart(model, chartId);
|
||||
model.dispatch("LINK_ODOO_MENU_TO_CHART", {
|
||||
chartId,
|
||||
odooMenuId: 1,
|
||||
});
|
||||
model.dispatch("DUPLICATE_SHEET", {
|
||||
sheetId,
|
||||
sheetIdTo: secondSheetId,
|
||||
sheetNameTo: "Next Name",
|
||||
});
|
||||
const newChartId = model.getters.getChartIds(secondSheetId)[0];
|
||||
expect(model.getters.getChartOdooMenu(newChartId)).toEqual(
|
||||
model.getters.getChartOdooMenu(chartId)
|
||||
);
|
||||
});
|
||||
|
|
@ -1,217 +0,0 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended";
|
||||
import { getBasicData } from "@spreadsheet/../tests/utils/data";
|
||||
import { createBasicChart } from "@spreadsheet/../tests/utils/commands";
|
||||
import { createSpreadsheetWithChart } from "@spreadsheet/../tests/utils/chart";
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { menuService } from "@web/webclient/menus/menu_service";
|
||||
import { actionService } from "@web/webclient/actions/action_service";
|
||||
import { ormService } from "@web/core/orm_service";
|
||||
import { viewService } from "@web/views/view_service";
|
||||
|
||||
const { Model } = spreadsheet;
|
||||
|
||||
const chartId = "uuid1";
|
||||
|
||||
QUnit.module(
|
||||
"spreadsheet > ir.ui.menu chart plugin",
|
||||
{
|
||||
beforeEach: function () {
|
||||
this.serverData = {};
|
||||
this.serverData.menus = {
|
||||
root: {
|
||||
id: "root",
|
||||
children: [1, 2],
|
||||
name: "root",
|
||||
appID: "root",
|
||||
},
|
||||
1: {
|
||||
id: 1,
|
||||
children: [],
|
||||
name: "test menu 1",
|
||||
xmlid: "documents_spreadsheet.test.menu",
|
||||
appID: 1,
|
||||
actionID: "menuAction",
|
||||
},
|
||||
2: {
|
||||
id: 2,
|
||||
children: [],
|
||||
name: "test menu 2",
|
||||
xmlid: "documents_spreadsheet.test.menu2",
|
||||
appID: 1,
|
||||
actionID: "menuAction2",
|
||||
},
|
||||
};
|
||||
this.serverData.actions = {
|
||||
menuAction: {
|
||||
id: 99,
|
||||
xml_id: "ir.ui.menu",
|
||||
name: "menuAction",
|
||||
res_model: "ir.ui.menu",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "list"]],
|
||||
},
|
||||
menuAction2: {
|
||||
id: 100,
|
||||
xml_id: "ir.ui.menu",
|
||||
name: "menuAction2",
|
||||
res_model: "ir.ui.menu",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "list"]],
|
||||
},
|
||||
};
|
||||
this.serverData.views = {};
|
||||
this.serverData.views["ir.ui.menu,false,list"] = `<tree></tree>`;
|
||||
this.serverData.views["ir.ui.menu,false,search"] = `<search></search>`;
|
||||
this.serverData.models = {
|
||||
...getBasicData(),
|
||||
"ir.ui.menu": {
|
||||
fields: {
|
||||
name: { string: "Name", type: "char" },
|
||||
action: { string: "Action", type: "char" },
|
||||
groups_id: {
|
||||
string: "Groups",
|
||||
type: "many2many",
|
||||
relation: "res.group",
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
name: "test menu 1",
|
||||
action: "action1",
|
||||
groups_id: [10],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "test menu 2",
|
||||
action: "action2",
|
||||
groups_id: [10],
|
||||
},
|
||||
],
|
||||
},
|
||||
"res.users": {
|
||||
fields: {
|
||||
name: { string: "Name", type: "char" },
|
||||
groups_id: {
|
||||
string: "Groups",
|
||||
type: "many2many",
|
||||
relation: "res.group",
|
||||
},
|
||||
},
|
||||
records: [{ id: 1, name: "Raoul", groups_id: [10] }],
|
||||
},
|
||||
"ir.actions": {
|
||||
fields: {
|
||||
name: { string: "Name", type: "char" },
|
||||
},
|
||||
records: [{ id: 1 }],
|
||||
},
|
||||
"res.group": {
|
||||
fields: { name: { string: "Name", type: "char" } },
|
||||
records: [{ id: 10, name: "test group" }],
|
||||
},
|
||||
};
|
||||
registry.category("services").add("menu", menuService).add("action", actionService);
|
||||
registry.category("services").add("view", viewService, { force: true }); // #action-serv-leg-compat-js-class
|
||||
registry.category("services").add("orm", ormService, { force: true }); // #action-serv-leg-compat-js-class
|
||||
},
|
||||
},
|
||||
|
||||
() => {
|
||||
QUnit.test(
|
||||
"Links between charts and ir.menus are correctly imported/exported",
|
||||
async function (assert) {
|
||||
const env = await makeTestEnv({ serverData: this.serverData });
|
||||
const model = new Model({}, { evalContext: { env } });
|
||||
createBasicChart(model, chartId);
|
||||
model.dispatch("LINK_ODOO_MENU_TO_CHART", {
|
||||
chartId,
|
||||
odooMenuId: 1,
|
||||
});
|
||||
const exportedData = model.exportData();
|
||||
assert.equal(
|
||||
exportedData.chartOdooMenusReferences[chartId],
|
||||
1,
|
||||
"Link to odoo menu is exported"
|
||||
);
|
||||
const importedModel = new Model(exportedData, { evalContext: { env } });
|
||||
const chartMenu = importedModel.getters.getChartOdooMenu(chartId);
|
||||
assert.equal(chartMenu.id, 1, "Link to odoo menu is imported");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("Can undo-redo a LINK_ODOO_MENU_TO_CHART", async function (assert) {
|
||||
const env = await makeTestEnv({ serverData: this.serverData });
|
||||
const model = new Model({}, { evalContext: { env } });
|
||||
createBasicChart(model, chartId);
|
||||
model.dispatch("LINK_ODOO_MENU_TO_CHART", {
|
||||
chartId,
|
||||
odooMenuId: 1,
|
||||
});
|
||||
assert.equal(model.getters.getChartOdooMenu(chartId).id, 1);
|
||||
model.dispatch("REQUEST_UNDO");
|
||||
assert.equal(model.getters.getChartOdooMenu(chartId), undefined);
|
||||
model.dispatch("REQUEST_REDO");
|
||||
assert.equal(model.getters.getChartOdooMenu(chartId).id, 1);
|
||||
});
|
||||
|
||||
QUnit.test("link is removed when figure is deleted", async function (assert) {
|
||||
const env = await makeTestEnv({ serverData: this.serverData });
|
||||
const model = new Model({}, { evalContext: { env } });
|
||||
createBasicChart(model, chartId);
|
||||
model.dispatch("LINK_ODOO_MENU_TO_CHART", {
|
||||
chartId,
|
||||
odooMenuId: 1,
|
||||
});
|
||||
assert.equal(model.getters.getChartOdooMenu(chartId).id, 1);
|
||||
model.dispatch("DELETE_FIGURE", {
|
||||
sheetId: model.getters.getActiveSheetId(),
|
||||
id: chartId,
|
||||
});
|
||||
assert.equal(model.getters.getChartOdooMenu(chartId), undefined);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"Links of Odoo charts are duplicated when duplicating a sheet",
|
||||
async function (assert) {
|
||||
const { model } = await createSpreadsheetWithChart({
|
||||
type: "odoo_pie",
|
||||
serverData: this.serverData,
|
||||
});
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
const secondSheetId = "mySecondSheetId";
|
||||
const chartId = model.getters.getChartIds(sheetId)[0];
|
||||
model.dispatch("DUPLICATE_SHEET", { sheetId, sheetIdTo: secondSheetId });
|
||||
const newChartId = model.getters.getChartIds(secondSheetId)[0];
|
||||
assert.deepEqual(
|
||||
model.getters.getChartOdooMenu(newChartId),
|
||||
model.getters.getChartOdooMenu(chartId)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"Links of standard charts are duplicated when duplicating a sheet",
|
||||
async function (assert) {
|
||||
const env = await makeTestEnv({ serverData: this.serverData });
|
||||
const model = new Model({}, { evalContext: { env } });
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
const secondSheetId = "mySecondSheetId";
|
||||
createBasicChart(model, chartId);
|
||||
model.dispatch("LINK_ODOO_MENU_TO_CHART", {
|
||||
chartId,
|
||||
odooMenuId: 1,
|
||||
});
|
||||
model.dispatch("DUPLICATE_SHEET", { sheetId, sheetIdTo: secondSheetId });
|
||||
const newChartId = model.getters.getChartIds(secondSheetId)[0];
|
||||
assert.deepEqual(
|
||||
model.getters.getChartOdooMenu(newChartId),
|
||||
model.getters.getChartOdooMenu(chartId)
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,481 +0,0 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { OdooBarChart } from "@spreadsheet/chart/odoo_chart/odoo_bar_chart";
|
||||
import { OdooChart } from "@spreadsheet/chart/odoo_chart/odoo_chart";
|
||||
import { OdooLineChart } from "@spreadsheet/chart/odoo_chart/odoo_line_chart";
|
||||
import { nextTick } from "@web/../tests/helpers/utils";
|
||||
import { createSpreadsheetWithChart, insertChartInSpreadsheet } from "../../utils/chart";
|
||||
import { createModelWithDataSource, waitForDataSourcesLoaded } from "../../utils/model";
|
||||
import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended";
|
||||
import { RPCError } from "@web/core/network/rpc_service";
|
||||
|
||||
const { toZone } = spreadsheet.helpers;
|
||||
|
||||
QUnit.module("spreadsheet > odoo chart plugin", {}, () => {
|
||||
QUnit.test("Can add an Odoo Bar chart", async (assert) => {
|
||||
const { model } = await createSpreadsheetWithChart({ type: "odoo_bar" });
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
assert.strictEqual(model.getters.getChartIds(sheetId).length, 1);
|
||||
const chartId = model.getters.getChartIds(sheetId)[0];
|
||||
const chart = model.getters.getChart(chartId);
|
||||
assert.ok(chart instanceof OdooBarChart);
|
||||
assert.strictEqual(chart.getDefinitionForExcel(), undefined);
|
||||
assert.strictEqual(model.getters.getChartRuntime(chartId).chartJsConfig.type, "bar");
|
||||
});
|
||||
|
||||
QUnit.test("Can add an Odoo Line chart", async (assert) => {
|
||||
const { model } = await createSpreadsheetWithChart({ type: "odoo_line" });
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
assert.strictEqual(model.getters.getChartIds(sheetId).length, 1);
|
||||
const chartId = model.getters.getChartIds(sheetId)[0];
|
||||
const chart = model.getters.getChart(chartId);
|
||||
assert.ok(chart instanceof OdooLineChart);
|
||||
assert.strictEqual(chart.getDefinitionForExcel(), undefined);
|
||||
assert.strictEqual(model.getters.getChartRuntime(chartId).chartJsConfig.type, "line");
|
||||
});
|
||||
|
||||
QUnit.test("Can add an Odoo Pie chart", async (assert) => {
|
||||
const { model } = await createSpreadsheetWithChart({ type: "odoo_pie" });
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
assert.strictEqual(model.getters.getChartIds(sheetId).length, 1);
|
||||
const chartId = model.getters.getChartIds(sheetId)[0];
|
||||
const chart = model.getters.getChart(chartId);
|
||||
assert.ok(chart instanceof OdooChart);
|
||||
assert.strictEqual(chart.getDefinitionForExcel(), undefined);
|
||||
assert.strictEqual(model.getters.getChartRuntime(chartId).chartJsConfig.type, "pie");
|
||||
});
|
||||
|
||||
QUnit.test("A data source is added after a chart creation", async (assert) => {
|
||||
const { model } = await createSpreadsheetWithChart();
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
const chartId = model.getters.getChartIds(sheetId)[0];
|
||||
assert.ok(model.getters.getChartDataSource(chartId));
|
||||
});
|
||||
|
||||
QUnit.test("Odoo bar chart runtime loads the data", async (assert) => {
|
||||
const { model } = await createSpreadsheetWithChart({
|
||||
type: "odoo_bar",
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "web_read_group") {
|
||||
assert.step("web_read_group");
|
||||
}
|
||||
},
|
||||
});
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
const chartId = model.getters.getChartIds(sheetId)[0];
|
||||
assert.verifySteps([], "it should not be loaded eagerly");
|
||||
assert.deepEqual(model.getters.getChartRuntime(chartId).chartJsConfig.data, {
|
||||
datasets: [],
|
||||
labels: [],
|
||||
});
|
||||
await nextTick();
|
||||
assert.deepEqual(model.getters.getChartRuntime(chartId).chartJsConfig.data, {
|
||||
datasets: [
|
||||
{
|
||||
backgroundColor: "rgb(31,119,180)",
|
||||
borderColor: "rgb(31,119,180)",
|
||||
data: [1, 3],
|
||||
label: "Count",
|
||||
},
|
||||
],
|
||||
labels: ["false", "true"],
|
||||
});
|
||||
assert.verifySteps(["web_read_group"], "it should have loaded the data");
|
||||
});
|
||||
|
||||
QUnit.test("Odoo pie chart runtime loads the data", async (assert) => {
|
||||
const { model } = await createSpreadsheetWithChart({
|
||||
type: "odoo_pie",
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "web_read_group") {
|
||||
assert.step("web_read_group");
|
||||
}
|
||||
},
|
||||
});
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
const chartId = model.getters.getChartIds(sheetId)[0];
|
||||
assert.verifySteps([], "it should not be loaded eagerly");
|
||||
assert.deepEqual(model.getters.getChartRuntime(chartId).chartJsConfig.data, {
|
||||
datasets: [],
|
||||
labels: [],
|
||||
});
|
||||
await nextTick();
|
||||
assert.deepEqual(model.getters.getChartRuntime(chartId).chartJsConfig.data, {
|
||||
datasets: [
|
||||
{
|
||||
backgroundColor: ["rgb(31,119,180)", "rgb(255,127,14)", "rgb(174,199,232)"],
|
||||
borderColor: "#FFFFFF",
|
||||
data: [1, 3],
|
||||
label: "",
|
||||
},
|
||||
],
|
||||
labels: ["false", "true"],
|
||||
});
|
||||
assert.verifySteps(["web_read_group"], "it should have loaded the data");
|
||||
});
|
||||
|
||||
QUnit.test("Odoo line chart runtime loads the data", async (assert) => {
|
||||
const { model } = await createSpreadsheetWithChart({
|
||||
type: "odoo_line",
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "web_read_group") {
|
||||
assert.step("web_read_group");
|
||||
}
|
||||
},
|
||||
});
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
const chartId = model.getters.getChartIds(sheetId)[0];
|
||||
assert.verifySteps([], "it should not be loaded eagerly");
|
||||
assert.deepEqual(model.getters.getChartRuntime(chartId).chartJsConfig.data, {
|
||||
datasets: [],
|
||||
labels: [],
|
||||
});
|
||||
await nextTick();
|
||||
assert.deepEqual(model.getters.getChartRuntime(chartId).chartJsConfig.data, {
|
||||
datasets: [
|
||||
{
|
||||
backgroundColor: "#1F77B466",
|
||||
borderColor: "rgb(31,119,180)",
|
||||
data: [1, 3],
|
||||
label: "Count",
|
||||
lineTension: 0,
|
||||
fill: "origin",
|
||||
pointBackgroundColor: "rgb(31,119,180)",
|
||||
},
|
||||
],
|
||||
labels: ["false", "true"],
|
||||
});
|
||||
assert.verifySteps(["web_read_group"], "it should have loaded the data");
|
||||
});
|
||||
|
||||
QUnit.test("Changing the chart type does not reload the data", async (assert) => {
|
||||
const { model } = await createSpreadsheetWithChart({
|
||||
type: "odoo_line",
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "web_read_group") {
|
||||
assert.step("web_read_group");
|
||||
}
|
||||
},
|
||||
});
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
const chartId = model.getters.getChartIds(sheetId)[0];
|
||||
const definition = model.getters.getChartDefinition(chartId);
|
||||
|
||||
// force runtime computation
|
||||
model.getters.getChartRuntime(chartId);
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps(["web_read_group"], "it should have loaded the data");
|
||||
model.dispatch("UPDATE_CHART", {
|
||||
definition: {
|
||||
...definition,
|
||||
type: "odoo_bar",
|
||||
},
|
||||
id: chartId,
|
||||
sheetId,
|
||||
});
|
||||
await nextTick();
|
||||
// force runtime computation
|
||||
model.getters.getChartRuntime(chartId);
|
||||
assert.verifySteps([], "it should have not have loaded the data a second time");
|
||||
});
|
||||
|
||||
QUnit.test("Can import/export an Odoo chart", async (assert) => {
|
||||
const model = await createModelWithDataSource();
|
||||
insertChartInSpreadsheet(model, "odoo_line");
|
||||
const data = model.exportData();
|
||||
const figures = data.sheets[0].figures;
|
||||
assert.strictEqual(figures.length, 1);
|
||||
const figure = figures[0];
|
||||
assert.strictEqual(figure.tag, "chart");
|
||||
assert.strictEqual(figure.data.type, "odoo_line");
|
||||
const m1 = await createModelWithDataSource({ spreadsheetData: data });
|
||||
const sheetId = m1.getters.getActiveSheetId();
|
||||
assert.strictEqual(m1.getters.getChartIds(sheetId).length, 1);
|
||||
const chartId = m1.getters.getChartIds(sheetId)[0];
|
||||
assert.ok(m1.getters.getChartDataSource(chartId));
|
||||
assert.strictEqual(m1.getters.getChartRuntime(chartId).chartJsConfig.type, "line");
|
||||
});
|
||||
|
||||
QUnit.test("Can undo/redo an Odoo chart creation", async (assert) => {
|
||||
const model = await createModelWithDataSource();
|
||||
insertChartInSpreadsheet(model, "odoo_line");
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
const chartId = model.getters.getChartIds(sheetId)[0];
|
||||
assert.ok(model.getters.getChartDataSource(chartId));
|
||||
model.dispatch("REQUEST_UNDO");
|
||||
assert.strictEqual(model.getters.getChartIds(sheetId).length, 0);
|
||||
model.dispatch("REQUEST_REDO");
|
||||
assert.ok(model.getters.getChartDataSource(chartId));
|
||||
assert.strictEqual(model.getters.getChartIds(sheetId).length, 1);
|
||||
});
|
||||
|
||||
QUnit.test("charts with no legend", async (assert) => {
|
||||
const { model } = await createSpreadsheetWithChart({ type: "odoo_pie" });
|
||||
insertChartInSpreadsheet(model, "odoo_bar");
|
||||
insertChartInSpreadsheet(model, "odoo_line");
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
const [pieChartId, barChartId, lineChartId] = model.getters.getChartIds(sheetId);
|
||||
const pie = model.getters.getChartDefinition(pieChartId);
|
||||
const bar = model.getters.getChartDefinition(barChartId);
|
||||
const line = model.getters.getChartDefinition(lineChartId);
|
||||
assert.strictEqual(
|
||||
model.getters.getChartRuntime(pieChartId).chartJsConfig.options.legend.display,
|
||||
true
|
||||
);
|
||||
assert.strictEqual(
|
||||
model.getters.getChartRuntime(barChartId).chartJsConfig.options.legend.display,
|
||||
true
|
||||
);
|
||||
assert.strictEqual(
|
||||
model.getters.getChartRuntime(lineChartId).chartJsConfig.options.legend.display,
|
||||
true
|
||||
);
|
||||
model.dispatch("UPDATE_CHART", {
|
||||
definition: {
|
||||
...pie,
|
||||
legendPosition: "none",
|
||||
},
|
||||
id: pieChartId,
|
||||
sheetId,
|
||||
});
|
||||
model.dispatch("UPDATE_CHART", {
|
||||
definition: {
|
||||
...bar,
|
||||
legendPosition: "none",
|
||||
},
|
||||
id: barChartId,
|
||||
sheetId,
|
||||
});
|
||||
model.dispatch("UPDATE_CHART", {
|
||||
definition: {
|
||||
...line,
|
||||
legendPosition: "none",
|
||||
},
|
||||
id: lineChartId,
|
||||
sheetId,
|
||||
});
|
||||
assert.strictEqual(
|
||||
model.getters.getChartRuntime(pieChartId).chartJsConfig.options.legend.display,
|
||||
false
|
||||
);
|
||||
assert.strictEqual(
|
||||
model.getters.getChartRuntime(barChartId).chartJsConfig.options.legend.display,
|
||||
false
|
||||
);
|
||||
assert.strictEqual(
|
||||
model.getters.getChartRuntime(lineChartId).chartJsConfig.options.legend.display,
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Bar chart with stacked attribute is supported", async (assert) => {
|
||||
const { model } = await createSpreadsheetWithChart({ type: "odoo_bar" });
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
const chartId = model.getters.getChartIds(sheetId)[0];
|
||||
const definition = model.getters.getChartDefinition(chartId);
|
||||
model.dispatch("UPDATE_CHART", {
|
||||
definition: {
|
||||
...definition,
|
||||
stacked: true,
|
||||
},
|
||||
id: chartId,
|
||||
sheetId,
|
||||
});
|
||||
assert.ok(
|
||||
model.getters.getChartRuntime(chartId).chartJsConfig.options.scales.xAxes[0].stacked
|
||||
);
|
||||
assert.ok(
|
||||
model.getters.getChartRuntime(chartId).chartJsConfig.options.scales.yAxes[0].stacked
|
||||
);
|
||||
model.dispatch("UPDATE_CHART", {
|
||||
definition: {
|
||||
...definition,
|
||||
stacked: false,
|
||||
},
|
||||
id: chartId,
|
||||
sheetId,
|
||||
});
|
||||
assert.notOk(
|
||||
model.getters.getChartRuntime(chartId).chartJsConfig.options.scales.xAxes[0].stacked
|
||||
);
|
||||
assert.notOk(
|
||||
model.getters.getChartRuntime(chartId).chartJsConfig.options.scales.yAxes[0].stacked
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Can copy/paste Odoo chart", async (assert) => {
|
||||
const { model } = await createSpreadsheetWithChart({ type: "odoo_pie" });
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
const chartId = model.getters.getChartIds(sheetId)[0];
|
||||
model.dispatch("SELECT_FIGURE", { id: chartId });
|
||||
model.dispatch("COPY");
|
||||
model.dispatch("PASTE", { target: [toZone("A1")] });
|
||||
const chartIds = model.getters.getChartIds(sheetId);
|
||||
assert.strictEqual(chartIds.length, 2);
|
||||
assert.ok(model.getters.getChart(chartIds[1]) instanceof OdooChart);
|
||||
assert.strictEqual(
|
||||
JSON.stringify(model.getters.getChartRuntime(chartIds[1])),
|
||||
JSON.stringify(model.getters.getChartRuntime(chartId))
|
||||
);
|
||||
|
||||
assert.notEqual(
|
||||
model.getters.getChart(chartId).dataSource,
|
||||
model.getters.getChart(chartIds[1]).dataSource,
|
||||
"The datasource is also duplicated"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Can cut/paste Odoo chart", async (assert) => {
|
||||
const { model } = await createSpreadsheetWithChart({ type: "odoo_pie" });
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
const chartId = model.getters.getChartIds(sheetId)[0];
|
||||
const chartRuntime = model.getters.getChartRuntime(chartId);
|
||||
model.dispatch("SELECT_FIGURE", { id: chartId });
|
||||
model.dispatch("CUT");
|
||||
model.dispatch("PASTE", { target: [toZone("A1")] });
|
||||
const chartIds = model.getters.getChartIds(sheetId);
|
||||
assert.strictEqual(chartIds.length, 1);
|
||||
assert.notEqual(chartIds[0], chartId);
|
||||
assert.ok(model.getters.getChart(chartIds[0]) instanceof OdooChart);
|
||||
assert.strictEqual(
|
||||
JSON.stringify(model.getters.getChartRuntime(chartIds[0])),
|
||||
JSON.stringify(chartRuntime)
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Duplicating a sheet correctly duplicates Odoo chart", async (assert) => {
|
||||
const { model } = await createSpreadsheetWithChart({ type: "odoo_bar" });
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
const secondSheetId = "secondSheetId";
|
||||
const chartId = model.getters.getChartIds(sheetId)[0];
|
||||
model.dispatch("DUPLICATE_SHEET", { sheetId, sheetIdTo: secondSheetId });
|
||||
const chartIds = model.getters.getChartIds(secondSheetId);
|
||||
assert.strictEqual(chartIds.length, 1);
|
||||
assert.ok(model.getters.getChart(chartIds[0]) instanceof OdooChart);
|
||||
assert.strictEqual(
|
||||
JSON.stringify(model.getters.getChartRuntime(chartIds[0])),
|
||||
JSON.stringify(model.getters.getChartRuntime(chartId))
|
||||
);
|
||||
|
||||
assert.notEqual(
|
||||
model.getters.getChart(chartId).dataSource,
|
||||
model.getters.getChart(chartIds[0]).dataSource,
|
||||
"The datasource is also duplicated"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Line chart with stacked attribute is supported", async (assert) => {
|
||||
const { model } = await createSpreadsheetWithChart({ type: "odoo_line" });
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
const chartId = model.getters.getChartIds(sheetId)[0];
|
||||
const definition = model.getters.getChartDefinition(chartId);
|
||||
model.dispatch("UPDATE_CHART", {
|
||||
definition: {
|
||||
...definition,
|
||||
stacked: true,
|
||||
},
|
||||
id: chartId,
|
||||
sheetId,
|
||||
});
|
||||
assert.notOk(
|
||||
model.getters.getChartRuntime(chartId).chartJsConfig.options.scales.xAxes[0].stacked
|
||||
);
|
||||
assert.ok(
|
||||
model.getters.getChartRuntime(chartId).chartJsConfig.options.scales.yAxes[0].stacked
|
||||
);
|
||||
model.dispatch("UPDATE_CHART", {
|
||||
definition: {
|
||||
...definition,
|
||||
stacked: false,
|
||||
},
|
||||
id: chartId,
|
||||
sheetId,
|
||||
});
|
||||
assert.notOk(
|
||||
model.getters.getChartRuntime(chartId).chartJsConfig.options.scales.xAxes[0].stacked
|
||||
);
|
||||
assert.notOk(
|
||||
model.getters.getChartRuntime(chartId).chartJsConfig.options.scales.yAxes[0].stacked
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"Load odoo chart spreadsheet with models that cannot be accessed",
|
||||
async function (assert) {
|
||||
let hasAccessRights = true;
|
||||
const { model } = await createSpreadsheetWithChart({
|
||||
mockRPC: async function (route, args) {
|
||||
if (
|
||||
args.model === "partner" &&
|
||||
args.method === "web_read_group" &&
|
||||
!hasAccessRights
|
||||
) {
|
||||
const error = new RPCError();
|
||||
error.data = { message: "ya done!" };
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
});
|
||||
const chartId = model.getters.getFigures(model.getters.getActiveSheetId())[0].id;
|
||||
const chartDataSource = model.getters.getChartDataSource(chartId);
|
||||
await waitForDataSourcesLoaded(model);
|
||||
const data = chartDataSource.getData();
|
||||
assert.equal(data.datasets.length, 1);
|
||||
assert.equal(data.labels.length, 2);
|
||||
|
||||
hasAccessRights = false;
|
||||
chartDataSource.load({ reload: true });
|
||||
await waitForDataSourcesLoaded(model);
|
||||
assert.deepEqual(chartDataSource.getData(), { datasets: [], labels: [] });
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("Line chart to support cumulative data", async (assert) => {
|
||||
const { model } = await createSpreadsheetWithChart({ type: "odoo_line" });
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
const chartId = model.getters.getChartIds(sheetId)[0];
|
||||
const definition = model.getters.getChartDefinition(chartId);
|
||||
await waitForDataSourcesLoaded(model);
|
||||
assert.deepEqual(
|
||||
model.getters.getChartRuntime(chartId).chartJsConfig.data.datasets[0].data,
|
||||
[1, 3]
|
||||
);
|
||||
model.dispatch("UPDATE_CHART", {
|
||||
definition: {
|
||||
...definition,
|
||||
cumulative: true,
|
||||
},
|
||||
id: chartId,
|
||||
sheetId,
|
||||
});
|
||||
assert.deepEqual(
|
||||
model.getters.getChartRuntime(chartId).chartJsConfig.data.datasets[0].data,
|
||||
[1, 4]
|
||||
);
|
||||
model.dispatch("UPDATE_CHART", {
|
||||
definition: {
|
||||
...definition,
|
||||
cumulative: false,
|
||||
},
|
||||
id: chartId,
|
||||
sheetId,
|
||||
});
|
||||
assert.deepEqual(
|
||||
model.getters.getChartRuntime(chartId).chartJsConfig.data.datasets[0].data,
|
||||
[1, 3]
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Remove odoo chart when sheet is deleted", async (assert) => {
|
||||
const { model } = await createSpreadsheetWithChart({ type: "odoo_line" });
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
model.dispatch("CREATE_SHEET", {
|
||||
sheetId: model.uuidGenerator.uuidv4(),
|
||||
position: model.getters.getSheetIds().length,
|
||||
});
|
||||
assert.strictEqual(model.getters.getOdooChartIds().length, 1);
|
||||
model.dispatch("DELETE_SHEET", { sheetId });
|
||||
assert.strictEqual(model.getters.getOdooChartIds().length, 0);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,241 @@
|
|||
import { click } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { expect, test, beforeEach } from "@odoo/hoot";
|
||||
import { getBasicData, defineSpreadsheetModels } from "@spreadsheet/../tests/helpers/data";
|
||||
import { createBasicChart } from "@spreadsheet/../tests/helpers/commands";
|
||||
import { mountSpreadsheet } from "@spreadsheet/../tests/helpers/ui";
|
||||
import { createModelWithDataSource } from "@spreadsheet/../tests/helpers/model";
|
||||
import { mockService, serverState } from "@web/../tests/web_test_helpers";
|
||||
|
||||
defineSpreadsheetModels();
|
||||
|
||||
/**
|
||||
* @typedef {import("@spreadsheet/../tests/helpers/data").ServerData} ServerData
|
||||
*/
|
||||
|
||||
const chartId = "uuid1";
|
||||
let serverData = /** @type {ServerData} */ ({});
|
||||
|
||||
/**
|
||||
* The chart menu is hidden by default, and visible on :hover, but this property
|
||||
* can't be triggered programmatically, so we artificially make it visible to be
|
||||
* able to interact with it.
|
||||
*/
|
||||
async function showChartMenu(fixture) {
|
||||
const chartMenu = fixture.querySelector(".o-figure-menu");
|
||||
chartMenu.style.display = "flex";
|
||||
await animationFrame();
|
||||
}
|
||||
|
||||
/** Click on external link of the first chart found in the page*/
|
||||
async function clickChartExternalLink(fixture) {
|
||||
await showChartMenu(fixture);
|
||||
const chartMenuItem = fixture.querySelector(".o-figure-menu-item.o-chart-external-link");
|
||||
await click(chartMenuItem);
|
||||
await animationFrame();
|
||||
}
|
||||
|
||||
function mockActionService(doActionStep) {
|
||||
const fakeActionService = {
|
||||
doAction: async (actionRequest, options = {}) => {
|
||||
if (actionRequest === "menuAction2") {
|
||||
expect.step(doActionStep);
|
||||
}
|
||||
},
|
||||
};
|
||||
mockService("action", fakeActionService);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
serverData = {};
|
||||
serverData.menus = {
|
||||
1: {
|
||||
id: 1,
|
||||
name: "test menu 1",
|
||||
xmlid: "spreadsheet.test.menu",
|
||||
appID: 1,
|
||||
actionID: "menuAction",
|
||||
},
|
||||
2: {
|
||||
id: 2,
|
||||
name: "test menu 2",
|
||||
xmlid: "spreadsheet.test.menu2",
|
||||
appID: 1,
|
||||
actionID: "menuAction2",
|
||||
},
|
||||
3: {
|
||||
id: 3,
|
||||
name: "test menu 2",
|
||||
xmlid: "spreadsheet.test.menu_without_action",
|
||||
appID: 1,
|
||||
},
|
||||
};
|
||||
serverData.actions = {
|
||||
menuAction: {
|
||||
id: 99,
|
||||
xml_id: "ir.ui.menu",
|
||||
name: "menuAction",
|
||||
res_model: "ir.ui.menu",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "list"]],
|
||||
},
|
||||
menuAction2: {
|
||||
id: 100,
|
||||
xml_id: "ir.ui.menu",
|
||||
name: "menuAction2",
|
||||
res_model: "ir.ui.menu",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "list"]],
|
||||
},
|
||||
};
|
||||
serverData.models = {
|
||||
...getBasicData(),
|
||||
"ir.ui.menu": {
|
||||
records: [
|
||||
{ id: 1, name: "test menu 1", action: "action1", group_ids: [10] },
|
||||
{ id: 2, name: "test menu 2", action: "action2", group_ids: [10] },
|
||||
],
|
||||
},
|
||||
"res.group": { records: [{ id: 10, name: "test group" }] },
|
||||
"res.users": {
|
||||
records: [{ id: 1, active: true, partner_id: serverState.partnerId, name: "Raoul" }],
|
||||
},
|
||||
"ir.actions": { records: [{ id: 1 }] },
|
||||
};
|
||||
serverState.userId = 1;
|
||||
});
|
||||
|
||||
test("icon external link isn't on the chart when its not linked to an odoo menu", async function () {
|
||||
const { model } = await createModelWithDataSource({
|
||||
serverData,
|
||||
});
|
||||
const fixture = await mountSpreadsheet(model);
|
||||
createBasicChart(model, chartId);
|
||||
await animationFrame();
|
||||
const odooMenu = model.getters.getChartOdooMenu(chartId);
|
||||
expect(odooMenu).toBe(undefined, { message: "No menu linked with the chart" });
|
||||
|
||||
const externalRefIcon = fixture.querySelector(".o-chart-external-link");
|
||||
expect(externalRefIcon).toBe(null);
|
||||
});
|
||||
|
||||
test("icon external link is on the chart when its linked to an odoo menu", async function () {
|
||||
const { model } = await createModelWithDataSource({
|
||||
serverData,
|
||||
});
|
||||
await mountSpreadsheet(model);
|
||||
createBasicChart(model, chartId);
|
||||
model.dispatch("LINK_ODOO_MENU_TO_CHART", {
|
||||
chartId,
|
||||
odooMenuId: 1,
|
||||
});
|
||||
|
||||
const chartMenu = model.getters.getChartOdooMenu(chartId);
|
||||
expect(chartMenu.id).toBe(1, { message: "Odoo menu is linked to chart" });
|
||||
await animationFrame();
|
||||
expect(".o-chart-external-link").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("icon external link is not on the chart when its linked to a wrong odoo menu", async function () {
|
||||
const { model } = await createModelWithDataSource({
|
||||
serverData,
|
||||
});
|
||||
await mountSpreadsheet(model);
|
||||
createBasicChart(model, chartId);
|
||||
model.dispatch("LINK_ODOO_MENU_TO_CHART", {
|
||||
chartId,
|
||||
odooMenuId: "menu which does not exist",
|
||||
});
|
||||
const chartMenu = model.getters.getChartOdooMenu(chartId);
|
||||
expect(chartMenu).toBe(undefined, { message: "cannot get a wrong menu" });
|
||||
await animationFrame();
|
||||
expect(".o-chart-external-link").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("icon external link isn't on the chart in dashboard mode", async function () {
|
||||
const { model } = await createModelWithDataSource({
|
||||
serverData,
|
||||
});
|
||||
await mountSpreadsheet(model);
|
||||
createBasicChart(model, chartId);
|
||||
model.dispatch("LINK_ODOO_MENU_TO_CHART", {
|
||||
chartId,
|
||||
odooMenuId: 1,
|
||||
});
|
||||
const chartMenu = model.getters.getChartOdooMenu(chartId);
|
||||
expect(chartMenu.id).toBe(1, { message: "Odoo menu is linked to chart" });
|
||||
model.updateMode("dashboard");
|
||||
await animationFrame();
|
||||
expect(".o-chart-external-link").toHaveCount(0, { message: "No link icon in dashboard" });
|
||||
});
|
||||
|
||||
test("click on icon external link on chart redirect to the odoo menu", async function () {
|
||||
const doActionStep = "doAction";
|
||||
mockActionService(doActionStep);
|
||||
|
||||
const { model } = await createModelWithDataSource({
|
||||
serverData,
|
||||
});
|
||||
const fixture = await mountSpreadsheet(model);
|
||||
|
||||
createBasicChart(model, chartId);
|
||||
model.dispatch("LINK_ODOO_MENU_TO_CHART", {
|
||||
chartId,
|
||||
odooMenuId: 2,
|
||||
});
|
||||
const chartMenu = model.getters.getChartOdooMenu(chartId);
|
||||
expect(chartMenu.id).toBe(2, { message: "Odoo menu is linked to chart" });
|
||||
await animationFrame();
|
||||
|
||||
await clickChartExternalLink(fixture);
|
||||
|
||||
expect.verifySteps([doActionStep]);
|
||||
});
|
||||
|
||||
test("can use menus xmlIds instead of menu ids", async function () {
|
||||
mockActionService("doAction");
|
||||
const { model } = await createModelWithDataSource({
|
||||
serverData,
|
||||
});
|
||||
const fixture = await mountSpreadsheet(model);
|
||||
|
||||
createBasicChart(model, chartId);
|
||||
model.dispatch("LINK_ODOO_MENU_TO_CHART", {
|
||||
chartId,
|
||||
odooMenuId: "spreadsheet.test.menu2",
|
||||
});
|
||||
await animationFrame();
|
||||
|
||||
await clickChartExternalLink(fixture);
|
||||
|
||||
expect.verifySteps(["doAction"]);
|
||||
});
|
||||
|
||||
test("Trying to open a menu without an action sends a notification to the user", async function () {
|
||||
mockActionService("doAction");
|
||||
mockService("notification", {
|
||||
add: (message) => {
|
||||
expect.step(message);
|
||||
return () => {};
|
||||
},
|
||||
});
|
||||
|
||||
const { model } = await createModelWithDataSource({
|
||||
serverData,
|
||||
});
|
||||
const fixture = await mountSpreadsheet(model);
|
||||
|
||||
createBasicChart(model, chartId);
|
||||
model.dispatch("LINK_ODOO_MENU_TO_CHART", {
|
||||
chartId,
|
||||
odooMenuId: "spreadsheet.test.menu_without_action",
|
||||
});
|
||||
await animationFrame();
|
||||
|
||||
await clickChartExternalLink(fixture);
|
||||
|
||||
const expectedNotificationMessage =
|
||||
"The menu linked to this chart doesn't have an corresponding action. Please link the chart to another menu.";
|
||||
// Notification was send and doAction wasn't called
|
||||
expect.verifySteps([expectedNotificationMessage]);
|
||||
});
|
||||
|
|
@ -1,336 +0,0 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { click, nextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { session } from "@web/session";
|
||||
import { getBasicData } from "@spreadsheet/../tests/utils/data";
|
||||
import { createBasicChart } from "@spreadsheet/../tests/utils/commands";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { menuService } from "@web/webclient/menus/menu_service";
|
||||
import { actionService } from "@web/webclient/actions/action_service";
|
||||
import { ormService } from "@web/core/orm_service";
|
||||
import { viewService } from "@web/views/view_service";
|
||||
import { mountSpreadsheet } from "@spreadsheet/../tests/utils/ui";
|
||||
import { createModelWithDataSource } from "@spreadsheet/../tests/utils/model";
|
||||
|
||||
const chartId = "uuid1";
|
||||
|
||||
/**
|
||||
* The chart menu is hidden by default, and visible on :hover, but this property
|
||||
* can't be triggered programmatically, so we artificially make it visible to be
|
||||
* able to interact with it.
|
||||
*/
|
||||
async function showChartMenu(fixture) {
|
||||
const chartMenu = fixture.querySelector(".o-chart-menu");
|
||||
chartMenu.style.display = "flex";
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
/** Click on external link of the first chart found in the page*/
|
||||
async function clickChartExternalLink(fixture) {
|
||||
await showChartMenu(fixture);
|
||||
const chartMenuItem = fixture.querySelector(".o-chart-menu-item.o-chart-external-link");
|
||||
await click(chartMenuItem);
|
||||
}
|
||||
|
||||
function mockActionService(assert, doActionStep) {
|
||||
const serviceRegistry = registry.category("services");
|
||||
serviceRegistry.add("actionMain", actionService);
|
||||
const fakeActionService = {
|
||||
dependencies: ["actionMain"],
|
||||
start(env, { actionMain }) {
|
||||
return {
|
||||
...actionMain,
|
||||
doAction: (actionRequest, options = {}) => {
|
||||
if (actionRequest === "menuAction2") {
|
||||
assert.step(doActionStep);
|
||||
}
|
||||
return actionMain.doAction(actionRequest, options);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
serviceRegistry.add("action", fakeActionService, {
|
||||
force: true,
|
||||
});
|
||||
}
|
||||
|
||||
QUnit.module(
|
||||
"spreadsheet > ir.ui.menu chart figure",
|
||||
{
|
||||
beforeEach: function () {
|
||||
this.serverData = {};
|
||||
this.serverData.menus = {
|
||||
root: {
|
||||
id: "root",
|
||||
children: [1, 2],
|
||||
name: "root",
|
||||
appID: "root",
|
||||
},
|
||||
1: {
|
||||
id: 1,
|
||||
children: [],
|
||||
name: "test menu 1",
|
||||
xmlid: "documents_spreadsheet.test.menu",
|
||||
appID: 1,
|
||||
actionID: "menuAction",
|
||||
},
|
||||
2: {
|
||||
id: 2,
|
||||
children: [],
|
||||
name: "test menu 2",
|
||||
xmlid: "documents_spreadsheet.test.menu2",
|
||||
appID: 1,
|
||||
actionID: "menuAction2",
|
||||
},
|
||||
};
|
||||
this.serverData.actions = {
|
||||
menuAction: {
|
||||
id: 99,
|
||||
xml_id: "ir.ui.menu",
|
||||
name: "menuAction",
|
||||
res_model: "ir.ui.menu",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "list"]],
|
||||
},
|
||||
menuAction2: {
|
||||
id: 100,
|
||||
xml_id: "ir.ui.menu",
|
||||
name: "menuAction2",
|
||||
res_model: "ir.ui.menu",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "list"]],
|
||||
},
|
||||
};
|
||||
this.serverData.views = {};
|
||||
this.serverData.views["ir.ui.menu,false,list"] = `<tree></tree>`;
|
||||
this.serverData.views["ir.ui.menu,false,search"] = `<search></search>`;
|
||||
this.serverData.models = {
|
||||
...getBasicData(),
|
||||
"ir.ui.menu": {
|
||||
fields: {
|
||||
name: { string: "Name", type: "char" },
|
||||
action: { string: "Action", type: "char" },
|
||||
groups_id: {
|
||||
string: "Groups",
|
||||
type: "many2many",
|
||||
relation: "res.group",
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
name: "test menu 1",
|
||||
action: "action1",
|
||||
groups_id: [10],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "test menu 2",
|
||||
action: "action2",
|
||||
groups_id: [10],
|
||||
},
|
||||
],
|
||||
},
|
||||
"res.users": {
|
||||
fields: {
|
||||
name: { string: "Name", type: "char" },
|
||||
groups_id: {
|
||||
string: "Groups",
|
||||
type: "many2many",
|
||||
relation: "res.group",
|
||||
},
|
||||
},
|
||||
records: [{ id: 1, name: "Raoul", groups_id: [10] }],
|
||||
},
|
||||
"ir.actions": {
|
||||
fields: {
|
||||
name: { string: "Name", type: "char" },
|
||||
},
|
||||
records: [{ id: 1 }],
|
||||
},
|
||||
"res.group": {
|
||||
fields: { name: { string: "Name", type: "char" } },
|
||||
records: [{ id: 10, name: "test group" }],
|
||||
},
|
||||
};
|
||||
patchWithCleanup(session, { uid: 1 });
|
||||
registry.category("services").add("menu", menuService).add("action", actionService);
|
||||
registry.category("services").add("view", viewService, { force: true }); // #action-serv-leg-compat-js-class
|
||||
registry.category("services").add("orm", ormService, { force: true }); // #action-serv-leg-compat-js-class
|
||||
},
|
||||
},
|
||||
|
||||
() => {
|
||||
QUnit.test(
|
||||
"icon external link isn't on the chart when its not linked to an odoo menu",
|
||||
async function (assert) {
|
||||
const model = await createModelWithDataSource({
|
||||
serverData: this.serverData,
|
||||
});
|
||||
const fixture = await mountSpreadsheet(model);
|
||||
createBasicChart(model, chartId);
|
||||
await nextTick();
|
||||
const odooMenu = model.getters.getChartOdooMenu(chartId);
|
||||
assert.equal(odooMenu, undefined, "No menu linked with the chart");
|
||||
|
||||
const externalRefIcon = fixture.querySelector(".o-chart-external-link");
|
||||
assert.equal(externalRefIcon, null);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"icon external link is on the chart when its linked to an odoo menu",
|
||||
async function (assert) {
|
||||
const model = await createModelWithDataSource({
|
||||
serverData: this.serverData,
|
||||
});
|
||||
const fixture = await mountSpreadsheet(model);
|
||||
createBasicChart(model, chartId);
|
||||
model.dispatch("LINK_ODOO_MENU_TO_CHART", {
|
||||
chartId,
|
||||
odooMenuId: 1,
|
||||
});
|
||||
const chartMenu = model.getters.getChartOdooMenu(chartId);
|
||||
assert.equal(chartMenu.id, 1, "Odoo menu is linked to chart");
|
||||
await nextTick();
|
||||
const externalRefIcon = fixture.querySelector(".o-chart-external-link");
|
||||
assert.ok(externalRefIcon);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"icon external link is not on the chart when its linked to a wrong odoo menu",
|
||||
async function (assert) {
|
||||
const model = await createModelWithDataSource({
|
||||
serverData: this.serverData,
|
||||
});
|
||||
const fixture = await mountSpreadsheet(model);
|
||||
createBasicChart(model, chartId);
|
||||
model.dispatch("LINK_ODOO_MENU_TO_CHART", {
|
||||
chartId,
|
||||
odooMenuId: "menu which does not exist",
|
||||
});
|
||||
const chartMenu = model.getters.getChartOdooMenu(chartId);
|
||||
assert.equal(chartMenu, undefined, "cannot get a wrong menu");
|
||||
await nextTick();
|
||||
assert.containsNone(fixture, ".o-chart-external-link");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"icon external link isn't on the chart in dashboard mode",
|
||||
async function (assert) {
|
||||
const model = await createModelWithDataSource({
|
||||
serverData: this.serverData,
|
||||
});
|
||||
const fixture = await mountSpreadsheet(model);
|
||||
createBasicChart(model, chartId);
|
||||
model.dispatch("LINK_ODOO_MENU_TO_CHART", {
|
||||
chartId,
|
||||
odooMenuId: 1,
|
||||
});
|
||||
const chartMenu = model.getters.getChartOdooMenu(chartId);
|
||||
assert.equal(chartMenu.id, 1, "Odoo menu is linked to chart");
|
||||
model.updateMode("dashboard");
|
||||
await nextTick();
|
||||
assert.containsNone(fixture, ".o-chart-external-link", "No link icon in dashboard");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"click on icon external link on chart redirect to the odoo menu",
|
||||
async function (assert) {
|
||||
const doActionStep = "doAction";
|
||||
mockActionService(assert, doActionStep);
|
||||
|
||||
const model = await createModelWithDataSource({
|
||||
serverData: this.serverData,
|
||||
});
|
||||
const fixture = await mountSpreadsheet(model);
|
||||
|
||||
createBasicChart(model, chartId);
|
||||
model.dispatch("LINK_ODOO_MENU_TO_CHART", {
|
||||
chartId,
|
||||
odooMenuId: 2,
|
||||
});
|
||||
const chartMenu = model.getters.getChartOdooMenu(chartId);
|
||||
assert.equal(chartMenu.id, 2, "Odoo menu is linked to chart");
|
||||
await nextTick();
|
||||
|
||||
await clickChartExternalLink(fixture);
|
||||
|
||||
assert.verifySteps([doActionStep]);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"Click on chart in dashboard mode redirect to the odoo menu",
|
||||
async function (assert) {
|
||||
const doActionStep = "doAction";
|
||||
mockActionService(assert, doActionStep);
|
||||
const model = await createModelWithDataSource({
|
||||
serverData: this.serverData,
|
||||
});
|
||||
const fixture = await mountSpreadsheet(model);
|
||||
|
||||
createBasicChart(model, chartId);
|
||||
model.dispatch("LINK_ODOO_MENU_TO_CHART", {
|
||||
chartId,
|
||||
odooMenuId: 2,
|
||||
});
|
||||
const chartMenu = model.getters.getChartOdooMenu(chartId);
|
||||
assert.equal(chartMenu.id, 2, "Odoo menu is linked to chart");
|
||||
await nextTick();
|
||||
|
||||
await click(fixture, ".o-chart-container");
|
||||
assert.verifySteps([], "Clicking on a chart while not dashboard mode do nothing");
|
||||
|
||||
model.updateMode("dashboard");
|
||||
await nextTick();
|
||||
await click(fixture, ".o-chart-container");
|
||||
assert.verifySteps(
|
||||
[doActionStep],
|
||||
"Clicking on a chart while on dashboard mode redirect to the odoo menu"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("can use menus xmlIds instead of menu ids", async function (assert) {
|
||||
const serviceRegistry = registry.category("services");
|
||||
serviceRegistry.add("actionMain", actionService);
|
||||
const fakeActionService = {
|
||||
dependencies: ["actionMain"],
|
||||
start(env, { actionMain }) {
|
||||
return {
|
||||
...actionMain,
|
||||
doAction: (actionRequest, options = {}) => {
|
||||
if (actionRequest === "menuAction2") {
|
||||
assert.step("doAction");
|
||||
}
|
||||
return actionMain.doAction(actionRequest, options);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
serviceRegistry.add("action", fakeActionService, {
|
||||
force: true,
|
||||
});
|
||||
|
||||
const model = await createModelWithDataSource({
|
||||
serverData: this.serverData,
|
||||
});
|
||||
const fixture = await mountSpreadsheet(model);
|
||||
|
||||
createBasicChart(model, chartId);
|
||||
model.dispatch("LINK_ODOO_MENU_TO_CHART", {
|
||||
chartId,
|
||||
odooMenuId: "documents_spreadsheet.test.menu2",
|
||||
});
|
||||
await nextTick();
|
||||
|
||||
await clickChartExternalLink(fixture);
|
||||
|
||||
assert.verifySteps(["doAction"]);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { mountSpreadsheet } from "@spreadsheet/../tests/helpers/ui";
|
||||
import { createModelWithDataSource } from "@spreadsheet/../tests/helpers/model";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { press } from "@odoo/hoot-dom";
|
||||
import { defineSpreadsheetModels } from "@spreadsheet/../tests/helpers/data";
|
||||
import { WebClient } from "@web/webclient/webclient";
|
||||
import { mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
const serverData = /** @type {ServerData} */ ({});
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineSpreadsheetModels();
|
||||
|
||||
test("Command palette is active on spreadsheet", async function () {
|
||||
await mountWithCleanup(WebClient);
|
||||
const { model } = await createModelWithDataSource({
|
||||
serverData,
|
||||
});
|
||||
await mountSpreadsheet(model);
|
||||
await press(["control", "k"]);
|
||||
await animationFrame();
|
||||
expect(".o_command_palette").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("First item of command palette is insert link", async function () {
|
||||
await mountWithCleanup(WebClient);
|
||||
const { model } = await createModelWithDataSource({
|
||||
serverData,
|
||||
});
|
||||
await mountSpreadsheet(model);
|
||||
await press(["control", "k"]);
|
||||
await animationFrame();
|
||||
expect(".o_command_name:first").toHaveText("Insert / Link");
|
||||
});
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { createModelWithDataSource } from "@spreadsheet/../tests/helpers/model";
|
||||
import { defineSpreadsheetModels } from "../helpers/data";
|
||||
import { LoadingDataError } from "@spreadsheet/o_spreadsheet/errors";
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
defineSpreadsheetModels();
|
||||
|
||||
test("get default currency format when it's in the config", async () => {
|
||||
const { model } = await createModelWithDataSource({
|
||||
modelConfig: { defaultCurrency: { position: "after", symbol: "θ", decimalPlaces: 2 } },
|
||||
mockRPC: async function (route, args) {
|
||||
throw new Error("Should not make any RPC");
|
||||
},
|
||||
});
|
||||
expect(model.getters.getCompanyCurrencyFormat()).toBe("#,##0.00[$θ]");
|
||||
});
|
||||
|
||||
test("get default currency format when it's not in the config", async () => {
|
||||
const { model } = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "get_company_currency_for_spreadsheet") {
|
||||
return {
|
||||
code: "Odoo",
|
||||
symbol: "θ",
|
||||
position: "after",
|
||||
decimalPlaces: 2,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
expect(() => model.getters.getCompanyCurrencyFormat()).toThrow(LoadingDataError);
|
||||
await animationFrame();
|
||||
expect(model.getters.getCompanyCurrencyFormat()).toBe("#,##0.00[$θ]");
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("get specific currency format", async () => {
|
||||
const { model } = await createModelWithDataSource({
|
||||
modelConfig: { defaultCurrency: { position: "after", symbol: "θ", decimalPlaces: 2 } },
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "get_company_currency_for_spreadsheet" && args.args[0] === 42) {
|
||||
return {
|
||||
code: "Odoo",
|
||||
symbol: "O",
|
||||
position: "after",
|
||||
decimalPlaces: 2,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
expect(() => model.getters.getCompanyCurrencyFormat(42)).toThrow(LoadingDataError);
|
||||
await animationFrame();
|
||||
expect(model.getters.getCompanyCurrencyFormat(42)).toBe("#,##0.00[$O]");
|
||||
});
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { setCellContent } from "@spreadsheet/../tests/helpers/commands";
|
||||
import { getCellValue, getEvaluatedCell } from "@spreadsheet/../tests/helpers/getters";
|
||||
import { createModelWithDataSource } from "@spreadsheet/../tests/helpers/model";
|
||||
import { waitForDataLoaded } from "@spreadsheet/helpers/model";
|
||||
import { defineSpreadsheetActions, defineSpreadsheetModels } from "../helpers/data";
|
||||
import { RPCError } from "@web/core/network/rpc";
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
defineSpreadsheetModels();
|
||||
defineSpreadsheetActions();
|
||||
|
||||
test("Basic exchange formula", async () => {
|
||||
const { model } = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "get_rates_for_spreadsheet") {
|
||||
const info = args.args[0][0];
|
||||
expect(info.from).toBe("EUR");
|
||||
expect(info.to).toBe("USD");
|
||||
expect(info.date).toBe(undefined);
|
||||
expect.step("rate fetched");
|
||||
return [{ ...info, rate: 0.9 }];
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.CURRENCY.RATE("EUR","USD")`);
|
||||
expect(getCellValue(model, "A1")).toBe("Loading...");
|
||||
await waitForDataLoaded(model);
|
||||
expect(getCellValue(model, "A1")).toBe(0.9);
|
||||
expect.verifySteps(["rate fetched"]);
|
||||
});
|
||||
|
||||
test("rate formula at a given date(time)", async () => {
|
||||
const { model } = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "get_rates_for_spreadsheet") {
|
||||
const [A1, A2] = args.args[0];
|
||||
expect(A1.date).toBe("2020-12-31");
|
||||
expect(A2.date).toBe("2020-11-30");
|
||||
expect.step("rate fetched");
|
||||
return [
|
||||
{ ...A1, rate: 0.9 },
|
||||
{ ...A2, rate: 0.9 },
|
||||
];
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.CURRENCY.RATE("EUR","USD", "12-31-2020")`);
|
||||
setCellContent(model, "A2", `=ODOO.CURRENCY.RATE("EUR","USD", "11-30-2020 00:00:00")`);
|
||||
await waitForDataLoaded(model);
|
||||
expect.verifySteps(["rate fetched"]);
|
||||
});
|
||||
|
||||
test("invalid date", async () => {
|
||||
const { model } = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "get_rates_for_spreadsheet") {
|
||||
throw new Error("Should not be called");
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.CURRENCY.RATE("EUR","USD", "hello")`);
|
||||
await waitForDataLoaded(model);
|
||||
expect(getCellValue(model, "A1")).toBe("#ERROR");
|
||||
expect(getEvaluatedCell(model, "A1").message).toBe(
|
||||
"The function ODOO.CURRENCY.RATE expects a number value, but 'hello' is a string, and cannot be coerced to a number."
|
||||
);
|
||||
});
|
||||
|
||||
test("rate formula at a given company", async () => {
|
||||
const { model } = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "get_rates_for_spreadsheet") {
|
||||
const [A1, A2] = args.args[0];
|
||||
expect(A1.company_id).toBe(1);
|
||||
expect(A2.company_id).toBe(2);
|
||||
expect.step("rate fetched");
|
||||
return [
|
||||
{ ...A1, rate: 0.7 },
|
||||
{ ...A2, rate: 0.9 },
|
||||
];
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.CURRENCY.RATE("EUR","USD",, 1)`);
|
||||
setCellContent(model, "A2", `=ODOO.CURRENCY.RATE("EUR","USD",, 2)`);
|
||||
await waitForDataLoaded(model);
|
||||
expect.verifySteps(["rate fetched"]);
|
||||
expect(getCellValue(model, "A1")).toBe(0.7);
|
||||
expect(getCellValue(model, "A2")).toBe(0.9);
|
||||
});
|
||||
|
||||
test("invalid company id", async () => {
|
||||
const { model } = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "get_rates_for_spreadsheet") {
|
||||
const error = new RPCError();
|
||||
error.data = { message: "Invalid company id." };
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.CURRENCY.RATE("EUR","USD",, 45)`);
|
||||
await waitForDataLoaded(model);
|
||||
expect(getCellValue(model, "A1")).toBe("#ERROR");
|
||||
expect(getEvaluatedCell(model, "A1").message).toBe("Invalid company id.");
|
||||
});
|
||||
|
||||
test("Currency rate throw with unknown currency", async () => {
|
||||
const { model } = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "get_rates_for_spreadsheet") {
|
||||
const info = args.args[0][0];
|
||||
return [{ ...info, rate: false }];
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.CURRENCY.RATE("INVALID","USD")`);
|
||||
await waitForDataLoaded(model);
|
||||
expect(getEvaluatedCell(model, "A1").message).toBe("Currency rate unavailable.");
|
||||
});
|
||||
|
||||
test("Currency rates are only loaded once", async () => {
|
||||
const { model } = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "get_rates_for_spreadsheet") {
|
||||
expect.step("FETCH");
|
||||
const info = args.args[0][0];
|
||||
return [{ ...info, rate: 0.9 }];
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.CURRENCY.RATE("EUR","USD")`);
|
||||
await waitForDataLoaded(model);
|
||||
expect.verifySteps(["FETCH"]);
|
||||
setCellContent(model, "A2", `=ODOO.CURRENCY.RATE("EUR","USD")`);
|
||||
await waitForDataLoaded(model);
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("Currency rates are loaded once by clock", async () => {
|
||||
const { model } = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "get_rates_for_spreadsheet") {
|
||||
expect.step("FETCH:" + args.args[0].length);
|
||||
const info1 = args.args[0][0];
|
||||
const info2 = args.args[0][1];
|
||||
return [
|
||||
{ ...info1, rate: 0.9 },
|
||||
{ ...info2, rate: 1 },
|
||||
];
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.CURRENCY.RATE("EUR","USD")`);
|
||||
setCellContent(model, "A2", `=ODOO.CURRENCY.RATE("EUR","SEK")`);
|
||||
await waitForDataLoaded(model);
|
||||
expect.verifySteps(["FETCH:2"]);
|
||||
});
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { setCellContent } from "@spreadsheet/../tests/utils/commands";
|
||||
import { getCell, getCellValue } from "@spreadsheet/../tests/utils/getters";
|
||||
import {
|
||||
createModelWithDataSource,
|
||||
waitForDataSourcesLoaded,
|
||||
} from "@spreadsheet/../tests/utils/model";
|
||||
|
||||
QUnit.module("spreadsheet > Currency");
|
||||
|
||||
QUnit.test("Basic exchange formula", async (assert) => {
|
||||
const model = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "get_rates_for_spreadsheet") {
|
||||
const info = args.args[0][0];
|
||||
assert.equal(info.from, "EUR");
|
||||
assert.equal(info.to, "USD");
|
||||
assert.equal(info.date, undefined);
|
||||
assert.step("rate fetched");
|
||||
return [{ ...info, rate: 0.9 }];
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.CURRENCY.RATE("EUR","USD")`);
|
||||
assert.strictEqual(getCellValue(model, "A1"), "Loading...");
|
||||
await waitForDataSourcesLoaded(model);
|
||||
assert.strictEqual(getCellValue(model, "A1"), 0.9);
|
||||
assert.verifySteps(["rate fetched"]);
|
||||
});
|
||||
|
||||
QUnit.test("rate formula at a given date(time)", async (assert) => {
|
||||
const model = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "get_rates_for_spreadsheet") {
|
||||
const [A1, A2] = args.args[0];
|
||||
assert.equal(A1.date, "2020-12-31");
|
||||
assert.equal(A2.date, "2020-11-30");
|
||||
assert.step("rate fetched");
|
||||
return [
|
||||
{ ...A1, rate: 0.9 },
|
||||
{ ...A2, rate: 0.9 },
|
||||
];
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.CURRENCY.RATE("EUR","USD", "12-31-2020")`);
|
||||
setCellContent(model, "A2", `=ODOO.CURRENCY.RATE("EUR","USD", "11-30-2020 00:00:00")`);
|
||||
await waitForDataSourcesLoaded(model);
|
||||
assert.verifySteps(["rate fetched"]);
|
||||
});
|
||||
|
||||
QUnit.test("invalid date", async (assert) => {
|
||||
const model = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "get_rates_for_spreadsheet") {
|
||||
throw new Error("Should not be called");
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.CURRENCY.RATE("EUR","USD", "hello")`);
|
||||
await waitForDataSourcesLoaded(model);
|
||||
assert.strictEqual(getCellValue(model, "A1"), "#ERROR");
|
||||
assert.strictEqual(
|
||||
getCell(model, "A1").evaluated.error.message,
|
||||
"The function ODOO.CURRENCY.RATE expects a number value, but 'hello' is a string, and cannot be coerced to a number."
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Currency rate throw with unknown currency", async (assert) => {
|
||||
const model = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "get_rates_for_spreadsheet") {
|
||||
const info = args.args[0][0];
|
||||
return [{ ...info, rate: false }];
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.CURRENCY.RATE("INVALID","USD")`);
|
||||
await waitForDataSourcesLoaded(model);
|
||||
assert.strictEqual(getCell(model, "A1").evaluated.error.message, "Currency rate unavailable.");
|
||||
});
|
||||
|
||||
QUnit.test("Currency rates are only loaded once", async (assert) => {
|
||||
const model = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "get_rates_for_spreadsheet") {
|
||||
assert.step("FETCH");
|
||||
const info = args.args[0][0];
|
||||
return [{ ...info, rate: 0.9 }];
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.CURRENCY.RATE("EUR","USD")`);
|
||||
await waitForDataSourcesLoaded(model);
|
||||
assert.verifySteps(["FETCH"]);
|
||||
setCellContent(model, "A2", `=ODOO.CURRENCY.RATE("EUR","USD")`);
|
||||
await waitForDataSourcesLoaded(model);
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test("Currency rates are loaded once by clock", async (assert) => {
|
||||
const model = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "get_rates_for_spreadsheet") {
|
||||
assert.step("FETCH:" + args.args[0].length);
|
||||
const info1 = args.args[0][0];
|
||||
const info2 = args.args[0][1];
|
||||
return [
|
||||
{ ...info1, rate: 0.9 },
|
||||
{ ...info2, rate: 1 },
|
||||
];
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.CURRENCY.RATE("EUR","USD")`);
|
||||
setCellContent(model, "A2", `=ODOO.CURRENCY.RATE("EUR","SEK")`);
|
||||
await waitForDataSourcesLoaded(model);
|
||||
assert.verifySteps(["FETCH:2"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { LoadableDataSource } from "@spreadsheet/data_sources/data_source";
|
||||
import { Deferred } from "@web/core/utils/concurrency";
|
||||
import { makeServerError } from "@web/../tests/web_test_helpers";
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { defineSpreadsheetActions, defineSpreadsheetModels } from "../helpers/data";
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
defineSpreadsheetModels();
|
||||
defineSpreadsheetActions();
|
||||
|
||||
test("data source is ready after all concurrent requests are resolved", async () => {
|
||||
const def1 = new Deferred();
|
||||
const def2 = new Deferred();
|
||||
let req = 0;
|
||||
class TestDataSource extends LoadableDataSource {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.data = null;
|
||||
}
|
||||
async _load() {
|
||||
this.data = null;
|
||||
switch (++req) {
|
||||
case 1:
|
||||
await def1;
|
||||
break;
|
||||
case 2:
|
||||
await def2;
|
||||
break;
|
||||
}
|
||||
this.data = "something";
|
||||
}
|
||||
}
|
||||
const dataSource = new TestDataSource({
|
||||
odooDataProvider: {
|
||||
notify: () => expect.step("notify"),
|
||||
notifyWhenPromiseResolves: () => expect.step("notify-from-promise"),
|
||||
cancelPromise: () => expect.step("cancel-promise"),
|
||||
},
|
||||
});
|
||||
dataSource.load();
|
||||
expect.verifySteps(["notify-from-promise"]);
|
||||
dataSource.load({ reload: true });
|
||||
expect(dataSource.isReady()).toBe(false);
|
||||
def1.resolve();
|
||||
await animationFrame();
|
||||
expect.verifySteps(["cancel-promise", "notify-from-promise"]);
|
||||
expect(dataSource.isReady()).toBe(false);
|
||||
def2.resolve();
|
||||
await animationFrame();
|
||||
expect(dataSource.isReady()).toBe(true);
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("Datasources handle errors thrown at _load", async () => {
|
||||
class TestDataSource extends LoadableDataSource {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.data = null;
|
||||
}
|
||||
async _load() {
|
||||
this.data = await this._orm.call();
|
||||
}
|
||||
}
|
||||
|
||||
const dataSource = new TestDataSource({
|
||||
odooDataProvider: {
|
||||
notify: () => expect.step("notify"),
|
||||
notifyWhenPromiseResolves: () => expect.step("notify-from-promise"),
|
||||
cancelPromise: () => expect.step("cancel-promise"),
|
||||
orm: {
|
||||
call: () => {
|
||||
throw makeServerError({ description: "Ya done!" });
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await dataSource.load();
|
||||
expect.verifySteps(["notify-from-promise"]);
|
||||
expect(dataSource._isFullyLoaded).toBe(true);
|
||||
expect(dataSource._isValid).toBe(false);
|
||||
expect(dataSource._loadError.message).toBe("Ya done!");
|
||||
});
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { nextTick } from "@web/../tests/helpers/utils";
|
||||
import { LoadableDataSource } from "@spreadsheet/data_sources/data_source";
|
||||
import { Deferred } from "@web/core/utils/concurrency";
|
||||
import { RPCError } from "@web/core/network/rpc_service";
|
||||
|
||||
QUnit.module("spreadsheet data source", {}, () => {
|
||||
QUnit.test(
|
||||
"data source is ready after all concurrent requests are resolved",
|
||||
async (assert) => {
|
||||
const def1 = new Deferred();
|
||||
const def2 = new Deferred();
|
||||
let req = 0;
|
||||
class TestDataSource extends LoadableDataSource {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.data = null;
|
||||
}
|
||||
async _load() {
|
||||
this.data = null;
|
||||
switch (++req) {
|
||||
case 1:
|
||||
await def1;
|
||||
break;
|
||||
case 2:
|
||||
await def2;
|
||||
break;
|
||||
}
|
||||
this.data = "something";
|
||||
}
|
||||
}
|
||||
const dataSource = new TestDataSource({
|
||||
notify: () => assert.step("notify"),
|
||||
notifyWhenPromiseResolves: () => assert.step("notify-from-promise"),
|
||||
cancelPromise: () => assert.step("cancel-promise"),
|
||||
});
|
||||
dataSource.load();
|
||||
assert.verifySteps(["notify-from-promise"]);
|
||||
dataSource.load({ reload: true });
|
||||
assert.strictEqual(dataSource.isReady(), false);
|
||||
def1.resolve();
|
||||
await nextTick();
|
||||
assert.verifySteps(["cancel-promise", "notify-from-promise"]);
|
||||
assert.strictEqual(dataSource.isReady(), false);
|
||||
def2.resolve();
|
||||
await nextTick();
|
||||
assert.strictEqual(dataSource.isReady(), true);
|
||||
assert.verifySteps([]);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("Datasources handle errors thrown at _load", async (assert) => {
|
||||
class TestDataSource extends LoadableDataSource {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.data = null;
|
||||
}
|
||||
async _load() {
|
||||
this.data = await this._orm.call();
|
||||
}
|
||||
}
|
||||
|
||||
const dataSource = new TestDataSource({
|
||||
notify: () => assert.step("notify"),
|
||||
notifyWhenPromiseResolves: () => assert.step("notify-from-promise"),
|
||||
cancelPromise: () => assert.step("cancel-promise"),
|
||||
orm: {
|
||||
call: () => {
|
||||
const error = new RPCError();
|
||||
error.data = { message: "Ya done!" };
|
||||
throw error;
|
||||
},
|
||||
},
|
||||
});
|
||||
await dataSource.load();
|
||||
assert.verifySteps(["notify-from-promise"]);
|
||||
assert.ok(dataSource._isFullyLoaded);
|
||||
assert.notOk(dataSource._isValid);
|
||||
assert.equal(dataSource._loadErrorMessage, "Ya done!");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,183 +0,0 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { nextTick } from "@web/../tests/helpers/utils";
|
||||
import { MetadataRepository } from "@spreadsheet/data_sources/metadata_repository";
|
||||
|
||||
QUnit.module("spreadsheet > Metadata Repository", {}, () => {
|
||||
QUnit.test("Fields_get are only loaded once", async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
const orm = {
|
||||
call: async (model, method) => {
|
||||
assert.step(`${method}-${model}`);
|
||||
return model;
|
||||
},
|
||||
};
|
||||
|
||||
const metadataRepository = new MetadataRepository(orm);
|
||||
|
||||
const first = await metadataRepository.fieldsGet("A");
|
||||
const second = await metadataRepository.fieldsGet("A");
|
||||
const third = await metadataRepository.fieldsGet("B");
|
||||
|
||||
assert.strictEqual(first, "A");
|
||||
assert.strictEqual(second, "A");
|
||||
assert.strictEqual(third, "B");
|
||||
|
||||
assert.verifySteps(["fields_get-A", "fields_get-B"]);
|
||||
});
|
||||
|
||||
QUnit.test("display_name_for on ir.model are only loaded once", async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
const orm = {
|
||||
call: async (model, method, args) => {
|
||||
if (method === "display_name_for" && model === "ir.model") {
|
||||
const [modelName] = args[0];
|
||||
assert.step(`${modelName}`);
|
||||
return [{ display_name: modelName, model: modelName }];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const metadataRepository = new MetadataRepository(orm);
|
||||
|
||||
const first = await metadataRepository.modelDisplayName("A");
|
||||
const second = await metadataRepository.modelDisplayName("A");
|
||||
const third = await metadataRepository.modelDisplayName("B");
|
||||
|
||||
assert.strictEqual(first, "A");
|
||||
assert.strictEqual(second, "A");
|
||||
assert.strictEqual(third, "B");
|
||||
|
||||
assert.verifySteps(["A", "B"]);
|
||||
});
|
||||
|
||||
QUnit.test("Register label correctly memorize labels", function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const metadataRepository = new MetadataRepository({});
|
||||
|
||||
assert.strictEqual(metadataRepository.getLabel("model", "field", "value"), undefined);
|
||||
const label = "label";
|
||||
metadataRepository.registerLabel("model", "field", "value", label);
|
||||
assert.strictEqual(metadataRepository.getLabel("model", "field", "value"), label);
|
||||
});
|
||||
|
||||
QUnit.test("Name_get are collected and executed once by clock", async function (assert) {
|
||||
const orm = {
|
||||
call: async (model, method, args) => {
|
||||
const ids = args[0];
|
||||
assert.step(`${method}-${model}-[${ids.join(",")}]`);
|
||||
return ids.map((id) => [id, id.toString()]);
|
||||
},
|
||||
};
|
||||
|
||||
const metadataRepository = new MetadataRepository(orm);
|
||||
metadataRepository.addEventListener("labels-fetched", () => {
|
||||
assert.step("labels-fetched");
|
||||
});
|
||||
|
||||
assert.throws(() => metadataRepository.getRecordDisplayName("A", 1), /Data is loading/);
|
||||
assert.throws(() => metadataRepository.getRecordDisplayName("A", 1), /Data is loading/);
|
||||
assert.throws(() => metadataRepository.getRecordDisplayName("A", 2), /Data is loading/);
|
||||
assert.throws(() => metadataRepository.getRecordDisplayName("B", 1), /Data is loading/);
|
||||
assert.verifySteps([]);
|
||||
|
||||
await nextTick();
|
||||
assert.verifySteps([
|
||||
"name_get-A-[1,2]",
|
||||
"name_get-B-[1]",
|
||||
"labels-fetched",
|
||||
"labels-fetched",
|
||||
]);
|
||||
|
||||
assert.strictEqual(metadataRepository.getRecordDisplayName("A", 1), "1");
|
||||
assert.strictEqual(metadataRepository.getRecordDisplayName("A", 2), "2");
|
||||
assert.strictEqual(metadataRepository.getRecordDisplayName("B", 1), "1");
|
||||
});
|
||||
|
||||
QUnit.test("Name_get to fetch are cleared after being fetched", async function (assert) {
|
||||
const orm = {
|
||||
call: async (model, method, args) => {
|
||||
const ids = args[0];
|
||||
assert.step(`${method}-${model}-[${ids.join(",")}]`);
|
||||
return ids.map((id) => [id, id.toString()]);
|
||||
},
|
||||
};
|
||||
|
||||
const metadataRepository = new MetadataRepository(orm);
|
||||
|
||||
assert.throws(() => metadataRepository.getRecordDisplayName("A", 1));
|
||||
assert.verifySteps([]);
|
||||
|
||||
await nextTick();
|
||||
assert.verifySteps(["name_get-A-[1]"]);
|
||||
|
||||
assert.throws(() => metadataRepository.getRecordDisplayName("A", 2));
|
||||
await nextTick();
|
||||
assert.verifySteps(["name_get-A-[2]"]);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"Assigning a result after triggering the request should not crash",
|
||||
async function (assert) {
|
||||
const orm = {
|
||||
call: async (model, method, args) => {
|
||||
const ids = args[0];
|
||||
assert.step(`${method}-${model}-[${ids.join(",")}]`);
|
||||
return ids.map((id) => [id, id.toString()]);
|
||||
},
|
||||
};
|
||||
|
||||
const metadataRepository = new MetadataRepository(orm);
|
||||
|
||||
assert.throws(() => metadataRepository.getRecordDisplayName("A", 1));
|
||||
assert.verifySteps([]);
|
||||
metadataRepository.setDisplayName("A", 1, "test");
|
||||
assert.strictEqual(metadataRepository.getRecordDisplayName("A", 1), "test");
|
||||
|
||||
await nextTick();
|
||||
assert.verifySteps(["name_get-A-[1]"]);
|
||||
assert.strictEqual(metadataRepository.getRecordDisplayName("A", 1), "1");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"Name_get will retry with one id by request in case of failure",
|
||||
async function (assert) {
|
||||
const orm = {
|
||||
call: async (model, method, args) => {
|
||||
const ids = args[0];
|
||||
assert.step(`${method}-${model}-[${ids.join(",")}]`);
|
||||
if (model === "B" && ids.includes(1)) {
|
||||
throw new Error("Missing");
|
||||
}
|
||||
return ids.map((id) => [id, id.toString()]);
|
||||
},
|
||||
};
|
||||
|
||||
const metadataRepository = new MetadataRepository(orm);
|
||||
|
||||
assert.throws(() => metadataRepository.getRecordDisplayName("A", 1), /Data is loading/);
|
||||
assert.throws(() => metadataRepository.getRecordDisplayName("B", 1), /Data is loading/);
|
||||
assert.throws(() => metadataRepository.getRecordDisplayName("B", 2), /Data is loading/);
|
||||
assert.verifySteps([]);
|
||||
|
||||
await nextTick();
|
||||
assert.verifySteps([
|
||||
"name_get-A-[1]",
|
||||
"name_get-B-[1,2]",
|
||||
"name_get-B-[1]",
|
||||
"name_get-B-[2]",
|
||||
]);
|
||||
|
||||
assert.strictEqual(metadataRepository.getRecordDisplayName("A", 1), "1");
|
||||
assert.throws(
|
||||
() => metadataRepository.getRecordDisplayName("B", 1),
|
||||
/Unable to fetch the label of 1 of model B/
|
||||
);
|
||||
assert.strictEqual(metadataRepository.getRecordDisplayName("B", 2), "2");
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { LoadingDataError } from "@spreadsheet/o_spreadsheet/errors";
|
||||
import { BatchEndpoint, Request, ServerData } from "@spreadsheet/data_sources/server_data";
|
||||
import { Deferred } from "@web/core/utils/concurrency";
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { defineSpreadsheetActions, defineSpreadsheetModels } from "../helpers/data";
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
defineSpreadsheetModels();
|
||||
defineSpreadsheetActions();
|
||||
|
||||
test("simple synchronous get", async () => {
|
||||
const orm = {
|
||||
call: async (model, method, args) => {
|
||||
expect.step(`${model}/${method}`);
|
||||
return args[0];
|
||||
},
|
||||
};
|
||||
const serverData = new ServerData(orm, {
|
||||
whenDataStartLoading: () => expect.step("data-fetching-notification"),
|
||||
});
|
||||
expect(() => serverData.get("partner", "get_something", [5])).toThrow(LoadingDataError, {
|
||||
message: "it should throw when it's not loaded",
|
||||
});
|
||||
expect.verifySteps(["partner/get_something", "data-fetching-notification"]);
|
||||
await animationFrame();
|
||||
expect(serverData.get("partner", "get_something", [5])).toBe(5);
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("synchronous get which returns an error", async () => {
|
||||
const orm = {
|
||||
call: async (model, method, args) => {
|
||||
expect.step(`${model}/${method}`);
|
||||
throw new Error("error while fetching data");
|
||||
},
|
||||
};
|
||||
const serverData = new ServerData(orm, {
|
||||
whenDataStartLoading: () => expect.step("data-fetching-notification"),
|
||||
});
|
||||
expect(() => serverData.get("partner", "get_something", [5])).toThrow(LoadingDataError, {
|
||||
message: "it should throw when it's not loaded",
|
||||
});
|
||||
expect.verifySteps(["partner/get_something", "data-fetching-notification"]);
|
||||
await animationFrame();
|
||||
expect(() => serverData.get("partner", "get_something", [5])).toThrow(Error);
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("batch get with a single item", async () => {
|
||||
const deferred = new Deferred();
|
||||
const orm = {
|
||||
call: async (model, method, args) => {
|
||||
await deferred;
|
||||
expect.step(`${model}/${method}`);
|
||||
return args[0];
|
||||
},
|
||||
};
|
||||
const serverData = new ServerData(orm, {
|
||||
whenDataStartLoading: () => expect.step("data-fetching-notification"),
|
||||
});
|
||||
expect(() => serverData.batch.get("partner", "get_something_in_batch", 5)).toThrow(
|
||||
LoadingDataError,
|
||||
{ message: "it should throw when it's not loaded" }
|
||||
);
|
||||
await animationFrame(); // wait for the next tick for the batch to be called
|
||||
expect.verifySteps(["data-fetching-notification"]);
|
||||
deferred.resolve();
|
||||
await animationFrame();
|
||||
expect.verifySteps(["partner/get_something_in_batch"]);
|
||||
expect(serverData.batch.get("partner", "get_something_in_batch", 5)).toBe(5);
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("batch get with multiple items", async () => {
|
||||
const orm = {
|
||||
call: async (model, method, args) => {
|
||||
expect.step(`${model}/${method}`);
|
||||
return args[0];
|
||||
},
|
||||
};
|
||||
const serverData = new ServerData(orm, {
|
||||
whenDataStartLoading: () => expect.step("data-fetching-notification"),
|
||||
});
|
||||
expect(() => serverData.batch.get("partner", "get_something_in_batch", 5)).toThrow(
|
||||
LoadingDataError,
|
||||
{ message: "it should throw when it's not loaded" }
|
||||
);
|
||||
expect(() => serverData.batch.get("partner", "get_something_in_batch", 6)).toThrow(
|
||||
LoadingDataError,
|
||||
{ message: "it should throw when it's not loaded" }
|
||||
);
|
||||
await animationFrame();
|
||||
expect.verifySteps(["partner/get_something_in_batch", "data-fetching-notification"]);
|
||||
expect(serverData.batch.get("partner", "get_something_in_batch", 5)).toBe(5);
|
||||
expect(serverData.batch.get("partner", "get_something_in_batch", 6)).toBe(6);
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("batch get with one error", async () => {
|
||||
const orm = {
|
||||
call: async (model, method, args) => {
|
||||
expect.step(`${model}/${method}`);
|
||||
if (args[0].includes(5)) {
|
||||
throw new Error("error while fetching data");
|
||||
}
|
||||
return args[0];
|
||||
},
|
||||
};
|
||||
const serverData = new ServerData(orm, {
|
||||
whenDataStartLoading: () => expect.step("data-fetching-notification"),
|
||||
});
|
||||
expect(() => serverData.batch.get("partner", "get_something_in_batch", 4)).toThrow(
|
||||
LoadingDataError,
|
||||
{ message: "it should throw when it's not loaded" }
|
||||
);
|
||||
expect(() => serverData.batch.get("partner", "get_something_in_batch", 5)).toThrow(
|
||||
LoadingDataError,
|
||||
{ message: "it should throw when it's not loaded" }
|
||||
);
|
||||
expect(() => serverData.batch.get("partner", "get_something_in_batch", 6)).toThrow(
|
||||
LoadingDataError,
|
||||
{ message: "it should throw when it's not loaded" }
|
||||
);
|
||||
await animationFrame();
|
||||
expect.verifySteps([
|
||||
// one call for the batch
|
||||
"partner/get_something_in_batch",
|
||||
"data-fetching-notification",
|
||||
// retries one by one
|
||||
"partner/get_something_in_batch",
|
||||
"partner/get_something_in_batch",
|
||||
"partner/get_something_in_batch",
|
||||
]);
|
||||
expect(serverData.batch.get("partner", "get_something_in_batch", 4)).toBe(4);
|
||||
expect(() => serverData.batch.get("partner", "get_something_in_batch", 5)).toThrow(Error);
|
||||
expect(serverData.batch.get("partner", "get_something_in_batch", 6)).toBe(6);
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("concurrently get and batch get the same request", async () => {
|
||||
const orm = {
|
||||
call: async (model, method, args) => {
|
||||
expect.step(`${model}/${method}`);
|
||||
return args[0];
|
||||
},
|
||||
};
|
||||
const serverData = new ServerData(orm, {
|
||||
whenDataStartLoading: () => expect.step("data-fetching-notification"),
|
||||
});
|
||||
expect(() => serverData.batch.get("partner", "get_something", 5)).toThrow(LoadingDataError);
|
||||
expect(() => serverData.get("partner", "get_something", [5])).toThrow(LoadingDataError);
|
||||
await animationFrame();
|
||||
// it should have fetch the data once
|
||||
expect.verifySteps(["partner/get_something", "data-fetching-notification"]);
|
||||
expect(serverData.get("partner", "get_something", [5])).toBe(5);
|
||||
expect(serverData.batch.get("partner", "get_something", 5)).toBe(5);
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("Call the correct callback after a batch result", async () => {
|
||||
const orm = {
|
||||
call: async (model, method, args) => {
|
||||
if (args[0].includes(5)) {
|
||||
throw new Error("error while fetching data");
|
||||
}
|
||||
return args[0];
|
||||
},
|
||||
};
|
||||
const batchEndpoint = new BatchEndpoint(orm, "partner", "get_something", {
|
||||
whenDataStartLoading: () => {},
|
||||
successCallback: () => expect.step("success-callback"),
|
||||
failureCallback: () => expect.step("failure-callback"),
|
||||
});
|
||||
const request = new Request("partner", "get_something", [4]);
|
||||
const request2 = new Request("partner", "get_something", [5]);
|
||||
batchEndpoint.call(request);
|
||||
batchEndpoint.call(request2);
|
||||
expect.verifySteps([]);
|
||||
await animationFrame();
|
||||
expect.verifySteps(["success-callback", "failure-callback"]);
|
||||
});
|
||||
|
|
@ -1,305 +0,0 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { nextTick } from "@web/../tests/helpers/utils";
|
||||
import { LoadingDataError } from "@spreadsheet/o_spreadsheet/errors";
|
||||
import BatchEndpoint, { Request, ServerData } from "@spreadsheet/data_sources/server_data";
|
||||
|
||||
QUnit.module("spreadsheet server data", {}, () => {
|
||||
QUnit.test("simple synchronous get", async (assert) => {
|
||||
const orm = {
|
||||
call: async (model, method, args) => {
|
||||
assert.step(`${model}/${method}`);
|
||||
return args[0];
|
||||
},
|
||||
};
|
||||
const serverData = new ServerData(orm, {
|
||||
whenDataIsFetched: () => assert.step("data-fetched-notification"),
|
||||
});
|
||||
assert.throws(
|
||||
() => serverData.get("partner", "get_something", [5]),
|
||||
LoadingDataError,
|
||||
"it should throw when it's not loaded"
|
||||
);
|
||||
await nextTick();
|
||||
assert.verifySteps(["partner/get_something", "data-fetched-notification"]);
|
||||
assert.deepEqual(serverData.get("partner", "get_something", [5]), 5);
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test("synchronous get which returns an error", async (assert) => {
|
||||
const orm = {
|
||||
call: async (model, method, args) => {
|
||||
assert.step(`${model}/${method}`);
|
||||
throw new Error("error while fetching data");
|
||||
},
|
||||
};
|
||||
const serverData = new ServerData(orm, {
|
||||
whenDataIsFetched: () => assert.step("data-fetched-notification"),
|
||||
});
|
||||
assert.throws(
|
||||
() => serverData.get("partner", "get_something", [5]),
|
||||
LoadingDataError,
|
||||
"it should throw when it's not loaded"
|
||||
);
|
||||
await nextTick();
|
||||
assert.verifySteps(["partner/get_something", "data-fetched-notification"]);
|
||||
assert.throws(() => serverData.get("partner", "get_something", [5]), Error);
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test("simple async fetch", async (assert) => {
|
||||
const orm = {
|
||||
call: async (model, method, args) => {
|
||||
assert.step(`${model}/${method}`);
|
||||
return args[0];
|
||||
},
|
||||
};
|
||||
const serverData = new ServerData(orm, {
|
||||
whenDataIsFetched: () => assert.step("data-fetched-notification"),
|
||||
});
|
||||
const result = await serverData.fetch("partner", "get_something", [5]);
|
||||
assert.deepEqual(result, 5);
|
||||
assert.verifySteps(["partner/get_something"]);
|
||||
assert.deepEqual(await serverData.fetch("partner", "get_something", [5]), 5);
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test("async fetch which throws an error", async (assert) => {
|
||||
const orm = {
|
||||
call: async (model, method, args) => {
|
||||
assert.step(`${model}/${method}`);
|
||||
throw new Error("error while fetching data");
|
||||
},
|
||||
};
|
||||
const serverData = new ServerData(orm, {
|
||||
whenDataIsFetched: () => assert.step("data-fetched-notification"),
|
||||
});
|
||||
assert.rejects(serverData.fetch("partner", "get_something", [5]));
|
||||
assert.verifySteps(["partner/get_something"]);
|
||||
assert.rejects(serverData.fetch("partner", "get_something", [5]));
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test("two identical concurrent async fetch", async (assert) => {
|
||||
const orm = {
|
||||
call: async (model, method, args) => {
|
||||
assert.step(`${model}/${method}`);
|
||||
return args[0];
|
||||
},
|
||||
};
|
||||
const serverData = new ServerData(orm, {
|
||||
whenDataIsFetched: () => assert.step("data-fetched-notification"),
|
||||
});
|
||||
const [result1, result2] = await Promise.all([
|
||||
serverData.fetch("partner", "get_something", [5]),
|
||||
serverData.fetch("partner", "get_something", [5]),
|
||||
]);
|
||||
assert.verifySteps(["partner/get_something"], "it should have fetch the data once");
|
||||
assert.deepEqual(result1, 5);
|
||||
assert.deepEqual(result2, 5);
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test("batch get with a single item", async (assert) => {
|
||||
const orm = {
|
||||
call: async (model, method, args) => {
|
||||
assert.step(`${model}/${method}`);
|
||||
return args[0];
|
||||
},
|
||||
};
|
||||
const serverData = new ServerData(orm, {
|
||||
whenDataIsFetched: () => assert.step("data-fetched-notification"),
|
||||
});
|
||||
assert.throws(
|
||||
() => serverData.batch.get("partner", "get_something_in_batch", 5),
|
||||
LoadingDataError,
|
||||
"it should throw when it's not loaded"
|
||||
);
|
||||
await nextTick();
|
||||
assert.verifySteps(["partner/get_something_in_batch", "data-fetched-notification"]);
|
||||
assert.deepEqual(serverData.batch.get("partner", "get_something_in_batch", 5), 5);
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test("batch get with multiple items", async (assert) => {
|
||||
const orm = {
|
||||
call: async (model, method, args) => {
|
||||
assert.step(`${model}/${method}`);
|
||||
return args[0];
|
||||
},
|
||||
};
|
||||
const serverData = new ServerData(orm, {
|
||||
whenDataIsFetched: () => assert.step("data-fetched-notification"),
|
||||
});
|
||||
assert.throws(
|
||||
() => serverData.batch.get("partner", "get_something_in_batch", 5),
|
||||
LoadingDataError,
|
||||
"it should throw when it's not loaded"
|
||||
);
|
||||
assert.throws(
|
||||
() => serverData.batch.get("partner", "get_something_in_batch", 6),
|
||||
LoadingDataError,
|
||||
"it should throw when it's not loaded"
|
||||
);
|
||||
await nextTick();
|
||||
assert.verifySteps(["partner/get_something_in_batch", "data-fetched-notification"]);
|
||||
assert.deepEqual(serverData.batch.get("partner", "get_something_in_batch", 5), 5);
|
||||
assert.deepEqual(serverData.batch.get("partner", "get_something_in_batch", 6), 6);
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test("batch get with one error", async (assert) => {
|
||||
const orm = {
|
||||
call: async (model, method, args) => {
|
||||
assert.step(`${model}/${method}`);
|
||||
if (args[0].includes(5)) {
|
||||
throw new Error("error while fetching data");
|
||||
}
|
||||
return args[0];
|
||||
},
|
||||
};
|
||||
const serverData = new ServerData(orm, {
|
||||
whenDataIsFetched: () => assert.step("data-fetched-notification"),
|
||||
});
|
||||
assert.throws(
|
||||
() => serverData.batch.get("partner", "get_something_in_batch", 4),
|
||||
LoadingDataError,
|
||||
"it should throw when it's not loaded"
|
||||
);
|
||||
assert.throws(
|
||||
() => serverData.batch.get("partner", "get_something_in_batch", 5),
|
||||
LoadingDataError,
|
||||
"it should throw when it's not loaded"
|
||||
);
|
||||
assert.throws(
|
||||
() => serverData.batch.get("partner", "get_something_in_batch", 6),
|
||||
LoadingDataError,
|
||||
"it should throw when it's not loaded"
|
||||
);
|
||||
await nextTick();
|
||||
assert.verifySteps([
|
||||
// one call for the batch
|
||||
"partner/get_something_in_batch",
|
||||
// retries one by one
|
||||
"partner/get_something_in_batch",
|
||||
"partner/get_something_in_batch",
|
||||
"partner/get_something_in_batch",
|
||||
"data-fetched-notification",
|
||||
]);
|
||||
assert.deepEqual(serverData.batch.get("partner", "get_something_in_batch", 4), 4);
|
||||
assert.throws(() => serverData.batch.get("partner", "get_something_in_batch", 5), Error);
|
||||
assert.deepEqual(serverData.batch.get("partner", "get_something_in_batch", 6), 6);
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test("concurrently fetch then get the same request", async (assert) => {
|
||||
const orm = {
|
||||
call: async (model, method, args) => {
|
||||
assert.step(`${model}/${method}`);
|
||||
return args[0];
|
||||
},
|
||||
};
|
||||
const serverData = new ServerData(orm, {
|
||||
whenDataIsFetched: () => assert.step("data-fetched-notification"),
|
||||
});
|
||||
const promise = serverData.fetch("partner", "get_something", [5]);
|
||||
assert.throws(() => serverData.get("partner", "get_something", [5]), LoadingDataError);
|
||||
const result = await promise;
|
||||
await nextTick();
|
||||
assert.verifySteps(
|
||||
["partner/get_something", "partner/get_something", "data-fetched-notification"],
|
||||
"it loads the data independently"
|
||||
);
|
||||
assert.deepEqual(result, 5);
|
||||
assert.deepEqual(serverData.get("partner", "get_something", [5]), 5);
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test("concurrently get then fetch the same request", async (assert) => {
|
||||
const orm = {
|
||||
call: async (model, method, args) => {
|
||||
assert.step(`${model}/${method}`);
|
||||
return args[0];
|
||||
},
|
||||
};
|
||||
const serverData = new ServerData(orm, {
|
||||
whenDataIsFetched: () => assert.step("data-fetched-notification"),
|
||||
});
|
||||
assert.throws(() => serverData.get("partner", "get_something", [5]), LoadingDataError);
|
||||
const result = await serverData.fetch("partner", "get_something", [5]);
|
||||
assert.verifySteps(
|
||||
["partner/get_something", "partner/get_something", "data-fetched-notification"],
|
||||
"it should have fetch the data once"
|
||||
);
|
||||
assert.deepEqual(result, 5);
|
||||
assert.deepEqual(serverData.get("partner", "get_something", [5]), 5);
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test("concurrently batch get then fetch the same request", async (assert) => {
|
||||
const orm = {
|
||||
call: async (model, method, args) => {
|
||||
assert.step(`${model}/${method}`);
|
||||
return args[0];
|
||||
},
|
||||
};
|
||||
const serverData = new ServerData(orm, {
|
||||
whenDataIsFetched: () => assert.step("data-fetched-notification"),
|
||||
});
|
||||
assert.throws(() => serverData.batch.get("partner", "get_something", 5), LoadingDataError);
|
||||
const result = await serverData.fetch("partner", "get_something", [5]);
|
||||
await nextTick();
|
||||
assert.verifySteps(
|
||||
["partner/get_something", "partner/get_something", "data-fetched-notification"],
|
||||
"it should have fetch the data once"
|
||||
);
|
||||
assert.deepEqual(result, 5);
|
||||
assert.deepEqual(serverData.batch.get("partner", "get_something", 5), 5);
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test("concurrently get and batch get the same request", async (assert) => {
|
||||
const orm = {
|
||||
call: async (model, method, args) => {
|
||||
assert.step(`${model}/${method}`);
|
||||
return args[0];
|
||||
},
|
||||
};
|
||||
const serverData = new ServerData(orm, {
|
||||
whenDataIsFetched: () => assert.step("data-fetched-notification"),
|
||||
});
|
||||
assert.throws(() => serverData.batch.get("partner", "get_something", 5), LoadingDataError);
|
||||
assert.throws(() => serverData.get("partner", "get_something", [5]), LoadingDataError);
|
||||
await nextTick();
|
||||
assert.verifySteps(
|
||||
["partner/get_something", "data-fetched-notification"],
|
||||
"it should have fetch the data once"
|
||||
);
|
||||
assert.deepEqual(serverData.get("partner", "get_something", [5]), 5);
|
||||
assert.deepEqual(serverData.batch.get("partner", "get_something", 5), 5);
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test("Call the correct callback after a batch result", async (assert) => {
|
||||
const orm = {
|
||||
call: async (model, method, args) => {
|
||||
if (args[0].includes(5)) {
|
||||
throw new Error("error while fetching data");
|
||||
}
|
||||
return args[0];
|
||||
},
|
||||
};
|
||||
const batchEndpoint = new BatchEndpoint(orm, "partner", "get_something", {
|
||||
whenDataIsFetched: () => {},
|
||||
successCallback: () => assert.step("success-callback"),
|
||||
failureCallback: () => assert.step("failure-callback"),
|
||||
});
|
||||
const request = new Request("partner", "get_something", [4]);
|
||||
const request2 = new Request("partner", "get_something", [5]);
|
||||
batchEndpoint.call(request);
|
||||
batchEndpoint.call(request2);
|
||||
assert.verifySteps([]);
|
||||
await nextTick();
|
||||
assert.verifySteps(["success-callback", "failure-callback"]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,469 @@
|
|||
import { describe, expect, test, getFixture, beforeEach } from "@odoo/hoot";
|
||||
import { mockDate } from "@odoo/hoot-mock";
|
||||
import { makeMockEnv, contains, mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
import { defineSpreadsheetModels } from "@spreadsheet/../tests/helpers/data";
|
||||
import { DateFilterValue } from "@spreadsheet/global_filters/components/date_filter_value/date_filter_value";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineSpreadsheetModels();
|
||||
|
||||
let fixture;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = getFixture();
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {{ model: Model, filter: object}} props
|
||||
*/
|
||||
async function mountDateFilterValue(env, props) {
|
||||
await mountWithCleanup(DateFilterValue, { props, env });
|
||||
}
|
||||
|
||||
test("basic date filter value", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountDateFilterValue(env, {
|
||||
value: undefined,
|
||||
update: () => {
|
||||
expect.step("update");
|
||||
},
|
||||
});
|
||||
expect.verifySteps([]);
|
||||
await contains("input").click();
|
||||
await contains(".o-date-filter-dropdown:first").click();
|
||||
expect.verifySteps(["update"]);
|
||||
});
|
||||
|
||||
test("Date filter without initial value", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountDateFilterValue(env, {
|
||||
value: undefined,
|
||||
update: () => {},
|
||||
});
|
||||
expect("input").toHaveValue("All time");
|
||||
});
|
||||
|
||||
test("Date filter with relative value", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountDateFilterValue(env, {
|
||||
value: { type: "relative", period: "last_7_days" },
|
||||
update: () => {},
|
||||
});
|
||||
expect("input").toHaveValue("Last 7 Days");
|
||||
});
|
||||
|
||||
test("Date filter with month value", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountDateFilterValue(env, {
|
||||
value: { type: "month", month: 1, year: 2023 },
|
||||
update: () => {},
|
||||
});
|
||||
expect("input").toHaveValue("January 2023");
|
||||
});
|
||||
|
||||
test("Date filter with quarter value", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountDateFilterValue(env, {
|
||||
value: { type: "quarter", quarter: 1, year: 2023 },
|
||||
update: () => {},
|
||||
});
|
||||
expect("input").toHaveValue("Q1 2023");
|
||||
});
|
||||
|
||||
test("Date filter with year value", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountDateFilterValue(env, {
|
||||
value: { type: "year", year: 2023 },
|
||||
update: () => {},
|
||||
});
|
||||
expect("input").toHaveValue("2023");
|
||||
});
|
||||
|
||||
test("Date filter with range value", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountDateFilterValue(env, {
|
||||
value: { type: "range", from: "2023-01-01", to: "2023-01-31" },
|
||||
update: () => {},
|
||||
});
|
||||
expect("input").toHaveValue("January 1 – 31, 2023");
|
||||
});
|
||||
|
||||
test("Date options are computed from the current date", async function () {
|
||||
mockDate("2022-07-14 00:00:00");
|
||||
const env = await makeMockEnv();
|
||||
await mountDateFilterValue(env, {
|
||||
value: undefined,
|
||||
update: () => {},
|
||||
});
|
||||
await contains("input").click();
|
||||
const inputs = fixture.querySelectorAll(".o-date-filter-dropdown input");
|
||||
expect(inputs.length).toBe(5);
|
||||
expect(inputs[0].value).toBe("July 2022");
|
||||
expect(inputs[1].value).toBe("Q3 2022");
|
||||
expect(inputs[2].value).toBe("2022");
|
||||
expect(inputs[3].value).toBe("07/01/2022");
|
||||
expect(inputs[4].value).toBe("07/31/2022");
|
||||
});
|
||||
|
||||
test("Month props value should override date options", async function () {
|
||||
mockDate("2022-07-14 00:00:00");
|
||||
const env = await makeMockEnv();
|
||||
await mountDateFilterValue(env, {
|
||||
value: { type: "month", month: 1, year: 2025 },
|
||||
update: () => {},
|
||||
});
|
||||
await contains("input").click();
|
||||
const inputs = fixture.querySelectorAll(".o-date-filter-dropdown input");
|
||||
expect(inputs.length).toBe(5);
|
||||
expect(inputs[0].value).toBe("January 2025");
|
||||
expect(inputs[1].value).toBe("Q3 2022");
|
||||
expect(inputs[2].value).toBe("2022");
|
||||
expect(inputs[3].value).toBe("01/01/2025");
|
||||
expect(inputs[4].value).toBe("01/31/2025");
|
||||
});
|
||||
|
||||
test("Quarter props value should override date options", async function () {
|
||||
mockDate("2022-07-14 00:00:00");
|
||||
const env = await makeMockEnv();
|
||||
await mountDateFilterValue(env, {
|
||||
value: { type: "quarter", quarter: 1, year: 2025 },
|
||||
update: () => {},
|
||||
});
|
||||
await contains("input").click();
|
||||
const inputs = fixture.querySelectorAll(".o-date-filter-dropdown input");
|
||||
expect(inputs.length).toBe(5);
|
||||
expect(inputs[0].value).toBe("July 2022");
|
||||
expect(inputs[1].value).toBe("Q1 2025");
|
||||
expect(inputs[2].value).toBe("2022");
|
||||
expect(inputs[3].value).toBe("01/01/2025");
|
||||
expect(inputs[4].value).toBe("03/31/2025");
|
||||
});
|
||||
|
||||
test("Year props value should override date options", async function () {
|
||||
mockDate("2022-07-14 00:00:00");
|
||||
const env = await makeMockEnv();
|
||||
await mountDateFilterValue(env, {
|
||||
value: { type: "year", year: 2025 },
|
||||
update: () => {},
|
||||
});
|
||||
await contains("input").click();
|
||||
const inputs = fixture.querySelectorAll(".o-date-filter-dropdown input");
|
||||
expect(inputs.length).toBe(5);
|
||||
expect(inputs[0].value).toBe("July 2022");
|
||||
expect(inputs[1].value).toBe("Q3 2022");
|
||||
expect(inputs[2].value).toBe("2025");
|
||||
expect(inputs[3].value).toBe("01/01/2025");
|
||||
expect(inputs[4].value).toBe("12/31/2025");
|
||||
});
|
||||
|
||||
test("All the options should be displayed", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountDateFilterValue(env, {
|
||||
value: undefined,
|
||||
update: () => {},
|
||||
});
|
||||
await contains("input").click();
|
||||
const options = fixture.querySelectorAll(".o-date-filter-dropdown .o-date-option-label");
|
||||
expect(options.length).toBe(14);
|
||||
expect(options[0].textContent).toBe("Today");
|
||||
expect(options[1].textContent).toBe("Yesterday");
|
||||
expect(options[2].textContent).toBe("Last 7 Days");
|
||||
expect(options[3].textContent).toBe("Last 30 Days");
|
||||
expect(options[4].textContent).toBe("Last 90 Days");
|
||||
expect(options[5].textContent).toBe("Month to Date");
|
||||
expect(options[6].textContent).toBe("Last Month");
|
||||
expect(options[7].textContent).toBe("Month");
|
||||
expect(options[8].textContent).toBe("Quarter");
|
||||
expect(options[9].textContent).toBe("Year to Date");
|
||||
expect(options[10].textContent).toBe("Last 12 Months");
|
||||
expect(options[11].textContent).toBe("Year");
|
||||
expect(options[12].textContent).toBe("All time");
|
||||
expect(options[13].textContent).toBe("Custom Range");
|
||||
});
|
||||
|
||||
test("Opening the custom range calendar does not trigger update", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountDateFilterValue(env, {
|
||||
value: { type: "range", from: "2023-01-01", to: "2023-01-31" },
|
||||
update: () => {
|
||||
expect.step("update");
|
||||
},
|
||||
});
|
||||
|
||||
expect.verifySteps([]);
|
||||
await contains("input").click();
|
||||
expect.verifySteps([]);
|
||||
await contains("input.o_datetime_input:first").click();
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("Can select a relative period", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountDateFilterValue(env, {
|
||||
value: undefined,
|
||||
update: (value) => {
|
||||
expect(value).toEqual({ type: "relative", period: "last_30_days" });
|
||||
expect.step("update");
|
||||
},
|
||||
});
|
||||
expect.verifySteps([]);
|
||||
await contains("input").click();
|
||||
await contains(".o-date-filter-dropdown[data-id='last_30_days']").click();
|
||||
expect.verifySteps(["update"]);
|
||||
});
|
||||
|
||||
test("Can select a month", async function () {
|
||||
mockDate("2022-07-14 00:00:00");
|
||||
const env = await makeMockEnv();
|
||||
await mountDateFilterValue(env, {
|
||||
value: undefined,
|
||||
update: (value) => {
|
||||
expect(value).toEqual({ type: "month", month: 7, year: 2022 });
|
||||
expect.step("update");
|
||||
},
|
||||
});
|
||||
expect.verifySteps([]);
|
||||
await contains("input").click();
|
||||
await contains(".o-date-filter-dropdown[data-id='month']").click();
|
||||
expect.verifySteps(["update"]);
|
||||
});
|
||||
|
||||
test("Can select previous month", async function () {
|
||||
mockDate("2022-07-14 00:00:00");
|
||||
const env = await makeMockEnv();
|
||||
await mountDateFilterValue(env, {
|
||||
value: undefined,
|
||||
update: (value) => {
|
||||
expect(value).toEqual({ type: "month", month: 6, year: 2022 });
|
||||
expect.step("update");
|
||||
},
|
||||
});
|
||||
expect.verifySteps([]);
|
||||
await contains("input").click();
|
||||
await contains(".o-date-filter-dropdown[data-id='month'] .btn-previous").click();
|
||||
expect.verifySteps(["update"]);
|
||||
});
|
||||
|
||||
test("Can select next month", async function () {
|
||||
mockDate("2022-07-14 00:00:00");
|
||||
const env = await makeMockEnv();
|
||||
await mountDateFilterValue(env, {
|
||||
value: undefined,
|
||||
update: (value) => {
|
||||
expect(value).toEqual({ type: "month", month: 8, year: 2022 });
|
||||
expect.step("update");
|
||||
},
|
||||
});
|
||||
expect.verifySteps([]);
|
||||
await contains("input").click();
|
||||
await contains(".o-date-filter-dropdown[data-id='month'] .btn-next").click();
|
||||
expect.verifySteps(["update"]);
|
||||
});
|
||||
|
||||
test("Can select a quarter", async function () {
|
||||
mockDate("2022-07-14 00:00:00");
|
||||
const env = await makeMockEnv();
|
||||
await mountDateFilterValue(env, {
|
||||
value: undefined,
|
||||
update: (value) => {
|
||||
expect(value).toEqual({ type: "quarter", quarter: 3, year: 2022 });
|
||||
expect.step("update");
|
||||
},
|
||||
});
|
||||
expect.verifySteps([]);
|
||||
await contains("input").click();
|
||||
await contains(".o-date-filter-dropdown[data-id='quarter']").click();
|
||||
expect.verifySteps(["update"]);
|
||||
});
|
||||
|
||||
test("Can select previous quarter", async function () {
|
||||
mockDate("2022-07-14 00:00:00");
|
||||
const env = await makeMockEnv();
|
||||
await mountDateFilterValue(env, {
|
||||
value: undefined,
|
||||
update: (value) => {
|
||||
expect(value).toEqual({ type: "quarter", quarter: 2, year: 2022 });
|
||||
expect.step("update");
|
||||
},
|
||||
});
|
||||
expect.verifySteps([]);
|
||||
await contains("input").click();
|
||||
await contains(".o-date-filter-dropdown[data-id='quarter'] .btn-previous").click();
|
||||
expect.verifySteps(["update"]);
|
||||
});
|
||||
|
||||
test("Can select next quarter", async function () {
|
||||
mockDate("2022-07-14 00:00:00");
|
||||
const env = await makeMockEnv();
|
||||
await mountDateFilterValue(env, {
|
||||
value: undefined,
|
||||
update: (value) => {
|
||||
expect(value).toEqual({ type: "quarter", quarter: 4, year: 2022 });
|
||||
expect.step("update");
|
||||
},
|
||||
});
|
||||
expect.verifySteps([]);
|
||||
await contains("input").click();
|
||||
await contains(".o-date-filter-dropdown[data-id='quarter'] .btn-next").click();
|
||||
expect.verifySteps(["update"]);
|
||||
});
|
||||
|
||||
test("Can select a year", async function () {
|
||||
mockDate("2022-07-14 00:00:00");
|
||||
const env = await makeMockEnv();
|
||||
await mountDateFilterValue(env, {
|
||||
value: undefined,
|
||||
update: (value) => {
|
||||
expect(value).toEqual({ type: "year", year: 2022 });
|
||||
expect.step("update");
|
||||
},
|
||||
});
|
||||
expect.verifySteps([]);
|
||||
await contains("input").click();
|
||||
await contains(".o-date-filter-dropdown[data-id='year']").click();
|
||||
expect.verifySteps(["update"]);
|
||||
});
|
||||
|
||||
test("Can select previous year", async function () {
|
||||
mockDate("2022-07-14 00:00:00");
|
||||
const env = await makeMockEnv();
|
||||
await mountDateFilterValue(env, {
|
||||
value: undefined,
|
||||
update: (value) => {
|
||||
expect(value).toEqual({ type: "year", year: 2021 });
|
||||
expect.step("update");
|
||||
},
|
||||
});
|
||||
expect.verifySteps([]);
|
||||
await contains("input").click();
|
||||
await contains(".o-date-filter-dropdown[data-id='year'] .btn-previous").click();
|
||||
expect.verifySteps(["update"]);
|
||||
});
|
||||
|
||||
test("Can select next year", async function () {
|
||||
mockDate("2022-07-14 00:00:00");
|
||||
const env = await makeMockEnv();
|
||||
await mountDateFilterValue(env, {
|
||||
value: undefined,
|
||||
update: (value) => {
|
||||
expect(value).toEqual({ type: "year", year: 2023 });
|
||||
expect.step("update");
|
||||
},
|
||||
});
|
||||
expect.verifySteps([]);
|
||||
await contains("input").click();
|
||||
await contains(".o-date-filter-dropdown[data-id='year'] .btn-next").click();
|
||||
expect.verifySteps(["update"]);
|
||||
});
|
||||
|
||||
test("Can select all time", async function () {
|
||||
mockDate("2022-07-14 00:00:00");
|
||||
const env = await makeMockEnv();
|
||||
await mountDateFilterValue(env, {
|
||||
value: { type: "relative", period: "last_7_days" },
|
||||
update: (value) => {
|
||||
expect(value).toBe(undefined);
|
||||
expect.step("update");
|
||||
},
|
||||
});
|
||||
expect.verifySteps([]);
|
||||
await contains("input").click();
|
||||
await contains(".o-date-filter-dropdown:not([data-id])").click();
|
||||
expect.verifySteps(["update"]);
|
||||
});
|
||||
|
||||
test("Input value is correct for relative period", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountDateFilterValue(env, {
|
||||
value: { type: "relative", period: "last_30_days" },
|
||||
update: () => {},
|
||||
});
|
||||
expect("input").toHaveValue("Last 30 Days");
|
||||
await contains("input").click();
|
||||
expect("div.selected .o-date-option-label").toHaveText("Last 30 Days");
|
||||
});
|
||||
|
||||
test("Input value is correct for month", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountDateFilterValue(env, {
|
||||
value: { type: "month", month: 1, year: 2023 },
|
||||
update: () => {},
|
||||
});
|
||||
expect("input").toHaveValue("January 2023");
|
||||
await contains("input").click();
|
||||
expect("div.selected .o-date-option-label").toHaveText("Month");
|
||||
});
|
||||
|
||||
test("Input value is correct for quarter", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountDateFilterValue(env, {
|
||||
value: { type: "quarter", quarter: 1, year: 2023 },
|
||||
update: () => {},
|
||||
});
|
||||
expect("input").toHaveValue("Q1 2023");
|
||||
await contains("input").click();
|
||||
expect("div.selected .o-date-option-label").toHaveText("Quarter");
|
||||
});
|
||||
|
||||
test("Input value is correct for year", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountDateFilterValue(env, {
|
||||
value: { type: "year", year: 2023 },
|
||||
update: () => {},
|
||||
});
|
||||
expect("input").toHaveValue("2023");
|
||||
await contains("input").click();
|
||||
expect("div.selected .o-date-option-label").toHaveText("Year");
|
||||
});
|
||||
|
||||
test("Input value is correct for range", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountDateFilterValue(env, {
|
||||
value: { type: "range", from: "2023-01-01", to: "2023-01-31" },
|
||||
update: () => {},
|
||||
});
|
||||
expect("input").toHaveValue("January 1 – 31, 2023");
|
||||
await contains("input").click();
|
||||
expect("div.selected .o-date-option-label").toHaveText("Custom Range");
|
||||
});
|
||||
|
||||
test("Input value is correct for all time", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountDateFilterValue(env, {
|
||||
value: undefined,
|
||||
update: () => {},
|
||||
});
|
||||
expect("input").toHaveValue("All time");
|
||||
await contains("input").click();
|
||||
expect("div.selected .o-date-option-label").toHaveText("All time");
|
||||
});
|
||||
|
||||
test("Can open date time picker to select a range", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountDateFilterValue(env, {
|
||||
value: { type: "range", from: "2023-01-01", to: "2023-01-31" },
|
||||
update: () => {},
|
||||
});
|
||||
await contains("input").click();
|
||||
expect(".o_datetime_picker").toHaveCount(0);
|
||||
await contains("input.o_datetime_input:first").click();
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("Choosing a from after the to will re-order dates", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountDateFilterValue(env, {
|
||||
value: { type: "range", from: "2023-01-30", to: "2023-01-31" },
|
||||
update: (value) => {
|
||||
expect(value).toEqual({ type: "range", from: "2023-01-01", to: "2023-01-30" });
|
||||
expect.step("update");
|
||||
},
|
||||
});
|
||||
await contains("input").click();
|
||||
await contains("input.o_datetime_input:last").click();
|
||||
// Select 1st of January 2023
|
||||
await contains(".o_date_item_cell.o_datetime_button:first").click();
|
||||
expect.verifySteps(["update"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
import { describe, expect, test, getFixture, beforeEach } from "@odoo/hoot";
|
||||
import { mockDate } from "@odoo/hoot-mock";
|
||||
import { makeMockEnv, contains, mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
import { defineSpreadsheetModels } from "@spreadsheet/../tests/helpers/data";
|
||||
import { DefaultDateValue } from "@spreadsheet/global_filters/components/default_date_value/default_date_value";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineSpreadsheetModels();
|
||||
|
||||
let fixture;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = getFixture();
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {{ model: Model, filter: object}} props
|
||||
*/
|
||||
async function mountDefaultDateValue(env, props) {
|
||||
await mountWithCleanup(DefaultDateValue, { props, env });
|
||||
}
|
||||
|
||||
test("Default date filter without initial value", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountDefaultDateValue(env, {
|
||||
value: undefined,
|
||||
update: () => {},
|
||||
});
|
||||
expect("input").toHaveValue("All time");
|
||||
});
|
||||
|
||||
test("Date filter with relative value", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountDefaultDateValue(env, {
|
||||
value: "last_7_days",
|
||||
update: () => {},
|
||||
});
|
||||
expect("input").toHaveValue("Last 7 Days");
|
||||
});
|
||||
|
||||
test("Default date with this_month", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountDefaultDateValue(env, {
|
||||
value: "this_month",
|
||||
update: () => {},
|
||||
});
|
||||
expect("input").toHaveValue("Current Month");
|
||||
});
|
||||
|
||||
test("Default date with this_quarter", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountDefaultDateValue(env, {
|
||||
value: "this_quarter",
|
||||
update: () => {},
|
||||
});
|
||||
expect("input").toHaveValue("Current Quarter");
|
||||
});
|
||||
|
||||
test("Default date with this_year", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountDefaultDateValue(env, {
|
||||
value: "this_year",
|
||||
update: () => {},
|
||||
});
|
||||
expect("input").toHaveValue("Current Year");
|
||||
});
|
||||
|
||||
test("All the options should be displayed", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountDefaultDateValue(env, {
|
||||
value: undefined,
|
||||
update: () => {},
|
||||
});
|
||||
await contains("input").click();
|
||||
const options = fixture.querySelectorAll(".o-dropdown-item");
|
||||
expect(options.length).toBe(13);
|
||||
expect(options[0].textContent).toBe("Today");
|
||||
expect(options[1].textContent).toBe("Yesterday");
|
||||
expect(options[2].textContent).toBe("Last 7 Days");
|
||||
expect(options[3].textContent).toBe("Last 30 Days");
|
||||
expect(options[4].textContent).toBe("Last 90 Days");
|
||||
expect(options[5].textContent).toBe("Month to Date");
|
||||
expect(options[6].textContent).toBe("Last Month");
|
||||
expect(options[7].textContent).toBe("Current Month");
|
||||
expect(options[8].textContent).toBe("Current Quarter");
|
||||
expect(options[9].textContent).toBe("Year to Date");
|
||||
expect(options[10].textContent).toBe("Last 12 Months");
|
||||
expect(options[11].textContent).toBe("Current Year");
|
||||
expect(options[12].textContent).toBe("All time");
|
||||
});
|
||||
|
||||
test("Can select a relative period", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountDefaultDateValue(env, {
|
||||
value: undefined,
|
||||
update: (value) => {
|
||||
expect(value).toBe("last_30_days");
|
||||
expect.step("update");
|
||||
},
|
||||
});
|
||||
expect.verifySteps([]);
|
||||
await contains("input").click();
|
||||
await contains(".o-dropdown-item[data-id='last_30_days']").click();
|
||||
expect.verifySteps(["update"]);
|
||||
});
|
||||
|
||||
test("Can select this month", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountDefaultDateValue(env, {
|
||||
value: undefined,
|
||||
update: (value) => {
|
||||
expect(value).toBe("this_month");
|
||||
expect.step("update");
|
||||
},
|
||||
});
|
||||
expect.verifySteps([]);
|
||||
await contains("input").click();
|
||||
await contains(".o-dropdown-item[data-id='this_month']").click();
|
||||
expect.verifySteps(["update"]);
|
||||
});
|
||||
|
||||
test("Can select all time", async function () {
|
||||
mockDate("2022-07-14 00:00:00");
|
||||
const env = await makeMockEnv();
|
||||
await mountDefaultDateValue(env, {
|
||||
value: "last_7_days",
|
||||
update: (value) => {
|
||||
expect(value).toBe(undefined);
|
||||
expect.step("update");
|
||||
},
|
||||
});
|
||||
expect.verifySteps([]);
|
||||
await contains("input").click();
|
||||
await contains(".o-dropdown-item:not([data-id])").click();
|
||||
expect.verifySteps(["update"]);
|
||||
});
|
||||
|
||||
test("Input value is correct for the current value", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountDefaultDateValue(env, {
|
||||
value: "last_30_days",
|
||||
update: () => {},
|
||||
});
|
||||
expect("input").toHaveValue("Last 30 Days");
|
||||
await contains("input").click();
|
||||
expect(".selected").toHaveText("Last 30 Days");
|
||||
});
|
||||
|
||||
test("Input value is correct for all time", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountDefaultDateValue(env, {
|
||||
value: undefined,
|
||||
update: () => {},
|
||||
});
|
||||
expect("input").toHaveValue("All time");
|
||||
await contains("input").click();
|
||||
expect(".selected").toHaveText("All time");
|
||||
});
|
||||
|
|
@ -0,0 +1,296 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { click, queryAllTexts } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { defineSpreadsheetModels } from "@spreadsheet/../tests/helpers/data";
|
||||
import { contains, makeMockEnv, mountWithCleanup, onRpc } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { Model } from "@odoo/o-spreadsheet";
|
||||
import {
|
||||
addGlobalFilter,
|
||||
editGlobalFilter,
|
||||
setCellContent,
|
||||
setCellFormat,
|
||||
} from "@spreadsheet/../tests/helpers/commands";
|
||||
import { toRangeData } from "@spreadsheet/../tests/helpers/zones";
|
||||
import { FilterValue } from "@spreadsheet/global_filters/components/filter_value/filter_value";
|
||||
import { user } from "@web/core/user";
|
||||
|
||||
import { OdooDataProvider } from "@spreadsheet/data_sources/odoo_data_provider";
|
||||
import { Component, onWillUnmount, xml } from "@odoo/owl";
|
||||
|
||||
describe.current.tags("headless");
|
||||
defineSpreadsheetModels();
|
||||
|
||||
class FilterValueWrapper extends Component {
|
||||
static template = xml`
|
||||
<FilterValue
|
||||
t-props="props"
|
||||
globalFilterValue="globalFilterValue"
|
||||
/>`;
|
||||
static components = { FilterValue };
|
||||
static props = FilterValue.props;
|
||||
|
||||
setup() {
|
||||
this.props.model.on("update", this, () => this.render(true));
|
||||
onWillUnmount(() => this.props.model.off("update", this));
|
||||
}
|
||||
|
||||
get globalFilterValue() {
|
||||
return (
|
||||
this.props.globalFilterValue ??
|
||||
this.props.model.getters.getGlobalFilterValue(this.props.filter.id)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {{ model: Model, filter: object}} props
|
||||
*/
|
||||
async function mountFilterValueComponent(props) {
|
||||
props = {
|
||||
setGlobalFilterValue: (id, value, displayNames) => {
|
||||
props.model.dispatch("SET_GLOBAL_FILTER_VALUE", {
|
||||
id,
|
||||
value,
|
||||
displayNames,
|
||||
});
|
||||
},
|
||||
...props,
|
||||
};
|
||||
await mountWithCleanup(FilterValueWrapper, { props });
|
||||
}
|
||||
|
||||
test("basic text filter", async function () {
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { odooDataProvider: new OdooDataProvider(env) } });
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "text",
|
||||
label: "Text Filter",
|
||||
});
|
||||
await mountFilterValueComponent({ model, filter: model.getters.getGlobalFilter("42") });
|
||||
await contains(".o-autocomplete input").edit("foo");
|
||||
await contains(".o-autocomplete input").press("Enter");
|
||||
expect(model.getters.getGlobalFilterValue("42")).toEqual(
|
||||
{ operator: "ilike", strings: ["foo"] },
|
||||
{ message: "value is set" }
|
||||
);
|
||||
});
|
||||
|
||||
test("can clear a text filter value", async function () {
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { odooDataProvider: new OdooDataProvider(env) } });
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "text",
|
||||
label: "Text Filter",
|
||||
defaultValue: { operator: "ilike", strings: ["foo", "bar"] },
|
||||
});
|
||||
await mountFilterValueComponent({ model, filter: model.getters.getGlobalFilter("42") });
|
||||
expect(queryAllTexts(".o_tag")).toEqual(["foo", "bar"]);
|
||||
expect(model.getters.getGlobalFilterValue("42")).toEqual({
|
||||
operator: "ilike",
|
||||
strings: ["foo", "bar"],
|
||||
});
|
||||
await contains(".o_tag .o_delete").click();
|
||||
expect(queryAllTexts(".o_tag")).toEqual(["bar"]);
|
||||
expect(model.getters.getGlobalFilterValue("42")).toEqual({
|
||||
operator: "ilike",
|
||||
strings: ["bar"],
|
||||
});
|
||||
});
|
||||
|
||||
test("text filter with range", async function () {
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { odooDataProvider: new OdooDataProvider(env) } });
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "text",
|
||||
label: "Text Filter",
|
||||
rangesOfAllowedValues: [toRangeData(sheetId, "A1:A3")],
|
||||
});
|
||||
setCellContent(model, "A1", "foo");
|
||||
setCellContent(model, "A2", "0");
|
||||
setCellFormat(model, "A2", "0.00");
|
||||
await mountFilterValueComponent({ model, filter: model.getters.getGlobalFilter("42") });
|
||||
expect(".o_tag").toHaveCount(0, { message: "no value is selected" });
|
||||
await click(".o-autocomplete input");
|
||||
await animationFrame();
|
||||
|
||||
expect(queryAllTexts(".dropdown-item")).toEqual(["foo", "0.00"], {
|
||||
message: "values are formatted",
|
||||
});
|
||||
await click(".dropdown-item:last");
|
||||
await animationFrame();
|
||||
expect(".o_tag").toHaveText("0.00");
|
||||
expect(model.getters.getGlobalFilterValue("42").strings).toEqual(["0"]);
|
||||
|
||||
// select a second value
|
||||
await click(".o-autocomplete input");
|
||||
await animationFrame();
|
||||
|
||||
await click(".dropdown-item:last");
|
||||
await animationFrame();
|
||||
expect(queryAllTexts(".o_tag")).toEqual(["0.00", "foo"]);
|
||||
expect(model.getters.getGlobalFilterValue("42").strings).toEqual(["0", "foo"]);
|
||||
});
|
||||
|
||||
test("cannot edit text filter input with range", async function () {
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { odooDataProvider: new OdooDataProvider(env) } });
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
setCellContent(model, "A1", "foo");
|
||||
const filter = {
|
||||
id: "42",
|
||||
type: "text",
|
||||
label: "Text Filter",
|
||||
};
|
||||
await addGlobalFilter(model, filter);
|
||||
await mountFilterValueComponent({ model, filter: model.getters.getGlobalFilter("42") });
|
||||
expect(".o-autocomplete input").not.toHaveAttribute("maxlength");
|
||||
editGlobalFilter(model, {
|
||||
...filter,
|
||||
rangesOfAllowedValues: [toRangeData(sheetId, "A1")],
|
||||
});
|
||||
await animationFrame();
|
||||
expect(".o-autocomplete input").toHaveAttribute("maxlength", "0");
|
||||
editGlobalFilter(model, filter);
|
||||
await animationFrame();
|
||||
expect(".o-autocomplete input").not.toHaveAttribute("maxlength");
|
||||
});
|
||||
|
||||
test("text filter cannot have the same value twice", async function () {
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { odooDataProvider: new OdooDataProvider(env) } });
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "text",
|
||||
label: "Text Filter",
|
||||
});
|
||||
await mountFilterValueComponent({ model, filter: model.getters.getGlobalFilter("42") });
|
||||
await contains(".o-autocomplete input").edit("foo");
|
||||
await contains(".o-autocomplete input").press("Enter");
|
||||
await contains(".o-autocomplete input").edit("foo");
|
||||
await contains(".o-autocomplete input").press("Enter");
|
||||
expect(model.getters.getGlobalFilterValue("42")).toEqual({
|
||||
operator: "ilike",
|
||||
strings: ["foo"],
|
||||
});
|
||||
});
|
||||
|
||||
test("relational filter with domain", async function () {
|
||||
onRpc("partner", "name_search", ({ kwargs }) => {
|
||||
expect.step("name_search");
|
||||
expect(kwargs.domain).toEqual(["&", ["display_name", "=", "Bob"], "!", ["id", "in", []]]);
|
||||
});
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { odooDataProvider: new OdooDataProvider(env) } });
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "relation",
|
||||
label: "My Filter",
|
||||
modelName: "partner",
|
||||
domainOfAllowedValues: [["display_name", "=", "Bob"]],
|
||||
});
|
||||
await mountFilterValueComponent({ model, filter: model.getters.getGlobalFilter("42") });
|
||||
await click(".o_multi_record_selector input");
|
||||
await animationFrame();
|
||||
expect.verifySteps(["name_search"]);
|
||||
});
|
||||
|
||||
test("Filter with showClear should display the clear icon", async function () {
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { odooDataProvider: new OdooDataProvider(env) } });
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "text",
|
||||
label: "Text Filter",
|
||||
defaultValue: { operator: "ilike", strings: ["foo"] },
|
||||
});
|
||||
await mountFilterValueComponent({
|
||||
model,
|
||||
filter: model.getters.getGlobalFilter("42"),
|
||||
showClear: true,
|
||||
});
|
||||
expect(".fa-times").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("Filter without showClear should not display the clear icon", async function () {
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { odooDataProvider: new OdooDataProvider(env) } });
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "text",
|
||||
label: "Text Filter",
|
||||
defaultValue: { operator: "ilike", strings: ["foo"] },
|
||||
});
|
||||
await mountFilterValueComponent({
|
||||
model,
|
||||
filter: model.getters.getGlobalFilter("42"),
|
||||
});
|
||||
expect(".fa-times").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("relational filter with a contextual domain", async function () {
|
||||
onRpc("partner", "name_search", ({ kwargs }) => {
|
||||
expect.step("name_search");
|
||||
expect(kwargs.domain).toEqual([
|
||||
"&",
|
||||
["user_ids", "in", [user.userId]],
|
||||
"!",
|
||||
["id", "in", []],
|
||||
]);
|
||||
});
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { odooDataProvider: new OdooDataProvider(env) } });
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "relation",
|
||||
label: "My Filter",
|
||||
modelName: "partner",
|
||||
domainOfAllowedValues: '[["user_ids", "in", [uid]]]',
|
||||
});
|
||||
await mountFilterValueComponent({ model, filter: model.getters.getGlobalFilter("42") });
|
||||
await click(".o_multi_record_selector input");
|
||||
await animationFrame();
|
||||
expect.verifySteps(["name_search"]);
|
||||
});
|
||||
|
||||
test("selection filter", async function () {
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { odooDataProvider: new OdooDataProvider(env) } });
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "selection",
|
||||
label: "Selection Filter",
|
||||
resModel: "res.currency",
|
||||
selectionField: "position",
|
||||
});
|
||||
await mountFilterValueComponent({ model, filter: model.getters.getGlobalFilter("42") });
|
||||
await contains("input").click();
|
||||
await contains("a:first").click();
|
||||
expect(model.getters.getGlobalFilterValue("42")).toEqual({
|
||||
operator: "in",
|
||||
selectionValues: ["after"],
|
||||
});
|
||||
});
|
||||
|
||||
test("numeric filter", async function () {
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { odooDataProvider: new OdooDataProvider(env) } });
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "numeric",
|
||||
label: "Numeric Filter",
|
||||
defaultValue: { operator: "=", targetValue: 1998 },
|
||||
});
|
||||
await mountFilterValueComponent({ model, filter: model.getters.getGlobalFilter("42") });
|
||||
await contains("input").edit(1998);
|
||||
await contains("input").press("Enter");
|
||||
expect(model.getters.getGlobalFilterValue("42")).toEqual(
|
||||
{ operator: "=", targetValue: 1998 },
|
||||
{ message: "value is set" }
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,429 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { click, edit, queryAllTexts } from "@odoo/hoot-dom";
|
||||
import { defineSpreadsheetModels } from "@spreadsheet/../tests/helpers/data";
|
||||
import { contains, makeMockEnv, mountWithCleanup, onRpc } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { Model } from "@odoo/o-spreadsheet";
|
||||
import { addGlobalFilter } from "@spreadsheet/../tests/helpers/commands";
|
||||
|
||||
import { OdooDataProvider } from "@spreadsheet/data_sources/odoo_data_provider";
|
||||
import { Component, onWillUnmount, xml } from "@odoo/owl";
|
||||
import { FilterValuesList } from "@spreadsheet/global_filters/components/filter_values_list/filter_values_list";
|
||||
|
||||
describe.current.tags("headless");
|
||||
defineSpreadsheetModels();
|
||||
|
||||
class FilterValuesListWrapper extends Component {
|
||||
static template = xml`<FilterValuesList t-props="props" />`;
|
||||
static components = { FilterValuesList };
|
||||
static props = {
|
||||
...FilterValuesList.props,
|
||||
close: { type: Function, optional: true },
|
||||
};
|
||||
static defaultProps = {
|
||||
close: () => {},
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.props.model.on("update", this, () => this.render(true));
|
||||
onWillUnmount(() => this.props.model.off("update", this));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} env
|
||||
* @param {{ model: Model }} props
|
||||
*/
|
||||
async function mountFilterValuesList(env, props) {
|
||||
//@ts-ignore
|
||||
env.dialogData = {
|
||||
isActive: true,
|
||||
close: () => {},
|
||||
};
|
||||
await mountWithCleanup(FilterValuesListWrapper, { props });
|
||||
}
|
||||
|
||||
test("basic text filter", async function () {
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { odooDataProvider: new OdooDataProvider(env) } });
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "text",
|
||||
label: "Text Filter",
|
||||
});
|
||||
await mountFilterValuesList(env, { model });
|
||||
expect(".o-filter-values").toHaveCount(1);
|
||||
expect(".fa-pencil").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("Edit filter is displayed when the props openFiltersEditor is set", async function () {
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { odooDataProvider: new OdooDataProvider(env) } });
|
||||
await mountFilterValuesList(env, {
|
||||
model,
|
||||
openFiltersEditor: () => {},
|
||||
});
|
||||
expect(queryAllTexts(".o-filter-values-footer button")).toEqual(["Filter", "Edit", "Discard"]);
|
||||
});
|
||||
|
||||
test("filter search dialog with no active filters", async function () {
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { odooDataProvider: new OdooDataProvider(env) } });
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "text",
|
||||
label: "Text Filter",
|
||||
});
|
||||
await mountFilterValuesList(env, { model });
|
||||
expect(".o-filter-values .o-filter-item").toHaveCount(1);
|
||||
expect(".o-filter-values .o-global-filter-text-value").toHaveText("");
|
||||
});
|
||||
|
||||
test("filter search dialog with active filters", async function () {
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { odooDataProvider: new OdooDataProvider(env) } });
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "text",
|
||||
label: "Text Filter",
|
||||
defaultValue: { operator: "ilike", strings: ["foo"] },
|
||||
});
|
||||
await mountFilterValuesList(env, { model });
|
||||
expect(".o-filter-values .o-filter-item").toHaveCount(1);
|
||||
expect(".o-filter-values .o-global-filter-text-value").toHaveText("foo");
|
||||
});
|
||||
|
||||
test("Can set a text filter value", async function () {
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { odooDataProvider: new OdooDataProvider(env) } });
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "text",
|
||||
label: "Text Filter",
|
||||
});
|
||||
await mountFilterValuesList(env, { model });
|
||||
await contains(".o-filter-values select").select("not ilike");
|
||||
await contains(".o-filter-values .o-filter-item .o-autocomplete input").edit("foo");
|
||||
await contains(".o-filter-values .o-filter-item .o-autocomplete input").press("Enter");
|
||||
expect(model.getters.getGlobalFilterValue("42")).toBe(undefined, {
|
||||
message: "value is not directly set",
|
||||
});
|
||||
await contains(".btn-primary").click();
|
||||
expect(model.getters.getGlobalFilterValue("42")).toEqual(
|
||||
{ operator: "not ilike", strings: ["foo"] },
|
||||
{ message: "value is set" }
|
||||
);
|
||||
});
|
||||
|
||||
test("Can set a numeric filter value with basic operator", async function () {
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { odooDataProvider: new OdooDataProvider(env) } });
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "numeric",
|
||||
label: "Numeric Filter",
|
||||
});
|
||||
await mountFilterValuesList(env, { model });
|
||||
await contains(".o-filter-values select").select(">");
|
||||
await contains("input").edit(1998);
|
||||
await contains("input").press("Enter");
|
||||
expect(model.getters.getGlobalFilterValue("42")).toBe(undefined, {
|
||||
message: "value is not directly set",
|
||||
});
|
||||
await contains(".btn-primary").click();
|
||||
expect(model.getters.getGlobalFilterValue("42")).toEqual(
|
||||
{ operator: ">", targetValue: 1998 },
|
||||
{ message: "value is set" }
|
||||
);
|
||||
});
|
||||
|
||||
test("Can set a numeric filter value with between operator", async function () {
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { odooDataProvider: new OdooDataProvider(env) } });
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "numeric",
|
||||
label: "Numeric Filter",
|
||||
});
|
||||
await mountFilterValuesList(env, { model });
|
||||
await contains(".o-filter-values select").select("between");
|
||||
const inputs = document.querySelectorAll(".o-global-filter-numeric-value");
|
||||
expect(inputs).toHaveLength(2);
|
||||
await click(inputs[0]);
|
||||
await edit(1);
|
||||
await click(inputs[1]);
|
||||
await edit(99);
|
||||
await contains(".btn-primary").click();
|
||||
expect(model.getters.getGlobalFilterValue("42")).toEqual(
|
||||
{ operator: "between", minimumValue: 1, maximumValue: 99 },
|
||||
{ message: "value is set" }
|
||||
);
|
||||
});
|
||||
|
||||
test("Can set a relation filter value", async function () {
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { odooDataProvider: new OdooDataProvider(env) } });
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "relation",
|
||||
modelName: "product",
|
||||
label: "Relation Filter",
|
||||
});
|
||||
await mountFilterValuesList(env, { model });
|
||||
await contains(".o-filter-values select").select("not in");
|
||||
await contains("input.o-autocomplete--input").click();
|
||||
await contains(".o-autocomplete--dropdown-item:first").click();
|
||||
expect(".o_tag").toHaveCount(1);
|
||||
await contains(".btn-primary").click();
|
||||
expect(model.getters.getGlobalFilterValue("42")).toEqual(
|
||||
{ operator: "not in", ids: [37] },
|
||||
{
|
||||
message: "value is set",
|
||||
}
|
||||
);
|
||||
await contains(".o_tag .o_delete").click();
|
||||
await contains(".btn-primary").click();
|
||||
expect(model.getters.getGlobalFilterValue("42")).toBe(undefined);
|
||||
});
|
||||
|
||||
test("Can remove a default relation filter value", async function () {
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { odooDataProvider: new OdooDataProvider(env) } });
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "relation",
|
||||
modelName: "product",
|
||||
label: "Relation Filter",
|
||||
defaultValue: { operator: "in", ids: [37] },
|
||||
});
|
||||
await mountFilterValuesList(env, { model });
|
||||
expect(".o_tag").toHaveCount(1);
|
||||
await contains(".o_tag .o_delete").click();
|
||||
expect(".o_tag").toHaveCount(0);
|
||||
await contains(".btn-primary").click();
|
||||
expect(model.getters.getGlobalFilterValue("42")).toBe(undefined);
|
||||
});
|
||||
|
||||
test("Default value for relation filter is correctly displayed", async function () {
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { odooDataProvider: new OdooDataProvider(env) } });
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "relation",
|
||||
modelName: "product",
|
||||
label: "Relation Filter",
|
||||
defaultValue: { operator: "in", ids: [37] },
|
||||
});
|
||||
await mountFilterValuesList(env, { model });
|
||||
expect(".o_tag").toHaveCount(1);
|
||||
expect(queryAllTexts(".o_tag")).toEqual(["xphone"]);
|
||||
});
|
||||
|
||||
test("Can change a boolean filter value", async function () {
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { odooDataProvider: new OdooDataProvider(env) } });
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "boolean",
|
||||
label: "Boolean Filter",
|
||||
});
|
||||
await mountFilterValuesList(env, { model });
|
||||
expect(".o-filter-values select").toHaveValue("");
|
||||
await contains(".o-filter-values select").select("not set");
|
||||
await contains(".btn-primary").click();
|
||||
expect(model.getters.getGlobalFilterValue("42")).toEqual({ operator: "not set" });
|
||||
|
||||
await contains(".o-filter-values select").select("");
|
||||
await contains(".btn-primary").click();
|
||||
expect(model.getters.getGlobalFilterValue("42")).toBe(undefined);
|
||||
});
|
||||
|
||||
test("Can set a date filter value", async function () {
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { odooDataProvider: new OdooDataProvider(env) } });
|
||||
const label = "Date Filter";
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "date",
|
||||
label,
|
||||
});
|
||||
await mountFilterValuesList(env, { model });
|
||||
await contains(".o-date-filter-input").click();
|
||||
await contains(".o-dropdown-item[data-id='last_7_days']").click();
|
||||
expect(".o-date-filter-input").toHaveValue("Last 7 Days");
|
||||
await contains(".btn-primary").click();
|
||||
expect(model.getters.getGlobalFilterValue("42")).toEqual({
|
||||
period: "last_7_days",
|
||||
type: "relative",
|
||||
});
|
||||
});
|
||||
|
||||
test("Readonly user can update a filter value", async function () {
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { odooDataProvider: new OdooDataProvider(env) } });
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "text",
|
||||
label: "Text Filter",
|
||||
});
|
||||
model.updateMode("readonly");
|
||||
await mountFilterValuesList(env, { model });
|
||||
await contains(".o-filter-values .o-filter-item .o-autocomplete input").edit("foo");
|
||||
await contains(".o-filter-values .o-filter-item .o-autocomplete input").press("Enter");
|
||||
await contains(".btn-primary").click();
|
||||
expect(model.getters.getGlobalFilterValue("42").strings).toEqual(["foo"], {
|
||||
message: "value is set",
|
||||
});
|
||||
});
|
||||
|
||||
test("Can clear a filter value removing the values manually", async function () {
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { odooDataProvider: new OdooDataProvider(env) } });
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "text",
|
||||
label: "Text Filter",
|
||||
defaultValue: { operator: "ilike", strings: ["foo"] },
|
||||
});
|
||||
await mountFilterValuesList(env, { model });
|
||||
expect(".o-filter-values .o-filter-item .o_tag").toHaveCount(1);
|
||||
await contains(".o-filter-values .o-filter-item .o_tag .o_delete").click();
|
||||
expect(".o-filter-values .o-filter-item .o_tag").toHaveCount(0);
|
||||
await contains(".btn-primary").click();
|
||||
expect(model.getters.getGlobalFilterValue("42")).toBe(undefined, {
|
||||
message: "value is cleared",
|
||||
});
|
||||
});
|
||||
|
||||
test("Can clear a filter value with the clear button", async function () {
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { odooDataProvider: new OdooDataProvider(env) } });
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "text",
|
||||
label: "Text Filter",
|
||||
defaultValue: { operator: "ilike", strings: ["foo"] },
|
||||
});
|
||||
await mountFilterValuesList(env, { model });
|
||||
expect(".o-filter-values .o-filter-item .o_tag").toHaveCount(1);
|
||||
await contains(".o-filter-values .o-filter-item .o-filter-clear button").click();
|
||||
expect(".o-filter-values .o-filter-item .o_tag").toHaveCount(0);
|
||||
await contains(".btn-primary").click();
|
||||
expect(model.getters.getGlobalFilterValue("42")).toBe(undefined);
|
||||
});
|
||||
|
||||
test("clearing a filter value preserves the operator", async function () {
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { odooDataProvider: new OdooDataProvider(env) } });
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "text",
|
||||
label: "Text Filter",
|
||||
defaultValue: { operator: "ilike", strings: ["foo"] },
|
||||
});
|
||||
await mountFilterValuesList(env, { model });
|
||||
await contains(".o-filter-values select").select("starts with");
|
||||
|
||||
// remove the only value
|
||||
await contains(".o-filter-values .o-filter-item .o_tag .o_delete").click();
|
||||
expect(".o-filter-values .o-filter-item .o_tag").toHaveCount(0);
|
||||
expect(".o-filter-values select").toHaveValue("starts with");
|
||||
|
||||
// add a value back
|
||||
await contains(".o-filter-values .o-filter-item .o-autocomplete input").edit("foo");
|
||||
await contains(".o-filter-values .o-filter-item .o-autocomplete input").press("Enter");
|
||||
await contains(".btn-primary").click();
|
||||
expect(model.getters.getGlobalFilterValue("42")).toEqual({
|
||||
operator: "starts with",
|
||||
strings: ["foo"],
|
||||
});
|
||||
});
|
||||
|
||||
test("Relational global filter with no parent/child model do not have the child of operator", async function () {
|
||||
onRpc("ir.model", "has_searchable_parent_relation", () => ({ partner: false }));
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { odooDataProvider: new OdooDataProvider(env) } });
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "relation",
|
||||
label: "Filter",
|
||||
modelName: "partner",
|
||||
defaultValue: { operator: "in", ids: [37] },
|
||||
});
|
||||
await mountFilterValuesList(env, { model });
|
||||
expect('option[value="child_of"]').toHaveCount(0);
|
||||
});
|
||||
|
||||
test("Relational global filter with a parent/child model adds the child of operator", async function () {
|
||||
onRpc("ir.model", "has_searchable_parent_relation", () => ({ partner: true }));
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { odooDataProvider: new OdooDataProvider(env) } });
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "relation",
|
||||
label: "Filter",
|
||||
modelName: "partner",
|
||||
defaultValue: { operator: "in", ids: [38] },
|
||||
});
|
||||
await mountFilterValuesList(env, { model });
|
||||
expect('option[value="child_of"]').toHaveCount(1);
|
||||
});
|
||||
|
||||
test(`Relational global filter with "set" operator doesn't have a record selector input`, async function () {
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { odooDataProvider: new OdooDataProvider(env) } });
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "relation",
|
||||
label: "Filter",
|
||||
modelName: "partner",
|
||||
defaultValue: { operator: "set" },
|
||||
});
|
||||
await mountFilterValuesList(env, { model });
|
||||
expect(".o-filter-value input").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("relational global filter operator options", async function () {
|
||||
onRpc("ir.model", "has_searchable_parent_relation", () => ({ partner: true }));
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { odooDataProvider: new OdooDataProvider(env) } });
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "relation",
|
||||
label: "Filter",
|
||||
modelName: "partner",
|
||||
defaultValue: { operator: "in", ids: [38] },
|
||||
});
|
||||
await mountFilterValuesList(env, { model });
|
||||
expect(queryAllTexts("option")).toEqual([
|
||||
"is in",
|
||||
"is not in",
|
||||
"child of",
|
||||
"contains",
|
||||
"does not contain",
|
||||
"is set",
|
||||
"is not set",
|
||||
]);
|
||||
});
|
||||
|
||||
test("text global filter operator options", async function () {
|
||||
const env = await makeMockEnv();
|
||||
const model = new Model({}, { custom: { odooDataProvider: new OdooDataProvider(env) } });
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "text",
|
||||
label: "Filter",
|
||||
defaultValue: { operator: "in", strings: ["hello"] },
|
||||
});
|
||||
await mountFilterValuesList(env, { model });
|
||||
expect(queryAllTexts("option")).toEqual([
|
||||
"contains",
|
||||
"does not contain",
|
||||
"is in",
|
||||
"is not in",
|
||||
"starts with",
|
||||
"is set",
|
||||
"is not set",
|
||||
]);
|
||||
});
|
||||
|
|
@ -0,0 +1,618 @@
|
|||
/** @ts-check */
|
||||
import { describe, expect, test, beforeEach } from "@odoo/hoot";
|
||||
import { mockDate } from "@odoo/hoot-mock";
|
||||
import {
|
||||
getDateDomain,
|
||||
getRelativeDateFromTo,
|
||||
dateFilterValueToString,
|
||||
getNextDateFilterValue,
|
||||
getPreviousDateFilterValue,
|
||||
getFacetInfo,
|
||||
RELATIVE_PERIODS,
|
||||
} from "@spreadsheet/global_filters/helpers";
|
||||
import {
|
||||
getDateDomainDurationInDays,
|
||||
assertDateDomainEqual,
|
||||
} from "@spreadsheet/../tests/helpers/date_domain";
|
||||
import { makeMockEnv, allowTranslations } from "@web/../tests/web_test_helpers";
|
||||
import { getOperatorLabel } from "@web/core/tree_editor/tree_editor_operator_editor";
|
||||
|
||||
import { defineSpreadsheetModels } from "../helpers/data";
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
const { DateTime } = luxon;
|
||||
|
||||
const LAZY_TRANSLATED_SET = getOperatorLabel("set");
|
||||
const LAZY_TRANSLATED_NOT_SET = getOperatorLabel("not set");
|
||||
const LAZY_TRANSLATED_CONTAINS = getOperatorLabel("ilike");
|
||||
|
||||
beforeEach(() => {
|
||||
allowTranslations();
|
||||
});
|
||||
|
||||
function getRelativeDateDomain(now, offset, period, fieldName, fieldType) {
|
||||
const { from, to } = getRelativeDateFromTo(now, offset, period);
|
||||
return getDateDomain(from, to, fieldName, fieldType);
|
||||
}
|
||||
|
||||
function valueToString(value) {
|
||||
return dateFilterValueToString(value).toString();
|
||||
}
|
||||
|
||||
test("getRelativeDateDomain > year_to_date (year to date)", async function () {
|
||||
const now = DateTime.fromISO("2022-05-16");
|
||||
const domain = getRelativeDateDomain(now, 0, "year_to_date", "field", "date");
|
||||
assertDateDomainEqual("field", "2022-01-01", "2022-05-16", domain);
|
||||
});
|
||||
|
||||
test("getRelativeDateDomain > today (today)", async function () {
|
||||
const now = DateTime.fromISO("2022-05-16");
|
||||
const domain = getRelativeDateDomain(now, 0, "today", "field", "date");
|
||||
expect(getDateDomainDurationInDays(domain)).toBe(1);
|
||||
assertDateDomainEqual("field", "2022-05-16", "2022-05-16", domain);
|
||||
});
|
||||
|
||||
test("getRelativeDateDomain > yesterday (yesterday)", async function () {
|
||||
const now = DateTime.fromISO("2022-05-16");
|
||||
const domain = getRelativeDateDomain(now, 0, "yesterday", "field", "date");
|
||||
expect(getDateDomainDurationInDays(domain)).toBe(1);
|
||||
assertDateDomainEqual("field", "2022-05-15", "2022-05-15", domain);
|
||||
});
|
||||
|
||||
test("getRelativeDateDomain > last_7_days (last 7 days)", async function () {
|
||||
const now = DateTime.fromISO("2022-05-16");
|
||||
const domain = getRelativeDateDomain(now, 0, "last_7_days", "field", "date");
|
||||
expect(getDateDomainDurationInDays(domain)).toBe(7);
|
||||
assertDateDomainEqual("field", "2022-05-10", "2022-05-16", domain);
|
||||
});
|
||||
|
||||
test("getRelativeDateDomain > last_30_days (last 30 days)", async function () {
|
||||
const now = DateTime.fromISO("2022-05-16");
|
||||
const domain = getRelativeDateDomain(now, 0, "last_30_days", "field", "date");
|
||||
expect(getDateDomainDurationInDays(domain)).toBe(30);
|
||||
assertDateDomainEqual("field", "2022-04-17", "2022-05-16", domain);
|
||||
});
|
||||
|
||||
test("getRelativeDateDomain > last_90_days (last 90 days)", async function () {
|
||||
const now = DateTime.fromISO("2022-05-16");
|
||||
const domain = getRelativeDateDomain(now, 0, "last_90_days", "field", "date");
|
||||
expect(getDateDomainDurationInDays(domain)).toBe(90);
|
||||
assertDateDomainEqual("field", "2022-02-16", "2022-05-16", domain);
|
||||
});
|
||||
|
||||
test("getRelativeDateDomain > month_to_date (Month to Date)", async function () {
|
||||
const now = DateTime.fromISO("2022-05-16");
|
||||
const domain = getRelativeDateDomain(now, 0, "month_to_date", "field", "date");
|
||||
expect(getDateDomainDurationInDays(domain)).toBe(16);
|
||||
assertDateDomainEqual("field", "2022-05-01", "2022-05-16", domain);
|
||||
});
|
||||
|
||||
test("getRelativeDateDomain > last_month (Last month)", async function () {
|
||||
const now = DateTime.fromISO("2022-05-16");
|
||||
const domain = getRelativeDateDomain(now, 0, "last_month", "field", "date");
|
||||
expect(getDateDomainDurationInDays(domain)).toBe(30);
|
||||
assertDateDomainEqual("field", "2022-04-01", "2022-04-30", domain);
|
||||
});
|
||||
|
||||
test("getRelativeDateDomain > last_12_months (last 12 months)", async function () {
|
||||
const now = DateTime.fromISO("2022-05-16");
|
||||
const domain = getRelativeDateDomain(now, 0, "last_12_months", "field", "date");
|
||||
expect(getDateDomainDurationInDays(domain)).toBe(365);
|
||||
assertDateDomainEqual("field", "2021-05-01", "2022-04-30", domain);
|
||||
});
|
||||
|
||||
test("getRelativeDateDomain > simple date time", async function () {
|
||||
const now = DateTime.fromISO("2022-05-16T00:00:00+00:00", { zone: "utc" });
|
||||
const domain = getRelativeDateDomain(now, 0, "last_7_days", "field", "datetime");
|
||||
expect(getDateDomainDurationInDays(domain)).toBe(7);
|
||||
assertDateDomainEqual("field", "2022-05-10 00:00:00", "2022-05-16 23:59:59", domain);
|
||||
});
|
||||
|
||||
test("getRelativeDateDomain > date time from middle of day", async function () {
|
||||
const now = DateTime.fromISO("2022-05-16T13:59:00+00:00", { zone: "utc" });
|
||||
const domain = getRelativeDateDomain(now, 0, "last_7_days", "field", "datetime");
|
||||
expect(getDateDomainDurationInDays(domain)).toBe(7);
|
||||
assertDateDomainEqual("field", "2022-05-10 00:00:00", "2022-05-16 23:59:59", domain);
|
||||
});
|
||||
|
||||
test("getRelativeDateDomain > date time with timezone", async function () {
|
||||
const now = DateTime.fromISO("2022-05-16T12:00:00+02:00", { zone: "UTC+2" });
|
||||
const domain = getRelativeDateDomain(now, 0, "last_7_days", "field", "datetime");
|
||||
expect(getDateDomainDurationInDays(domain)).toBe(7);
|
||||
assertDateDomainEqual("field", "2022-05-09 22:00:00", "2022-05-16 21:59:59", domain);
|
||||
});
|
||||
|
||||
test("getRelativeDateDomain > date time with timezone on different day than UTC", async function () {
|
||||
const now = DateTime.fromISO("2022-05-16T01:00:00+02:00", { zone: "UTC+2" });
|
||||
const domain = getRelativeDateDomain(now, 0, "last_7_days", "field", "datetime");
|
||||
expect(getDateDomainDurationInDays(domain)).toBe(7);
|
||||
assertDateDomainEqual("field", "2022-05-09 22:00:00", "2022-05-16 21:59:59", domain);
|
||||
});
|
||||
|
||||
test("getRelativeDateDomain > with offset > year_to_date (year to date)", async function () {
|
||||
const now = DateTime.fromISO("2022-05-16");
|
||||
const domain = getRelativeDateDomain(now, -1, "year_to_date", "field", "date");
|
||||
assertDateDomainEqual("field", "2021-01-01", "2021-05-16", domain);
|
||||
});
|
||||
|
||||
test("getRelativeDateDomain > with offset > today (today)", async function () {
|
||||
const now = DateTime.fromISO("2022-05-16");
|
||||
const domain = getRelativeDateDomain(now, -1, "today", "field", "date");
|
||||
expect(getDateDomainDurationInDays(domain)).toBe(1);
|
||||
assertDateDomainEqual("field", "2022-05-15", "2022-05-15", domain);
|
||||
});
|
||||
|
||||
test("getRelativeDateDomain > with offset > yesterday (yesterday)", async function () {
|
||||
const now = DateTime.fromISO("2022-05-16");
|
||||
const domain = getRelativeDateDomain(now, -1, "yesterday", "field", "date");
|
||||
expect(getDateDomainDurationInDays(domain)).toBe(1);
|
||||
assertDateDomainEqual("field", "2022-05-14", "2022-05-14", domain);
|
||||
});
|
||||
|
||||
test("getRelativeDateDomain > with offset > last_7_days (last 7 days)", async function () {
|
||||
const now = DateTime.fromISO("2022-05-16");
|
||||
const domain = getRelativeDateDomain(now, -1, "last_7_days", "field", "date");
|
||||
expect(getDateDomainDurationInDays(domain)).toBe(7);
|
||||
assertDateDomainEqual("field", "2022-05-03", "2022-05-09", domain);
|
||||
});
|
||||
|
||||
test("getRelativeDateDomain > with offset (last 30 days)", async function () {
|
||||
const now = DateTime.fromISO("2022-05-16");
|
||||
const domain = getRelativeDateDomain(now, -2, "last_30_days", "field", "date");
|
||||
expect(getDateDomainDurationInDays(domain)).toBe(30);
|
||||
assertDateDomainEqual("field", "2022-02-16", "2022-03-17", domain);
|
||||
});
|
||||
|
||||
test("getRelativeDateDomain > with offset > month_to_date (Month to Date)", async function () {
|
||||
const now = DateTime.fromISO("2022-05-16");
|
||||
const domain = getRelativeDateDomain(now, -1, "month_to_date", "field", "date");
|
||||
expect(getDateDomainDurationInDays(domain)).toBe(16);
|
||||
assertDateDomainEqual("field", "2022-04-01", "2022-04-16", domain);
|
||||
});
|
||||
|
||||
test("getRelativeDateDomain > with offset > last_month (Last month)", async function () {
|
||||
const now = DateTime.fromISO("2022-05-16");
|
||||
const domain = getRelativeDateDomain(now, -1, "last_month", "field", "date");
|
||||
expect(getDateDomainDurationInDays(domain)).toBe(31);
|
||||
assertDateDomainEqual("field", "2022-03-01", "2022-03-31", domain);
|
||||
});
|
||||
|
||||
test("getRelativeDateDomain > with offset > last_12_months (last 12 months)", async function () {
|
||||
const now = DateTime.fromISO("2022-05-16");
|
||||
const domain = getRelativeDateDomain(now, 1, "last_12_months", "field", "date");
|
||||
expect(getDateDomainDurationInDays(domain)).toBe(365);
|
||||
assertDateDomainEqual("field", "2022-05-01", "2023-04-30", domain);
|
||||
});
|
||||
|
||||
test("getRelativeDateDomain > with offset > simple date time", async function () {
|
||||
const now = DateTime.fromISO("2022-05-16T00:00:00+00:00", { zone: "utc" });
|
||||
const domain = getRelativeDateDomain(now, -1, "last_7_days", "field", "datetime");
|
||||
expect(getDateDomainDurationInDays(domain)).toBe(7);
|
||||
assertDateDomainEqual("field", "2022-05-03 00:00:00", "2022-05-09 23:59:59", domain);
|
||||
});
|
||||
|
||||
test("dateFilterValueToString > relative periods", function () {
|
||||
expect(valueToString({ type: "relative", period: "today" })).toBe("Today");
|
||||
expect(valueToString({ type: "relative", period: "yesterday" })).toBe("Yesterday");
|
||||
expect(valueToString({ type: "relative", period: "last_7_days" })).toBe("Last 7 Days");
|
||||
expect(valueToString({ type: "relative", period: "last_30_days" })).toBe("Last 30 Days");
|
||||
expect(valueToString({ type: "relative", period: "last_90_days" })).toBe("Last 90 Days");
|
||||
expect(valueToString({ type: "relative", period: "month_to_date" })).toBe("Month to Date");
|
||||
expect(valueToString({ type: "relative", period: "last_month" })).toBe("Last Month");
|
||||
expect(valueToString({ type: "relative", period: "year_to_date" })).toBe("Year to Date");
|
||||
expect(valueToString({ type: "relative", period: "last_12_months" })).toBe("Last 12 Months");
|
||||
});
|
||||
test("dateFilterValueToString > month", function () {
|
||||
expect(valueToString({ type: "month", year: 2022, month: 5 })).toBe("May 2022");
|
||||
});
|
||||
|
||||
test("dateFilterValueToString > quarter", function () {
|
||||
expect(valueToString({ type: "quarter", year: 2022, quarter: 2 })).toBe("Q2 2022");
|
||||
});
|
||||
|
||||
test("dateFilterValueToString > year", function () {
|
||||
expect(valueToString({ type: "year", year: 2022 })).toBe("2022");
|
||||
});
|
||||
|
||||
test("dateFilterValueToString > range", function () {
|
||||
expect(valueToString({ type: "range", from: "2022-01-01", to: "2022-12-31" })).toBe(
|
||||
"January 1 – December 31, 2022"
|
||||
);
|
||||
expect(valueToString({ type: "range", from: "2022-01-01", to: "2022-01-01" })).toBe(
|
||||
"January 1, 2022"
|
||||
);
|
||||
expect(valueToString({ type: "range", from: "2022-01-01" })).toBe("Since January 1, 2022");
|
||||
expect(valueToString({ type: "range", to: "2022-12-31" })).toBe("Until December 31, 2022");
|
||||
expect(valueToString({ type: "range" })).toBe("All time");
|
||||
});
|
||||
|
||||
test("dateFilterValueToString > all time", function () {
|
||||
expect(valueToString({ type: undefined })).toBe("All time");
|
||||
expect(valueToString({})).toBe("All time");
|
||||
});
|
||||
|
||||
test("dateFilterValueToString > invalid value", function () {
|
||||
expect(valueToString({ type: "invalid" })).toBe("All time");
|
||||
expect(valueToString(undefined)).toBe("All time");
|
||||
});
|
||||
|
||||
describe("getNextDateFilterValue", () => {
|
||||
test("month: December rolls over to January next year", () => {
|
||||
expect(getNextDateFilterValue({ type: "month", year: 2022, month: 12 })).toEqual({
|
||||
type: "month",
|
||||
year: 2023,
|
||||
month: 1,
|
||||
});
|
||||
});
|
||||
test("month: increments month", () => {
|
||||
expect(getNextDateFilterValue({ type: "month", year: 2022, month: 5 })).toEqual({
|
||||
type: "month",
|
||||
year: 2022,
|
||||
month: 6,
|
||||
});
|
||||
});
|
||||
test("quarter: Q4 rolls over to Q1 next year", () => {
|
||||
expect(getNextDateFilterValue({ type: "quarter", year: 2022, quarter: 4 })).toEqual({
|
||||
type: "quarter",
|
||||
year: 2023,
|
||||
quarter: 1,
|
||||
});
|
||||
});
|
||||
test("quarter: increments quarter", () => {
|
||||
expect(getNextDateFilterValue({ type: "quarter", year: 2022, quarter: 2 })).toEqual({
|
||||
type: "quarter",
|
||||
year: 2022,
|
||||
quarter: 3,
|
||||
});
|
||||
});
|
||||
test("year: increments year", () => {
|
||||
expect(getNextDateFilterValue({ type: "year", year: 2022 })).toEqual({
|
||||
type: "year",
|
||||
year: 2023,
|
||||
});
|
||||
});
|
||||
|
||||
test("relative", () => {
|
||||
mockDate("2022-07-14 00:00:00");
|
||||
|
||||
let result = getNextDateFilterValue({ type: "relative", period: "last_7_days" });
|
||||
expect(result).toEqual({
|
||||
type: "range",
|
||||
from: "2022-07-15",
|
||||
to: "2022-07-21",
|
||||
});
|
||||
|
||||
result = getNextDateFilterValue({ type: "relative", period: "last_30_days" });
|
||||
expect(result).toEqual({
|
||||
type: "range",
|
||||
from: "2022-07-15",
|
||||
to: "2022-08-13",
|
||||
});
|
||||
|
||||
result = getNextDateFilterValue({ type: "relative", period: "last_90_days" });
|
||||
expect(result).toEqual({
|
||||
type: "range",
|
||||
from: "2022-07-15",
|
||||
to: "2022-10-12",
|
||||
});
|
||||
|
||||
result = getNextDateFilterValue({ type: "relative", period: "year_to_date" });
|
||||
expect(result).toEqual({
|
||||
type: "year",
|
||||
year: 2023,
|
||||
});
|
||||
|
||||
result = getNextDateFilterValue({ type: "relative", period: "last_12_months" });
|
||||
expect(result).toEqual({
|
||||
type: "range",
|
||||
from: "2022-07-01",
|
||||
to: "2023-06-30",
|
||||
});
|
||||
|
||||
result = getNextDateFilterValue({ type: "relative", period: "today" });
|
||||
expect(result).toEqual({
|
||||
type: "range",
|
||||
from: "2022-07-15",
|
||||
to: "2022-07-15",
|
||||
});
|
||||
|
||||
result = getNextDateFilterValue({ type: "relative", period: "yesterday" });
|
||||
expect(result).toEqual({
|
||||
type: "range",
|
||||
from: "2022-07-14",
|
||||
to: "2022-07-14",
|
||||
});
|
||||
|
||||
result = getNextDateFilterValue({ type: "relative", period: "last_month" });
|
||||
expect(result).toEqual({
|
||||
type: "month",
|
||||
year: 2022,
|
||||
month: 7,
|
||||
});
|
||||
|
||||
result = getNextDateFilterValue({ type: "relative", period: "month_to_date" });
|
||||
expect(result).toEqual({
|
||||
type: "month",
|
||||
year: 2022,
|
||||
month: 8,
|
||||
});
|
||||
});
|
||||
|
||||
test("range: shifts range forward", () => {
|
||||
expect(
|
||||
getNextDateFilterValue({ type: "range", from: "2022-01-01", to: "2022-01-10" })
|
||||
).toEqual({ type: "range", from: "2022-01-11", to: "2022-01-20" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("getPreviousDateFilterValue", () => {
|
||||
test("month: January rolls back to December previous year", () => {
|
||||
expect(getPreviousDateFilterValue({ type: "month", year: 2022, month: 1 })).toEqual({
|
||||
type: "month",
|
||||
year: 2021,
|
||||
month: 12,
|
||||
});
|
||||
});
|
||||
test("month: decrements month", () => {
|
||||
expect(getPreviousDateFilterValue({ type: "month", year: 2022, month: 6 })).toEqual({
|
||||
type: "month",
|
||||
year: 2022,
|
||||
month: 5,
|
||||
});
|
||||
});
|
||||
test("quarter: Q1 rolls back to Q4 previous year", () => {
|
||||
expect(getPreviousDateFilterValue({ type: "quarter", year: 2022, quarter: 1 })).toEqual({
|
||||
type: "quarter",
|
||||
year: 2021,
|
||||
quarter: 4,
|
||||
});
|
||||
});
|
||||
test("quarter: decrements quarter", () => {
|
||||
expect(getPreviousDateFilterValue({ type: "quarter", year: 2022, quarter: 3 })).toEqual({
|
||||
type: "quarter",
|
||||
year: 2022,
|
||||
quarter: 2,
|
||||
});
|
||||
});
|
||||
test("year: decrements year", () => {
|
||||
expect(getPreviousDateFilterValue({ type: "year", year: 2022 })).toEqual({
|
||||
type: "year",
|
||||
year: 2021,
|
||||
});
|
||||
});
|
||||
|
||||
test("relative", () => {
|
||||
mockDate("2022-07-14 00:00:00");
|
||||
|
||||
let result = getPreviousDateFilterValue({ type: "relative", period: "last_7_days" });
|
||||
expect(result).toEqual({
|
||||
type: "range",
|
||||
from: "2022-07-01",
|
||||
to: "2022-07-07",
|
||||
});
|
||||
|
||||
result = getPreviousDateFilterValue({ type: "relative", period: "last_30_days" });
|
||||
expect(result).toEqual({
|
||||
type: "range",
|
||||
from: "2022-05-16",
|
||||
to: "2022-06-14",
|
||||
});
|
||||
|
||||
result = getPreviousDateFilterValue({ type: "relative", period: "last_90_days" });
|
||||
expect(result).toEqual({
|
||||
type: "range",
|
||||
|
||||
from: "2022-01-16",
|
||||
to: "2022-04-15",
|
||||
});
|
||||
|
||||
result = getPreviousDateFilterValue({ type: "relative", period: "year_to_date" });
|
||||
expect(result).toEqual({
|
||||
type: "year",
|
||||
year: 2021,
|
||||
});
|
||||
|
||||
result = getPreviousDateFilterValue({ type: "relative", period: "last_12_months" });
|
||||
expect(result).toEqual({
|
||||
type: "range",
|
||||
from: "2020-07-01",
|
||||
to: "2021-06-30",
|
||||
});
|
||||
|
||||
result = getPreviousDateFilterValue({ type: "relative", period: "today" });
|
||||
expect(result).toEqual({
|
||||
type: "range",
|
||||
from: "2022-07-13",
|
||||
to: "2022-07-13",
|
||||
});
|
||||
|
||||
result = getPreviousDateFilterValue({ type: "relative", period: "yesterday" });
|
||||
expect(result).toEqual({
|
||||
type: "range",
|
||||
from: "2022-07-12",
|
||||
to: "2022-07-12",
|
||||
});
|
||||
|
||||
result = getPreviousDateFilterValue({ type: "relative", period: "last_month" });
|
||||
expect(result).toEqual({
|
||||
type: "month",
|
||||
year: 2022,
|
||||
month: 5,
|
||||
});
|
||||
|
||||
result = getPreviousDateFilterValue({ type: "relative", period: "month_to_date" });
|
||||
expect(result).toEqual({
|
||||
type: "month",
|
||||
year: 2022,
|
||||
month: 6,
|
||||
});
|
||||
});
|
||||
|
||||
test("range: shifts range backward", () => {
|
||||
expect(
|
||||
getPreviousDateFilterValue({ type: "range", from: "2022-01-11", to: "2022-01-20" })
|
||||
).toEqual({ type: "range", from: "2022-01-01", to: "2022-01-10" });
|
||||
});
|
||||
});
|
||||
|
||||
test("getFacetInfo for boolean values", async () => {
|
||||
const filter = {
|
||||
type: "boolean",
|
||||
label: "Boolean Filter",
|
||||
id: "1",
|
||||
};
|
||||
const env = {};
|
||||
expect(await getFacetInfo(env, filter, { operator: "set" })).toEqual({
|
||||
title: "Boolean Filter",
|
||||
id: "1",
|
||||
separator: "or",
|
||||
operator: "",
|
||||
values: [LAZY_TRANSLATED_SET],
|
||||
});
|
||||
expect(await getFacetInfo(env, filter, { operator: "not set" })).toEqual({
|
||||
title: "Boolean Filter",
|
||||
id: "1",
|
||||
separator: "or",
|
||||
operator: "",
|
||||
values: [LAZY_TRANSLATED_NOT_SET],
|
||||
});
|
||||
});
|
||||
|
||||
test("getFacetInfo for text values", async () => {
|
||||
const filter = {
|
||||
type: "text",
|
||||
label: "Text Filter",
|
||||
id: "1",
|
||||
};
|
||||
const env = {};
|
||||
expect(await getFacetInfo(env, filter, { operator: "ilike", strings: ["hello"] })).toEqual({
|
||||
title: "Text Filter",
|
||||
id: "1",
|
||||
operator: LAZY_TRANSLATED_CONTAINS,
|
||||
separator: "or",
|
||||
values: ["hello"],
|
||||
});
|
||||
});
|
||||
|
||||
test("getFacetInfo for date values", async () => {
|
||||
const filter = {
|
||||
type: "date",
|
||||
label: "Date Filter",
|
||||
id: "1",
|
||||
};
|
||||
const env = {};
|
||||
for (const [period, label] of Object.entries(RELATIVE_PERIODS)) {
|
||||
expect(await getFacetInfo(env, filter, { type: "relative", period })).toEqual({
|
||||
title: "Date Filter",
|
||||
id: "1",
|
||||
separator: "or",
|
||||
operator: "",
|
||||
values: [label],
|
||||
});
|
||||
}
|
||||
expect(
|
||||
await getFacetInfo(env, filter, { type: "range", from: "2022-01-01", to: "2022-12-31" })
|
||||
).toEqual({
|
||||
title: "Date Filter",
|
||||
id: "1",
|
||||
separator: "or",
|
||||
operator: "",
|
||||
values: ["January 1 – December 31, 2022"],
|
||||
});
|
||||
expect(await getFacetInfo(env, filter, { type: "range", from: "2022-01-01" })).toEqual({
|
||||
title: "Date Filter",
|
||||
id: "1",
|
||||
separator: "or",
|
||||
operator: "",
|
||||
values: ["Since January 1, 2022"],
|
||||
});
|
||||
expect(await getFacetInfo(env, filter, { type: "range", to: "2022-12-31" })).toEqual({
|
||||
title: "Date Filter",
|
||||
id: "1",
|
||||
separator: "or",
|
||||
operator: "",
|
||||
values: ["Until December 31, 2022"],
|
||||
});
|
||||
expect(await getFacetInfo(env, filter, { type: "month", month: 1, year: 2022 })).toEqual({
|
||||
title: "Date Filter",
|
||||
id: "1",
|
||||
separator: "or",
|
||||
operator: "",
|
||||
values: ["January 2022"],
|
||||
});
|
||||
expect(await getFacetInfo(env, filter, { type: "quarter", quarter: 1, year: 2022 })).toEqual({
|
||||
title: "Date Filter",
|
||||
id: "1",
|
||||
separator: "or",
|
||||
operator: "",
|
||||
values: ["Q1 2022"],
|
||||
});
|
||||
expect(await getFacetInfo(env, filter, { type: "year", year: 2022 })).toEqual({
|
||||
title: "Date Filter",
|
||||
id: "1",
|
||||
separator: "or",
|
||||
operator: "",
|
||||
values: ["2022"],
|
||||
});
|
||||
});
|
||||
|
||||
test("getFacetInfo for relation values", async () => {
|
||||
const filter = {
|
||||
type: "relation",
|
||||
label: "Relation Filter",
|
||||
id: "1",
|
||||
};
|
||||
const nameService = {
|
||||
loadDisplayNames: (resModel, ids) => ids.map((id) => `Name ${id}`),
|
||||
};
|
||||
const env = await makeMockEnv({
|
||||
services: {
|
||||
name: nameService,
|
||||
},
|
||||
});
|
||||
expect(await getFacetInfo(env, filter, { operator: "in", ids: [1] })).toEqual({
|
||||
title: "Relation Filter",
|
||||
id: "1",
|
||||
separator: "or",
|
||||
operator: "",
|
||||
values: ["Name 1"],
|
||||
});
|
||||
expect(await getFacetInfo(env, filter, { operator: "in", ids: [1, 2] })).toEqual({
|
||||
title: "Relation Filter",
|
||||
id: "1",
|
||||
separator: "or",
|
||||
operator: "",
|
||||
values: ["Name 1", "Name 2"],
|
||||
});
|
||||
});
|
||||
|
||||
test("getFacetInfo for selection values", async () => {
|
||||
defineSpreadsheetModels();
|
||||
const filter = {
|
||||
id: "42",
|
||||
type: "selection",
|
||||
label: "Selection Filter",
|
||||
resModel: "res.currency",
|
||||
selectionField: "position",
|
||||
};
|
||||
const env = await makeMockEnv();
|
||||
expect(await getFacetInfo(env, filter, { operator: "in", selectionValues: ["after"] })).toEqual(
|
||||
{
|
||||
title: "Selection Filter",
|
||||
id: "42",
|
||||
separator: "or",
|
||||
operator: "",
|
||||
values: ["A"],
|
||||
}
|
||||
);
|
||||
expect(
|
||||
await getFacetInfo(env, filter, { operator: "in", selectionValues: ["after", "before"] })
|
||||
).toEqual({
|
||||
title: "Selection Filter",
|
||||
id: "42",
|
||||
separator: "or",
|
||||
operator: "",
|
||||
values: ["A", "B"],
|
||||
});
|
||||
});
|
||||
|
|
@ -1,160 +0,0 @@
|
|||
/** @odoo-module */
|
||||
import { getRelativeDateDomain } from "@spreadsheet/global_filters/helpers";
|
||||
import {
|
||||
getDateDomainDurationInDays,
|
||||
assertDateDomainEqual,
|
||||
} from "@spreadsheet/../tests/utils/date_domain";
|
||||
|
||||
const { DateTime } = luxon;
|
||||
|
||||
QUnit.module("spreadsheet > Global filters helpers", {}, () => {
|
||||
QUnit.test("getRelativeDateDomain > last_week (last 7 days)", async function (assert) {
|
||||
const now = DateTime.fromISO("2022-05-16");
|
||||
const domain = getRelativeDateDomain(now, 0, "last_week", "field", "date");
|
||||
assert.equal(getDateDomainDurationInDays(domain), 7);
|
||||
assertDateDomainEqual(assert, "field", "2022-05-09", "2022-05-15", domain);
|
||||
});
|
||||
|
||||
QUnit.test("getRelativeDateDomain > last_month (last 30 days)", async function (assert) {
|
||||
const now = DateTime.fromISO("2022-05-16");
|
||||
const domain = getRelativeDateDomain(now, 0, "last_month", "field", "date");
|
||||
assert.equal(getDateDomainDurationInDays(domain), 30);
|
||||
assertDateDomainEqual(assert, "field", "2022-04-16", "2022-05-15", domain);
|
||||
});
|
||||
|
||||
QUnit.test("getRelativeDateDomain > last_three_months (last 90 days)", async function (assert) {
|
||||
const now = DateTime.fromISO("2022-05-16");
|
||||
const domain = getRelativeDateDomain(now, 0, "last_three_months", "field", "date");
|
||||
assert.equal(getDateDomainDurationInDays(domain), 90);
|
||||
assertDateDomainEqual(assert, "field", "2022-02-15", "2022-05-15", domain);
|
||||
});
|
||||
|
||||
QUnit.test("getRelativeDateDomain > last_six_months (last 180 days)", async function (assert) {
|
||||
const now = DateTime.fromISO("2022-05-16");
|
||||
const domain = getRelativeDateDomain(now, 0, "last_six_months", "field", "date");
|
||||
assert.equal(getDateDomainDurationInDays(domain), 180);
|
||||
assertDateDomainEqual(assert, "field", "2021-11-17", "2022-05-15", domain);
|
||||
});
|
||||
|
||||
QUnit.test("getRelativeDateDomain > last_year (last 365 days)", async function (assert) {
|
||||
const now = DateTime.fromISO("2022-05-16");
|
||||
const domain = getRelativeDateDomain(now, 0, "last_year", "field", "date");
|
||||
assert.equal(getDateDomainDurationInDays(domain), 365);
|
||||
assertDateDomainEqual(assert, "field", "2021-05-16", "2022-05-15", domain);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"getRelativeDateDomain > last_three_years (last 3 * 365 days)",
|
||||
async function (assert) {
|
||||
const now = DateTime.fromISO("2022-05-16");
|
||||
const domain = getRelativeDateDomain(now, 0, "last_three_years", "field", "date");
|
||||
assert.equal(getDateDomainDurationInDays(domain), 3 * 365);
|
||||
assertDateDomainEqual(assert, "field", "2019-05-17", "2022-05-15", domain);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("getRelativeDateDomain > simple date time", async function (assert) {
|
||||
const now = DateTime.fromISO("2022-05-16T00:00:00+00:00", { zone: "utc" });
|
||||
const domain = getRelativeDateDomain(now, 0, "last_week", "field", "datetime");
|
||||
assert.equal(getDateDomainDurationInDays(domain), 7);
|
||||
assertDateDomainEqual(
|
||||
assert,
|
||||
"field",
|
||||
"2022-05-09 00:00:00",
|
||||
"2022-05-15 23:59:59",
|
||||
domain
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("getRelativeDateDomain > date time from middle of day", async function (assert) {
|
||||
const now = DateTime.fromISO("2022-05-16T13:59:00+00:00", { zone: "utc" });
|
||||
const domain = getRelativeDateDomain(now, 0, "last_week", "field", "datetime");
|
||||
assert.equal(getDateDomainDurationInDays(domain), 7);
|
||||
assertDateDomainEqual(
|
||||
assert,
|
||||
"field",
|
||||
"2022-05-09 00:00:00",
|
||||
"2022-05-15 23:59:59",
|
||||
domain
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("getRelativeDateDomain > date time with timezone", async function (assert) {
|
||||
const now = DateTime.fromISO("2022-05-16T12:00:00+02:00", { zone: "UTC+2" });
|
||||
const domain = getRelativeDateDomain(now, 0, "last_week", "field", "datetime");
|
||||
assert.equal(getDateDomainDurationInDays(domain), 7);
|
||||
assertDateDomainEqual(
|
||||
assert,
|
||||
"field",
|
||||
"2022-05-08 22:00:00",
|
||||
"2022-05-15 21:59:59",
|
||||
domain
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"getRelativeDateDomain > date time with timezone on different day than UTC",
|
||||
async function (assert) {
|
||||
const now = DateTime.fromISO("2022-05-16T01:00:00+02:00", { zone: "UTC+2" });
|
||||
const domain = getRelativeDateDomain(now, 0, "last_week", "field", "datetime");
|
||||
assert.equal(getDateDomainDurationInDays(domain), 7);
|
||||
assertDateDomainEqual(
|
||||
assert,
|
||||
"field",
|
||||
"2022-05-08 22:00:00",
|
||||
"2022-05-15 21:59:59",
|
||||
domain
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"getRelativeDateDomain > with offset > last_week (last 7 days)",
|
||||
async function (assert) {
|
||||
const now = DateTime.fromISO("2022-05-16");
|
||||
const domain = getRelativeDateDomain(now, -1, "last_week", "field", "date");
|
||||
assert.equal(getDateDomainDurationInDays(domain), 7);
|
||||
assertDateDomainEqual(assert, "field", "2022-05-02", "2022-05-08", domain);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("getRelativeDateDomain > with offset (last 30 days)", async function (assert) {
|
||||
const now = DateTime.fromISO("2022-05-16");
|
||||
const domain = getRelativeDateDomain(now, -2, "last_month", "field", "date");
|
||||
assert.equal(getDateDomainDurationInDays(domain), 30);
|
||||
assertDateDomainEqual(assert, "field", "2022-02-15", "2022-03-16", domain);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"getRelativeDateDomain > with offset > last_year (last 365 days)",
|
||||
async function (assert) {
|
||||
const now = DateTime.fromISO("2022-05-16");
|
||||
const domain = getRelativeDateDomain(now, 1, "last_year", "field", "date");
|
||||
assert.equal(getDateDomainDurationInDays(domain), 365);
|
||||
assertDateDomainEqual(assert, "field", "2022-05-16", "2023-05-15", domain);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"getRelativeDateDomain > with offset > last_three_years (last 3 * 365 days)",
|
||||
async function (assert) {
|
||||
const now = DateTime.fromISO("2022-05-16");
|
||||
const domain = getRelativeDateDomain(now, -1, "last_three_years", "field", "date");
|
||||
assert.equal(getDateDomainDurationInDays(domain), 3 * 365);
|
||||
assertDateDomainEqual(assert, "field", "2016-05-17", "2019-05-16", domain);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("getRelativeDateDomain > with offset > simple date time", async function (assert) {
|
||||
const now = DateTime.fromISO("2022-05-16T00:00:00+00:00", { zone: "utc" });
|
||||
const domain = getRelativeDateDomain(now, -1, "last_week", "field", "datetime");
|
||||
assert.equal(getDateDomainDurationInDays(domain), 7);
|
||||
assertDateDomainEqual(
|
||||
assert,
|
||||
"field",
|
||||
"2022-05-02 00:00:00",
|
||||
"2022-05-08 23:59:59",
|
||||
domain
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
/** @ts-check */
|
||||
import { mockDate } from "@odoo/hoot-mock";
|
||||
import { defineSpreadsheetModels } from "@spreadsheet/../tests/helpers/data";
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
|
||||
import { createSpreadsheetWithChart } from "@spreadsheet/../tests/helpers/chart";
|
||||
import { addGlobalFilter, setGlobalFilterValue } from "@spreadsheet/../tests/helpers/commands";
|
||||
import { globalFieldMatchingRegistry } from "@spreadsheet/global_filters/helpers";
|
||||
import { THIS_YEAR_GLOBAL_FILTER } from "../helpers/global_filter";
|
||||
|
||||
const { DateTime } = luxon;
|
||||
|
||||
describe.current.tags("headless");
|
||||
defineSpreadsheetModels();
|
||||
|
||||
/**
|
||||
* @typedef {import("@spreadsheet").DateGlobalFilter} DateGlobalFilter
|
||||
*/
|
||||
|
||||
async function addChartGlobalFilter(model) {
|
||||
const chartId = model.getters.getChartIds(model.getters.getActiveSheetId())[0];
|
||||
await addGlobalFilter(model, THIS_YEAR_GLOBAL_FILTER, {
|
||||
chart: { [chartId]: { chain: "date", type: "date" } },
|
||||
});
|
||||
}
|
||||
|
||||
test("Can add a chart global filter", async function () {
|
||||
const { model } = await createSpreadsheetWithChart();
|
||||
expect(model.getters.getGlobalFilters().length).toBe(0);
|
||||
await addChartGlobalFilter(model);
|
||||
expect(model.getters.getGlobalFilters().length).toBe(1);
|
||||
const chartId = model.getters.getChartIds(model.getters.getActiveSheetId())[0];
|
||||
const computedDomain = model.getters.getChartDataSource(chartId).getComputedDomain();
|
||||
expect(computedDomain.length).toBe(3);
|
||||
expect(computedDomain[0]).toBe("&");
|
||||
});
|
||||
|
||||
test("Chart is loaded with computed domain", async function () {
|
||||
const { model } = await createSpreadsheetWithChart({
|
||||
mockRPC: function (route, { model, method, kwargs }) {
|
||||
if (model === "partner" && method === "formatted_read_group") {
|
||||
expect(kwargs.domain.length).toBe(3);
|
||||
expect(kwargs.domain[0]).toBe("&");
|
||||
expect(kwargs.domain[1][0]).toBe("date");
|
||||
}
|
||||
},
|
||||
});
|
||||
await addChartGlobalFilter(model);
|
||||
});
|
||||
|
||||
test("Chart is impacted by global filter in dashboard mode", async function () {
|
||||
const { model } = await createSpreadsheetWithChart();
|
||||
expect(model.getters.getGlobalFilters().length).toBe(0);
|
||||
const chartId = model.getters.getChartIds(model.getters.getActiveSheetId())[0];
|
||||
|
||||
/** @type {DateGlobalFilter}*/
|
||||
const filter = {
|
||||
id: "42",
|
||||
type: "date",
|
||||
label: "Last Year",
|
||||
};
|
||||
await addGlobalFilter(model, filter, {
|
||||
chart: { [chartId]: { chain: "date", type: "date" } },
|
||||
});
|
||||
model.updateMode("dashboard");
|
||||
let computedDomain = model.getters.getChartDataSource(chartId).getComputedDomain();
|
||||
expect(computedDomain).toEqual([]);
|
||||
await setGlobalFilterValue(model, {
|
||||
id: "42",
|
||||
value: { type: "year", year: DateTime.local().year - 1 },
|
||||
});
|
||||
computedDomain = model.getters.getChartDataSource(chartId).getComputedDomain();
|
||||
expect(computedDomain.length).toBe(3);
|
||||
expect(computedDomain[0]).toBe("&");
|
||||
});
|
||||
|
||||
test("field matching is removed when chart is deleted", async function () {
|
||||
const { model } = await createSpreadsheetWithChart();
|
||||
await addChartGlobalFilter(model);
|
||||
const [filter] = model.getters.getGlobalFilters();
|
||||
const [chartId] = model.getters.getChartIds(model.getters.getActiveSheetId());
|
||||
const matching = {
|
||||
chain: "date",
|
||||
type: "date",
|
||||
};
|
||||
expect(model.getters.getChartFieldMatch(chartId)[filter.id]).toEqual(matching);
|
||||
model.dispatch("DELETE_FIGURE", {
|
||||
sheetId: model.getters.getActiveSheetId(),
|
||||
figureId: model.getters.getFigureIdFromChartId(chartId),
|
||||
});
|
||||
expect(globalFieldMatchingRegistry.get("chart").getIds(model.getters)).toEqual([], {
|
||||
message: "it should have removed the chart and its fieldMatching and datasource altogether",
|
||||
});
|
||||
model.dispatch("REQUEST_UNDO");
|
||||
expect(model.getters.getChartFieldMatch(chartId)[filter.id]).toEqual(matching);
|
||||
model.dispatch("REQUEST_REDO");
|
||||
expect(globalFieldMatchingRegistry.get("chart").getIds(model.getters)).toEqual([]);
|
||||
});
|
||||
|
||||
test("field matching is removed when filter is deleted", async function () {
|
||||
mockDate("2022-07-10 00:00:00");
|
||||
const { model } = await createSpreadsheetWithChart();
|
||||
await addChartGlobalFilter(model);
|
||||
const [filter] = model.getters.getGlobalFilters();
|
||||
const [chartId] = model.getters.getChartIds(model.getters.getActiveSheetId());
|
||||
const matching = {
|
||||
chain: "date",
|
||||
type: "date",
|
||||
};
|
||||
expect(model.getters.getChartFieldMatch(chartId)[filter.id]).toEqual(matching);
|
||||
expect(model.getters.getChartDataSource(chartId).getComputedDomain()).toEqual([
|
||||
"&",
|
||||
["date", ">=", "2022-01-01"],
|
||||
["date", "<=", "2022-12-31"],
|
||||
]);
|
||||
model.dispatch("REMOVE_GLOBAL_FILTER", {
|
||||
id: filter.id,
|
||||
});
|
||||
expect(model.getters.getChartFieldMatch(chartId)[filter.id]).toBe(undefined, {
|
||||
message: "it should have removed the chart and its fieldMatching and datasource altogether",
|
||||
});
|
||||
expect(model.getters.getChartDataSource(chartId).getComputedDomain()).toEqual([]);
|
||||
model.dispatch("REQUEST_UNDO");
|
||||
expect(model.getters.getChartFieldMatch(chartId)[filter.id]).toEqual(matching);
|
||||
expect(model.getters.getChartDataSource(chartId).getComputedDomain()).toEqual([
|
||||
"&",
|
||||
["date", ">=", "2022-01-01"],
|
||||
["date", "<=", "2022-12-31"],
|
||||
]);
|
||||
model.dispatch("REQUEST_REDO");
|
||||
expect(model.getters.getChartFieldMatch(chartId)[filter.id]).toBe(undefined);
|
||||
expect(model.getters.getChartDataSource(chartId).getComputedDomain()).toEqual([]);
|
||||
});
|
||||
|
|
@ -1,137 +0,0 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { globalFiltersFieldMatchers } from "../../src/global_filters/plugins/global_filters_core_plugin";
|
||||
import { createSpreadsheetWithChart } from "../utils/chart";
|
||||
import { addGlobalFilter, setGlobalFilterValue } from "../utils/commands";
|
||||
import { patchDate } from "@web/../tests/helpers/utils";
|
||||
|
||||
async function addChartGlobalFilter(model) {
|
||||
const chartId = model.getters.getChartIds(model.getters.getActiveSheetId())[0];
|
||||
const filter = {
|
||||
id: "42",
|
||||
type: "date",
|
||||
label: "Last Year",
|
||||
rangeType: "year",
|
||||
defaultValue: { yearOffset: -1 },
|
||||
};
|
||||
await addGlobalFilter(
|
||||
model,
|
||||
{ filter },
|
||||
{ chart: { [chartId]: { chain: "date", type: "date" } } }
|
||||
);
|
||||
}
|
||||
|
||||
QUnit.module("spreadsheet > Global filters chart", {}, () => {
|
||||
QUnit.test("Can add a chart global filter", async function (assert) {
|
||||
const { model } = await createSpreadsheetWithChart();
|
||||
assert.equal(model.getters.getGlobalFilters().length, 0);
|
||||
await addChartGlobalFilter(model);
|
||||
assert.equal(model.getters.getGlobalFilters().length, 1);
|
||||
const chartId = model.getters.getChartIds(model.getters.getActiveSheetId())[0];
|
||||
const computedDomain = model.getters.getChartDataSource(chartId).getComputedDomain();
|
||||
assert.equal(computedDomain.length, 3);
|
||||
assert.equal(computedDomain[0], "&");
|
||||
});
|
||||
|
||||
QUnit.test("Chart is loaded with computed domain", async function (assert) {
|
||||
const { model } = await createSpreadsheetWithChart({
|
||||
mockRPC: function (route, { model, method, kwargs }) {
|
||||
if (model === "partner" && method === "web_read_group") {
|
||||
assert.strictEqual(kwargs.domain.length, 3);
|
||||
assert.strictEqual(kwargs.domain[0], "&");
|
||||
assert.strictEqual(kwargs.domain[1][0], "date");
|
||||
}
|
||||
},
|
||||
});
|
||||
await addChartGlobalFilter(model);
|
||||
});
|
||||
|
||||
QUnit.test("Chart is impacted by global filter in dashboard mode", async function (assert) {
|
||||
const { model } = await createSpreadsheetWithChart();
|
||||
assert.equal(model.getters.getGlobalFilters().length, 0);
|
||||
const chartId = model.getters.getChartIds(model.getters.getActiveSheetId())[0];
|
||||
const filter = {
|
||||
id: "42",
|
||||
type: "date",
|
||||
label: "Last Year",
|
||||
rangeType: "year",
|
||||
};
|
||||
await addGlobalFilter(
|
||||
model,
|
||||
{ filter },
|
||||
{ chart: { [chartId]: { chain: "date", type: "date" } } }
|
||||
);
|
||||
model.updateMode("dashboard");
|
||||
let computedDomain = model.getters.getChartDataSource(chartId).getComputedDomain();
|
||||
assert.deepEqual(computedDomain, []);
|
||||
await setGlobalFilterValue(model, {
|
||||
id: "42",
|
||||
value: { yearOffset: -1 },
|
||||
});
|
||||
computedDomain = model.getters.getChartDataSource(chartId).getComputedDomain();
|
||||
assert.equal(computedDomain.length, 3);
|
||||
assert.equal(computedDomain[0], "&");
|
||||
});
|
||||
|
||||
QUnit.test("field matching is removed when chart is deleted", async function (assert) {
|
||||
const { model } = await createSpreadsheetWithChart();
|
||||
await addChartGlobalFilter(model);
|
||||
const [filter] = model.getters.getGlobalFilters();
|
||||
const [chartId] = model.getters.getChartIds(model.getters.getActiveSheetId());
|
||||
const matching = {
|
||||
chain: "date",
|
||||
type: "date",
|
||||
};
|
||||
assert.deepEqual(model.getters.getChartFieldMatch(chartId)[filter.id], matching);
|
||||
model.dispatch("DELETE_FIGURE", {
|
||||
sheetId: model.getters.getActiveSheetId(),
|
||||
id: chartId,
|
||||
});
|
||||
assert.deepEqual(
|
||||
globalFiltersFieldMatchers["chart"].geIds(),
|
||||
[],
|
||||
"it should have removed the chart and its fieldMatching and datasource altogether"
|
||||
);
|
||||
model.dispatch("REQUEST_UNDO");
|
||||
assert.deepEqual(model.getters.getChartFieldMatch(chartId)[filter.id], matching);
|
||||
model.dispatch("REQUEST_REDO");
|
||||
assert.deepEqual(globalFiltersFieldMatchers["chart"].geIds(), []);
|
||||
});
|
||||
|
||||
QUnit.test("field matching is removed when filter is deleted", async function (assert) {
|
||||
patchDate(2022, 6, 10, 0, 0, 0);
|
||||
const { model } = await createSpreadsheetWithChart();
|
||||
await addChartGlobalFilter(model);
|
||||
const [filter] = model.getters.getGlobalFilters();
|
||||
const [chartId] = model.getters.getChartIds(model.getters.getActiveSheetId());
|
||||
const matching = {
|
||||
chain: "date",
|
||||
type: "date",
|
||||
};
|
||||
assert.deepEqual(model.getters.getChartFieldMatch(chartId)[filter.id], matching);
|
||||
assert.deepEqual(model.getters.getChartDataSource(chartId).getComputedDomain(), [
|
||||
"&",
|
||||
["date", ">=", "2021-01-01"],
|
||||
["date", "<=", "2021-12-31"],
|
||||
]);
|
||||
model.dispatch("REMOVE_GLOBAL_FILTER", {
|
||||
id: filter.id,
|
||||
});
|
||||
assert.deepEqual(
|
||||
model.getters.getChartFieldMatch(chartId)[filter.id],
|
||||
undefined,
|
||||
"it should have removed the chart and its fieldMatching and datasource altogether"
|
||||
);
|
||||
assert.deepEqual(model.getters.getChartDataSource(chartId).getComputedDomain(), []);
|
||||
model.dispatch("REQUEST_UNDO");
|
||||
assert.deepEqual(model.getters.getChartFieldMatch(chartId)[filter.id], matching);
|
||||
assert.deepEqual(model.getters.getChartDataSource(chartId).getComputedDomain(), [
|
||||
"&",
|
||||
["date", ">=", "2021-01-01"],
|
||||
["date", "<=", "2021-12-31"],
|
||||
]);
|
||||
model.dispatch("REQUEST_REDO");
|
||||
assert.deepEqual(model.getters.getChartFieldMatch(chartId)[filter.id], undefined);
|
||||
assert.deepEqual(model.getters.getChartDataSource(chartId).getComputedDomain(), []);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,324 @@
|
|||
/** @ts-check */
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
|
||||
import { Model } from "@odoo/o-spreadsheet";
|
||||
import { defineSpreadsheetModels } from "@spreadsheet/../tests/helpers/data";
|
||||
import {
|
||||
addGlobalFilterWithoutReload,
|
||||
setGlobalFilterValueWithoutReload,
|
||||
} from "@spreadsheet/../tests/helpers/commands";
|
||||
import { RELATIVE_PERIODS } from "@spreadsheet/global_filters/helpers";
|
||||
|
||||
describe.current.tags("headless");
|
||||
defineSpreadsheetModels();
|
||||
|
||||
test("Value of text filter", () => {
|
||||
const model = new Model();
|
||||
addGlobalFilterWithoutReload(model, {
|
||||
id: "1",
|
||||
type: "text",
|
||||
label: "Text Filter",
|
||||
});
|
||||
|
||||
let result = setGlobalFilterValueWithoutReload(model, {
|
||||
id: "1",
|
||||
value: { operator: "ilike", strings: "test" },
|
||||
});
|
||||
expect(result.isSuccessful).toBe(false);
|
||||
expect(result.reasons).toEqual(["InvalidValueTypeCombination"]);
|
||||
|
||||
result = addGlobalFilterWithoutReload(model, {
|
||||
id: "2",
|
||||
type: "text",
|
||||
label: "Default value is an array",
|
||||
defaultValue: { operator: "ilike", strings: ["default value"] },
|
||||
});
|
||||
expect(result.isSuccessful).toBe(true);
|
||||
|
||||
result = setGlobalFilterValueWithoutReload(model, {
|
||||
id: "1",
|
||||
value: { operator: "ilike", strings: 5 },
|
||||
});
|
||||
expect(result.isSuccessful).toBe(false);
|
||||
expect(result.reasons).toEqual(["InvalidValueTypeCombination"]);
|
||||
|
||||
result = setGlobalFilterValueWithoutReload(model, {
|
||||
id: "1",
|
||||
value: { operator: "ilike", strings: false },
|
||||
});
|
||||
expect(result.isSuccessful).toBe(false);
|
||||
expect(result.reasons).toEqual(["InvalidValueTypeCombination"]);
|
||||
|
||||
result = setGlobalFilterValueWithoutReload(model, {
|
||||
id: "1",
|
||||
value: { operator: "ilike", strings: [] },
|
||||
});
|
||||
expect(result.isSuccessful).toBe(false);
|
||||
expect(result.reasons).toEqual(["InvalidValueTypeCombination"]);
|
||||
});
|
||||
|
||||
test("Value of selection filter", () => {
|
||||
const model = new Model();
|
||||
addGlobalFilterWithoutReload(model, {
|
||||
id: "1",
|
||||
type: "selection",
|
||||
label: "selection Filter",
|
||||
resModel: "res.currency",
|
||||
selectionField: "position",
|
||||
});
|
||||
|
||||
let result = setGlobalFilterValueWithoutReload(model, {
|
||||
id: "1",
|
||||
value: { operator: "in", selectionValues: "test" },
|
||||
});
|
||||
expect(result.isSuccessful).toBe(false);
|
||||
expect(result.reasons).toEqual(["InvalidValueTypeCombination"]);
|
||||
|
||||
result = addGlobalFilterWithoutReload(model, {
|
||||
id: "2",
|
||||
type: "selection",
|
||||
label: "Default value is an array",
|
||||
resModel: "res.currency",
|
||||
selectionField: "position",
|
||||
defaultValue: { operator: "in", selectionValues: ["default value"] },
|
||||
});
|
||||
expect(result.isSuccessful).toBe(true);
|
||||
|
||||
result = setGlobalFilterValueWithoutReload(model, {
|
||||
id: "1",
|
||||
value: { operator: "in", selectionValues: 5 },
|
||||
});
|
||||
expect(result.isSuccessful).toBe(false);
|
||||
expect(result.reasons).toEqual(["InvalidValueTypeCombination"]);
|
||||
|
||||
result = setGlobalFilterValueWithoutReload(model, {
|
||||
id: "1",
|
||||
value: { operator: "in", selectionValues: false },
|
||||
});
|
||||
expect(result.isSuccessful).toBe(false);
|
||||
expect(result.reasons).toEqual(["InvalidValueTypeCombination"]);
|
||||
});
|
||||
|
||||
test("Value of numeric filter", () => {
|
||||
const model = new Model();
|
||||
addGlobalFilterWithoutReload(model, {
|
||||
id: "1",
|
||||
type: "numeric",
|
||||
label: "Numeric Filter",
|
||||
defaultValue: { operator: "=", targetValue: 10 },
|
||||
});
|
||||
|
||||
let result = setGlobalFilterValueWithoutReload(model, {
|
||||
id: "1",
|
||||
value: { operator: "=", targetValue: false },
|
||||
});
|
||||
expect(result.isSuccessful).toBe(false);
|
||||
expect(result.reasons).toEqual(["InvalidValueTypeCombination"]);
|
||||
|
||||
result = setGlobalFilterValueWithoutReload(model, {
|
||||
id: "1",
|
||||
value: { operator: "=", targetValue: "value" },
|
||||
});
|
||||
expect(result.isSuccessful).toBe(false);
|
||||
expect(result.reasons).toEqual(["InvalidValueTypeCombination"]);
|
||||
|
||||
result = setGlobalFilterValueWithoutReload(model, {
|
||||
id: "1",
|
||||
value: { operator: "=", targetValue: "5" },
|
||||
});
|
||||
expect(result.isSuccessful).toBe(false);
|
||||
expect(result.reasons).toEqual(["InvalidValueTypeCombination"]);
|
||||
|
||||
result = setGlobalFilterValueWithoutReload(model, {
|
||||
id: "1",
|
||||
value: { operator: "=", targetValue: "" },
|
||||
});
|
||||
expect(result.isSuccessful).toBe(false);
|
||||
expect(result.reasons).toEqual(["InvalidValueTypeCombination"]);
|
||||
|
||||
result = addGlobalFilterWithoutReload(model, {
|
||||
id: "2",
|
||||
type: "numeric",
|
||||
label: "Default value is a number",
|
||||
defaultValue: { operator: "=", targetValue: 99 },
|
||||
});
|
||||
expect(result.isSuccessful).toBe(true);
|
||||
|
||||
result = setGlobalFilterValueWithoutReload(model, {
|
||||
id: "2",
|
||||
});
|
||||
expect(result.isSuccessful).toBe(true);
|
||||
});
|
||||
|
||||
test("Value of date filter", () => {
|
||||
const model = new Model();
|
||||
addGlobalFilterWithoutReload(model, {
|
||||
id: "1",
|
||||
type: "date",
|
||||
label: "Date Filter",
|
||||
});
|
||||
|
||||
let result = setGlobalFilterValueWithoutReload(model, {
|
||||
id: "1",
|
||||
value: "test",
|
||||
});
|
||||
expect(result.isSuccessful).toBe(false);
|
||||
expect(result.reasons).toEqual(["InvalidValueTypeCombination"]);
|
||||
|
||||
result = setGlobalFilterValueWithoutReload(model, {
|
||||
id: "1",
|
||||
value: { type: "year" },
|
||||
});
|
||||
expect(result.isSuccessful).toBe(false);
|
||||
expect(result.reasons).toEqual(["InvalidValueTypeCombination"]);
|
||||
|
||||
result = setGlobalFilterValueWithoutReload(model, {
|
||||
id: "1",
|
||||
value: { type: "year", year: 2022 },
|
||||
});
|
||||
expect(result.isSuccessful).toBe(true);
|
||||
|
||||
result = setGlobalFilterValueWithoutReload(model, {
|
||||
id: "1",
|
||||
});
|
||||
expect(result.isSuccessful).toBe(true);
|
||||
|
||||
result = setGlobalFilterValueWithoutReload(model, {
|
||||
id: "1",
|
||||
value: 5,
|
||||
});
|
||||
expect(result.isSuccessful).toBe(false);
|
||||
expect(result.reasons).toEqual(["InvalidValueTypeCombination"]);
|
||||
|
||||
result = setGlobalFilterValueWithoutReload(model, {
|
||||
id: "1",
|
||||
value: false,
|
||||
});
|
||||
expect(result.isSuccessful).toBe(false);
|
||||
expect(result.reasons).toEqual(["InvalidValueTypeCombination"]);
|
||||
|
||||
result = setGlobalFilterValueWithoutReload(model, {
|
||||
id: "1",
|
||||
value: { type: "month", month: 5 },
|
||||
});
|
||||
expect(result.isSuccessful).toBe(false);
|
||||
expect(result.reasons).toEqual(["InvalidValueTypeCombination"]);
|
||||
|
||||
result = setGlobalFilterValueWithoutReload(model, {
|
||||
id: "1",
|
||||
value: { type: "month", year: 2020 },
|
||||
});
|
||||
expect(result.isSuccessful).toBe(false);
|
||||
expect(result.reasons).toEqual(["InvalidValueTypeCombination"]);
|
||||
|
||||
result = setGlobalFilterValueWithoutReload(model, {
|
||||
id: "1",
|
||||
value: { type: "month", month: 5, year: 2016 },
|
||||
});
|
||||
expect(result.isSuccessful).toBe(true);
|
||||
|
||||
result = setGlobalFilterValueWithoutReload(model, {
|
||||
id: "1",
|
||||
value: { type: "quarter", year: 2020 },
|
||||
});
|
||||
expect(result.isSuccessful).toBe(false);
|
||||
expect(result.reasons).toEqual(["InvalidValueTypeCombination"]);
|
||||
|
||||
result = setGlobalFilterValueWithoutReload(model, {
|
||||
id: "1",
|
||||
value: { type: "quarter", quarter: 3 },
|
||||
});
|
||||
expect(result.isSuccessful).toBe(false);
|
||||
expect(result.reasons).toEqual(["InvalidValueTypeCombination"]);
|
||||
|
||||
result = setGlobalFilterValueWithoutReload(model, {
|
||||
id: "1",
|
||||
value: { type: "quarter", quarter: 3, year: 2016 },
|
||||
});
|
||||
expect(result.isSuccessful).toBe(true);
|
||||
|
||||
result = setGlobalFilterValueWithoutReload(model, {
|
||||
id: "1",
|
||||
value: { type: "year" },
|
||||
});
|
||||
expect(result.isSuccessful).toBe(false);
|
||||
expect(result.reasons).toEqual(["InvalidValueTypeCombination"]);
|
||||
|
||||
result = setGlobalFilterValueWithoutReload(model, {
|
||||
id: "1",
|
||||
value: { type: "year", year: 2016 },
|
||||
});
|
||||
expect(result.isSuccessful).toBe(true);
|
||||
|
||||
for (const period of Object.keys(RELATIVE_PERIODS)) {
|
||||
result = setGlobalFilterValueWithoutReload(model, {
|
||||
id: "1",
|
||||
value: { type: "relative", period },
|
||||
});
|
||||
expect(result.isSuccessful).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test("Value of relation filter", () => {
|
||||
const model = new Model();
|
||||
addGlobalFilterWithoutReload(model, {
|
||||
id: "1",
|
||||
type: "relation",
|
||||
label: "Relation Filter",
|
||||
});
|
||||
|
||||
let result = setGlobalFilterValueWithoutReload(model, {
|
||||
id: "1",
|
||||
value: { operator: "in", ids: "test" },
|
||||
});
|
||||
expect(result.isSuccessful).toBe(false);
|
||||
expect(result.reasons).toEqual(["InvalidValueTypeCombination"]);
|
||||
|
||||
result = setGlobalFilterValueWithoutReload(model, {
|
||||
id: "1",
|
||||
value: { operator: "in", ids: [1, 2, 3] },
|
||||
});
|
||||
expect(result.isSuccessful).toBe(true);
|
||||
|
||||
result = setGlobalFilterValueWithoutReload(model, {
|
||||
id: "1",
|
||||
});
|
||||
expect(result.isSuccessful).toBe(true);
|
||||
|
||||
result = setGlobalFilterValueWithoutReload(model, {
|
||||
id: "1",
|
||||
value: { operator: "in", ids: 5 },
|
||||
});
|
||||
expect(result.isSuccessful).toBe(false);
|
||||
expect(result.reasons).toEqual(["InvalidValueTypeCombination"]);
|
||||
|
||||
result = addGlobalFilterWithoutReload(model, {
|
||||
id: "5",
|
||||
type: "relation",
|
||||
label: "Default value cannot be a boolean",
|
||||
defaultValue: { operator: "in", ids: false },
|
||||
});
|
||||
expect(result.isSuccessful).toBe(false);
|
||||
expect(result.reasons).toEqual(["InvalidValueTypeCombination"]);
|
||||
|
||||
result = setGlobalFilterValueWithoutReload(model, {
|
||||
id: "1",
|
||||
value: { operator: "in", ids: "current_user" }, // TODO check this
|
||||
});
|
||||
expect(result.isSuccessful).toBe(false);
|
||||
expect(result.reasons).toEqual(["InvalidValueTypeCombination"]);
|
||||
|
||||
result = setGlobalFilterValueWithoutReload(model, {
|
||||
id: "1",
|
||||
value: { operator: "in", ids: ["1"] },
|
||||
});
|
||||
expect(result.isSuccessful).toBe(false);
|
||||
expect(result.reasons).toEqual(["InvalidValueTypeCombination"]);
|
||||
|
||||
result = setGlobalFilterValueWithoutReload(model, {
|
||||
id: "1",
|
||||
value: { operator: "in", ids: [] },
|
||||
});
|
||||
expect(result.isSuccessful).toBe(false);
|
||||
expect(result.reasons).toEqual(["InvalidValueTypeCombination"]);
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,158 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { keyDown } from "@odoo/hoot-dom";
|
||||
import {
|
||||
makeMockEnv,
|
||||
contains,
|
||||
mountWithCleanup,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { defineSpreadsheetModels } from "@spreadsheet/../tests/helpers/data";
|
||||
import { getTemplate } from "@web/core/templates";
|
||||
import { NumericFilterValue } from "@spreadsheet/global_filters/components/numeric_filter_value/numeric_filter_value";
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineSpreadsheetModels();
|
||||
|
||||
async function mountNumericFilterValue(env, props) {
|
||||
await mountWithCleanup(NumericFilterValue, { props, env, getTemplate });
|
||||
}
|
||||
|
||||
test("numeric filter with no default value prop", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountNumericFilterValue(env, {
|
||||
onValueChanged: (value) => {
|
||||
expect(value).toEqual(1998);
|
||||
expect.step("update");
|
||||
},
|
||||
});
|
||||
expect.verifySteps([]);
|
||||
expect(".o_input").toHaveText("");
|
||||
await contains("input").edit(1998);
|
||||
await contains("input").press("Enter");
|
||||
expect.verifySteps(["update"]);
|
||||
expect(".o_input").toHaveValue(1998);
|
||||
});
|
||||
|
||||
test("numeric filter with default value prop", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountNumericFilterValue(env, {
|
||||
value: 1999,
|
||||
onValueChanged: () => {},
|
||||
});
|
||||
expect(".o_input").toHaveValue(1999);
|
||||
});
|
||||
|
||||
test("change value of numeric filter with default value prop", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountNumericFilterValue(env, {
|
||||
value: 1999,
|
||||
onValueChanged: (value) => {
|
||||
expect(value).toEqual(2000);
|
||||
expect.step("update");
|
||||
},
|
||||
});
|
||||
expect.verifySteps([]);
|
||||
expect(".o_input").toHaveValue(1999);
|
||||
await contains("input").edit(2000);
|
||||
await contains("input").press("Enter");
|
||||
expect.verifySteps(["update"]);
|
||||
expect(".o_input").toHaveValue(2000);
|
||||
});
|
||||
|
||||
test("clearing numeric filter input should not reset its value to 0", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountNumericFilterValue(env, {
|
||||
value: 1998,
|
||||
onValueChanged: (value) => {
|
||||
expect(value).toEqual(undefined);
|
||||
expect.step("reset");
|
||||
},
|
||||
});
|
||||
await contains("input").edit("");
|
||||
await contains("input").press("Enter");
|
||||
expect.verifySteps(["reset"]);
|
||||
expect(".o_input").toHaveValue("");
|
||||
});
|
||||
|
||||
test("setting a string value to numeric filter input should reset its value to 0", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountNumericFilterValue(env, {
|
||||
value: undefined,
|
||||
onValueChanged: (value) => {
|
||||
expect(value).toEqual(0);
|
||||
expect.step("reset");
|
||||
},
|
||||
});
|
||||
await contains("input").edit("hola");
|
||||
await contains("input").press("Enter");
|
||||
expect.verifySteps(["reset"]);
|
||||
expect(".o_input").toHaveValue(0);
|
||||
});
|
||||
|
||||
test("numeric filter input value is correctly parsed based on localization", async function () {
|
||||
const env = await makeMockEnv();
|
||||
patchWithCleanup(localization, {
|
||||
decimalPoint: ",",
|
||||
thousandsSep: ".",
|
||||
});
|
||||
await mountNumericFilterValue(env, {
|
||||
onValueChanged: (value) => {
|
||||
expect(value).toEqual(40048789.87);
|
||||
expect.step("parsed");
|
||||
},
|
||||
});
|
||||
await contains("input").edit("40.048.789,87");
|
||||
await contains("input").press("Enter");
|
||||
expect.verifySteps(["parsed"]);
|
||||
});
|
||||
|
||||
test("numeric filter input should insert localized decimal separator when numpad decimal key is pressed", async function () {
|
||||
const env = await makeMockEnv();
|
||||
patchWithCleanup(localization, {
|
||||
decimalPoint: ",",
|
||||
thousandsSep: ".",
|
||||
});
|
||||
await mountNumericFilterValue(env, {
|
||||
onValueChanged: (value) => {
|
||||
expect(value).toEqual(0.12);
|
||||
expect.step("parsed");
|
||||
},
|
||||
});
|
||||
await contains("input").focus();
|
||||
await keyDown(".", { code: "NumpadDecimal" });
|
||||
await keyDown("1", { code: "Digit1" });
|
||||
await keyDown("2", { code: "Digit2" });
|
||||
expect(".o_input").toHaveValue(",12");
|
||||
const input = document.querySelector(".o_input");
|
||||
input.dispatchEvent(new Event("change"));
|
||||
expect.verifySteps(["parsed"]);
|
||||
});
|
||||
|
||||
test("default value is saved after saving and editing again", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountNumericFilterValue(env, {
|
||||
value: 2001,
|
||||
onValueChanged: () => {},
|
||||
});
|
||||
expect(".o_input").toHaveValue(2001);
|
||||
await mountNumericFilterValue(env, {
|
||||
onValueChanged: () => {},
|
||||
});
|
||||
expect(".o_input").toHaveText("");
|
||||
});
|
||||
|
||||
test("default value does not disappear when pressing enter", async function () {
|
||||
let savedValue = null;
|
||||
const env = await makeMockEnv();
|
||||
await mountNumericFilterValue(env, {
|
||||
onValueChanged: (value) => {
|
||||
savedValue = value;
|
||||
},
|
||||
});
|
||||
await contains("input").edit("2024");
|
||||
await contains("input").press("Enter");
|
||||
expect(savedValue).toEqual(2024);
|
||||
|
||||
expect(".o_input").toHaveValue(2024);
|
||||
});
|
||||
|
|
@ -0,0 +1,254 @@
|
|||
/** @ts-check */
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { animationFrame, mockDate } from "@odoo/hoot-mock";
|
||||
import {
|
||||
defineSpreadsheetModels,
|
||||
} from "@spreadsheet/../tests/helpers/data";
|
||||
import {
|
||||
createModelWithDataSource,
|
||||
} from "@spreadsheet/../tests/helpers/model";
|
||||
|
||||
import {
|
||||
addGlobalFilter,
|
||||
setCellContent,
|
||||
setGlobalFilterValue,
|
||||
} from "@spreadsheet/../tests/helpers/commands";
|
||||
import {
|
||||
getCellValue,
|
||||
getEvaluatedCell,
|
||||
} from "@spreadsheet/../tests/helpers/getters";
|
||||
|
||||
describe.current.tags("headless");
|
||||
defineSpreadsheetModels();
|
||||
|
||||
const { DateTime } = luxon;
|
||||
|
||||
/**
|
||||
* @typedef {import("@spreadsheet").GlobalFilter} GlobalFilter
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
test("ODOO.FILTER.VALUE.V18 text filter", async function () {
|
||||
const { model } = await createModelWithDataSource();
|
||||
setCellContent(model, "A10", `=ODOO.FILTER.VALUE.V18("Text Filter")`);
|
||||
await animationFrame();
|
||||
expect(getCellValue(model, "A10")).toBe("#ERROR");
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "text",
|
||||
label: "Text Filter",
|
||||
});
|
||||
await animationFrame();
|
||||
expect(getCellValue(model, "A10")).toBe("");
|
||||
const [filter] = model.getters.getGlobalFilters();
|
||||
await setGlobalFilterValue(model, {
|
||||
id: filter.id,
|
||||
value: { operator: "ilike", strings: ["Hello"] },
|
||||
});
|
||||
await animationFrame();
|
||||
expect(getCellValue(model, "A10")).toBe("Hello");
|
||||
});
|
||||
|
||||
test("ODOO.FILTER.VALUE.V18 empty date filter does't spill", async function () {
|
||||
mockDate("2022-03-10 00:00:00");
|
||||
const { model } = await createModelWithDataSource();
|
||||
setCellContent(model, "A10", `=ODOO.FILTER.VALUE.V18("Date Filter")`);
|
||||
setCellContent(model, "B10", "something");
|
||||
await animationFrame();
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "date",
|
||||
label: "Date Filter",
|
||||
});
|
||||
expect(getCellValue(model, "A10")).toBe("");
|
||||
});
|
||||
|
||||
test("ODOO.FILTER.VALUE.V18 date filter", async function () {
|
||||
mockDate("2022-03-10 00:00:00");
|
||||
const { model } = await createModelWithDataSource();
|
||||
setCellContent(model, "A10", `=ODOO.FILTER.VALUE.V18("Date Filter")`);
|
||||
await animationFrame();
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "date",
|
||||
label: "Date Filter",
|
||||
});
|
||||
await animationFrame();
|
||||
const [filter] = model.getters.getGlobalFilters();
|
||||
await setGlobalFilterValue(model, {
|
||||
id: filter.id,
|
||||
value: {
|
||||
type: "quarter",
|
||||
year: 2022,
|
||||
quarter: 1,
|
||||
},
|
||||
});
|
||||
await animationFrame();
|
||||
expect(getCellValue(model, "A10")).toBe(`Q1/${DateTime.now().year}`);
|
||||
await setGlobalFilterValue(model, {
|
||||
id: filter.id,
|
||||
value: {
|
||||
type: "year",
|
||||
year: 2022,
|
||||
},
|
||||
});
|
||||
await animationFrame();
|
||||
expect(getCellValue(model, "A10")).toBe(`${DateTime.now().year}`);
|
||||
await setGlobalFilterValue(model, {
|
||||
id: filter.id,
|
||||
value: {
|
||||
type: "month",
|
||||
year: 2022,
|
||||
month: 1,
|
||||
},
|
||||
});
|
||||
await animationFrame();
|
||||
expect(getCellValue(model, "A10")).toBe(`01/${DateTime.now().year}`);
|
||||
await setGlobalFilterValue(model, {
|
||||
id: filter.id,
|
||||
});
|
||||
await animationFrame();
|
||||
expect(getCellValue(model, "A10")).toBe(``);
|
||||
});
|
||||
|
||||
test("ODOO.FILTER.VALUE.V18 date from/to without values", async function () {
|
||||
const { model } = await createModelWithDataSource();
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "date",
|
||||
label: "Date Filter",
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.FILTER.VALUE.V18("Date Filter")`);
|
||||
expect(getEvaluatedCell(model, "A1").value).toBe("");
|
||||
expect(getEvaluatedCell(model, "B1").value).toBe(null);
|
||||
});
|
||||
|
||||
test("ODOO.FILTER.VALUE.V18 date from/to with only from defined", async function () {
|
||||
const { model } = await createModelWithDataSource();
|
||||
setCellContent(model, "A1", `=ODOO.FILTER.VALUE.V18("Date Filter")`);
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "date",
|
||||
label: "Date Filter",
|
||||
});
|
||||
await setGlobalFilterValue(model, {
|
||||
id: "42",
|
||||
value: {
|
||||
type: "range",
|
||||
from: "2020-01-01",
|
||||
},
|
||||
});
|
||||
expect(getEvaluatedCell(model, "A1").value).toBe(43831);
|
||||
expect(getEvaluatedCell(model, "A1").format).toBe("m/d/yyyy");
|
||||
expect(getEvaluatedCell(model, "A1").formattedValue).toBe("1/1/2020");
|
||||
expect(getEvaluatedCell(model, "B1").value).toBe("");
|
||||
});
|
||||
|
||||
test("ODOO.FILTER.VALUE.V18 date from/to with only to defined", async function () {
|
||||
const { model } = await createModelWithDataSource();
|
||||
setCellContent(model, "A1", `=ODOO.FILTER.VALUE.V18("Date Filter")`);
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "date",
|
||||
label: "Date Filter",
|
||||
});
|
||||
await setGlobalFilterValue(model, {
|
||||
id: "42",
|
||||
value: {
|
||||
type: "range",
|
||||
to: "2020-01-01",
|
||||
},
|
||||
});
|
||||
expect(getEvaluatedCell(model, "A1").value).toBe("");
|
||||
expect(getEvaluatedCell(model, "B1").value).toBe(43831);
|
||||
expect(getEvaluatedCell(model, "B1").format).toBe("m/d/yyyy");
|
||||
expect(getEvaluatedCell(model, "B1").formattedValue).toBe("1/1/2020");
|
||||
});
|
||||
|
||||
test("ODOO.FILTER.VALUE.V18 date from/to with from and to defined", async function () {
|
||||
const { model } = await createModelWithDataSource();
|
||||
setCellContent(model, "A1", `=ODOO.FILTER.VALUE.V18("Date Filter")`);
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "date",
|
||||
label: "Date Filter",
|
||||
});
|
||||
await setGlobalFilterValue(model, {
|
||||
id: "42",
|
||||
value: {
|
||||
type: "range",
|
||||
from: "2020-01-01",
|
||||
to: "2021-01-01",
|
||||
},
|
||||
});
|
||||
expect(getEvaluatedCell(model, "A1").value).toBe(43831);
|
||||
expect(getEvaluatedCell(model, "A1").format).toBe("m/d/yyyy");
|
||||
expect(getEvaluatedCell(model, "A1").formattedValue).toBe("1/1/2020");
|
||||
expect(getEvaluatedCell(model, "B1").value).toBe(44197);
|
||||
expect(getEvaluatedCell(model, "B1").format).toBe("m/d/yyyy");
|
||||
expect(getEvaluatedCell(model, "B1").formattedValue).toBe("1/1/2021");
|
||||
});
|
||||
|
||||
test("ODOO.FILTER.VALUE.V18 relation filter", async function () {
|
||||
const { model } = await createModelWithDataSource({
|
||||
mockRPC: function (route, { method, args }) {
|
||||
if (method === "web_search_read") {
|
||||
const resIds = args[0][0][2];
|
||||
const names = {
|
||||
1: "Jean-Jacques",
|
||||
2: "Raoul Grosbedon",
|
||||
};
|
||||
expect.step(`read_${resIds}`);
|
||||
return { records: resIds.map((resId) => ({ id: resId, display_name: names[resId] })) };
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A10", `=ODOO.FILTER.VALUE.V18("Relation Filter")`);
|
||||
await animationFrame();
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "relation",
|
||||
label: "Relation Filter",
|
||||
modelName: "partner",
|
||||
});
|
||||
await animationFrame();
|
||||
const [filter] = model.getters.getGlobalFilters();
|
||||
expect.verifySteps([]);
|
||||
// One record; displayNames not defined => rpc
|
||||
await setGlobalFilterValue(model, {
|
||||
id: filter.id,
|
||||
value: { operator: "in", ids: [1] },
|
||||
});
|
||||
await animationFrame();
|
||||
expect(getCellValue(model, "A10")).toBe("Jean-Jacques");
|
||||
|
||||
// Two records; displayNames defined => no rpc
|
||||
await setGlobalFilterValue(model, {
|
||||
id: filter.id,
|
||||
value: { operator: "in", ids: [1, 2] },
|
||||
});
|
||||
await animationFrame();
|
||||
expect(getCellValue(model, "A10")).toBe("Jean-Jacques, Raoul Grosbedon");
|
||||
|
||||
// another record; displayNames not defined => rpc
|
||||
await setGlobalFilterValue(model, {
|
||||
id: filter.id,
|
||||
value: { operator: "in", ids: [2] },
|
||||
});
|
||||
await animationFrame();
|
||||
expect(getCellValue(model, "A10")).toBe("Raoul Grosbedon");
|
||||
expect.verifySteps(["read_1", "read_1,2", "read_2"]);
|
||||
});
|
||||
|
||||
test("ODOO.FILTER.VALUE.V18 with escaped quotes in the filter label", async function () {
|
||||
const { model } = await createModelWithDataSource();
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "text",
|
||||
label: 'my "special" filter',
|
||||
defaultValue: { operator: "ilike", strings: ["Jean-Jacques"] },
|
||||
});
|
||||
setCellContent(model, "A1", '=ODOO.FILTER.VALUE.V18("my \\"special\\" filter")');
|
||||
expect(getCellValue(model, "A1")).toBe("Jean-Jacques");
|
||||
});
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import { describe, expect, test, getFixture } from "@odoo/hoot";
|
||||
import { makeMockEnv, contains, mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
import { defineSpreadsheetModels } from "@spreadsheet/../tests/helpers/data";
|
||||
import { getTemplate } from "@web/core/templates";
|
||||
import { SelectionFilterValue } from "@spreadsheet/global_filters/components/selection_filter_value/selection_filter_value";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineSpreadsheetModels();
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {{ model: Model, filter: object}} props
|
||||
*/
|
||||
async function mountSelectionFilterValue(env, props) {
|
||||
await mountWithCleanup(SelectionFilterValue, { props, env, getTemplate });
|
||||
}
|
||||
|
||||
/**
|
||||
* res.currency.position is a selection field with 2 options:
|
||||
* - after (A)
|
||||
* - before (B)
|
||||
*/
|
||||
|
||||
test("basic selection filter value", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountSelectionFilterValue(env, {
|
||||
value: [],
|
||||
resModel: "res.currency",
|
||||
field: "position",
|
||||
onValueChanged: (values) => {
|
||||
expect(values).toEqual(["after"]);
|
||||
expect.step("onValueChanged");
|
||||
},
|
||||
});
|
||||
expect.verifySteps([]);
|
||||
await contains("input").click();
|
||||
const options = getFixture().querySelectorAll(".o-autocomplete a");
|
||||
expect(options.length).toBe(2);
|
||||
expect(options[0].textContent).toBe("A");
|
||||
expect(options[1].textContent).toBe("B");
|
||||
await contains("a:first").click();
|
||||
expect.verifySteps(["onValueChanged"]);
|
||||
});
|
||||
|
||||
test("Autocomplete only provide values that are not selected", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountSelectionFilterValue(env, {
|
||||
value: ["after"],
|
||||
resModel: "res.currency",
|
||||
field: "position",
|
||||
onValueChanged: () => {},
|
||||
});
|
||||
await contains("input").click();
|
||||
const options = getFixture().querySelectorAll(".o-autocomplete a");
|
||||
expect(options.length).toBe(1);
|
||||
expect(options[0].textContent).toBe("B");
|
||||
});
|
||||
|
||||
test("Can click on delete", async function () {
|
||||
const env = await makeMockEnv();
|
||||
await mountSelectionFilterValue(env, {
|
||||
value: ["after", "before"],
|
||||
resModel: "res.currency",
|
||||
field: "position",
|
||||
onValueChanged: (values) => {
|
||||
expect(values).toEqual(["before"]);
|
||||
expect.step("onValueChanged");
|
||||
},
|
||||
});
|
||||
expect.verifySteps([]);
|
||||
await contains(".o_badge:first .o_delete").click();
|
||||
expect.verifySteps(["onValueChanged"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
|
||||
import * as spreadsheet from "@odoo/o-spreadsheet";
|
||||
import { createModelWithDataSource } from "@spreadsheet/../tests/helpers/model";
|
||||
const uuidGenerator = new spreadsheet.helpers.UuidGenerator();
|
||||
|
||||
/**
|
||||
* @typedef {import("@odoo/o-spreadsheet").Model} Model
|
||||
* @typedef {import("@spreadsheet").OdooSpreadsheetModel} OdooSpreadsheetModel
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Model} model
|
||||
* @param {string} type
|
||||
* @param {import("@spreadsheet/chart/odoo_chart/odoo_chart").OdooChartDefinition} definition
|
||||
*/
|
||||
export function insertChartInSpreadsheet(model, type = "odoo_bar", definition = {}) {
|
||||
definition = { ...getChartDefinition(type), ...definition };
|
||||
model.dispatch("CREATE_CHART", {
|
||||
sheetId: model.getters.getActiveSheetId(),
|
||||
chartId: definition.id,
|
||||
figureId: uuidGenerator.smallUuid(),
|
||||
col: 0,
|
||||
row: 0,
|
||||
offset: {
|
||||
x: 10,
|
||||
y: 10,
|
||||
},
|
||||
definition,
|
||||
});
|
||||
return definition.id;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {Object} params
|
||||
* @param {function} [params.definition]
|
||||
* @param {function} [params.mockRPC]
|
||||
* @param {string} [params.type]
|
||||
* @param {import("./data").ServerData} [params.serverData]
|
||||
*
|
||||
* @returns { Promise<{ model: OdooSpreadsheetModel, env: Object }>}
|
||||
*/
|
||||
export async function createSpreadsheetWithChart(params = {}) {
|
||||
const { model, env } = await createModelWithDataSource(params);
|
||||
|
||||
insertChartInSpreadsheet(model, params.type, params.definition);
|
||||
|
||||
await animationFrame();
|
||||
return { model, env };
|
||||
}
|
||||
|
||||
function getChartDefinition(type) {
|
||||
return {
|
||||
metaData: {
|
||||
groupBy: ["foo", "bar"],
|
||||
measure: "__count",
|
||||
order: null,
|
||||
resModel: "partner",
|
||||
},
|
||||
searchParams: {
|
||||
comparison: null,
|
||||
context: {},
|
||||
domain: [],
|
||||
groupBy: [],
|
||||
orderBy: [],
|
||||
},
|
||||
stacked: true,
|
||||
title: { text: "Partners" },
|
||||
background: "#FFFFFF",
|
||||
legendPosition: "top",
|
||||
verticalAxisPosition: "left",
|
||||
dataSourceId: uuidGenerator.smallUuid(),
|
||||
id: uuidGenerator.smallUuid(),
|
||||
type,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,373 @@
|
|||
/** @ts-check */
|
||||
|
||||
import * as spreadsheet from "@odoo/o-spreadsheet";
|
||||
import { waitForDataLoaded } from "@spreadsheet/helpers/model";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
|
||||
const { toCartesian, toZone, lettersToNumber, deepCopy } = spreadsheet.helpers;
|
||||
|
||||
/**
|
||||
* @typedef {import("@spreadsheet").GlobalFilter} GlobalFilter
|
||||
* @typedef {import("@spreadsheet").CmdGlobalFilter} CmdGlobalFilter
|
||||
* @typedef {import("@spreadsheet").OdooSpreadsheetModel} OdooSpreadsheetModel
|
||||
* @typedef {import("@odoo/o-spreadsheet").UID} UID
|
||||
*/
|
||||
|
||||
/**
|
||||
* Select a cell
|
||||
*/
|
||||
export function selectCell(model, xc, sheetId = model.getters.getActiveSheetId()) {
|
||||
const { col, row } = toCartesian(xc);
|
||||
if (sheetId !== model.getters.getActiveSheetId()) {
|
||||
model.dispatch("ACTIVATE_SHEET", { sheetIdTo: sheetId });
|
||||
}
|
||||
return model.selection.selectCell(col, row);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a global filter. Does not wait for the data sources to be reloaded
|
||||
* @param {import("@spreadsheet").OdooSpreadsheetModel} model
|
||||
* @param {CmdGlobalFilter} filter
|
||||
*/
|
||||
export function addGlobalFilterWithoutReload(model, filter, fieldMatchings = {}) {
|
||||
return model.dispatch("ADD_GLOBAL_FILTER", { filter, ...fieldMatchings });
|
||||
}
|
||||
|
||||
export function setGlobalFilterValueWithoutReload(model, payload) {
|
||||
return model.dispatch("SET_GLOBAL_FILTER_VALUE", payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a global filter and ensure the data sources are completely reloaded
|
||||
* @param {import("@spreadsheet").OdooSpreadsheetModel} model
|
||||
* @param {CmdGlobalFilter} filter
|
||||
*/
|
||||
export async function addGlobalFilter(model, filter, fieldMatchings = {}) {
|
||||
const result = model.dispatch("ADD_GLOBAL_FILTER", { filter, ...fieldMatchings });
|
||||
// Wait for the fetch of DisplayNames
|
||||
await animationFrame();
|
||||
await waitForDataLoaded(model);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a global filter and ensure the data sources are completely reloaded
|
||||
*/
|
||||
export async function removeGlobalFilter(model, id) {
|
||||
const result = model.dispatch("REMOVE_GLOBAL_FILTER", { id });
|
||||
// Wait for the fetch of DisplayNames
|
||||
await animationFrame();
|
||||
await waitForDataLoaded(model);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a global filter and ensure the data sources are completely reloaded
|
||||
* @param {Model} model
|
||||
* @param {CmdGlobalFilter} filter
|
||||
*/
|
||||
export async function editGlobalFilter(model, filter) {
|
||||
const result = model.dispatch("EDIT_GLOBAL_FILTER", { filter });
|
||||
// Wait for the fetch of DisplayNames
|
||||
await animationFrame();
|
||||
await waitForDataLoaded(model);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value of a global filter and ensure the data sources are completely
|
||||
* reloaded
|
||||
*/
|
||||
export async function setGlobalFilterValue(model, payload) {
|
||||
const result = model.dispatch("SET_GLOBAL_FILTER_VALUE", payload);
|
||||
// Wait for the fetch of DisplayNames
|
||||
await animationFrame();
|
||||
await waitForDataLoaded(model);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function moveGlobalFilter(model, id, delta) {
|
||||
return model.dispatch("MOVE_GLOBAL_FILTER", { id, delta });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the selection
|
||||
*/
|
||||
export function setSelection(model, xc) {
|
||||
const zone = toZone(xc);
|
||||
model.selection.selectZone({ cell: { col: zone.left, row: zone.top }, zone });
|
||||
}
|
||||
|
||||
/**
|
||||
* Autofill from a zone to a cell
|
||||
*/
|
||||
export function autofill(model, from, to) {
|
||||
setSelection(model, from);
|
||||
model.dispatch("AUTOFILL_SELECT", toCartesian(to));
|
||||
model.dispatch("AUTOFILL");
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the content of a cell
|
||||
*/
|
||||
export function setCellContent(model, xc, content, sheetId = model.getters.getActiveSheetId()) {
|
||||
model.dispatch("UPDATE_CELL", { ...toCartesian(xc), sheetId, content });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the format of a cell
|
||||
*/
|
||||
export function setCellFormat(model, xc, format, sheetId = model.getters.getActiveSheetId()) {
|
||||
model.dispatch("UPDATE_CELL", { ...toCartesian(xc), sheetId, format });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the style of a cell
|
||||
*/
|
||||
export function setCellStyle(model, xc, style, sheetId = model.getters.getActiveSheetId()) {
|
||||
model.dispatch("UPDATE_CELL", { ...toCartesian(xc), sheetId, style });
|
||||
}
|
||||
|
||||
/**
|
||||
* Add columns
|
||||
* @param {OdooSpreadsheetModel} model
|
||||
* @param {"before"|"after"} position
|
||||
* @param {string} column
|
||||
* @param {number} quantity
|
||||
* @param {UID} sheetId
|
||||
*/
|
||||
export function addColumns(
|
||||
model,
|
||||
position,
|
||||
column,
|
||||
quantity,
|
||||
sheetId = model.getters.getActiveSheetId()
|
||||
) {
|
||||
return model.dispatch("ADD_COLUMNS_ROWS", {
|
||||
sheetId,
|
||||
dimension: "COL",
|
||||
position,
|
||||
base: lettersToNumber(column),
|
||||
quantity,
|
||||
});
|
||||
}
|
||||
|
||||
export function addRows(
|
||||
model,
|
||||
position,
|
||||
row,
|
||||
quantity,
|
||||
sheetId = model.getters.getActiveSheetId()
|
||||
) {
|
||||
return model.dispatch("ADD_COLUMNS_ROWS", {
|
||||
sheetId,
|
||||
dimension: "ROW",
|
||||
position,
|
||||
base: row,
|
||||
quantity,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete columns
|
||||
* @param {OdooSpreadsheetModel} model
|
||||
* @param {string[]} columns
|
||||
* @param {UID} sheetId
|
||||
*/
|
||||
export function deleteColumns(model, columns, sheetId = model.getters.getActiveSheetId()) {
|
||||
return model.dispatch("REMOVE_COLUMNS_ROWS", {
|
||||
sheetId,
|
||||
dimension: "COL",
|
||||
elements: columns.map(lettersToNumber),
|
||||
});
|
||||
}
|
||||
|
||||
/** Create a test chart in the active sheet*/
|
||||
export function createBasicChart(
|
||||
model,
|
||||
chartId,
|
||||
definition,
|
||||
sheetId = model.getters.getActiveSheetId(),
|
||||
figureId = model.uuidGenerator.smallUuid()
|
||||
) {
|
||||
model.dispatch("CREATE_CHART", {
|
||||
chartId,
|
||||
figureId,
|
||||
col: 0,
|
||||
row: 0,
|
||||
offset: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
sheetId: sheetId,
|
||||
definition: {
|
||||
title: { text: "test" },
|
||||
dataSets: [{ dataRange: "A1" }],
|
||||
type: "bar",
|
||||
background: "#fff",
|
||||
verticalAxisPosition: "left",
|
||||
legendPosition: "top",
|
||||
stackedBar: false,
|
||||
...definition,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Create a test scorecard chart in the active sheet*/
|
||||
export function createScorecardChart(
|
||||
model,
|
||||
chartId,
|
||||
sheetId = model.getters.getActiveSheetId(),
|
||||
figureId = model.uuidGenerator.smallUuid()
|
||||
) {
|
||||
model.dispatch("CREATE_CHART", {
|
||||
figureId,
|
||||
chartId,
|
||||
col: 0,
|
||||
row: 0,
|
||||
offset: { x: 0, y: 0 },
|
||||
sheetId: sheetId,
|
||||
definition: {
|
||||
title: { text: "test" },
|
||||
keyValue: "A1",
|
||||
type: "scorecard",
|
||||
background: "#fff",
|
||||
baselineColorDown: "#DC6965",
|
||||
baselineColorUp: "#00A04A",
|
||||
baselineMode: "absolute",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Create a test scorecard chart in the active sheet*/
|
||||
export function createGaugeChart(
|
||||
model,
|
||||
chartId,
|
||||
sheetId = model.getters.getActiveSheetId(),
|
||||
figureId = model.uuidGenerator.smallUuid()
|
||||
) {
|
||||
model.dispatch("CREATE_CHART", {
|
||||
figureId,
|
||||
chartId,
|
||||
col: 0,
|
||||
row: 0,
|
||||
offset: { x: 0, y: 0 },
|
||||
sheetId: sheetId,
|
||||
definition: {
|
||||
title: { text: "test" },
|
||||
type: "gauge",
|
||||
background: "#fff",
|
||||
dataRange: "A1",
|
||||
sectionRule: {
|
||||
rangeMin: "0",
|
||||
rangeMax: "100",
|
||||
colors: {
|
||||
lowerColor: "#112233",
|
||||
middleColor: "#445566",
|
||||
upperColor: "#778899",
|
||||
},
|
||||
lowerInflectionPoint: {
|
||||
type: "number",
|
||||
value: "25",
|
||||
},
|
||||
upperInflectionPoint: {
|
||||
type: "number",
|
||||
value: "85",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function updateChart(model, chartId, partialDefinition) {
|
||||
const definition = model.getters.getChartDefinition(chartId);
|
||||
return model.dispatch("UPDATE_CHART", {
|
||||
definition: { ...definition, ...partialDefinition },
|
||||
chartId,
|
||||
figureId: model.getters.getFigureIdFromChartId(chartId),
|
||||
sheetId: model.getters.getActiveSheetId(),
|
||||
});
|
||||
}
|
||||
|
||||
export function undo(model) {
|
||||
model.dispatch("REQUEST_UNDO");
|
||||
}
|
||||
|
||||
export function redo(model) {
|
||||
model.dispatch("REQUEST_REDO");
|
||||
}
|
||||
|
||||
export function updatePivot(model, pivotId, pivotData) {
|
||||
const pivot = {
|
||||
...model.getters.getPivotCoreDefinition(pivotId),
|
||||
...pivotData,
|
||||
};
|
||||
return model.dispatch("UPDATE_PIVOT", { pivotId, pivot });
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy a zone
|
||||
*/
|
||||
export function copy(model, xc) {
|
||||
setSelection(model, xc);
|
||||
return model.dispatch("COPY");
|
||||
}
|
||||
|
||||
/**
|
||||
* Cut a zone
|
||||
*/
|
||||
export function cut(model, xc) {
|
||||
setSelection(model, xc);
|
||||
return model.dispatch("CUT");
|
||||
}
|
||||
|
||||
/**
|
||||
* Paste on a zone
|
||||
*/
|
||||
export function paste(model, range, pasteOption) {
|
||||
return model.dispatch("PASTE", { target: [toZone(range)], pasteOption });
|
||||
}
|
||||
|
||||
export function updatePivotMeasureDisplay(model, pivotId, measureId, display) {
|
||||
const measures = deepCopy(model.getters.getPivotCoreDefinition(pivotId)).measures;
|
||||
const measure = measures.find((m) => m.id === measureId);
|
||||
measure.display = display;
|
||||
updatePivot(model, pivotId, { measures });
|
||||
}
|
||||
|
||||
export function createSheet(model, data = {}) {
|
||||
const sheetId = data.sheetId || model.uuidGenerator.smallUuid();
|
||||
return model.dispatch("CREATE_SHEET", {
|
||||
position: data.position !== undefined ? data.position : 1,
|
||||
sheetId,
|
||||
cols: data.cols,
|
||||
rows: data.rows,
|
||||
name: data.name,
|
||||
});
|
||||
}
|
||||
|
||||
export function createCarousel(model, data = { items: [] }, carouselId, sheetId, figureData = {}) {
|
||||
return model.dispatch("CREATE_CAROUSEL", {
|
||||
figureId: carouselId || model.uuidGenerator.smallUuid(),
|
||||
sheetId: sheetId || model.getters.getActiveSheetId(),
|
||||
col: 0,
|
||||
row: 0,
|
||||
definition: data,
|
||||
size: { width: 500, height: 300 },
|
||||
offset: { x: 0, y: 0 },
|
||||
...figureData,
|
||||
});
|
||||
}
|
||||
|
||||
export function addChartFigureToCarousel(
|
||||
model,
|
||||
carouselId,
|
||||
chartFigureId,
|
||||
sheetId = model.getters.getActiveSheetId()
|
||||
) {
|
||||
return model.dispatch("ADD_FIGURE_CHART_TO_CAROUSEL", {
|
||||
carouselFigureId: carouselId,
|
||||
chartFigureId,
|
||||
sheetId,
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,693 @@
|
|||
import {
|
||||
MockServer,
|
||||
defineActions,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
onRpc,
|
||||
serverState,
|
||||
webModels,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { mailModels } from "@mail/../tests/mail_test_helpers";
|
||||
import { RPCError } from "@web/core/network/rpc";
|
||||
|
||||
/**
|
||||
* @typedef {object} ServerData
|
||||
* @property {object} [models]
|
||||
* @property {object} [views]
|
||||
* @property {object} [menus]
|
||||
* @property {object} [actions]
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get a basic arch for a pivot, which is compatible with the data given by
|
||||
* getBasicData().
|
||||
*
|
||||
* Here is the pivot created:
|
||||
* A B C D E F
|
||||
* 1 1 2 12 17 Total
|
||||
* 2 Proba Proba Proba Proba Proba
|
||||
* 3 false 15 15
|
||||
* 4 true 11 10 95 116
|
||||
* 5 Total 11 15 10 95 131
|
||||
*/
|
||||
export function getBasicPivotArch() {
|
||||
return /* xml */ `
|
||||
<pivot string="Partners">
|
||||
<field name="foo" type="col"/>
|
||||
<field name="bar" type="row"/>
|
||||
<field name="probability" type="measure"/>
|
||||
</pivot>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a basic arch for a list, which is compatible with the data given by
|
||||
* getBasicData().
|
||||
*
|
||||
* Here is the list created:
|
||||
* A B C D
|
||||
* 1 Foo bar Date Product
|
||||
* 2 12 True 2016-04-14 xphone
|
||||
* 3 1 True 2016-10-26 xpad
|
||||
* 4 17 True 2016-12-15 xpad
|
||||
* 5 2 False 2016-12-11 xpad
|
||||
*/
|
||||
export function getBasicListArch() {
|
||||
return /* xml */ `
|
||||
<list string="Partners">
|
||||
<field name="foo"/>
|
||||
<field name="bar"/>
|
||||
<field name="date"/>
|
||||
<field name="product_id"/>
|
||||
</list>
|
||||
`;
|
||||
}
|
||||
|
||||
export function getBasicGraphArch() {
|
||||
return /* xml */ `
|
||||
<graph string="PartnerGraph">
|
||||
<field name="bar" />
|
||||
</graph>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {ServerData}
|
||||
*/
|
||||
export function getBasicServerData() {
|
||||
return {
|
||||
models: getBasicData(),
|
||||
views: {},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} model
|
||||
* @param {Array<string>} columns
|
||||
* @param {{name: string, asc: boolean}[]} orderBy
|
||||
*
|
||||
* @returns { {definition: Object, columns: Array<Object>}}
|
||||
*/
|
||||
export function generateListDefinition(model, columns, actionXmlId, orderBy = []) {
|
||||
const cols = [];
|
||||
for (const name of columns) {
|
||||
const PyModel = Object.values(SpreadsheetModels).find((m) => m._name === model);
|
||||
cols.push({
|
||||
name,
|
||||
type: PyModel._fields[name].type,
|
||||
});
|
||||
}
|
||||
return {
|
||||
definition: {
|
||||
metaData: {
|
||||
resModel: model,
|
||||
columns,
|
||||
},
|
||||
searchParams: {
|
||||
domain: [],
|
||||
context: {},
|
||||
orderBy,
|
||||
},
|
||||
name: "List",
|
||||
actionXmlId,
|
||||
},
|
||||
columns: cols,
|
||||
};
|
||||
}
|
||||
|
||||
export function getBasicListArchs() {
|
||||
return {
|
||||
"partner,false,list": getBasicListArch(),
|
||||
};
|
||||
}
|
||||
|
||||
function mockSpreadsheetDataController(_request, { res_model, res_id }) {
|
||||
const [record] = this.env[res_model].search_read([["id", "=", parseInt(res_id)]]);
|
||||
if (!record) {
|
||||
const error = new RPCError(`Spreadsheet ${res_id} does not exist`);
|
||||
error.data = {};
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
data: JSON.parse(record.spreadsheet_data),
|
||||
name: record.name,
|
||||
revisions: [],
|
||||
isReadonly: false,
|
||||
writable_rec_name_field: "name",
|
||||
};
|
||||
}
|
||||
|
||||
onRpc("/spreadsheet/data/<string:res_model>/<int:res_id>", mockSpreadsheetDataController);
|
||||
|
||||
export function defineSpreadsheetModels() {
|
||||
defineModels(SpreadsheetModels);
|
||||
}
|
||||
|
||||
export function defineSpreadsheetActions() {
|
||||
defineActions([
|
||||
{
|
||||
id: 1,
|
||||
name: "partner Action",
|
||||
res_model: "partner",
|
||||
xml_id: "spreadsheet.partner_action",
|
||||
views: [
|
||||
[false, "list"],
|
||||
[false, "pivot"],
|
||||
[false, "graph"],
|
||||
[false, "search"],
|
||||
[false, "form"],
|
||||
],
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
export class IrModel extends webModels.IrModel {
|
||||
display_name_for(models) {
|
||||
const records = this.env["ir.model"].search_read(
|
||||
[["model", "in", models]],
|
||||
["name", "model"]
|
||||
);
|
||||
const result = [];
|
||||
for (const model of models) {
|
||||
const record = records.find((record) => record.model === model);
|
||||
if (record) {
|
||||
result.push({ model: model, display_name: record.name });
|
||||
} else {
|
||||
result.push({ model: model, display_name: model });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} modelNames
|
||||
*/
|
||||
has_searchable_parent_relation(modelNames) {
|
||||
return Object.fromEntries(modelNames.map((modelName) => [modelName, false]));
|
||||
}
|
||||
|
||||
get_available_models() {
|
||||
return this.env["ir.model"].search_read([], ["display_name", "model"]);
|
||||
}
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 37,
|
||||
name: "Product",
|
||||
model: "product",
|
||||
},
|
||||
{
|
||||
id: 40,
|
||||
name: "Partner",
|
||||
model: "partner",
|
||||
},
|
||||
{
|
||||
id: 55,
|
||||
name: "Users",
|
||||
model: "res.users",
|
||||
},
|
||||
{
|
||||
id: 56,
|
||||
name: "Currency",
|
||||
model: "res.currency",
|
||||
},
|
||||
{
|
||||
id: 57,
|
||||
name: "Tag",
|
||||
model: "tag",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export class IrUIMenu extends models.Model {
|
||||
_name = "ir.ui.menu";
|
||||
|
||||
name = fields.Char({ string: "Name" });
|
||||
action = fields.Char({ string: "Action" });
|
||||
group_ids = fields.Many2many({ string: "Groups", relation: "res.group" });
|
||||
}
|
||||
|
||||
export class IrActions extends models.Model {
|
||||
_name = "ir.actions";
|
||||
}
|
||||
export class ResGroup extends models.Model {
|
||||
_name = "res.group";
|
||||
name = fields.Char({ string: "Name" });
|
||||
}
|
||||
|
||||
export class ResUsers extends mailModels.ResUsers {
|
||||
_name = "res.users";
|
||||
|
||||
name = fields.Char({ string: "Name" });
|
||||
group_ids = fields.Many2many({ string: "Groups", relation: "res.group" });
|
||||
}
|
||||
|
||||
export class SpreadsheetMixin extends models.Model {
|
||||
_name = "spreadsheet.mixin";
|
||||
|
||||
spreadsheet_binary_data = fields.Binary({ string: "Spreadsheet file" });
|
||||
spreadsheet_data = fields.Text();
|
||||
display_thumbnail = fields.Binary();
|
||||
|
||||
get_display_names_for_spreadsheet(args) {
|
||||
const result = [];
|
||||
for (const { model, id } of args) {
|
||||
const record = this.env[model].search_read([["id", "=", id]])[0];
|
||||
result.push(record?.display_name ?? null);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
get_selector_spreadsheet_models() {
|
||||
return [
|
||||
{
|
||||
model: "documents.document",
|
||||
display_name: "Spreadsheets",
|
||||
allow_create: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export class ResCurrency extends models.Model {
|
||||
_name = "res.currency";
|
||||
|
||||
name = fields.Char({ string: "Code" });
|
||||
symbol = fields.Char({ string: "Symbol" });
|
||||
position = fields.Selection({
|
||||
string: "Position",
|
||||
selection: [
|
||||
["after", "A"],
|
||||
["before", "B"],
|
||||
],
|
||||
});
|
||||
decimal_places = fields.Integer({ string: "decimal" });
|
||||
|
||||
get_company_currency_for_spreadsheet() {
|
||||
return {
|
||||
code: "EUR",
|
||||
symbol: "€",
|
||||
position: "after",
|
||||
decimalPlaces: 2,
|
||||
};
|
||||
}
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
name: "EUR",
|
||||
symbol: "€",
|
||||
position: "after",
|
||||
decimal_places: 2,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "USD",
|
||||
symbol: "$",
|
||||
position: "before",
|
||||
decimal_places: 2,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export class ResCountry extends webModels.ResCountry {
|
||||
_name = "res.country";
|
||||
name = fields.Char({ string: "Country" });
|
||||
code = fields.Char({ string: "Code" });
|
||||
|
||||
_records = [
|
||||
{ id: 1, name: "Belgium", code: "BE" },
|
||||
{ id: 2, name: "France", code: "FR" },
|
||||
{ id: 3, name: "United States", code: "US" },
|
||||
];
|
||||
}
|
||||
|
||||
export class ResCountryState extends models.Model {
|
||||
_name = "res.country.state";
|
||||
name = fields.Char({ string: "Name" });
|
||||
code = fields.Char({ string: "Code" });
|
||||
country_id = fields.Many2one({ relation: "res.country" });
|
||||
display_name = fields.Char({ string: "Display Name" });
|
||||
|
||||
_records = [
|
||||
{ id: 1, name: "California", code: "CA", country_id: 3, display_name: "California (US)" },
|
||||
{ id: 2, name: "New York", code: "NY", country_id: 3, display_name: "New York (US)" },
|
||||
{ id: 3, name: "Texas", code: "TX", country_id: 3, display_name: "Texas (US)" },
|
||||
];
|
||||
}
|
||||
|
||||
export class Partner extends models.Model {
|
||||
_name = "partner";
|
||||
|
||||
foo = fields.Integer({
|
||||
string: "Foo",
|
||||
store: true,
|
||||
searchable: true,
|
||||
aggregator: "sum",
|
||||
groupable: false,
|
||||
});
|
||||
bar = fields.Boolean({
|
||||
string: "Bar",
|
||||
store: true,
|
||||
sortable: true,
|
||||
groupable: true,
|
||||
searchable: true,
|
||||
});
|
||||
name = fields.Char({
|
||||
string: "name",
|
||||
store: true,
|
||||
sortable: true,
|
||||
groupable: true,
|
||||
searchable: true,
|
||||
});
|
||||
date = fields.Date({
|
||||
string: "Date",
|
||||
store: true,
|
||||
sortable: true,
|
||||
groupable: true,
|
||||
searchable: true,
|
||||
});
|
||||
create_date = fields.Datetime({
|
||||
string: "Creation Date",
|
||||
store: true,
|
||||
sortable: true,
|
||||
groupable: true,
|
||||
});
|
||||
active = fields.Boolean({
|
||||
string: "Active",
|
||||
default: true,
|
||||
searchable: true,
|
||||
groupable: false,
|
||||
});
|
||||
product_id = fields.Many2one({
|
||||
string: "Product",
|
||||
relation: "product",
|
||||
store: true,
|
||||
sortable: true,
|
||||
groupable: true,
|
||||
searchable: true,
|
||||
});
|
||||
tag_ids = fields.Many2many({
|
||||
string: "Tags",
|
||||
relation: "tag",
|
||||
store: true,
|
||||
sortable: true,
|
||||
groupable: true,
|
||||
searchable: true,
|
||||
});
|
||||
probability = fields.Float({
|
||||
string: "Probability",
|
||||
searchable: true,
|
||||
store: true,
|
||||
aggregator: "avg",
|
||||
groupable: false,
|
||||
});
|
||||
field_with_array_agg = fields.Integer({
|
||||
string: "field_with_array_agg",
|
||||
searchable: true,
|
||||
groupable: false,
|
||||
aggregator: "array_agg",
|
||||
});
|
||||
currency_id = fields.Many2one({
|
||||
string: "Currency",
|
||||
relation: "res.currency",
|
||||
store: true,
|
||||
sortable: true,
|
||||
groupable: true,
|
||||
searchable: true,
|
||||
});
|
||||
pognon = fields.Monetary({
|
||||
string: "Money!",
|
||||
currency_field: "currency_id",
|
||||
store: true,
|
||||
sortable: true,
|
||||
aggregator: "avg",
|
||||
groupable: true,
|
||||
searchable: true,
|
||||
});
|
||||
partner_properties = fields.Properties({
|
||||
string: "Properties",
|
||||
store: true,
|
||||
sortable: true,
|
||||
groupable: true,
|
||||
searchable: true,
|
||||
definition_record: "product_id",
|
||||
definition_record_field: "properties_definitions",
|
||||
});
|
||||
jsonField = fields.Json({ string: "Json Field", store: true, groupable: false });
|
||||
user_ids = fields.Many2many({
|
||||
relation: "res.users",
|
||||
string: "Users",
|
||||
searchable: true,
|
||||
groupable: false,
|
||||
});
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
foo: 12,
|
||||
bar: true,
|
||||
name: "Raoul",
|
||||
date: "2016-04-14",
|
||||
create_date: "2016-04-03 00:00:00",
|
||||
product_id: 37,
|
||||
probability: 10,
|
||||
field_with_array_agg: 1,
|
||||
tag_ids: [42, 67],
|
||||
currency_id: 1,
|
||||
pognon: 74.4,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
foo: 1,
|
||||
bar: true,
|
||||
name: "Steven",
|
||||
date: "2016-10-26",
|
||||
create_date: "2014-04-03 00:05:32",
|
||||
product_id: 41,
|
||||
probability: 11,
|
||||
field_with_array_agg: 2,
|
||||
tag_ids: [42, 67],
|
||||
currency_id: 2,
|
||||
pognon: 74.8,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
foo: 17,
|
||||
bar: true,
|
||||
name: "Taylor",
|
||||
date: "2016-12-15",
|
||||
create_date: "2006-01-03 11:30:50",
|
||||
product_id: 41,
|
||||
probability: 95,
|
||||
field_with_array_agg: 3,
|
||||
tag_ids: [],
|
||||
currency_id: 1,
|
||||
pognon: 4,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
foo: 2,
|
||||
bar: false,
|
||||
name: "Zara",
|
||||
date: "2016-12-11",
|
||||
create_date: "2016-12-10 21:59:59",
|
||||
product_id: 41,
|
||||
probability: 15,
|
||||
field_with_array_agg: 4,
|
||||
tag_ids: [42],
|
||||
currency_id: 2,
|
||||
pognon: 1000,
|
||||
},
|
||||
];
|
||||
|
||||
// TODO: check which views are actually needed in the tests
|
||||
_views = {
|
||||
list: getBasicListArch(),
|
||||
pivot: getBasicPivotArch(),
|
||||
graph: getBasicGraphArch(),
|
||||
};
|
||||
}
|
||||
|
||||
export class Product extends models.Model {
|
||||
_name = "product";
|
||||
|
||||
name = fields.Char({ string: "Product Name" });
|
||||
display_name = fields.Char({ string: "Product Name" });
|
||||
active = fields.Boolean({ string: "Active", default: true });
|
||||
template_id = fields.Many2one({
|
||||
string: "Template",
|
||||
relation: "product",
|
||||
store: true,
|
||||
sortable: true,
|
||||
groupable: true,
|
||||
searchable: true,
|
||||
});
|
||||
properties_definitions = fields.PropertiesDefinition();
|
||||
pognon = fields.Monetary({
|
||||
string: "Money!",
|
||||
currency_field: "currency_id",
|
||||
store: true,
|
||||
sortable: true,
|
||||
aggregator: "avg",
|
||||
groupable: true,
|
||||
searchable: true,
|
||||
});
|
||||
currency_id = fields.Many2one({
|
||||
string: "Currency",
|
||||
relation: "res.currency",
|
||||
store: true,
|
||||
sortable: true,
|
||||
groupable: true,
|
||||
searchable: true,
|
||||
});
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 37,
|
||||
display_name: "xphone",
|
||||
name: "xphone",
|
||||
currency_id: 2,
|
||||
pognon: 699.99,
|
||||
},
|
||||
{
|
||||
id: 41,
|
||||
display_name: "xpad",
|
||||
template_id: 37,
|
||||
name: "xpad",
|
||||
currency_id: 2,
|
||||
pognon: 599.99,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export class Tag extends models.Model {
|
||||
_name = "tag";
|
||||
|
||||
name = fields.Char({ string: "Tag Name" });
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 42,
|
||||
name: "isCool",
|
||||
},
|
||||
{
|
||||
id: 67,
|
||||
name: "Growing",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function getBasicData() {
|
||||
return {
|
||||
"documents.document": {},
|
||||
"ir.model": {},
|
||||
"ir.embedded.actions": {},
|
||||
"documents.tag": {},
|
||||
"spreadsheet.template": {},
|
||||
"res.currency": {},
|
||||
"res.users": {},
|
||||
partner: {},
|
||||
product: {},
|
||||
tag: {},
|
||||
};
|
||||
}
|
||||
|
||||
export const SpreadsheetModels = {
|
||||
...webModels,
|
||||
...mailModels,
|
||||
IrModel,
|
||||
IrUIMenu,
|
||||
IrActions,
|
||||
ResGroup,
|
||||
ResUsers,
|
||||
ResCountry,
|
||||
ResCountryState,
|
||||
SpreadsheetMixin,
|
||||
ResCurrency,
|
||||
Partner,
|
||||
Product,
|
||||
Tag,
|
||||
};
|
||||
|
||||
/**
|
||||
* Add the records inside serverData in the MockServer
|
||||
*
|
||||
* @param {ServerData} serverData
|
||||
*/
|
||||
export function addRecordsFromServerData(serverData) {
|
||||
for (const modelName of Object.keys(serverData.models)) {
|
||||
const records = serverData.models[modelName].records;
|
||||
if (!records) {
|
||||
continue;
|
||||
}
|
||||
const PyModel = getSpreadsheetModel(modelName);
|
||||
if (!PyModel) {
|
||||
throw new Error(`Model ${modelName} not found inside SpreadsheetModels`);
|
||||
}
|
||||
checkRecordsValidity(modelName, records);
|
||||
PyModel._records = records;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the views inside serverData in the MockServer
|
||||
*
|
||||
* @param {ServerData} serverData
|
||||
*
|
||||
* @example
|
||||
* addViewsFromServerData({ "partner,false,search": "<search/>" });
|
||||
* Will set the default search view for the partner model
|
||||
*/
|
||||
export function addViewsFromServerData(serverData) {
|
||||
for (const fullViewKey of Object.keys(serverData.views)) {
|
||||
const viewArch = serverData.views[fullViewKey];
|
||||
const splitted = fullViewKey.split(",");
|
||||
const modelName = splitted[0];
|
||||
const viewType = splitted[2];
|
||||
const recordId = splitted[1];
|
||||
const PyModel = getSpreadsheetModel(modelName);
|
||||
if (!PyModel) {
|
||||
throw new Error(`Model ${modelName} not found inside SpreadsheetModels`);
|
||||
}
|
||||
const viewKey = viewType + "," + recordId;
|
||||
PyModel._views[viewKey] = viewArch;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the records are valid.
|
||||
* This is mainly to avoid the mail's service crashing if the res.users are not correctly set.
|
||||
*/
|
||||
function checkRecordsValidity(modelName, records) {
|
||||
if (modelName === "res.users") {
|
||||
const serverUserId = serverState.userId;
|
||||
const currentUser = records.find((record) => record.id === serverUserId);
|
||||
if (!currentUser) {
|
||||
throw new Error(
|
||||
`The current user (${serverUserId}) is not in the records. did you forget to set serverState.userId ?`
|
||||
);
|
||||
}
|
||||
if (!currentUser.active) {
|
||||
throw new Error(`The current user (${serverUserId}) is not active`);
|
||||
}
|
||||
if (!currentUser.partner_id) {
|
||||
throw new Error(
|
||||
`The current user (${serverUserId}) has no partner_id. It should be set to serverState.partnerId`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getPyEnv() {
|
||||
const mockServer = MockServer.current;
|
||||
if (!mockServer) {
|
||||
throw new Error("No mock server found");
|
||||
}
|
||||
return mockServer.env;
|
||||
}
|
||||
|
||||
export function getSpreadsheetModel(modelName) {
|
||||
return Object.values(SpreadsheetModels).find((model) => model._name === modelName);
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
/** @odoo-module */
|
||||
|
||||
const { DateTime } = luxon;
|
||||
import { Domain } from "@web/core/domain";
|
||||
import { expect } from "@odoo/hoot";
|
||||
|
||||
function getDateDomainBounds(domain) {
|
||||
const startDateStr = domain[1][2];
|
||||
|
|
@ -27,17 +26,16 @@ function getDateDomainBounds(domain) {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {object} assert
|
||||
* @param {string} field
|
||||
* @param {string} start
|
||||
* @param {string} end
|
||||
* @param {import("@web/core/domain").DomainRepr} domain
|
||||
*/
|
||||
export function assertDateDomainEqual(assert, field, start, end, domain) {
|
||||
export function assertDateDomainEqual(field, start, end, domain) {
|
||||
domain = new Domain(domain).toList();
|
||||
assert.deepEqual(domain[0], "&");
|
||||
assert.deepEqual(domain[1], [field, ">=", start]);
|
||||
assert.deepEqual(domain[2], [field, "<=", end]);
|
||||
expect(domain[0]).toBe("&");
|
||||
expect(domain[1]).toEqual([field, ">=", start]);
|
||||
expect(domain[2]).toEqual([field, "<=", end]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
import * as spreadsheet from "@odoo/o-spreadsheet";
|
||||
import { range } from "@web/core/utils/numbers";
|
||||
|
||||
const { toCartesian, toZone, toXC } = spreadsheet.helpers;
|
||||
|
||||
/**
|
||||
* Get the value of the given cell
|
||||
*/
|
||||
export function getCellValue(model, xc, sheetId = model.getters.getActiveSheetId()) {
|
||||
const { col, row } = toCartesian(xc);
|
||||
const cell = model.getters.getEvaluatedCell({ sheetId, col, row });
|
||||
return cell.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cell of the given xc
|
||||
*/
|
||||
export function getCell(model, xc, sheetId = model.getters.getActiveSheetId()) {
|
||||
const { col, row } = toCartesian(xc);
|
||||
return model.getters.getCell({ sheetId, col, row });
|
||||
}
|
||||
|
||||
export function getEvaluatedCell(model, xc, sheetId = model.getters.getActiveSheetId()) {
|
||||
const { col, row } = toCartesian(xc);
|
||||
return model.getters.getEvaluatedCell({ sheetId, col, row });
|
||||
}
|
||||
|
||||
export function getEvaluatedGrid(model, zoneXc, sheetId = model.getters.getActiveSheetId()) {
|
||||
const { top, bottom, left, right } = toZone(zoneXc);
|
||||
const grid = [];
|
||||
for (const row of range(top, bottom + 1)) {
|
||||
const colValues = [];
|
||||
grid.push(colValues);
|
||||
for (const col of range(left, right + 1)) {
|
||||
const cell = model.getters.getEvaluatedCell({ sheetId, col, row });
|
||||
colValues.push(cell.value);
|
||||
}
|
||||
}
|
||||
return grid;
|
||||
}
|
||||
|
||||
export function getEvaluatedFormatGrid(model, zoneXc, sheetId = model.getters.getActiveSheetId()) {
|
||||
const { top, bottom, left, right } = toZone(zoneXc);
|
||||
const grid = [];
|
||||
for (const row of range(top, bottom + 1)) {
|
||||
grid.push([]);
|
||||
for (const col of range(left, right + 1)) {
|
||||
const cell = model.getters.getEvaluatedCell({ sheetId, col, row });
|
||||
grid[row][col] = cell.format;
|
||||
}
|
||||
}
|
||||
return grid;
|
||||
}
|
||||
|
||||
export function getFormattedValueGrid(model, zoneXc, sheetId = model.getters.getActiveSheetId()) {
|
||||
const { top, bottom, left, right } = toZone(zoneXc);
|
||||
const grid = {};
|
||||
for (const row of range(top, bottom + 1)) {
|
||||
for (const col of range(left, right + 1)) {
|
||||
const cell = model.getters.getEvaluatedCell({ sheetId, col, row });
|
||||
grid[toXC(col, row)] = cell.formattedValue;
|
||||
}
|
||||
}
|
||||
return grid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cells of the given sheet (or active sheet if not provided)
|
||||
*/
|
||||
export function getCells(model, sheetId = model.getters.getActiveSheetId()) {
|
||||
return model.getters.getCells(sheetId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the formula of the given xc
|
||||
*/
|
||||
export function getCellFormula(model, xc, sheetId = model.getters.getActiveSheetId()) {
|
||||
const cell = getCell(model, xc, sheetId);
|
||||
return cell && cell.isFormula ? cell.content : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content of the given xc
|
||||
*/
|
||||
export function getCellContent(model, xc, sheetId = model.getters.getActiveSheetId()) {
|
||||
const { col, row } = toCartesian(xc);
|
||||
return model.getters.getCellText({ sheetId, col, row }, { showFormula: true });
|
||||
}
|
||||
|
||||
export function getCorrespondingCellFormula(model, xc, sheetId = model.getters.getActiveSheetId()) {
|
||||
const cell = model.getters.getCorrespondingFormulaCell({ sheetId, ...toCartesian(xc) });
|
||||
return cell && cell.isFormula ? cell.content : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of the merges (["A1:A2"]) of the sheet
|
||||
*/
|
||||
export function getMerges(model, sheetId = model.getters.getActiveSheetId()) {
|
||||
return model.exportData().sheets.find((sheet) => sheet.id === sheetId).merges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the borders at the given XC
|
||||
*/
|
||||
export function getBorders(model, xc, sheetId = model.getters.getActiveSheetId()) {
|
||||
const { col, row } = toCartesian(xc);
|
||||
const borders = model.getters.getCellBorder({ sheetId, col, row });
|
||||
if (!borders) {
|
||||
return null;
|
||||
}
|
||||
Object.keys(borders).forEach((key) => borders[key] === undefined && delete borders[key]);
|
||||
return borders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the formatted value of the given xc
|
||||
*/
|
||||
export function getCellFormattedValue(model, xc, sheetId = model.getters.getActiveSheetId()) {
|
||||
const { col, row } = toCartesian(xc);
|
||||
return model.getters.getCellText({ sheetId, col, row }, false);
|
||||
}
|
||||
|
||||
export function getCellIcons(model, xc) {
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
return model.getters.getCellIcons({ ...toCartesian(xc), sheetId });
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
* @typedef {import("@spreadsheet/global_filters/plugins/global_filters_core_plugin").GlobalFilter} GlobalFilter
|
||||
*
|
||||
*/
|
||||
|
||||
/** @type FixedPeriodDateGlobalFilter */
|
||||
export const THIS_YEAR_GLOBAL_FILTER = {
|
||||
id: "43",
|
||||
type: "date",
|
||||
label: "This Year",
|
||||
defaultValue: "this_year",
|
||||
};
|
||||
|
|
@ -1,12 +1,10 @@
|
|||
/** @odoo-module */
|
||||
import { waitForDataLoaded } from "@spreadsheet/helpers/model";
|
||||
import { generateListDefinition } from "@spreadsheet/../tests/helpers/data";
|
||||
import { createModelWithDataSource } from "@spreadsheet/../tests/helpers/model";
|
||||
|
||||
import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended";
|
||||
import { generateListDefinition } from "./data";
|
||||
import { createModelWithDataSource, waitForDataSourcesLoaded } from "./model";
|
||||
|
||||
const uuidGenerator = new spreadsheet.helpers.UuidGenerator();
|
||||
|
||||
/** @typedef {import("@spreadsheet/o_spreadsheet/o_spreadsheet").Model} Model */
|
||||
/**
|
||||
* @typedef {import("@spreadsheet/o_spreadsheet/o_spreadsheet").Model} Model
|
||||
*/
|
||||
|
||||
/**
|
||||
* Insert a list in a spreadsheet model.
|
||||
|
|
@ -18,9 +16,15 @@ const uuidGenerator = new spreadsheet.helpers.UuidGenerator();
|
|||
* @param {number} [params.linesNumber]
|
||||
* @param {[number, number]} [params.position]
|
||||
* @param {string} [params.sheetId]
|
||||
* @param {{name: string, asc: boolean}[]} [params.orderBy]
|
||||
*/
|
||||
export function insertListInSpreadsheet(model, params) {
|
||||
const { definition, columns } = generateListDefinition(params.model, params.columns);
|
||||
const { definition, columns } = generateListDefinition(
|
||||
params.model,
|
||||
params.columns,
|
||||
params.actionXmlId,
|
||||
params.orderBy
|
||||
);
|
||||
const [col, row] = params.position || [0, 0];
|
||||
|
||||
model.dispatch("INSERT_ODOO_LIST", {
|
||||
|
|
@ -31,7 +35,6 @@ export function insertListInSpreadsheet(model, params) {
|
|||
id: model.getters.getNextListId(),
|
||||
col,
|
||||
row,
|
||||
dataSourceId: uuidGenerator.uuidv4(),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -45,13 +48,16 @@ export function insertListInSpreadsheet(model, params) {
|
|||
* @param {number} [params.linesNumber]
|
||||
* @param {[number, number]} [params.position]
|
||||
* @param {string} [params.sheetId]
|
||||
* @param {object} [params.modelConfig]
|
||||
* @param {{name: string, asc: boolean}[]} [params.orderBy]
|
||||
*
|
||||
* @returns { Promise<{ model: Model, env: Object }>}
|
||||
* @returns { Promise<{ model: OdooSpreadsheetModel, env: Object }>}
|
||||
*/
|
||||
export async function createSpreadsheetWithList(params = {}) {
|
||||
const model = await createModelWithDataSource({
|
||||
const { model, env } = await createModelWithDataSource({
|
||||
mockRPC: params.mockRPC,
|
||||
serverData: params.serverData,
|
||||
modelConfig: params.modelConfig,
|
||||
});
|
||||
|
||||
insertListInSpreadsheet(model, {
|
||||
|
|
@ -60,10 +66,9 @@ export async function createSpreadsheetWithList(params = {}) {
|
|||
linesNumber: params.linesNumber,
|
||||
position: params.position,
|
||||
sheetId: params.sheetId,
|
||||
orderBy: params.orderBy,
|
||||
});
|
||||
|
||||
const env = model.config.evalContext.env;
|
||||
env.model = model;
|
||||
await waitForDataSourcesLoaded(model);
|
||||
await waitForDataLoaded(model);
|
||||
return { model, env };
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { Model } from "@odoo/o-spreadsheet";
|
||||
import { OdooDataProvider } from "@spreadsheet/data_sources/odoo_data_provider";
|
||||
import {
|
||||
defineActions,
|
||||
defineMenus,
|
||||
getMockEnv,
|
||||
makeMockEnv,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { setCellContent } from "./commands";
|
||||
import { addRecordsFromServerData, addViewsFromServerData } from "./data";
|
||||
|
||||
/**
|
||||
* @typedef {import("@spreadsheet/../tests/helpers/data").ServerData} ServerData
|
||||
* @typedef {import("@spreadsheet/helpers/model").OdooSpreadsheetModel} OdooSpreadsheetModel
|
||||
*/
|
||||
|
||||
export function setupDataSourceEvaluation(model) {
|
||||
model.config.custom.odooDataProvider.addEventListener("data-source-updated", () => {
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
model.dispatch("EVALUATE_CELLS", { sheetId });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a spreadsheet model with a mocked server environnement
|
||||
*
|
||||
* @param {object} params
|
||||
* @param {object} [params.spreadsheetData] Spreadsheet data to import
|
||||
* @param {object} [params.modelConfig]
|
||||
* @param {ServerData} [params.serverData] Data to be injected in the mock server
|
||||
* @param {function} [params.mockRPC] Mock rpc function
|
||||
* @returns {Promise<{ model: OdooSpreadsheetModel, env: Object }>}
|
||||
*/
|
||||
export async function createModelWithDataSource(params = {}) {
|
||||
const env = await makeSpreadsheetMockEnv(params);
|
||||
const config = params.modelConfig;
|
||||
/** @type any*/
|
||||
const model = new Model(params.spreadsheetData, {
|
||||
...config,
|
||||
custom: {
|
||||
env,
|
||||
odooDataProvider: new OdooDataProvider(env),
|
||||
...config?.custom,
|
||||
},
|
||||
});
|
||||
env.model = model;
|
||||
// if (params.serverData) {
|
||||
// await addRecordsFromServerData(params.serverData);
|
||||
// }
|
||||
setupDataSourceEvaluation(model);
|
||||
await animationFrame(); // initial async formulas loading
|
||||
return { model, env };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a mocked server environnement
|
||||
*
|
||||
* @param {object} params
|
||||
* @param {object} [params.spreadsheetData] Spreadsheet data to import
|
||||
* @param {ServerData} [params.serverData] Data to be injected in the mock server
|
||||
* @param {function} [params.mockRPC] Mock rpc function
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export async function makeSpreadsheetMockEnv(params = {}) {
|
||||
if (params.mockRPC) {
|
||||
// Note: calling onRpc with only a callback only works for routes such as orm routes that have a default listener
|
||||
// For arbitrary rpc request (eg. /web/domain/validate) we need to call onRpc("/my/route", callback)
|
||||
onRpc((args) => params.mockRPC(args.route, args)); // separate route from args for legacy (& forward ports) compatibility
|
||||
}
|
||||
if (params.serverData?.menus) {
|
||||
defineMenus(Object.values(params.serverData.menus));
|
||||
}
|
||||
if (params.serverData?.actions) {
|
||||
defineActions(Object.values(params.serverData.actions));
|
||||
}
|
||||
if (params.serverData?.models) {
|
||||
addRecordsFromServerData(params.serverData);
|
||||
}
|
||||
if (params.serverData?.views) {
|
||||
addViewsFromServerData(params.serverData);
|
||||
}
|
||||
const env = getMockEnv() || (await makeMockEnv());
|
||||
return env;
|
||||
}
|
||||
|
||||
export function createModelFromGrid(grid) {
|
||||
const model = new Model();
|
||||
for (const xc in grid) {
|
||||
if (grid[xc] !== undefined) {
|
||||
setCellContent(model, xc, grid[xc]);
|
||||
}
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
import { PivotArchParser } from "@web/views/pivot/pivot_arch_parser";
|
||||
import { OdooPivot } from "@spreadsheet/pivot/odoo_pivot";
|
||||
import { getBasicPivotArch, getPyEnv } from "@spreadsheet/../tests/helpers/data";
|
||||
import { createModelWithDataSource } from "@spreadsheet/../tests/helpers/model";
|
||||
import { waitForDataLoaded } from "@spreadsheet/helpers/model";
|
||||
import { helpers } from "@odoo/o-spreadsheet";
|
||||
const { parseDimension, isDateOrDatetimeField } = helpers;
|
||||
|
||||
/**
|
||||
* @typedef {import("@spreadsheet").OdooSpreadsheetModel} OdooSpreadsheetModel
|
||||
* @typedef {import("@spreadsheet").Zone} Zone
|
||||
*/
|
||||
|
||||
function addEmptyGranularity(dimensions, fields) {
|
||||
return dimensions.map((dimension) => {
|
||||
if (dimension.fieldName !== "id" && isDateOrDatetimeField(fields[dimension.fieldName])) {
|
||||
return {
|
||||
granularity: "month",
|
||||
...dimension,
|
||||
};
|
||||
}
|
||||
return dimension;
|
||||
});
|
||||
}
|
||||
|
||||
async function insertStaticPivot(model, pivotId, params) {
|
||||
const ds = model.getters.getPivot(pivotId);
|
||||
if (!(ds instanceof OdooPivot)) {
|
||||
throw new Error("The pivot data source is not an OdooPivot");
|
||||
}
|
||||
const [col, row] = params.anchor || [0, 0];
|
||||
await ds.load();
|
||||
const { cols, rows, measures, fieldsType } = ds.getExpandedTableStructure().export();
|
||||
const table = {
|
||||
cols,
|
||||
rows,
|
||||
measures,
|
||||
fieldsType,
|
||||
};
|
||||
model.dispatch("INSERT_PIVOT", {
|
||||
pivotId,
|
||||
sheetId: params.sheetId || model.getters.getActiveSheetId(),
|
||||
col,
|
||||
row,
|
||||
table,
|
||||
});
|
||||
}
|
||||
|
||||
function insertDynamicPivot(model, pivotId, params) {
|
||||
const pivotFormulaId = model.getters.getPivotFormulaId(pivotId);
|
||||
const [col, row] = params.anchor || [0, 0];
|
||||
model.dispatch("UPDATE_CELL", {
|
||||
sheetId: params.sheetId || model.getters.getActiveSheetId(),
|
||||
col,
|
||||
row,
|
||||
content: `=PIVOT(${pivotFormulaId})`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {OdooSpreadsheetModel} model
|
||||
* @param {string} pivotId
|
||||
* @param {object} params
|
||||
* @param {string} [params.arch]
|
||||
* @param {string} [params.resModel]
|
||||
* @param {object} [params.serverData]
|
||||
* @param {string} [params.sheetId]
|
||||
* @param {["static"|"dynamic"]} [params.pivotType]
|
||||
* @param {[number, number]} [params.anchor]
|
||||
*/
|
||||
export async function insertPivotInSpreadsheet(model, pivotId, params) {
|
||||
const archInfo = new PivotArchParser().parse(params.arch || getBasicPivotArch());
|
||||
const resModel = params.resModel || "partner";
|
||||
|
||||
const pyEnv = getPyEnv();
|
||||
const pivot = {
|
||||
type: "ODOO",
|
||||
domain: [],
|
||||
context: {},
|
||||
measures: archInfo.activeMeasures.map((measure) => ({
|
||||
id: pyEnv[resModel]._fields[measure]?.aggregator
|
||||
? `${measure}:${pyEnv[resModel]._fields[measure].aggregator}`
|
||||
: measure,
|
||||
fieldName: measure,
|
||||
aggregator: pyEnv[resModel]._fields[measure]?.aggregator,
|
||||
})),
|
||||
model: resModel,
|
||||
columns: addEmptyGranularity(
|
||||
archInfo.colGroupBys.map(parseDimension),
|
||||
pyEnv[resModel]._fields
|
||||
),
|
||||
rows: addEmptyGranularity(
|
||||
archInfo.rowGroupBys.map(parseDimension),
|
||||
pyEnv[resModel]._fields
|
||||
),
|
||||
name: "Partner Pivot",
|
||||
};
|
||||
model.dispatch("ADD_PIVOT", {
|
||||
pivotId,
|
||||
pivot,
|
||||
});
|
||||
if (params.pivotType === "static") {
|
||||
await insertStaticPivot(model, pivotId, params);
|
||||
} else {
|
||||
insertDynamicPivot(model, pivotId, params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} params
|
||||
* @param {string} [params.arch]
|
||||
* @param {object} [params.serverData]
|
||||
* @param {function} [params.mockRPC]
|
||||
* @param {"dynamic"|"static"} [params.pivotType]
|
||||
* @returns {Promise<{ model: OdooSpreadsheetModel, env: object, pivotId: string}>}
|
||||
*/
|
||||
export async function createSpreadsheetWithPivot(params = {}) {
|
||||
const { model, env } = await createModelWithDataSource({
|
||||
mockRPC: params.mockRPC,
|
||||
serverData: params.serverData,
|
||||
});
|
||||
const arch = params.arch || getBasicPivotArch();
|
||||
const pivotId = "PIVOT#1";
|
||||
await insertPivotInSpreadsheet(model, pivotId, { arch, pivotType: params.pivotType });
|
||||
await waitForDataLoaded(model);
|
||||
return { model, env, pivotId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the zone that contains all the cells of the given data source
|
||||
*
|
||||
* TODO: Un-duplicate this once this or #50623 is merged
|
||||
*
|
||||
* @param model
|
||||
* @param {"pivot" | "list"} dataSourceType
|
||||
* @param {string} id
|
||||
* @returns {Zone}
|
||||
*/
|
||||
export function getZoneOfInsertedDataSource(model, dataSourceType, id) {
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
const cells = model.getters.getCells(sheetId);
|
||||
const positions = Object.keys(cells).map(model.getters.getCellPosition);
|
||||
|
||||
let top = 0;
|
||||
let left = 0;
|
||||
let bottom = 0;
|
||||
let right = 0;
|
||||
for (const position of positions) {
|
||||
const cellDataSourceId =
|
||||
dataSourceType === "pivot"
|
||||
? model.getters.getPivotIdFromPosition({ sheetId, ...position })
|
||||
: model.getters.getListIdFromPosition({ sheetId, ...position });
|
||||
if (id !== cellDataSourceId) {
|
||||
continue;
|
||||
}
|
||||
top = Math.min(top, position.row);
|
||||
left = Math.min(left, position.col);
|
||||
bottom = Math.max(bottom, position.row);
|
||||
right = Math.max(right, position.col);
|
||||
}
|
||||
return { top, bottom, left, right };
|
||||
}
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { nextTick } from "@web/../tests/helpers/utils";
|
||||
import { createSpreadsheetWithPivot } from "./pivot";
|
||||
import { insertListInSpreadsheet } from "./list";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { createSpreadsheetWithPivot } from "@spreadsheet/../tests/helpers/pivot";
|
||||
import { insertListInSpreadsheet } from "@spreadsheet/../tests/helpers/list";
|
||||
|
||||
export async function createSpreadsheetWithPivotAndList() {
|
||||
const { model, env } = await createSpreadsheetWithPivot();
|
||||
|
|
@ -10,6 +8,6 @@ export async function createSpreadsheetWithPivotAndList() {
|
|||
model: "partner",
|
||||
columns: ["foo", "bar", "date", "product_id"],
|
||||
});
|
||||
await nextTick();
|
||||
await animationFrame();
|
||||
return { env, model };
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
// @ts-check
|
||||
|
||||
import { stores } from "@odoo/o-spreadsheet";
|
||||
import { createModelWithDataSource } from "@spreadsheet/../tests/helpers/model";
|
||||
|
||||
const { ModelStore, NotificationStore, DependencyContainer } = stores;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {import("@odoo/o-spreadsheet").StoreConstructor<T>} StoreConstructor<T>
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import("@spreadsheet").OdooSpreadsheetModel} OdooSpreadsheetModel
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {StoreConstructor<T>} Store
|
||||
* @param {any[]} args
|
||||
* @return {Promise<{ store: T, container: InstanceType<DependencyContainer>, model: OdooSpreadsheetModel }>}
|
||||
*/
|
||||
export async function makeStore(Store, ...args) {
|
||||
const { model } = await createModelWithDataSource();
|
||||
return makeStoreWithModel(model, Store, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {import("@odoo/o-spreadsheet").Model} model
|
||||
* @param {StoreConstructor<T>} Store
|
||||
* @param {any[]} args
|
||||
* @return {{ store: T, container: InstanceType<DependencyContainer>, model: OdooSpreadsheetModel }}
|
||||
*/
|
||||
export function makeStoreWithModel(model, Store, ...args) {
|
||||
const container = new DependencyContainer();
|
||||
container.inject(ModelStore, model);
|
||||
container.inject(NotificationStore, makeTestNotificationStore());
|
||||
return {
|
||||
store: container.instantiate(Store, ...args),
|
||||
container,
|
||||
// @ts-ignore
|
||||
model: container.get(ModelStore),
|
||||
};
|
||||
}
|
||||
|
||||
function makeTestNotificationStore() {
|
||||
return {
|
||||
notifyUser: () => {},
|
||||
raiseError: () => {},
|
||||
askConfirmation: () => {},
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import { Model, Spreadsheet } from "@odoo/o-spreadsheet";
|
||||
import { loadBundle } from "@web/core/assets";
|
||||
|
||||
import { getFixture } from "@odoo/hoot";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { useSpreadsheetNotificationStore } from "@spreadsheet/hooks";
|
||||
import { PublicReadonlySpreadsheet } from "@spreadsheet/public_readonly_app/public_readonly";
|
||||
import { mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`<Spreadsheet model="props.model"/>`;
|
||||
static components = { Spreadsheet };
|
||||
static props = { model: Model };
|
||||
setup() {
|
||||
useSpreadsheetNotificationStore();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mount o-spreadsheet component with the given spreadsheet model
|
||||
* @param {Model} model
|
||||
* @returns {Promise<HTMLElement>}
|
||||
*/
|
||||
export async function mountSpreadsheet(model) {
|
||||
// const serviceRegistry = registry.category("services");
|
||||
// serviceRegistry.add("dialog", makeFakeDialogService(), { force: true });
|
||||
// serviceRegistry.add("notification", makeFakeNotificationService(), { force: true });
|
||||
await loadBundle("web.chartjs_lib");
|
||||
mountWithCleanup(Parent, {
|
||||
props: {
|
||||
model,
|
||||
},
|
||||
env: model.config.custom.env,
|
||||
noMainContainer: true,
|
||||
});
|
||||
await animationFrame();
|
||||
return getFixture();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mount public spreadsheet component with the given data
|
||||
* @returns {Promise<HTMLElement>}
|
||||
*/
|
||||
export async function mountPublicSpreadsheet(dataUrl, mode, downloadExcelUrl = "") {
|
||||
mountWithCleanup(PublicReadonlySpreadsheet, {
|
||||
props: {
|
||||
dataUrl,
|
||||
downloadExcelUrl,
|
||||
mode,
|
||||
},
|
||||
noMainContainer: true,
|
||||
});
|
||||
await animationFrame();
|
||||
return getFixture();
|
||||
}
|
||||
|
||||
export async function doMenuAction(registry, path, env) {
|
||||
await getActionMenu(registry, path, env).execute(env);
|
||||
}
|
||||
|
||||
export function getActionMenu(registry, _path, env) {
|
||||
const path = [..._path];
|
||||
let items = registry.getMenuItems();
|
||||
while (items.length && path.length) {
|
||||
const id = path.shift();
|
||||
const item = items.find((item) => item.id === id);
|
||||
if (!item) {
|
||||
throw new Error(`Menu item ${id} not found`);
|
||||
}
|
||||
if (path.length === 0) {
|
||||
return item;
|
||||
}
|
||||
items = item.children(env);
|
||||
}
|
||||
throw new Error(`Menu item not found`);
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { helpers } from "@odoo/o-spreadsheet";
|
||||
|
||||
const { toUnboundedZone } = helpers;
|
||||
|
||||
export function toRangeData(sheetId, xc) {
|
||||
return { _zone: toUnboundedZone(xc), _sheetId: sheetId };
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { __debug__ } from "@odoo/hoot";
|
||||
import { stores } from "@odoo/o-spreadsheet";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
const { RendererStore } = stores;
|
||||
|
||||
/**
|
||||
* When rendering a spreadsheet, drawing the grid in a canvas is a costly operation,
|
||||
* perhaps the most costly operation. This patch allows to skip the grid/canvas drawing
|
||||
* in standard tests, which speeds up the overall execution.
|
||||
*
|
||||
* For debugging urpose, the patch is ineffective when running tests in debug mode
|
||||
* and the grid is drawn as usual.
|
||||
*/
|
||||
patch(RendererStore.prototype, {
|
||||
drawLayer(ctx, layer) {
|
||||
if (__debug__.debug) {
|
||||
return super.drawLayer(ctx, layer);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -1,56 +1,82 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { getBasicData } from "@spreadsheet/../tests/utils/data";
|
||||
|
||||
import { getBasicData } from "@spreadsheet/../tests/helpers/data";
|
||||
import { serverState } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export function getMenuServerData() {
|
||||
const serverData = {};
|
||||
serverData.menus = {
|
||||
root: { id: "root", children: [1, 2], name: "root", appID: "root" },
|
||||
1: {
|
||||
id: 1,
|
||||
children: [],
|
||||
name: "menu with xmlid",
|
||||
name: "App_1",
|
||||
appID: 1,
|
||||
xmlid: "test_menu",
|
||||
actionID: "action1",
|
||||
xmlid: "app_1",
|
||||
children: [
|
||||
{
|
||||
id: 11,
|
||||
name: "menu with xmlid",
|
||||
appID: 1,
|
||||
xmlid: "test_menu",
|
||||
actionID: "spreadsheet.action1",
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: "menu without xmlid",
|
||||
actionID: "spreadsheet.action1",
|
||||
appID: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
2: { id: 2, children: [], name: "menu without xmlid", appID: 2 },
|
||||
};
|
||||
serverData.actions = {
|
||||
action1: {
|
||||
id: 99,
|
||||
xml_id: "action1",
|
||||
xml_id: "spreadsheet.action1",
|
||||
name: "action1",
|
||||
res_model: "ir.ui.menu",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "list"]],
|
||||
views: [
|
||||
[false, "list"],
|
||||
[false, "form"],
|
||||
],
|
||||
},
|
||||
action2: {
|
||||
id: 199,
|
||||
xml_id: "spreadsheet.action2",
|
||||
name: "action1",
|
||||
res_model: "ir.ui.menu",
|
||||
type: "ir.actions.act_window",
|
||||
views: [
|
||||
[false, "graph"],
|
||||
[false, "pivot"],
|
||||
],
|
||||
},
|
||||
};
|
||||
serverData.views = {};
|
||||
serverData.views["ir.ui.menu,false,list"] = `<tree></tree>`;
|
||||
serverData.views["ir.ui.menu,false,search"] = `<search></search>`;
|
||||
serverData.models = {
|
||||
...getBasicData(),
|
||||
"ir.ui.menu": {
|
||||
fields: {
|
||||
name: { string: "Name", type: "char" },
|
||||
action: { string: "Action", type: "char" },
|
||||
groups_id: { string: "Groups", type: "many2many", relation: "res.group" },
|
||||
group_ids: { string: "Groups", type: "many2many", relation: "res.group" },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, name: "menu with xmlid", action: "action1", groups_id: [10] },
|
||||
{ id: 2, name: "menu without xmlid", action: "action2", groups_id: [10] },
|
||||
{ id: 11, name: "menu with xmlid", action: "action1", group_ids: [10] },
|
||||
{ id: 12, name: "menu without xmlid", action: "action1", group_ids: [10] },
|
||||
],
|
||||
},
|
||||
"res.users": {
|
||||
fields: {
|
||||
name: { string: "Name", type: "char" },
|
||||
groups_id: { string: "Groups", type: "many2many", relation: "res.group" },
|
||||
group_ids: { string: "Groups", type: "many2many", relation: "res.group" },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, name: "Raoul", groups_id: [10] },
|
||||
{ id: 2, name: "Joseph", groups_id: [] },
|
||||
{
|
||||
id: 1,
|
||||
name: "Raoul",
|
||||
active: true,
|
||||
partner_id: serverState.partnerId,
|
||||
group_ids: [10],
|
||||
},
|
||||
{ id: 2, name: "Joseph", group_ids: [] },
|
||||
],
|
||||
},
|
||||
"res.group": {
|
||||
|
|
@ -58,5 +84,6 @@ export function getMenuServerData() {
|
|||
records: [{ id: 10, name: "test group" }],
|
||||
},
|
||||
};
|
||||
serverState.userId = 1;
|
||||
return serverData;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,100 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { Model } from "@odoo/o-spreadsheet";
|
||||
import { defineSpreadsheetModels } from "@spreadsheet/../tests/helpers/data";
|
||||
import { makeSpreadsheetMockEnv } from "@spreadsheet/../tests/helpers/model";
|
||||
|
||||
import { setCellContent } from "@spreadsheet/../tests/helpers/commands";
|
||||
import { getCell, getEvaluatedCell } from "@spreadsheet/../tests/helpers/getters";
|
||||
import { getMenuServerData } from "../menu_data_utils";
|
||||
|
||||
describe.current.tags("headless");
|
||||
defineSpreadsheetModels();
|
||||
|
||||
test("ir.menu linked based on xml id", async function () {
|
||||
const env = await makeSpreadsheetMockEnv({ serverData: getMenuServerData() });
|
||||
const model = new Model({}, { custom: { env } });
|
||||
setCellContent(model, "A1", "[label](odoo://ir_menu_xml_id/test_menu)");
|
||||
const cell = getCell(model, "A1");
|
||||
const evaluatedCell = getEvaluatedCell(model, "A1");
|
||||
expect(evaluatedCell.value).toBe("label", { message: "The value should be the menu name" });
|
||||
expect(cell.content).toBe("[label](odoo://ir_menu_xml_id/test_menu)", {
|
||||
message: "The content should be the complete markdown link",
|
||||
});
|
||||
expect(evaluatedCell.link.label).toBe("label", {
|
||||
message: "The link label should be the menu name",
|
||||
});
|
||||
expect(evaluatedCell.link.url).toBe("odoo://ir_menu_xml_id/test_menu", {
|
||||
message: "The link url should reference the correct menu",
|
||||
});
|
||||
});
|
||||
|
||||
test("ir.menu linked based on record id", async function () {
|
||||
const env = await makeSpreadsheetMockEnv({ serverData: getMenuServerData() });
|
||||
const model = new Model({}, { custom: { env } });
|
||||
setCellContent(model, "A1", "[label](odoo://ir_menu_id/12)");
|
||||
const cell = getCell(model, "A1");
|
||||
const evaluatedCell = getEvaluatedCell(model, "A1");
|
||||
expect(evaluatedCell.value).toBe("label", { message: "The value should be the menu name" });
|
||||
expect(cell.content).toBe("[label](odoo://ir_menu_id/12)", {
|
||||
message: "The content should be the complete markdown link",
|
||||
});
|
||||
expect(evaluatedCell.link.label).toBe("label", {
|
||||
message: "The link label should be the menu name",
|
||||
});
|
||||
expect(evaluatedCell.link.url).toBe("odoo://ir_menu_id/12", {
|
||||
message: "The link url should reference the correct menu",
|
||||
});
|
||||
});
|
||||
|
||||
test("ir.menu linked based on xml id which does not exists", async function () {
|
||||
const env = await makeSpreadsheetMockEnv({ serverData: getMenuServerData() });
|
||||
const model = new Model({}, { custom: { env } });
|
||||
setCellContent(model, "A1", "[label](odoo://ir_menu_xml_id/does_not_exists)");
|
||||
expect(getCell(model, "A1").content).toBe("[label](odoo://ir_menu_xml_id/does_not_exists)");
|
||||
expect(getEvaluatedCell(model, "A1").value).toBe("#LINK");
|
||||
expect(getEvaluatedCell(model, "A1").message).toBe(
|
||||
"Menu does_not_exists not found. You may not have the required access rights."
|
||||
);
|
||||
});
|
||||
|
||||
test("ir.menu linked based on record id which does not exists", async function () {
|
||||
const env = await makeSpreadsheetMockEnv({ serverData: getMenuServerData() });
|
||||
const model = new Model({}, { custom: { env } });
|
||||
setCellContent(model, "A1", "[label](odoo://ir_menu_id/9999)");
|
||||
expect(getCell(model, "A1").content).toBe("[label](odoo://ir_menu_id/9999)");
|
||||
expect(getEvaluatedCell(model, "A1").value).toBe("#LINK");
|
||||
expect(getEvaluatedCell(model, "A1").message).toBe(
|
||||
"Menu 9999 not found. You may not have the required access rights."
|
||||
);
|
||||
});
|
||||
|
||||
test("Odoo link cells can be imported/exported", async function () {
|
||||
const env = await makeSpreadsheetMockEnv({ serverData: getMenuServerData() });
|
||||
const model = new Model({}, { custom: { env } });
|
||||
setCellContent(model, "A1", "[label](odoo://ir_menu_id/12)");
|
||||
let cell = getCell(model, "A1");
|
||||
let evaluatedCell = getEvaluatedCell(model, "A1");
|
||||
expect(evaluatedCell.value).toBe("label", { message: "The value should be the menu name" });
|
||||
expect(cell.content).toBe("[label](odoo://ir_menu_id/12)", {
|
||||
message: "The content should be the complete markdown link",
|
||||
});
|
||||
expect(evaluatedCell.link.label).toBe("label", {
|
||||
message: "The link label should be the menu name",
|
||||
});
|
||||
expect(evaluatedCell.link.url).toBe("odoo://ir_menu_id/12", {
|
||||
message: "The link url should reference the correct menu",
|
||||
});
|
||||
const model2 = new Model(model.exportData(), { custom: { env } });
|
||||
cell = getCell(model2, "A1");
|
||||
evaluatedCell = getEvaluatedCell(model, "A1");
|
||||
expect(evaluatedCell.value).toBe("label", { message: "The value should be the menu name" });
|
||||
expect(cell.content).toBe("[label](odoo://ir_menu_id/12)", {
|
||||
message: "The content should be the complete markdown link",
|
||||
});
|
||||
expect(evaluatedCell.link.label).toBe("label", {
|
||||
message: "The link label should be the menu name",
|
||||
});
|
||||
expect(evaluatedCell.link.url).toBe("odoo://ir_menu_id/12", {
|
||||
message: "The link url should reference the correct menu",
|
||||
});
|
||||
});
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
/** @odoo-module */
|
||||
import { spreadsheetLinkMenuCellService } from "@spreadsheet/ir_ui_menu/index";
|
||||
import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { actionService } from "@web/webclient/actions/action_service";
|
||||
import { ormService } from "@web/core/orm_service";
|
||||
import { viewService } from "@web/views/view_service";
|
||||
import { menuService } from "@web/webclient/menus/menu_service";
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import { setCellContent } from "@spreadsheet/../tests/utils/commands";
|
||||
import { getCell } from "@spreadsheet/../tests/utils/getters";
|
||||
import { getMenuServerData } from "../menu_data_utils";
|
||||
|
||||
const { Model } = spreadsheet;
|
||||
|
||||
function beforeEach() {
|
||||
registry
|
||||
.category("services")
|
||||
.add("menu", menuService)
|
||||
.add("action", actionService)
|
||||
.add("spreadsheetLinkMenuCell", spreadsheetLinkMenuCellService);
|
||||
registry.category("services").add("view", viewService, { force: true }); // #action-serv-leg-compat-js-class
|
||||
registry.category("services").add("orm", ormService, { force: true }); // #action-serv-leg-compat-js-class
|
||||
}
|
||||
|
||||
QUnit.module("spreadsheet > menu link cells", { beforeEach }, () => {
|
||||
QUnit.test("ir.menu linked based on xml id", async function (assert) {
|
||||
const env = await makeTestEnv({ serverData: getMenuServerData() });
|
||||
const model = new Model({}, { evalContext: { env } });
|
||||
setCellContent(model, "A1", "[label](odoo://ir_menu_xml_id/test_menu)");
|
||||
const cell = getCell(model, "A1");
|
||||
assert.equal(cell.evaluated.value, "label", "The value should be the menu name");
|
||||
assert.equal(
|
||||
cell.content,
|
||||
"[label](odoo://ir_menu_xml_id/test_menu)",
|
||||
"The content should be the complete markdown link"
|
||||
);
|
||||
assert.equal(cell.link.label, "label", "The link label should be the menu name");
|
||||
assert.equal(
|
||||
cell.link.url,
|
||||
"odoo://ir_menu_xml_id/test_menu",
|
||||
"The link url should reference the correct menu"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("ir.menu linked based on record id", async function (assert) {
|
||||
const env = await makeTestEnv({ serverData: getMenuServerData() });
|
||||
const model = new Model({}, { evalContext: { env } });
|
||||
setCellContent(model, "A1", "[label](odoo://ir_menu_id/2)");
|
||||
const cell = getCell(model, "A1");
|
||||
assert.equal(cell.evaluated.value, "label", "The value should be the menu name");
|
||||
assert.equal(
|
||||
cell.content,
|
||||
"[label](odoo://ir_menu_id/2)",
|
||||
"The content should be the complete markdown link"
|
||||
);
|
||||
assert.equal(cell.link.label, "label", "The link label should be the menu name");
|
||||
assert.equal(
|
||||
cell.link.url,
|
||||
"odoo://ir_menu_id/2",
|
||||
"The link url should reference the correct menu"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("ir.menu linked based on xml id which does not exists", async function (assert) {
|
||||
const env = await makeTestEnv({ serverData: getMenuServerData() });
|
||||
const model = new Model({}, { evalContext: { env } });
|
||||
setCellContent(model, "A1", "[label](odoo://ir_menu_xml_id/does_not_exists)");
|
||||
const cell = getCell(model, "A1");
|
||||
assert.equal(cell.content, "[label](odoo://ir_menu_xml_id/does_not_exists)");
|
||||
assert.equal(cell.evaluated.value, "#BAD_EXPR");
|
||||
});
|
||||
|
||||
QUnit.test("ir.menu linked based on record id which does not exists", async function (assert) {
|
||||
const env = await makeTestEnv({ serverData: getMenuServerData() });
|
||||
const model = new Model({}, { evalContext: { env } });
|
||||
setCellContent(model, "A1", "[label](odoo://ir_menu_id/9999)");
|
||||
const cell = getCell(model, "A1");
|
||||
assert.equal(cell.content, "[label](odoo://ir_menu_id/9999)");
|
||||
assert.equal(cell.evaluated.value, "#BAD_EXPR");
|
||||
});
|
||||
|
||||
QUnit.test("Odoo link cells can be imported/exported", async function (assert) {
|
||||
const env = await makeTestEnv({ serverData: getMenuServerData() });
|
||||
const model = new Model({}, { evalContext: { env } });
|
||||
setCellContent(model, "A1", "[label](odoo://ir_menu_id/2)");
|
||||
let cell = getCell(model, "A1");
|
||||
assert.equal(cell.evaluated.value, "label", "The value should be the menu name");
|
||||
assert.equal(
|
||||
cell.content,
|
||||
"[label](odoo://ir_menu_id/2)",
|
||||
"The content should be the complete markdown link"
|
||||
);
|
||||
assert.equal(cell.link.label, "label", "The link label should be the menu name");
|
||||
assert.equal(
|
||||
cell.link.url,
|
||||
"odoo://ir_menu_id/2",
|
||||
"The link url should reference the correct menu"
|
||||
);
|
||||
const model2 = new Model(model.exportData(), { evalContext: { env } });
|
||||
cell = getCell(model2, "A1");
|
||||
assert.equal(cell.evaluated.value, "label", "The value should be the menu name");
|
||||
assert.equal(
|
||||
cell.content,
|
||||
"[label](odoo://ir_menu_id/2)",
|
||||
"The content should be the complete markdown link"
|
||||
);
|
||||
assert.equal(cell.link.label, "label", "The link label should be the menu name");
|
||||
assert.equal(
|
||||
cell.link.url,
|
||||
"odoo://ir_menu_id/2",
|
||||
"The link url should reference the correct menu"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,230 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import * as spreadsheet from "@odoo/o-spreadsheet";
|
||||
import { defineSpreadsheetModels } from "@spreadsheet/../tests/helpers/data";
|
||||
import { makeSpreadsheetMockEnv } from "@spreadsheet/../tests/helpers/model";
|
||||
import { makeMockEnv, mockService, patchWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { getMenuServerData } from "@spreadsheet/../tests/links/menu_data_utils";
|
||||
|
||||
import { setCellContent } from "@spreadsheet/../tests/helpers/commands";
|
||||
import { getEvaluatedCell } from "@spreadsheet/../tests/helpers/getters";
|
||||
|
||||
describe.current.tags("headless");
|
||||
defineSpreadsheetModels();
|
||||
|
||||
const { Model } = spreadsheet;
|
||||
const { urlRepresentation, openLink } = spreadsheet.links;
|
||||
|
||||
test("click a web link", async () => {
|
||||
patchWithCleanup(window, {
|
||||
open: (href) => {
|
||||
expect.step(href.toString());
|
||||
},
|
||||
});
|
||||
const env = await makeMockEnv();
|
||||
const data = {
|
||||
sheets: [
|
||||
{
|
||||
cells: { A1: "[Odoo](https://odoo.com)" },
|
||||
},
|
||||
],
|
||||
};
|
||||
const model = new Model(data, { custom: { env } });
|
||||
const cell = getEvaluatedCell(model, "A1");
|
||||
expect(urlRepresentation(cell.link, model.getters)).toBe("https://odoo.com");
|
||||
openLink(cell.link, env);
|
||||
expect.verifySteps(["https://odoo.com"]);
|
||||
});
|
||||
|
||||
test("click a menu link", async () => {
|
||||
const fakeActionService = {
|
||||
doAction(action) {
|
||||
expect.step(action);
|
||||
},
|
||||
// TODO: this is the conversion 1/1 of the old test, where the mock action service didn't contain a loadAction
|
||||
// method, but that's not something that happens in the real world, so we should probably refactor this test
|
||||
loadAction: undefined,
|
||||
};
|
||||
mockService("action", fakeActionService);
|
||||
const env = await makeSpreadsheetMockEnv({ serverData: getMenuServerData() });
|
||||
const data = {
|
||||
sheets: [
|
||||
{
|
||||
cells: { A1: "[label](odoo://ir_menu_xml_id/test_menu)" },
|
||||
},
|
||||
],
|
||||
};
|
||||
const model = new Model(data, { custom: { env } });
|
||||
const cell = getEvaluatedCell(model, "A1");
|
||||
expect(urlRepresentation(cell.link, model.getters)).toBe("menu with xmlid");
|
||||
openLink(cell.link, env);
|
||||
expect.verifySteps(["spreadsheet.action1"]);
|
||||
});
|
||||
|
||||
test("middle-click a menu link", async () => {
|
||||
mockService("action", {
|
||||
doAction(_, options) {
|
||||
expect.step("doAction");
|
||||
expect(options).toEqual({
|
||||
newWindow: true,
|
||||
});
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
});
|
||||
|
||||
const env = await makeSpreadsheetMockEnv({ serverData: getMenuServerData() });
|
||||
const data = {
|
||||
sheets: [
|
||||
{
|
||||
cells: { A1: "[label](odoo://ir_menu_xml_id/test_menu)" },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const model = new Model(data, { custom: { env } });
|
||||
const cell = getEvaluatedCell(model, "A1");
|
||||
expect(urlRepresentation(cell.link, model.getters)).toBe("menu with xmlid");
|
||||
openLink(cell.link, env, true);
|
||||
expect.verifySteps(["doAction"]);
|
||||
});
|
||||
|
||||
test("click a menu link [2]", async () => {
|
||||
const fakeActionService = {
|
||||
doAction(action) {
|
||||
expect.step("do-action");
|
||||
expect(action).toEqual({
|
||||
name: "an odoo view",
|
||||
res_model: "partner",
|
||||
target: "current",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "list"]],
|
||||
});
|
||||
},
|
||||
// TODO: same as the above test
|
||||
loadAction: undefined,
|
||||
};
|
||||
mockService("action", fakeActionService);
|
||||
const env = await makeSpreadsheetMockEnv({ serverData: getMenuServerData() });
|
||||
const view = {
|
||||
name: "an odoo view",
|
||||
viewType: "list",
|
||||
action: {
|
||||
modelName: "partner",
|
||||
views: [[false, "list"]],
|
||||
},
|
||||
};
|
||||
|
||||
const model = new Model({}, { custom: { env } });
|
||||
setCellContent(model, "A1", `[a view](odoo://view/${JSON.stringify(view)})`);
|
||||
const cell = getEvaluatedCell(model, "A1");
|
||||
expect(urlRepresentation(cell.link, model.getters)).toBe("an odoo view");
|
||||
openLink(cell.link, env);
|
||||
expect.verifySteps(["do-action"]);
|
||||
});
|
||||
|
||||
test("Click a link containing an action xml id", async () => {
|
||||
mockService("action", {
|
||||
doAction: (action) => {
|
||||
expect.step("do-action");
|
||||
expect(action.name).toBe("My Action Name");
|
||||
expect(action.res_model).toBe("ir.ui.menu");
|
||||
expect(action.target).toBe("current");
|
||||
expect(action.type).toBe("ir.actions.act_window");
|
||||
expect(action.views).toEqual([[false, "list"]]);
|
||||
expect(action.domain).toEqual([[1, "=", 1]]);
|
||||
},
|
||||
});
|
||||
const env = await makeSpreadsheetMockEnv({ serverData: getMenuServerData() });
|
||||
|
||||
const view = {
|
||||
name: "My Action Name",
|
||||
viewType: "list",
|
||||
action: {
|
||||
modelName: "ir.ui.menu",
|
||||
views: [[false, "list"]],
|
||||
domain: [[1, "=", 1]],
|
||||
xmlId: "spreadsheet.action1",
|
||||
},
|
||||
};
|
||||
|
||||
const model = new Model({}, { custom: { env } });
|
||||
setCellContent(model, "A1", `[an action link](odoo://view/${JSON.stringify(view)})`);
|
||||
const cell = getEvaluatedCell(model, "A1");
|
||||
expect(urlRepresentation(cell.link, model.getters)).toBe("My Action Name");
|
||||
await openLink(cell.link, env);
|
||||
await animationFrame();
|
||||
expect.verifySteps(["do-action"]);
|
||||
});
|
||||
|
||||
test("Can open link when some views are absent from the referred action", async () => {
|
||||
const env = await makeSpreadsheetMockEnv({ serverData: getMenuServerData() });
|
||||
env.services.action = {
|
||||
...env.services.action,
|
||||
doAction(action) {
|
||||
expect.step("do-action");
|
||||
expect(action.name).toBe("My Action Name");
|
||||
expect(action.res_model).toBe("ir.ui.menu");
|
||||
expect(action.target).toBe("current");
|
||||
expect(action.type).toBe("ir.actions.act_window");
|
||||
expect(action.views).toEqual([
|
||||
[false, "list"],
|
||||
[false, "form"],
|
||||
]);
|
||||
expect(action.domain).toEqual([(1, "=", 1)]);
|
||||
},
|
||||
};
|
||||
|
||||
const view = {
|
||||
name: "My Action Name",
|
||||
viewType: "list",
|
||||
action: {
|
||||
modelName: "ir.ui.menu",
|
||||
views: [
|
||||
[false, "list"],
|
||||
[false, "form"],
|
||||
],
|
||||
domain: [(1, "=", 1)],
|
||||
xmlId: "spreadsheet.action2",
|
||||
},
|
||||
};
|
||||
|
||||
const model = new Model({}, { custom: { env } });
|
||||
setCellContent(model, "A1", `[an action link](odoo://view/${JSON.stringify(view)})`);
|
||||
const cell = getEvaluatedCell(model, "A1");
|
||||
expect(urlRepresentation(cell.link, model.getters)).toBe("My Action Name");
|
||||
await openLink(cell.link, env);
|
||||
await animationFrame();
|
||||
expect.verifySteps(["do-action"]);
|
||||
});
|
||||
|
||||
test("Context is passed correctly to the action service", async () => {
|
||||
const env = await makeSpreadsheetMockEnv({ serverData: getMenuServerData() });
|
||||
env.services.action = {
|
||||
...env.services.action,
|
||||
loadAction(_, context) {
|
||||
expect.step("load-action");
|
||||
expect(context).toEqual({ search_default_partner: 1 });
|
||||
},
|
||||
};
|
||||
|
||||
const view = {
|
||||
name: "My Action Name",
|
||||
viewType: "list",
|
||||
action: {
|
||||
modelName: "ir.ui.menu",
|
||||
views: [[false, "list"]],
|
||||
domain: [(1, "=", 1)],
|
||||
xmlId: "spreadsheet.action1",
|
||||
context: { search_default_partner: 1 },
|
||||
},
|
||||
};
|
||||
|
||||
const model = new Model({}, { custom: { env } });
|
||||
setCellContent(model, "A1", `[an action link](odoo://view/${JSON.stringify(view)})`);
|
||||
const cell = getEvaluatedCell(model, "A1");
|
||||
expect(urlRepresentation(cell.link, model.getters)).toBe("My Action Name");
|
||||
await openLink(cell.link, env);
|
||||
await animationFrame();
|
||||
expect.verifySteps(["load-action"]);
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,609 +0,0 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { session } from "@web/session";
|
||||
import { nextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
|
||||
import CommandResult from "@spreadsheet/o_spreadsheet/cancelled_reason";
|
||||
import { createModelWithDataSource, waitForDataSourcesLoaded } from "../utils/model";
|
||||
import { addGlobalFilter, selectCell, setCellContent } from "../utils/commands";
|
||||
import { getCell, getCellContent, getCellFormula, getCells, getCellValue } from "../utils/getters";
|
||||
import { createSpreadsheetWithList } from "../utils/list";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { RPCError } from "@web/core/network/rpc_service";
|
||||
import { getBasicServerData } from "../utils/data";
|
||||
|
||||
QUnit.module("spreadsheet > list plugin", {}, () => {
|
||||
QUnit.test("List export", async (assert) => {
|
||||
const { model } = await createSpreadsheetWithList();
|
||||
const total = 4 + 10 * 4; // 4 Headers + 10 lines
|
||||
assert.strictEqual(Object.values(getCells(model)).length, total);
|
||||
assert.strictEqual(getCellFormula(model, "A1"), `=ODOO.LIST.HEADER(1,"foo")`);
|
||||
assert.strictEqual(getCellFormula(model, "B1"), `=ODOO.LIST.HEADER(1,"bar")`);
|
||||
assert.strictEqual(getCellFormula(model, "C1"), `=ODOO.LIST.HEADER(1,"date")`);
|
||||
assert.strictEqual(getCellFormula(model, "D1"), `=ODOO.LIST.HEADER(1,"product_id")`);
|
||||
assert.strictEqual(getCellFormula(model, "A2"), `=ODOO.LIST(1,1,"foo")`);
|
||||
assert.strictEqual(getCellFormula(model, "B2"), `=ODOO.LIST(1,1,"bar")`);
|
||||
assert.strictEqual(getCellFormula(model, "C2"), `=ODOO.LIST(1,1,"date")`);
|
||||
assert.strictEqual(getCellFormula(model, "D2"), `=ODOO.LIST(1,1,"product_id")`);
|
||||
assert.strictEqual(getCellFormula(model, "A3"), `=ODOO.LIST(1,2,"foo")`);
|
||||
assert.strictEqual(getCellFormula(model, "A11"), `=ODOO.LIST(1,10,"foo")`);
|
||||
assert.strictEqual(getCellFormula(model, "A12"), "");
|
||||
});
|
||||
|
||||
QUnit.test("Return display name of selection field", async (assert) => {
|
||||
const { model } = await createSpreadsheetWithList({
|
||||
model: "documents.document",
|
||||
columns: ["handler"],
|
||||
});
|
||||
assert.strictEqual(getCellValue(model, "A2", "Spreadsheet"));
|
||||
});
|
||||
|
||||
QUnit.test("Return name_get of many2one field", async (assert) => {
|
||||
const { model } = await createSpreadsheetWithList({ columns: ["product_id"] });
|
||||
assert.strictEqual(getCellValue(model, "A2"), "xphone");
|
||||
});
|
||||
|
||||
QUnit.test("Boolean fields are correctly formatted", async (assert) => {
|
||||
const { model } = await createSpreadsheetWithList({ columns: ["bar"] });
|
||||
assert.strictEqual(getCellValue(model, "A2"), "TRUE");
|
||||
assert.strictEqual(getCellValue(model, "A5"), "FALSE");
|
||||
});
|
||||
|
||||
QUnit.test("properties field displays property display names", async (assert) => {
|
||||
const serverData = getBasicServerData();
|
||||
serverData.models.partner.records = [
|
||||
{
|
||||
id: 45,
|
||||
partner_properties: [
|
||||
{ name: "dbfc66e0afaa6a8d", type: "date", string: "prop 1", default: false },
|
||||
{ name: "f80b6fb58d0d4c72", type: "integer", string: "prop 2", default: 0 },
|
||||
],
|
||||
},
|
||||
];
|
||||
const { model } = await createSpreadsheetWithList({
|
||||
serverData,
|
||||
columns: ["partner_properties"],
|
||||
});
|
||||
assert.strictEqual(getCellValue(model, "A2"), "prop 1, prop 2");
|
||||
});
|
||||
|
||||
QUnit.test("Can display a field which is not in the columns", async function (assert) {
|
||||
const { model } = await createSpreadsheetWithList();
|
||||
setCellContent(model, "A1", `=ODOO.LIST(1,1,"active")`);
|
||||
assert.strictEqual(getCellValue(model, "A1"), "Loading...");
|
||||
await waitForDataSourcesLoaded(model); // Await for batching collection of missing fields
|
||||
assert.strictEqual(getCellValue(model, "A1"), true);
|
||||
});
|
||||
|
||||
QUnit.test("Can remove a list with undo after editing a cell", async function (assert) {
|
||||
const { model } = await createSpreadsheetWithList();
|
||||
assert.ok(getCellContent(model, "B1").startsWith("=ODOO.LIST.HEADER"));
|
||||
setCellContent(model, "G10", "should be undoable");
|
||||
model.dispatch("REQUEST_UNDO");
|
||||
assert.equal(getCellContent(model, "G10"), "");
|
||||
model.dispatch("REQUEST_UNDO");
|
||||
assert.equal(getCellContent(model, "B1"), "");
|
||||
assert.equal(model.getters.getListIds().length, 0);
|
||||
});
|
||||
|
||||
QUnit.test("List formulas are correctly formatted at evaluation", async function (assert) {
|
||||
const { model } = await createSpreadsheetWithList({
|
||||
columns: ["foo", "probability", "bar", "date", "create_date", "product_id", "pognon"],
|
||||
linesNumber: 2,
|
||||
});
|
||||
await waitForDataSourcesLoaded(model);
|
||||
assert.strictEqual(getCell(model, "A2").format, undefined);
|
||||
assert.strictEqual(getCell(model, "B2").format, undefined);
|
||||
assert.strictEqual(getCell(model, "C2").format, undefined);
|
||||
assert.strictEqual(getCell(model, "D2").format, undefined);
|
||||
assert.strictEqual(getCell(model, "E2").format, undefined);
|
||||
assert.strictEqual(getCell(model, "F2").format, undefined);
|
||||
assert.strictEqual(getCell(model, "G2").format, undefined);
|
||||
assert.strictEqual(getCell(model, "G3").format, undefined);
|
||||
|
||||
assert.strictEqual(getCell(model, "A2").evaluated.format, "0");
|
||||
assert.strictEqual(getCell(model, "B2").evaluated.format, "#,##0.00");
|
||||
assert.strictEqual(getCell(model, "C2").evaluated.format, undefined);
|
||||
assert.strictEqual(getCell(model, "D2").evaluated.format, "m/d/yyyy");
|
||||
assert.strictEqual(getCell(model, "E2").evaluated.format, "m/d/yyyy hh:mm:ss");
|
||||
assert.strictEqual(getCell(model, "F2").evaluated.format, undefined);
|
||||
assert.strictEqual(getCell(model, "G2").evaluated.format, "#,##0.00[$€]");
|
||||
assert.strictEqual(getCell(model, "G3").evaluated.format, "[$$]#,##0.00");
|
||||
});
|
||||
|
||||
QUnit.test("Json fields are not supported in list formulas", async function (assert) {
|
||||
const { model } = await createSpreadsheetWithList({
|
||||
columns: ["foo", "jsonField"],
|
||||
linesNumber: 2,
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.LIST(1,1,"foo")`);
|
||||
setCellContent(model, "A2", `=ODOO.LIST(1,1,"jsonField")`);
|
||||
await waitForDataSourcesLoaded(model);
|
||||
assert.strictEqual(getCell(model, "A1").evaluated.value, 12);
|
||||
assert.strictEqual(getCell(model, "A2").evaluated.value, "#ERROR");
|
||||
assert.strictEqual(
|
||||
getCell(model, "A2").evaluated.error.message,
|
||||
`Fields of type "json" are not supported`
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("can select a List from cell formula", async function (assert) {
|
||||
const { model } = await createSpreadsheetWithList();
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
const listId = model.getters.getListIdFromPosition(sheetId, 0, 0);
|
||||
model.dispatch("SELECT_ODOO_LIST", { listId });
|
||||
const selectedListId = model.getters.getSelectedListId();
|
||||
assert.strictEqual(selectedListId, "1");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"can select a List from cell formula with '-' before the formula",
|
||||
async function (assert) {
|
||||
const { model } = await createSpreadsheetWithList();
|
||||
setCellContent(model, "A1", `=-ODOO.LIST("1","1","foo")`);
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
const listId = model.getters.getListIdFromPosition(sheetId, 0, 0);
|
||||
model.dispatch("SELECT_ODOO_LIST", { listId });
|
||||
const selectedListId = model.getters.getSelectedListId();
|
||||
assert.strictEqual(selectedListId, "1");
|
||||
}
|
||||
);
|
||||
QUnit.test(
|
||||
"can select a List from cell formula with other numerical values",
|
||||
async function (assert) {
|
||||
const { model } = await createSpreadsheetWithList();
|
||||
setCellContent(model, "A1", `=3*ODOO.LIST("1","1","foo")`);
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
const listId = model.getters.getListIdFromPosition(sheetId, 0, 0);
|
||||
model.dispatch("SELECT_ODOO_LIST", { listId });
|
||||
const selectedListId = model.getters.getSelectedListId();
|
||||
assert.strictEqual(selectedListId, "1");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("List datasource is loaded with correct linesNumber", async function (assert) {
|
||||
const { model } = await createSpreadsheetWithList({ linesNumber: 2 });
|
||||
const [listId] = model.getters.getListIds();
|
||||
const dataSource = model.getters.getListDataSource(listId);
|
||||
assert.strictEqual(dataSource.maxPosition, 2);
|
||||
});
|
||||
|
||||
QUnit.test("can select a List from cell formula within a formula", async function (assert) {
|
||||
const { model } = await createSpreadsheetWithList();
|
||||
setCellContent(model, "A1", `=SUM(ODOO.LIST("1","1","foo"),1)`);
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
const listId = model.getters.getListIdFromPosition(sheetId, 0, 0);
|
||||
model.dispatch("SELECT_ODOO_LIST", { listId });
|
||||
const selectedListId = model.getters.getSelectedListId();
|
||||
assert.strictEqual(selectedListId, "1");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"can select a List from cell formula where the id is a reference",
|
||||
async function (assert) {
|
||||
const { model } = await createSpreadsheetWithList();
|
||||
setCellContent(model, "A1", `=ODOO.LIST(G10,"1","foo")`);
|
||||
setCellContent(model, "G10", "1");
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
const listId = model.getters.getListIdFromPosition(sheetId, 0, 0);
|
||||
model.dispatch("SELECT_ODOO_LIST", { listId });
|
||||
const selectedListId = model.getters.getSelectedListId();
|
||||
assert.strictEqual(selectedListId, "1");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("Referencing non-existing fields does not crash", async function (assert) {
|
||||
assert.expect(4);
|
||||
const forbiddenFieldName = "product_id";
|
||||
let spreadsheetLoaded = false;
|
||||
const { model } = await createSpreadsheetWithList({
|
||||
columns: ["bar", "product_id"],
|
||||
mockRPC: async function (route, args, performRPC) {
|
||||
if (
|
||||
spreadsheetLoaded &&
|
||||
args.method === "search_read" &&
|
||||
args.model === "partner" &&
|
||||
args.kwargs.fields &&
|
||||
args.kwargs.fields.includes(forbiddenFieldName)
|
||||
) {
|
||||
// We should not go through this condition if the forbidden fields is properly filtered
|
||||
assert.ok(false, `${forbiddenFieldName} should have been ignored`);
|
||||
}
|
||||
if (this) {
|
||||
// @ts-ignore
|
||||
return this._super.apply(this, arguments);
|
||||
}
|
||||
},
|
||||
});
|
||||
const listId = model.getters.getListIds()[0];
|
||||
// remove forbidden field from the fields of the list.
|
||||
delete model.getters.getListDataSource(listId).getFields()[forbiddenFieldName];
|
||||
spreadsheetLoaded = true;
|
||||
model.dispatch("REFRESH_ALL_DATA_SOURCES");
|
||||
await nextTick();
|
||||
setCellContent(model, "A1", `=ODOO.LIST.HEADER("1", "${forbiddenFieldName}")`);
|
||||
setCellContent(model, "A2", `=ODOO.LIST("1","1","${forbiddenFieldName}")`);
|
||||
|
||||
assert.equal(
|
||||
model.getters.getListDataSource(listId).getFields()[forbiddenFieldName],
|
||||
undefined
|
||||
);
|
||||
assert.strictEqual(getCellValue(model, "A1"), forbiddenFieldName);
|
||||
const A2 = getCell(model, "A2");
|
||||
assert.equal(A2.evaluated.type, "error");
|
||||
assert.equal(
|
||||
A2.evaluated.error.message,
|
||||
`The field ${forbiddenFieldName} does not exist or you do not have access to that field`
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("don't fetch list data if no formula use it", async function (assert) {
|
||||
const spreadsheetData = {
|
||||
sheets: [
|
||||
{
|
||||
id: "sheet1",
|
||||
},
|
||||
{
|
||||
id: "sheet2",
|
||||
cells: {
|
||||
A1: { content: `=ODOO.LIST("1", "1", "foo")` },
|
||||
},
|
||||
},
|
||||
],
|
||||
lists: {
|
||||
1: {
|
||||
id: 1,
|
||||
columns: ["foo", "contact_name"],
|
||||
domain: [],
|
||||
model: "partner",
|
||||
orderBy: [],
|
||||
context: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
const model = await createModelWithDataSource({
|
||||
spreadsheetData,
|
||||
mockRPC: function (_, { model, method }) {
|
||||
if (!["partner", "ir.model"].includes(model)) {
|
||||
return;
|
||||
}
|
||||
assert.step(`${model}/${method}`);
|
||||
},
|
||||
});
|
||||
assert.verifySteps([]);
|
||||
model.dispatch("ACTIVATE_SHEET", { sheetIdFrom: "sheet1", sheetIdTo: "sheet2" });
|
||||
/*
|
||||
* Ask a first time the value => It will trigger a loading of the data source.
|
||||
*/
|
||||
assert.equal(getCellValue(model, "A1"), "Loading...");
|
||||
await nextTick();
|
||||
assert.equal(getCellValue(model, "A1"), 12);
|
||||
assert.verifySteps(["partner/fields_get", "partner/search_read"]);
|
||||
});
|
||||
|
||||
QUnit.test("user context is combined with list 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.LIST("1", "1", "name")` },
|
||||
},
|
||||
},
|
||||
],
|
||||
lists: {
|
||||
1: {
|
||||
id: 1,
|
||||
columns: ["name", "contact_name"],
|
||||
domain: [],
|
||||
model: "partner",
|
||||
orderBy: [],
|
||||
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 "search_read":
|
||||
assert.step("search_read");
|
||||
assert.deepEqual(
|
||||
kwargs.context,
|
||||
expectedFetchContext,
|
||||
"search_read context"
|
||||
);
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
await waitForDataSourcesLoaded(model);
|
||||
assert.verifySteps(["search_read"]);
|
||||
});
|
||||
|
||||
QUnit.test("rename list with empty name is refused", async (assert) => {
|
||||
const { model } = await createSpreadsheetWithList();
|
||||
const result = model.dispatch("RENAME_ODOO_LIST", {
|
||||
listId: "1",
|
||||
name: "",
|
||||
});
|
||||
assert.deepEqual(result.reasons, [CommandResult.EmptyName]);
|
||||
});
|
||||
|
||||
QUnit.test("rename list with incorrect id is refused", async (assert) => {
|
||||
const { model } = await createSpreadsheetWithList();
|
||||
const result = model.dispatch("RENAME_ODOO_LIST", {
|
||||
listId: "invalid",
|
||||
name: "name",
|
||||
});
|
||||
assert.deepEqual(result.reasons, [CommandResult.ListIdNotFound]);
|
||||
});
|
||||
|
||||
QUnit.test("Undo/Redo for RENAME_ODOO_LIST", async function (assert) {
|
||||
assert.expect(4);
|
||||
const { model } = await createSpreadsheetWithList();
|
||||
assert.equal(model.getters.getListName("1"), "List");
|
||||
model.dispatch("RENAME_ODOO_LIST", { listId: "1", name: "test" });
|
||||
assert.equal(model.getters.getListName("1"), "test");
|
||||
model.dispatch("REQUEST_UNDO");
|
||||
assert.equal(model.getters.getListName("1"), "List");
|
||||
model.dispatch("REQUEST_REDO");
|
||||
assert.equal(model.getters.getListName("1"), "test");
|
||||
});
|
||||
|
||||
QUnit.test("Can delete list", async function (assert) {
|
||||
const { model } = await createSpreadsheetWithList();
|
||||
model.dispatch("REMOVE_ODOO_LIST", { listId: "1" });
|
||||
assert.strictEqual(model.getters.getListIds().length, 0);
|
||||
const B4 = getCell(model, "B4");
|
||||
assert.equal(B4.evaluated.error.message, `There is no list with id "1"`);
|
||||
assert.equal(B4.evaluated.value, `#ERROR`);
|
||||
});
|
||||
|
||||
QUnit.test("Can undo/redo a delete list", async function (assert) {
|
||||
const { model } = await createSpreadsheetWithList();
|
||||
const value = getCell(model, "B4").evaluated.value;
|
||||
model.dispatch("REMOVE_ODOO_LIST", { listId: "1" });
|
||||
model.dispatch("REQUEST_UNDO");
|
||||
assert.strictEqual(model.getters.getListIds().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.getListIds().length, 0);
|
||||
B4 = getCell(model, "B4");
|
||||
assert.equal(B4.evaluated.error.message, `There is no list with id "1"`);
|
||||
assert.equal(B4.evaluated.value, `#ERROR`);
|
||||
});
|
||||
|
||||
QUnit.test("can edit list domain", async (assert) => {
|
||||
const { model } = await createSpreadsheetWithList();
|
||||
const [listId] = model.getters.getListIds();
|
||||
assert.deepEqual(model.getters.getListDefinition(listId).domain, []);
|
||||
assert.strictEqual(getCellValue(model, "B2"), "TRUE");
|
||||
model.dispatch("UPDATE_ODOO_LIST_DOMAIN", {
|
||||
listId,
|
||||
domain: [["foo", "in", [55]]],
|
||||
});
|
||||
assert.deepEqual(model.getters.getListDefinition(listId).domain, [["foo", "in", [55]]]);
|
||||
await waitForDataSourcesLoaded(model);
|
||||
assert.strictEqual(getCellValue(model, "B2"), "");
|
||||
model.dispatch("REQUEST_UNDO");
|
||||
await waitForDataSourcesLoaded(model);
|
||||
assert.deepEqual(model.getters.getListDefinition(listId).domain, []);
|
||||
await waitForDataSourcesLoaded(model);
|
||||
assert.strictEqual(getCellValue(model, "B2"), "TRUE");
|
||||
model.dispatch("REQUEST_REDO");
|
||||
assert.deepEqual(model.getters.getListDefinition(listId).domain, [["foo", "in", [55]]]);
|
||||
await waitForDataSourcesLoaded(model);
|
||||
assert.strictEqual(getCellValue(model, "B2"), "");
|
||||
const result = model.dispatch("UPDATE_ODOO_LIST_DOMAIN", {
|
||||
listId: "invalid",
|
||||
domain: [],
|
||||
});
|
||||
assert.deepEqual(result.reasons, [CommandResult.ListIdNotFound]);
|
||||
});
|
||||
|
||||
QUnit.test("edited domain is exported", async (assert) => {
|
||||
const { model } = await createSpreadsheetWithList();
|
||||
const [listId] = model.getters.getListIds();
|
||||
model.dispatch("UPDATE_ODOO_LIST_DOMAIN", {
|
||||
listId,
|
||||
domain: [["foo", "in", [55]]],
|
||||
});
|
||||
assert.deepEqual(model.exportData().lists["1"].domain, [["foo", "in", [55]]]);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"Cannot see record of a list in dashboard mode if wrong list formula",
|
||||
async function (assert) {
|
||||
const fakeActionService = {
|
||||
dependencies: [],
|
||||
start: (env) => ({
|
||||
doAction: (params) => {
|
||||
assert.step(params.res_model);
|
||||
assert.step(params.res_id.toString());
|
||||
},
|
||||
}),
|
||||
};
|
||||
registry.category("services").add("action", fakeActionService);
|
||||
const { model } = await createSpreadsheetWithList();
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
model.dispatch("UPDATE_CELL", {
|
||||
col: 0,
|
||||
row: 1,
|
||||
sheetId,
|
||||
content: "=ODOO.LIST()",
|
||||
});
|
||||
model.updateMode("dashboard");
|
||||
selectCell(model, "A2");
|
||||
assert.verifySteps([]);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("field matching is removed when filter is deleted", async function (assert) {
|
||||
const { model } = await createSpreadsheetWithList();
|
||||
await addGlobalFilter(
|
||||
model,
|
||||
{
|
||||
filter: {
|
||||
id: "42",
|
||||
type: "relation",
|
||||
label: "test",
|
||||
defaultValue: [41],
|
||||
modelName: undefined,
|
||||
rangeType: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
list: { 1: { chain: "product_id", type: "many2one" } },
|
||||
}
|
||||
);
|
||||
const [filter] = model.getters.getGlobalFilters();
|
||||
const matching = {
|
||||
chain: "product_id",
|
||||
type: "many2one",
|
||||
};
|
||||
assert.deepEqual(model.getters.getListFieldMatching("1", filter.id), matching);
|
||||
assert.deepEqual(model.getters.getListDataSource("1").getComputedDomain(), [
|
||||
["product_id", "in", [41]],
|
||||
]);
|
||||
model.dispatch("REMOVE_GLOBAL_FILTER", {
|
||||
id: filter.id,
|
||||
});
|
||||
assert.deepEqual(
|
||||
model.getters.getListFieldMatching("1", filter.id),
|
||||
undefined,
|
||||
"it should have removed the pivot and its fieldMatching and datasource altogether"
|
||||
);
|
||||
assert.deepEqual(model.getters.getListDataSource("1").getComputedDomain(), []);
|
||||
model.dispatch("REQUEST_UNDO");
|
||||
assert.deepEqual(model.getters.getListFieldMatching("1", filter.id), matching);
|
||||
assert.deepEqual(model.getters.getListDataSource("1").getComputedDomain(), [
|
||||
["product_id", "in", [41]],
|
||||
]);
|
||||
model.dispatch("REQUEST_REDO");
|
||||
assert.deepEqual(model.getters.getListFieldMatching("1", filter.id), undefined);
|
||||
assert.deepEqual(model.getters.getListDataSource("1").getComputedDomain(), []);
|
||||
});
|
||||
|
||||
QUnit.test("Preload currency of monetary field", async function (assert) {
|
||||
assert.expect(3);
|
||||
await createSpreadsheetWithList({
|
||||
columns: ["pognon"],
|
||||
mockRPC: async function (route, args, performRPC) {
|
||||
if (args.method === "search_read" && args.model === "partner") {
|
||||
assert.strictEqual(args.kwargs.fields.length, 2);
|
||||
assert.strictEqual(args.kwargs.fields[0], "pognon");
|
||||
assert.strictEqual(args.kwargs.fields[1], "currency_id");
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"List record limit is computed during the import and UPDATE_CELL",
|
||||
async function (assert) {
|
||||
const spreadsheetData = {
|
||||
sheets: [
|
||||
{
|
||||
id: "sheet1",
|
||||
cells: {
|
||||
A1: { content: `=ODOO.LIST("1", "1", "foo")` },
|
||||
},
|
||||
},
|
||||
],
|
||||
lists: {
|
||||
1: {
|
||||
id: 1,
|
||||
columns: ["foo", "contact_name"],
|
||||
domain: [],
|
||||
model: "partner",
|
||||
orderBy: [],
|
||||
context: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
const model = await createModelWithDataSource({ spreadsheetData });
|
||||
const ds = model.getters.getListDataSource("1");
|
||||
assert.strictEqual(ds.maxPosition, 1);
|
||||
assert.strictEqual(ds.maxPositionFetched, 0);
|
||||
setCellContent(model, "A1", `=ODOO.LIST("1", "42", "foo", 2)`);
|
||||
assert.strictEqual(ds.maxPosition, 42);
|
||||
assert.strictEqual(ds.maxPositionFetched, 0);
|
||||
await waitForDataSourcesLoaded(model);
|
||||
assert.strictEqual(ds.maxPosition, 42);
|
||||
assert.strictEqual(ds.maxPositionFetched, 42);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"Load list spreadsheet with models that cannot be accessed",
|
||||
async function (assert) {
|
||||
let hasAccessRights = true;
|
||||
const { model } = await createSpreadsheetWithList({
|
||||
mockRPC: async function (route, args) {
|
||||
if (
|
||||
args.model === "partner" &&
|
||||
args.method === "search_read" &&
|
||||
!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, 1);
|
||||
assert.equal(cell.evaluated.value, 42669);
|
||||
|
||||
hasAccessRights = false;
|
||||
model.dispatch("REFRESH_ODOO_LIST", { listId: "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!");
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
@ -1,298 +0,0 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { migrate, ODOO_VERSION } from "@spreadsheet/o_spreadsheet/migration";
|
||||
import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended";
|
||||
|
||||
const { Model } = spreadsheet;
|
||||
|
||||
QUnit.module("spreadsheet > migrations");
|
||||
|
||||
QUnit.test("Odoo formulas are migrated", (assert) => {
|
||||
const data = {
|
||||
sheets: [
|
||||
{
|
||||
cells: {
|
||||
A1: { content: `=PIVOT("1")` },
|
||||
A2: { content: `=PIVOT.HEADER("1")` },
|
||||
A3: { content: `=FILTER.VALUE("1")` },
|
||||
A4: { content: `=LIST("1")` },
|
||||
A5: { content: `=LIST.HEADER("1")` },
|
||||
A6: { content: `=PIVOT.POSITION("1")` },
|
||||
A7: { content: `=pivot("1")` },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const migratedData = migrate(data);
|
||||
assert.strictEqual(migratedData.sheets[0].cells.A1.content, `=ODOO.PIVOT("1")`);
|
||||
assert.strictEqual(migratedData.sheets[0].cells.A2.content, `=ODOO.PIVOT.HEADER("1")`);
|
||||
assert.strictEqual(migratedData.sheets[0].cells.A3.content, `=ODOO.FILTER.VALUE("1")`);
|
||||
assert.strictEqual(migratedData.sheets[0].cells.A4.content, `=ODOO.LIST("1")`);
|
||||
assert.strictEqual(migratedData.sheets[0].cells.A5.content, `=ODOO.LIST.HEADER("1")`);
|
||||
assert.strictEqual(migratedData.sheets[0].cells.A6.content, `=ODOO.PIVOT.POSITION("1")`);
|
||||
assert.strictEqual(migratedData.sheets[0].cells.A7.content, `=ODOO.PIVOT("1")`);
|
||||
});
|
||||
|
||||
QUnit.test("Pivot 'day' arguments are migrated", (assert) => {
|
||||
const data = {
|
||||
odooVersion: 1,
|
||||
sheets: [
|
||||
{
|
||||
cells: {
|
||||
A1: { content: `=ODOO.PIVOT("1","21/07/2022")` },
|
||||
A2: { content: `=ODOO.PIVOT.HEADER("1","11/12/2022")` },
|
||||
A3: { content: `=odoo.pivot("1","21/07/2021")` },
|
||||
A4: { content: `=ODOO.PIVOT("1","test")` },
|
||||
A5: { content: `=odoo.pivot("1","21/07/2021")+"21/07/2021"` },
|
||||
A6: { content: `=BAD_FORMULA(` },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const migratedData = migrate(data);
|
||||
assert.strictEqual(migratedData.sheets[0].cells.A1.content, `=ODOO.PIVOT("1","07/21/2022")`);
|
||||
assert.strictEqual(
|
||||
migratedData.sheets[0].cells.A2.content,
|
||||
`=ODOO.PIVOT.HEADER("1","12/11/2022")`
|
||||
);
|
||||
assert.strictEqual(migratedData.sheets[0].cells.A3.content, `=odoo.pivot("1","07/21/2021")`);
|
||||
assert.strictEqual(migratedData.sheets[0].cells.A4.content, `=ODOO.PIVOT("1","test")`);
|
||||
assert.strictEqual(
|
||||
migratedData.sheets[0].cells.A5.content,
|
||||
`=odoo.pivot("1","07/21/2021")+"21/07/2021"`
|
||||
);
|
||||
assert.strictEqual(migratedData.sheets[0].cells.A6.content, `=BAD_FORMULA(`);
|
||||
});
|
||||
|
||||
QUnit.test("Global filters: pivot fields is correctly added", (assert) => {
|
||||
const data = {
|
||||
globalFilters: [
|
||||
{
|
||||
id: "Filter1",
|
||||
type: "relation",
|
||||
label: "Relation Filter",
|
||||
fields: {
|
||||
1: {
|
||||
field: "foo",
|
||||
type: "char",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
pivots: {
|
||||
1: {
|
||||
name: "test",
|
||||
},
|
||||
},
|
||||
};
|
||||
const migratedData = migrate(data);
|
||||
const filter = migratedData.globalFilters[0];
|
||||
const pivot = migratedData.pivots["1"];
|
||||
assert.deepEqual(pivot.fieldMatching, {
|
||||
Filter1: {
|
||||
chain: "foo",
|
||||
type: "char",
|
||||
},
|
||||
});
|
||||
assert.strictEqual(filter.fields, undefined);
|
||||
});
|
||||
|
||||
QUnit.test("Global filters: date is correctly migrated", (assert) => {
|
||||
const data = {
|
||||
globalFilters: [
|
||||
{
|
||||
id: "1",
|
||||
type: "date",
|
||||
rangeType: "year",
|
||||
defaultValue: { year: "last_year" },
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
type: "date",
|
||||
rangeType: "year",
|
||||
defaultValue: { year: "antepenultimate_year" },
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
type: "date",
|
||||
rangeType: "year",
|
||||
defaultValue: { year: "this_year" },
|
||||
},
|
||||
],
|
||||
};
|
||||
const migratedData = migrate(data);
|
||||
const [f1, f2, f3] = migratedData.globalFilters;
|
||||
assert.deepEqual(f1.defaultValue, { yearOffset: -1 });
|
||||
assert.deepEqual(f2.defaultValue, { yearOffset: -2 });
|
||||
assert.deepEqual(f3.defaultValue, { yearOffset: 0 });
|
||||
});
|
||||
|
||||
QUnit.test("List name default is model name", (assert) => {
|
||||
const data = {
|
||||
lists: {
|
||||
1: {
|
||||
name: "Name",
|
||||
model: "Model",
|
||||
},
|
||||
2: {
|
||||
model: "Model",
|
||||
},
|
||||
},
|
||||
};
|
||||
const migratedData = migrate(data);
|
||||
assert.strictEqual(Object.values(migratedData.lists).length, 2);
|
||||
assert.strictEqual(migratedData.lists["1"].name, "Name");
|
||||
assert.strictEqual(migratedData.lists["2"].name, "Model");
|
||||
});
|
||||
|
||||
QUnit.test("Pivot name default is model name", (assert) => {
|
||||
const data = {
|
||||
pivots: {
|
||||
1: {
|
||||
name: "Name",
|
||||
model: "Model",
|
||||
},
|
||||
2: {
|
||||
model: "Model",
|
||||
},
|
||||
},
|
||||
};
|
||||
const migratedData = migrate(data);
|
||||
assert.strictEqual(Object.values(migratedData.pivots).length, 2);
|
||||
assert.strictEqual(migratedData.pivots["1"].name, "Name");
|
||||
assert.strictEqual(migratedData.pivots["2"].name, "Model");
|
||||
});
|
||||
|
||||
QUnit.test("fieldMatchings are moved from filters to their respective datasources", (assert) => {
|
||||
const data = {
|
||||
globalFilters: [
|
||||
{
|
||||
id: "Filter",
|
||||
label: "MyFilter1",
|
||||
type: "relation",
|
||||
listFields: {
|
||||
1: {
|
||||
field: "parent_id",
|
||||
type: "many2one",
|
||||
},
|
||||
},
|
||||
pivotFields: {
|
||||
1: {
|
||||
field: "parent_id",
|
||||
type: "many2one",
|
||||
},
|
||||
},
|
||||
graphFields: {
|
||||
fig1: {
|
||||
field: "parent_id",
|
||||
type: "many2one",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
pivots: {
|
||||
1: {
|
||||
name: "Name",
|
||||
},
|
||||
},
|
||||
lists: {
|
||||
1: {
|
||||
name: "Name",
|
||||
},
|
||||
},
|
||||
sheets: [
|
||||
{
|
||||
figures: [
|
||||
{
|
||||
id: "fig1",
|
||||
tag: "chart",
|
||||
data: {
|
||||
type: "odoo_bar",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const migratedData = migrate(data);
|
||||
assert.deepEqual(migratedData.pivots["1"].fieldMatching, {
|
||||
Filter: { chain: "parent_id", type: "many2one" },
|
||||
});
|
||||
assert.deepEqual(migratedData.lists["1"].fieldMatching, {
|
||||
Filter: { chain: "parent_id", type: "many2one" },
|
||||
});
|
||||
assert.deepEqual(migratedData.sheets[0].figures[0].data.fieldMatching, {
|
||||
Filter: { chain: "parent_id", type: "many2one" },
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("fieldMatchings offsets are correctly preserved after migration", (assert) => {
|
||||
const data = {
|
||||
globalFilters: [
|
||||
{
|
||||
id: "Filter",
|
||||
label: "MyFilter1",
|
||||
type: "relation",
|
||||
listFields: {
|
||||
1: {
|
||||
field: "parent_id",
|
||||
type: "date",
|
||||
offset: "-1",
|
||||
},
|
||||
},
|
||||
pivotFields: {
|
||||
1: {
|
||||
field: "parent_id",
|
||||
type: "date",
|
||||
offset: "-1",
|
||||
},
|
||||
},
|
||||
graphFields: {
|
||||
fig1: {
|
||||
field: "parent_id",
|
||||
type: "date",
|
||||
offset: "-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
pivots: {
|
||||
1: {
|
||||
name: "Name",
|
||||
},
|
||||
},
|
||||
lists: {
|
||||
1: {
|
||||
name: "Name",
|
||||
},
|
||||
},
|
||||
sheets: [
|
||||
{
|
||||
figures: [
|
||||
{
|
||||
id: "fig1",
|
||||
tag: "chart",
|
||||
data: {
|
||||
type: "odoo_bar",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const migratedData = migrate(data);
|
||||
assert.deepEqual(migratedData.pivots["1"].fieldMatching, {
|
||||
Filter: { chain: "parent_id", type: "date", offset: "-1" },
|
||||
});
|
||||
assert.deepEqual(migratedData.lists["1"].fieldMatching, {
|
||||
Filter: { chain: "parent_id", type: "date", offset: "-1" },
|
||||
});
|
||||
assert.deepEqual(migratedData.sheets[0].figures[0].data.fieldMatching, {
|
||||
Filter: { chain: "parent_id", type: "date", offset: "-1" },
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("Odoo version is exported", (assert) => {
|
||||
const model = new Model();
|
||||
assert.strictEqual(model.exportData().odooVersion, ODOO_VERSION);
|
||||
});
|
||||
|
|
@ -0,0 +1,834 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { load } from "@odoo/o-spreadsheet";
|
||||
import { defineSpreadsheetActions, defineSpreadsheetModels } from "../helpers/data";
|
||||
|
||||
defineSpreadsheetModels();
|
||||
defineSpreadsheetActions();
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
test("Odoo formulas are migrated", () => {
|
||||
const data = {
|
||||
version: 16,
|
||||
sheets: [
|
||||
{
|
||||
cells: {
|
||||
A1: { content: `=PIVOT("1")` },
|
||||
A2: { content: `=PIVOT.HEADER("1")` },
|
||||
A3: { content: `=FILTER.VALUE("1")` },
|
||||
A4: { content: `=LIST("1")` },
|
||||
A5: { content: `=LIST.HEADER("1")` },
|
||||
A6: { content: `=PIVOT.POSITION("1")` },
|
||||
A7: { content: `=pivot("1")` },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const migratedData = load(data);
|
||||
expect(migratedData.sheets[0].cells.A1).toBe(`=PIVOT.VALUE("1")`);
|
||||
expect(migratedData.sheets[0].cells.A2).toBe(`=PIVOT.HEADER("1")`);
|
||||
expect(migratedData.sheets[0].cells.A3).toBe(`=ODOO.FILTER.VALUE.V18("1")`);
|
||||
expect(migratedData.sheets[0].cells.A4).toBe(`=ODOO.LIST("1")`);
|
||||
expect(migratedData.sheets[0].cells.A5).toBe(`=ODOO.LIST.HEADER("1")`);
|
||||
expect(migratedData.sheets[0].cells.A6).toBe(`=ODOO.PIVOT.POSITION("1")`);
|
||||
expect(migratedData.sheets[0].cells.A7).toBe(`=PIVOT.VALUE("1")`);
|
||||
});
|
||||
|
||||
test("Pivot 'day' arguments are migrated", () => {
|
||||
const data = {
|
||||
version: 16,
|
||||
odooVersion: 1,
|
||||
sheets: [
|
||||
{
|
||||
cells: {
|
||||
A1: { content: `=ODOO.PIVOT("1","21/07/2022")` },
|
||||
A2: { content: `=ODOO.PIVOT.HEADER("1","11/12/2022")` },
|
||||
A3: { content: `=odoo.pivot("1","21/07/2021")` },
|
||||
A4: { content: `=ODOO.PIVOT("1","test")` },
|
||||
A5: { content: `=odoo.pivot("1","21/07/2021")+"21/07/2021"` },
|
||||
A6: { content: `=BAD_FORMULA(` },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const migratedData = load(data);
|
||||
expect(migratedData.sheets[0].cells.A1).toBe(`=PIVOT.VALUE("1","07/21/2022")`);
|
||||
expect(migratedData.sheets[0].cells.A2).toBe(`=PIVOT.HEADER("1","12/11/2022")`);
|
||||
expect(migratedData.sheets[0].cells.A3).toBe(`=PIVOT.VALUE("1","07/21/2021")`);
|
||||
expect(migratedData.sheets[0].cells.A4).toBe(`=PIVOT.VALUE("1","test")`);
|
||||
expect(migratedData.sheets[0].cells.A5).toBe(`=PIVOT.VALUE("1","07/21/2021")+"21/07/2021"`);
|
||||
expect(migratedData.sheets[0].cells.A6).toBe(`=BAD_FORMULA(`);
|
||||
});
|
||||
|
||||
test("Global filters: pivot fields is correctly added", () => {
|
||||
const data = {
|
||||
version: 16,
|
||||
globalFilters: [
|
||||
{
|
||||
id: "Filter1",
|
||||
type: "relation",
|
||||
label: "Relation Filter",
|
||||
fields: {
|
||||
1: {
|
||||
field: "foo",
|
||||
type: "char",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
pivots: {
|
||||
1: {
|
||||
name: "test",
|
||||
measures: [],
|
||||
colGroupBys: [],
|
||||
rowGroupBys: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
const migratedData = load(data);
|
||||
const filter = migratedData.globalFilters[0];
|
||||
const pivot = migratedData.pivots["1"];
|
||||
expect(pivot.fieldMatching).toEqual({
|
||||
Filter1: {
|
||||
chain: "foo",
|
||||
type: "char",
|
||||
},
|
||||
});
|
||||
expect(filter.fields).toBe(undefined);
|
||||
});
|
||||
|
||||
test("Global filters: date is correctly migrated", () => {
|
||||
const data = {
|
||||
version: 16,
|
||||
globalFilters: [
|
||||
{
|
||||
id: "1",
|
||||
type: "date",
|
||||
rangeType: "year",
|
||||
defaultValue: { year: "last_year" },
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
type: "date",
|
||||
rangeType: "year",
|
||||
defaultValue: { year: "antepenultimate_year" },
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
type: "date",
|
||||
rangeType: "year",
|
||||
defaultValue: { year: "this_year" },
|
||||
},
|
||||
],
|
||||
};
|
||||
const migratedData = load(data);
|
||||
const [f1, f2, f3] = migratedData.globalFilters;
|
||||
expect(f1.defaultValue).toBe(undefined);
|
||||
expect(f2.defaultValue).toBe(undefined);
|
||||
expect(f3.defaultValue).toBe(undefined);
|
||||
});
|
||||
|
||||
test("List name default is model name", () => {
|
||||
const data = {
|
||||
version: 16,
|
||||
lists: {
|
||||
1: {
|
||||
name: "Name",
|
||||
model: "Model",
|
||||
},
|
||||
2: {
|
||||
model: "Model",
|
||||
},
|
||||
},
|
||||
};
|
||||
const migratedData = load(data);
|
||||
expect(Object.values(migratedData.lists).length).toBe(2);
|
||||
expect(migratedData.lists["1"].name).toBe("Name");
|
||||
expect(migratedData.lists["2"].name).toBe("Model");
|
||||
});
|
||||
|
||||
test("Pivot name default is model name", () => {
|
||||
const data = {
|
||||
version: 16,
|
||||
pivots: {
|
||||
1: {
|
||||
name: "Name",
|
||||
model: "Model",
|
||||
measures: [],
|
||||
colGroupBys: [],
|
||||
rowGroupBys: [],
|
||||
},
|
||||
2: {
|
||||
model: "Model",
|
||||
measures: [],
|
||||
colGroupBys: [],
|
||||
rowGroupBys: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
const migratedData = load(data);
|
||||
expect(Object.values(migratedData.pivots).length).toBe(2);
|
||||
expect(migratedData.pivots["1"].name).toBe("Name");
|
||||
expect(migratedData.pivots["2"].name).toBe("Model");
|
||||
});
|
||||
|
||||
test("fieldMatchings are moved from filters to their respective datasources", () => {
|
||||
const data = {
|
||||
version: 16,
|
||||
globalFilters: [
|
||||
{
|
||||
id: "Filter",
|
||||
label: "MyFilter1",
|
||||
type: "relation",
|
||||
listFields: {
|
||||
1: {
|
||||
field: "parent_id",
|
||||
type: "many2one",
|
||||
},
|
||||
},
|
||||
pivotFields: {
|
||||
1: {
|
||||
field: "parent_id",
|
||||
type: "many2one",
|
||||
},
|
||||
},
|
||||
graphFields: {
|
||||
fig1: {
|
||||
field: "parent_id",
|
||||
type: "many2one",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
pivots: {
|
||||
1: {
|
||||
name: "Name",
|
||||
measures: [],
|
||||
colGroupBys: [],
|
||||
rowGroupBys: [],
|
||||
},
|
||||
},
|
||||
lists: {
|
||||
1: {
|
||||
name: "Name",
|
||||
},
|
||||
},
|
||||
sheets: [
|
||||
{
|
||||
figures: [
|
||||
{
|
||||
id: "fig1",
|
||||
tag: "chart",
|
||||
data: {
|
||||
type: "odoo_bar",
|
||||
metaData: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const migratedData = load(data);
|
||||
expect(migratedData.pivots["1"].fieldMatching).toEqual({
|
||||
Filter: { chain: "parent_id", type: "many2one" },
|
||||
});
|
||||
expect(migratedData.lists["1"].fieldMatching).toEqual({
|
||||
Filter: { chain: "parent_id", type: "many2one" },
|
||||
});
|
||||
expect(migratedData.sheets[0].figures[0].data.fieldMatching).toEqual({
|
||||
Filter: { chain: "parent_id", type: "many2one" },
|
||||
});
|
||||
});
|
||||
|
||||
test("fieldMatchings offsets are correctly preserved after migration", () => {
|
||||
const data = {
|
||||
version: 16,
|
||||
globalFilters: [
|
||||
{
|
||||
id: "Filter",
|
||||
label: "MyFilter1",
|
||||
type: "relation",
|
||||
listFields: {
|
||||
1: {
|
||||
field: "parent_id",
|
||||
type: "date",
|
||||
offset: "-1",
|
||||
},
|
||||
},
|
||||
pivotFields: {
|
||||
1: {
|
||||
field: "parent_id",
|
||||
type: "date",
|
||||
offset: "-1",
|
||||
},
|
||||
},
|
||||
graphFields: {
|
||||
fig1: {
|
||||
field: "parent_id",
|
||||
type: "date",
|
||||
offset: "-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
pivots: {
|
||||
1: {
|
||||
name: "Name",
|
||||
measures: [],
|
||||
colGroupBys: [],
|
||||
rowGroupBys: [],
|
||||
},
|
||||
},
|
||||
lists: {
|
||||
1: {
|
||||
name: "Name",
|
||||
},
|
||||
},
|
||||
sheets: [
|
||||
{
|
||||
figures: [
|
||||
{
|
||||
id: "fig1",
|
||||
tag: "chart",
|
||||
data: {
|
||||
type: "odoo_bar",
|
||||
metaData: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const migratedData = load(data);
|
||||
expect(migratedData.pivots["1"].fieldMatching).toEqual({
|
||||
Filter: { chain: "parent_id", type: "date", offset: "-1" },
|
||||
});
|
||||
expect(migratedData.lists["1"].fieldMatching).toEqual({
|
||||
Filter: { chain: "parent_id", type: "date", offset: "-1" },
|
||||
});
|
||||
expect(migratedData.sheets[0].figures[0].data.fieldMatching).toEqual({
|
||||
Filter: { chain: "parent_id", type: "date", offset: "-1" },
|
||||
});
|
||||
});
|
||||
|
||||
test("group year/quarter/month filters to a single filter type", () => {
|
||||
const data = {
|
||||
version: 14,
|
||||
odooVersion: 5,
|
||||
globalFilters: [
|
||||
{
|
||||
id: "1",
|
||||
type: "relation",
|
||||
label: "a relational filter",
|
||||
defaultValue: [2],
|
||||
defaultValueDisplayNames: ["Mitchell Admin"],
|
||||
modelName: "res.users",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
type: "date",
|
||||
label: "a year relational filter",
|
||||
rangeType: "year",
|
||||
defaultsToCurrentPeriod: true,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
type: "date",
|
||||
label: "a quarter relational filter",
|
||||
rangeType: "quarter",
|
||||
defaultsToCurrentPeriod: true,
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
type: "date",
|
||||
label: "a month relational filter",
|
||||
rangeType: "month",
|
||||
defaultsToCurrentPeriod: true,
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
type: "date",
|
||||
label: "a relative date filter",
|
||||
defaultValue: "last_week",
|
||||
rangeType: "relative",
|
||||
defaultsToCurrentPeriod: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
const migratedData = load(data);
|
||||
const filters = migratedData.globalFilters;
|
||||
expect(filters).toEqual([
|
||||
{
|
||||
id: "1",
|
||||
type: "relation",
|
||||
label: "a relational filter",
|
||||
defaultValue: { operator: "in", ids: [2] },
|
||||
defaultValueDisplayNames: ["Mitchell Admin"],
|
||||
modelName: "res.users",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
type: "date",
|
||||
label: "a year relational filter",
|
||||
defaultValue: "this_year",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
type: "date",
|
||||
label: "a quarter relational filter",
|
||||
defaultValue: "this_quarter",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
type: "date",
|
||||
label: "a month relational filter",
|
||||
defaultValue: "this_month",
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
type: "date",
|
||||
label: "a relative date filter",
|
||||
defaultValue: "last_7_days",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("Pivot are migrated from 6 to 9", () => {
|
||||
const data = {
|
||||
version: 16,
|
||||
pivots: {
|
||||
1: {
|
||||
name: "Name",
|
||||
model: "Model",
|
||||
measures: [],
|
||||
colGroupBys: [],
|
||||
rowGroupBys: [],
|
||||
fieldMatching: { 1: { chain: "foo", type: "char" } },
|
||||
},
|
||||
},
|
||||
};
|
||||
const migratedData = load(data);
|
||||
expect(Object.values(migratedData.pivots).length).toBe(1);
|
||||
expect(migratedData.pivots["1"]).toEqual({
|
||||
type: "ODOO",
|
||||
fieldMatching: { 1: { chain: "foo", type: "char" } },
|
||||
name: "Name",
|
||||
model: "Model",
|
||||
measures: [],
|
||||
columns: [],
|
||||
rows: [],
|
||||
formulaId: "1",
|
||||
});
|
||||
});
|
||||
|
||||
test("Pivot are migrated from 9 to 10", () => {
|
||||
const data = {
|
||||
version: 16,
|
||||
odooVersion: 9,
|
||||
pivots: {
|
||||
1: {
|
||||
type: "ODOO",
|
||||
name: "Name",
|
||||
model: "res.model",
|
||||
measures: ["probability"],
|
||||
colGroupBys: ["foo"],
|
||||
rowGroupBys: ["create_date:month"],
|
||||
formulaId: "1",
|
||||
},
|
||||
},
|
||||
};
|
||||
const migratedData = load(data);
|
||||
expect(Object.values(migratedData.pivots).length).toBe(1);
|
||||
expect(migratedData.pivots["1"]).toEqual({
|
||||
type: "ODOO",
|
||||
name: "Name",
|
||||
model: "res.model",
|
||||
measures: [{ id: "probability", fieldName: "probability", aggregator: undefined }],
|
||||
columns: [{ fieldName: "foo", granularity: undefined, order: undefined }],
|
||||
rows: [{ fieldName: "create_date", granularity: "month", order: undefined }],
|
||||
formulaId: "1",
|
||||
});
|
||||
});
|
||||
|
||||
test("Pivot formulas are migrated from 9 to 10", () => {
|
||||
const data = {
|
||||
version: 16,
|
||||
odooVersion: 9,
|
||||
sheets: [
|
||||
{
|
||||
cells: {
|
||||
A1: { content: `=ODOO.PIVOT("1")` },
|
||||
A2: { content: `=ODOO.PIVOT.HEADER("1")` },
|
||||
A3: { content: `=ODOO.PIVOT.POSITION("1")` },
|
||||
A4: { content: `=ODOO.PIVOT.TABLE("1")` },
|
||||
A5: { content: `=odoo.pivot("1")` },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const migratedData = load(data);
|
||||
expect(migratedData.sheets[0].cells.A1).toBe(`=PIVOT.VALUE("1")`);
|
||||
expect(migratedData.sheets[0].cells.A2).toBe(`=PIVOT.HEADER("1")`);
|
||||
expect(migratedData.sheets[0].cells.A3).toBe(`=ODOO.PIVOT.POSITION("1")`);
|
||||
expect(migratedData.sheets[0].cells.A4).toBe(`=PIVOT("1")`);
|
||||
expect(migratedData.sheets[0].cells.A5).toBe(`=PIVOT.VALUE("1")`);
|
||||
});
|
||||
|
||||
test("Pivot formulas using pivot positions are migrated (11 to 12)", () => {
|
||||
const data = {
|
||||
version: 16,
|
||||
odooVersion: 9,
|
||||
sheets: [
|
||||
{
|
||||
cells: {
|
||||
A1: {
|
||||
content: `=-PIVOT.VALUE("1","balance","account_id",ODOO.PIVOT.POSITION("1","account_id",12),"date:quarter","4/"&ODOO.FILTER.VALUE("Year"))`,
|
||||
},
|
||||
A2: {
|
||||
content: `=PIVOT.HEADER("1","account_id",ODOO.PIVOT.POSITION("1","account_id",14))`,
|
||||
},
|
||||
A3: { content: `=ODOO.PIVOT.POSITION("1","account_id",14)` },
|
||||
A4: { content: `=ODOO.PIVOT.POSITION("1",14)` },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const migratedData = load(data);
|
||||
expect(migratedData.sheets[0].cells.A1).toBe(
|
||||
`=-PIVOT.VALUE("1","balance","#account_id",12,"date:quarter","4/"&ODOO.FILTER.VALUE.V18("Year"))`
|
||||
);
|
||||
expect(migratedData.sheets[0].cells.A2).toBe(`=PIVOT.HEADER("1","#account_id",14)`);
|
||||
expect(migratedData.sheets[0].cells.A3).toBe(`=ODOO.PIVOT.POSITION("1","account_id",14)`);
|
||||
expect(migratedData.sheets[0].cells.A4).toBe(`=ODOO.PIVOT.POSITION("1",14)`);
|
||||
});
|
||||
|
||||
test("Pivot sorted columns are migrated (12 to 13)", () => {
|
||||
const data = {
|
||||
version: 23,
|
||||
odooVersion: 12,
|
||||
sheets: [],
|
||||
pivots: {
|
||||
1: {
|
||||
name: "test",
|
||||
sortedColumn: { groupId: [[], []], measure: "testMeasure", order: "desc" },
|
||||
columns: [],
|
||||
rows: [],
|
||||
measures: [{ id: "testMeasure:sum", fieldName: "testMeasure", aggregator: "sum" }],
|
||||
},
|
||||
2: {
|
||||
name: "test2",
|
||||
sortedColumn: { groupId: [[], [1]], measure: "testMeasure", order: "desc" },
|
||||
columns: [{ fieldName: "product_id" }],
|
||||
rows: [],
|
||||
measures: [{ id: "testMeasure:sum", fieldName: "testMeasure", aggregator: "sum" }],
|
||||
},
|
||||
3: {
|
||||
name: "test",
|
||||
// sortedColumn is not in the measures
|
||||
sortedColumn: { groupId: [[], []], measure: "testMeasure", order: "desc" },
|
||||
columns: [],
|
||||
rows: [],
|
||||
measures: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
const migratedData = load(data);
|
||||
expect(migratedData.pivots["1"].sortedColumn).toEqual({
|
||||
domain: [],
|
||||
measure: "testMeasure:sum",
|
||||
order: "desc",
|
||||
});
|
||||
expect(migratedData.pivots["2"].sortedColumn).toBe(undefined);
|
||||
expect(migratedData.pivots["3"].sortedColumn).toBe(undefined);
|
||||
});
|
||||
|
||||
test("Chart cumulatedStart is set to true if cumulative at migration", () => {
|
||||
const data = {
|
||||
version: 18.0,
|
||||
odooVersion: 9,
|
||||
sheets: [
|
||||
{
|
||||
figures: [
|
||||
{
|
||||
id: "fig1",
|
||||
tag: "chart",
|
||||
data: {
|
||||
type: "odoo_bar",
|
||||
metaData: {
|
||||
cumulatedStart: undefined,
|
||||
cumulative: true,
|
||||
},
|
||||
cumulative: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "fig2",
|
||||
tag: "chart",
|
||||
data: {
|
||||
type: "odoo_bar",
|
||||
metaData: {
|
||||
cumulative: false,
|
||||
},
|
||||
cumulative: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "fig3",
|
||||
tag: "chart",
|
||||
data: {
|
||||
type: "odoo_bar",
|
||||
metaData: {
|
||||
cumulative: true,
|
||||
cumulatedStart: false,
|
||||
},
|
||||
cumulative: true,
|
||||
cumulatedStart: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const migratedData = load(data);
|
||||
const sheet = migratedData.sheets[0];
|
||||
expect(sheet.figures[0].data.metaData.cumulatedStart).toBe(true);
|
||||
expect(sheet.figures[0].data.cumulatedStart).toBe(true);
|
||||
expect(sheet.figures[1].data.metaData.cumulatedStart).toBe(false);
|
||||
expect(sheet.figures[1].data.cumulatedStart).toBe(false);
|
||||
expect(sheet.figures[2].data.metaData.cumulatedStart).toBe(false);
|
||||
expect(sheet.figures[2].data.cumulatedStart).toBe(false);
|
||||
});
|
||||
|
||||
test("text global filter default value is now an array of strings", () => {
|
||||
const data = {
|
||||
version: "18.3.0",
|
||||
globalFilters: [
|
||||
{
|
||||
id: "1",
|
||||
type: "text",
|
||||
defaultValue: "foo",
|
||||
rangeOfAllowedValues: "Sheet1!A1:A2",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
type: "text",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
type: "text",
|
||||
defaultValue: "",
|
||||
},
|
||||
],
|
||||
};
|
||||
const migratedData = load(data);
|
||||
expect(migratedData.globalFilters[0].defaultValue).toEqual({
|
||||
operator: "ilike",
|
||||
strings: ["foo"],
|
||||
});
|
||||
expect(migratedData.globalFilters[0].rangeOfAllowedValues).toBe(undefined);
|
||||
expect(migratedData.globalFilters[0].rangesOfAllowedValues).toEqual(["Sheet1!A1:A2"]);
|
||||
expect(migratedData.globalFilters[1].defaultValue).toBe(undefined);
|
||||
expect(migratedData.globalFilters[1].rangeOfAllowedValues).toBe(undefined);
|
||||
expect(migratedData.globalFilters[1].rangesOfAllowedValues).toBe(undefined);
|
||||
expect(migratedData.globalFilters[2].defaultValue).toBe(undefined);
|
||||
});
|
||||
|
||||
test("global filter default value have operators", () => {
|
||||
const data = {
|
||||
version: "18.4.14",
|
||||
globalFilters: [
|
||||
{
|
||||
id: "1",
|
||||
type: "text",
|
||||
defaultValue: ["foo"],
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
type: "relation",
|
||||
modelName: "res.partner",
|
||||
defaultValue: [1],
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
type: "relation",
|
||||
modelName: "res.company",
|
||||
defaultValue: [2],
|
||||
includeChildren: true,
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
type: "boolean",
|
||||
defaultValue: [true],
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
type: "boolean",
|
||||
defaultValue: [false],
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
type: "boolean",
|
||||
defaultValue: [true, false],
|
||||
},
|
||||
],
|
||||
};
|
||||
const migratedData = load(data);
|
||||
expect(migratedData.globalFilters[0].defaultValue).toEqual({
|
||||
operator: "ilike",
|
||||
strings: ["foo"],
|
||||
});
|
||||
expect(migratedData.globalFilters[1].defaultValue).toEqual({
|
||||
operator: "in",
|
||||
ids: [1],
|
||||
});
|
||||
expect(migratedData.globalFilters[2].defaultValue).toEqual({
|
||||
operator: "child_of",
|
||||
ids: [2],
|
||||
});
|
||||
expect(migratedData.globalFilters[3].defaultValue).toEqual({ operator: "set" });
|
||||
expect(migratedData.globalFilters[4].defaultValue).toEqual({ operator: "not set" });
|
||||
expect(migratedData.globalFilters[5].defaultValue).toBe(undefined);
|
||||
});
|
||||
|
||||
test("Date with antepenultimate_year is not supported anymore", () => {
|
||||
const data = {
|
||||
version: "1",
|
||||
globalFilters: [
|
||||
{
|
||||
id: "1",
|
||||
type: "date",
|
||||
defaultValue: { year: "antepenultimate_year" },
|
||||
rangeType: "fixedPeriod",
|
||||
},
|
||||
],
|
||||
};
|
||||
const migratedData = load(data);
|
||||
expect(migratedData.globalFilters[0].defaultValue).toBe(undefined);
|
||||
});
|
||||
|
||||
test("Default value is now undefined", () => {
|
||||
const data = {
|
||||
version: "1",
|
||||
globalFilters: [
|
||||
{
|
||||
id: "1",
|
||||
type: "relation",
|
||||
label: "a relation filter",
|
||||
defaultValue: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
const migratedData = load(data);
|
||||
expect(migratedData.globalFilters[0].defaultValue).toBe(undefined);
|
||||
});
|
||||
|
||||
test("period values are correctly renamed/removed", () => {
|
||||
const data = {
|
||||
version: 14,
|
||||
odooVersion: 5,
|
||||
globalFilters: [
|
||||
{
|
||||
id: "1",
|
||||
type: "date",
|
||||
label: "My label",
|
||||
rangeType: "relative",
|
||||
defaultValue: "last_six_month",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
type: "date",
|
||||
label: "My label",
|
||||
rangeType: "relative",
|
||||
defaultValue: "last_three_years",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
type: "date",
|
||||
label: "My label",
|
||||
rangeType: "relative",
|
||||
defaultValue: "last_month",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
type: "date",
|
||||
label: "My label",
|
||||
rangeType: "relative",
|
||||
defaultValue: "last_week",
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
type: "date",
|
||||
label: "My label",
|
||||
rangeType: "relative",
|
||||
defaultValue: "last_three_months",
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
type: "date",
|
||||
label: "My label",
|
||||
rangeType: "relative",
|
||||
defaultValue: "last_year",
|
||||
},
|
||||
],
|
||||
};
|
||||
const migratedData = load(data);
|
||||
const filters = migratedData.globalFilters;
|
||||
expect(filters[0].defaultValue).toBe(undefined);
|
||||
expect(filters[1].defaultValue).toBe(undefined);
|
||||
expect(filters[2].defaultValue).toBe("last_30_days");
|
||||
expect(filters[3].defaultValue).toBe("last_7_days");
|
||||
expect(filters[4].defaultValue).toBe("last_90_days");
|
||||
expect(filters[5].defaultValue).toBe("last_12_months");
|
||||
});
|
||||
|
||||
test("Date filters are migrated", () => {
|
||||
const data = {
|
||||
version: 14,
|
||||
odooVersion: 5,
|
||||
globalFilters: [
|
||||
{
|
||||
id: "1",
|
||||
type: "date",
|
||||
label: "Fixed Period",
|
||||
rangeType: "fixedPeriod",
|
||||
disabledPeriods: ["quarter"],
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
type: "date",
|
||||
label: "Relative",
|
||||
rangeType: "relative",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
type: "date",
|
||||
label: "From/to",
|
||||
rangeType: "fromTo",
|
||||
},
|
||||
],
|
||||
};
|
||||
const migratedData = load(data);
|
||||
const filters = migratedData.globalFilters;
|
||||
expect(filters[0].rangeType).toBe(undefined);
|
||||
expect(filters[1].rangeType).toBe(undefined);
|
||||
expect(filters[2].rangeType).toBe(undefined);
|
||||
|
||||
expect(filters[0].disabledPeriods).toBe(undefined);
|
||||
});
|
||||
|
||||
test("18.5.10: ODOO.FILTER.VALUE to ODOO.FILTER.VALUE.V18 in cells", () => {
|
||||
const data = {
|
||||
version: "18.4.14",
|
||||
sheets: [
|
||||
{
|
||||
cells: {
|
||||
A1: '=ODOO.FILTER.VALUE("MyFilter")+odoo.filter.value("AnotherFilter")',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const migratedData = load(data);
|
||||
expect(migratedData.sheets[0].cells.A1).toBe(
|
||||
`=ODOO.FILTER.VALUE.V18("MyFilter")+ODOO.FILTER.VALUE.V18("AnotherFilter")`
|
||||
);
|
||||
});
|
||||
|
|
@ -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" }]);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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],
|
||||
]);
|
||||
});
|
||||
|
|
@ -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"] } }]);
|
||||
});
|
||||
|
|
@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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"));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -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]]);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -0,0 +1,352 @@
|
|||
import { animationFrame, describe, expect, test } from "@odoo/hoot";
|
||||
import { registries } from "@odoo/o-spreadsheet";
|
||||
import { createSpreadsheetWithChart } from "@spreadsheet/../tests/helpers/chart";
|
||||
import {
|
||||
addGlobalFilter,
|
||||
setCellContent,
|
||||
setCellFormat,
|
||||
setCellStyle,
|
||||
setGlobalFilterValue,
|
||||
createCarousel,
|
||||
addChartFigureToCarousel,
|
||||
} from "@spreadsheet/../tests/helpers/commands";
|
||||
import { defineSpreadsheetModels } from "@spreadsheet/../tests/helpers/data";
|
||||
import { getCell, getEvaluatedCell } from "@spreadsheet/../tests/helpers/getters";
|
||||
import { THIS_YEAR_GLOBAL_FILTER } from "@spreadsheet/../tests/helpers/global_filter";
|
||||
import { createModelWithDataSource } from "@spreadsheet/../tests/helpers/model";
|
||||
import { createSpreadsheetWithPivot } from "@spreadsheet/../tests/helpers/pivot";
|
||||
import { freezeOdooData, waitForDataLoaded } from "@spreadsheet/helpers/model";
|
||||
import { OdooPivot, OdooPivotRuntimeDefinition } from "@spreadsheet/pivot/odoo_pivot";
|
||||
|
||||
const { pivotRegistry } = registries;
|
||||
|
||||
import { getMenuServerData } from "@spreadsheet/../tests/links/menu_data_utils";
|
||||
import { createSpreadsheetWithList } from "../helpers/list";
|
||||
|
||||
describe.current.tags("headless");
|
||||
defineSpreadsheetModels();
|
||||
|
||||
test("odoo pivot functions are replaced with their value", async function () {
|
||||
const { model } = await createSpreadsheetWithPivot({ pivotType: "static" });
|
||||
expect(getCell(model, "A3").content).toBe('=PIVOT.HEADER(1,"bar",FALSE)');
|
||||
expect(getCell(model, "C3").content).toBe(
|
||||
'=PIVOT.VALUE(1,"probability:avg","bar",FALSE,"foo",2)'
|
||||
);
|
||||
expect(getEvaluatedCell(model, "A3").value).toBe("No");
|
||||
expect(getEvaluatedCell(model, "C3").value).toBe(15);
|
||||
const data = await freezeOdooData(model);
|
||||
const cells = data.sheets[0].cells;
|
||||
expect(cells.A3).toBe("No", { message: "the content is replaced with the value" });
|
||||
expect(cells.C3).toBe("15", { message: "the content is replaced with the value" });
|
||||
expect(data.formats[data.sheets[0].formats.C3]).toBe("#,##0.00");
|
||||
});
|
||||
|
||||
test("Pivot with a type different of ODOO is not converted", async function () {
|
||||
// Add a pivot with a type different of ODOO
|
||||
pivotRegistry.add("NEW_KIND_OF_PIVOT", {
|
||||
ui: OdooPivot,
|
||||
definition: OdooPivotRuntimeDefinition,
|
||||
externalData: true,
|
||||
onIterationEndEvaluation: () => {},
|
||||
granularities: [],
|
||||
isMeasureCandidate: () => false,
|
||||
isGroupable: () => false,
|
||||
});
|
||||
const spreadsheetData = {
|
||||
pivots: {
|
||||
1: {
|
||||
type: "NEW_KIND_OF_PIVOT",
|
||||
name: "Name",
|
||||
model: "partner",
|
||||
measures: ["probability"],
|
||||
formulaId: "1",
|
||||
colGroupBys: ["foo"],
|
||||
rowGroupBys: ["bar"],
|
||||
},
|
||||
},
|
||||
};
|
||||
const { model } = await createModelWithDataSource({ spreadsheetData });
|
||||
setCellContent(model, "A1", `=PIVOT.VALUE(1, "probability:avg")`);
|
||||
setCellContent(model, "A2", `=PIVOT.HEADER(1, "measure", "probability:avg")`);
|
||||
const data = await freezeOdooData(model);
|
||||
const cells = data.sheets[0].cells;
|
||||
expect(cells.A1).toBe(`=PIVOT.VALUE(1, "probability:avg")`, {
|
||||
message: "the content is not replaced with the value",
|
||||
});
|
||||
expect(cells.A2).toBe(`=PIVOT.HEADER(1, "measure", "probability:avg")`, {
|
||||
message: "the content is not replaced with the value",
|
||||
});
|
||||
});
|
||||
|
||||
test("values are not exported formatted", async function () {
|
||||
const { model } = await createSpreadsheetWithPivot({ pivotType: "static" });
|
||||
expect(getCell(model, "A3").content).toBe('=PIVOT.HEADER(1,"bar",FALSE)');
|
||||
expect(getCell(model, "C3").content).toBe(
|
||||
'=PIVOT.VALUE(1,"probability:avg","bar",FALSE,"foo",2)'
|
||||
);
|
||||
setCellFormat(model, "C3", "mmmm yyyy");
|
||||
setCellContent(model, "C4", "=C3+31");
|
||||
expect(getEvaluatedCell(model, "C3").value).toBe(15);
|
||||
expect(getEvaluatedCell(model, "C3").formattedValue).toBe("January 1900");
|
||||
expect(getEvaluatedCell(model, "C4").value).toBe(46);
|
||||
expect(getEvaluatedCell(model, "C4").formattedValue).toBe("February 1900");
|
||||
const data = await freezeOdooData(model);
|
||||
const { model: sharedModel } = await createModelWithDataSource({ spreadsheetData: data });
|
||||
expect(getEvaluatedCell(sharedModel, "C3").value).toBe(15);
|
||||
expect(getEvaluatedCell(sharedModel, "C3").formattedValue).toBe("January 1900");
|
||||
expect(getEvaluatedCell(sharedModel, "C4").value).toBe(46);
|
||||
expect(getEvaluatedCell(sharedModel, "C4").formattedValue).toBe("February 1900");
|
||||
});
|
||||
|
||||
test("invalid expression with pivot function", async function () {
|
||||
const { model } = await createSpreadsheetWithPivot();
|
||||
setCellContent(model, "A1", "=PIVOT.VALUE(1)+"); // invalid expression
|
||||
expect(getEvaluatedCell(model, "A1").value).toBe("#BAD_EXPR");
|
||||
const data = await freezeOdooData(model);
|
||||
const cells = data.sheets[0].cells;
|
||||
expect(cells.A1).toBe("=PIVOT.VALUE(1)+", {
|
||||
message: "the content is left as is when the expression is invalid",
|
||||
});
|
||||
});
|
||||
|
||||
test("odoo pivot functions detection is not case sensitive", async function () {
|
||||
const { model } = await createSpreadsheetWithPivot();
|
||||
setCellContent(model, "A1", '=pivot.value(1,"probability:avg")');
|
||||
const data = await freezeOdooData(model);
|
||||
const A1 = data.sheets[0].cells.A1;
|
||||
expect(A1).toBe("131", { message: "the content is replaced with the value" });
|
||||
});
|
||||
|
||||
test("computed format is exported", async function () {
|
||||
const { model } = await createSpreadsheetWithPivot({
|
||||
arch: /* xml */ `
|
||||
<pivot>
|
||||
<field name="pognon" type="measure"/>
|
||||
</pivot>
|
||||
`,
|
||||
});
|
||||
setCellContent(model, "A1", '=PIVOT.VALUE(1,"pognon:avg")');
|
||||
await animationFrame();
|
||||
expect(getCell(model, "A1").format).toBe(undefined);
|
||||
expect(getEvaluatedCell(model, "A1").format).toBe("#,##0.00[$€]");
|
||||
const data = await freezeOdooData(model);
|
||||
const formatId = data.sheets[0].formats.A1;
|
||||
const format = data.formats[formatId];
|
||||
expect(format).toBe("#,##0.00[$€]");
|
||||
const { model: sharedModel } = await createModelWithDataSource({ spreadsheetData: data });
|
||||
expect(getCell(sharedModel, "A1").format).toBe("#,##0.00[$€]");
|
||||
});
|
||||
|
||||
test("odoo charts are replaced with an image", async function () {
|
||||
const { model } = await createSpreadsheetWithChart({ type: "odoo_bar" });
|
||||
const data = await freezeOdooData(model);
|
||||
expect(data.sheets[0].figures.length).toBe(1);
|
||||
expect(data.sheets[0].figures[0].tag).toBe("image");
|
||||
});
|
||||
|
||||
test("geo charts are replaced with an image", async function () {
|
||||
const { model } = await createSpreadsheetWithList({
|
||||
modelConfig: { external: { geoJsonService: { getAvailableRegions: () => [] } } },
|
||||
});
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
model.dispatch("CREATE_CHART", {
|
||||
sheetId,
|
||||
figureId: "1",
|
||||
chartId: "chartId",
|
||||
col: 0,
|
||||
row: 0,
|
||||
offset: { x: 0, y: 0 },
|
||||
definition: {
|
||||
type: "geo",
|
||||
dataSets: [],
|
||||
dataSetsHaveTitle: false,
|
||||
title: {},
|
||||
legendPosition: "none",
|
||||
},
|
||||
});
|
||||
|
||||
const data = await freezeOdooData(model);
|
||||
expect(data.sheets[0].figures.length).toBe(1);
|
||||
expect(data.sheets[0].figures[0].tag).toBe("image");
|
||||
});
|
||||
|
||||
test("Carousels figure with odoo data is converted to an image", async function () {
|
||||
const { model } = await createSpreadsheetWithChart({ type: "odoo_bar" });
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
const chartFigureId = model.getters.getFigures(sheetId)[0].id;
|
||||
createCarousel(model, { items: [] }, "carouselId");
|
||||
addChartFigureToCarousel(model, "carouselId", chartFigureId);
|
||||
|
||||
const data = await freezeOdooData(model);
|
||||
expect(data.sheets[0].figures.length).toBe(1);
|
||||
expect(data.sheets[0].figures[0].tag).toBe("image");
|
||||
});
|
||||
|
||||
test("translation function are replaced with their value", async function () {
|
||||
const { model } = await createModelWithDataSource();
|
||||
setCellContent(model, "A1", `=_t("example")`);
|
||||
setCellContent(model, "A2", `=CONCATENATE("for",_t(" example"))`);
|
||||
expect(getEvaluatedCell(model, "A1").value).toBe("example");
|
||||
expect(getEvaluatedCell(model, "A2").value).toBe("for example");
|
||||
const data = await freezeOdooData(model);
|
||||
const cells = data.sheets[0].cells;
|
||||
expect(cells.A1).toBe("example", {
|
||||
message: "the content is replaced with the value",
|
||||
});
|
||||
expect(cells.A2).toBe("for example", {
|
||||
message: "the content is replaced with the value even when translation function is nested",
|
||||
});
|
||||
});
|
||||
|
||||
test("a new sheet is added for global filters", async function () {
|
||||
const { model } = await createModelWithDataSource();
|
||||
await addGlobalFilter(model, THIS_YEAR_GLOBAL_FILTER);
|
||||
const data = await freezeOdooData(model);
|
||||
expect(data.sheets.length).toBe(2);
|
||||
expect(data.sheets[1].name).toBe("Active Filters");
|
||||
expect(data.sheets[1].cells.A2).toBe("This Year");
|
||||
});
|
||||
|
||||
test("global filters and their display value are exported", async function () {
|
||||
const { model } = await createModelWithDataSource();
|
||||
await addGlobalFilter(model, THIS_YEAR_GLOBAL_FILTER);
|
||||
const year = new Date().getFullYear().toString();
|
||||
const data = await freezeOdooData(model);
|
||||
expect(data.globalFilters.length).toBe(1);
|
||||
expect(data.globalFilters[0].label).toBe("This Year");
|
||||
expect(data.globalFilters[0].value).toBe(`1/1/${year}, 12/31/${year}`);
|
||||
});
|
||||
|
||||
test("from/to global filters are exported", async function () {
|
||||
const { model } = await createModelWithDataSource();
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "date",
|
||||
label: "Date Filter",
|
||||
});
|
||||
await setGlobalFilterValue(model, {
|
||||
id: "42",
|
||||
value: {
|
||||
type: "range",
|
||||
from: "2020-01-01",
|
||||
to: "2021-01-01",
|
||||
},
|
||||
});
|
||||
const data = await freezeOdooData(model);
|
||||
const filterSheet = data.sheets[1];
|
||||
expect(filterSheet.cells.B2).toBe("43831");
|
||||
expect(filterSheet.cells.C2).toBe("44197");
|
||||
expect(filterSheet.formats.B2).toBe(1);
|
||||
expect(filterSheet.formats.C2).toBe(1);
|
||||
expect(data.formats[1]).toBe("m/d/yyyy");
|
||||
expect(data.globalFilters.length).toBe(1);
|
||||
expect(data.globalFilters[0].label).toBe("Date Filter");
|
||||
expect(data.globalFilters[0].value).toBe("1/1/2020, 1/1/2021");
|
||||
});
|
||||
|
||||
test("from/to global filter without value is exported", async function () {
|
||||
const { model } = await createModelWithDataSource();
|
||||
await addGlobalFilter(model, {
|
||||
id: "42",
|
||||
type: "date",
|
||||
label: "Date Filter",
|
||||
});
|
||||
const data = await freezeOdooData(model);
|
||||
const filterSheet = data.sheets[1];
|
||||
expect(filterSheet.cells.A2).toBe("Date Filter");
|
||||
expect(filterSheet.cells.B2).toBe("");
|
||||
expect(filterSheet.cells.B2).toBe("");
|
||||
expect(filterSheet.formats.B2).toBe(1);
|
||||
expect(filterSheet.formats.C2).toBe(1);
|
||||
expect(data.formats[1]).toBe("m/d/yyyy");
|
||||
expect(data.globalFilters.length).toBe(1);
|
||||
expect(data.globalFilters[0].label).toBe("Date Filter");
|
||||
expect(data.globalFilters[0].value).toBe("");
|
||||
});
|
||||
|
||||
test("Empty ODOO.LIST result is frozen to an empty string", async function () {
|
||||
const { model } = await createSpreadsheetWithList();
|
||||
setCellContent(model, "A1", '=ODOO.LIST(1, 9999,"probability")'); // has no record
|
||||
await waitForDataLoaded(model);
|
||||
expect(getEvaluatedCell(model, "A1").value).toBe("");
|
||||
const frozenData = await freezeOdooData(model);
|
||||
expect(frozenData.sheets[0].cells.A1).toBe('=""');
|
||||
});
|
||||
|
||||
test("odoo links are replaced with their label", async function () {
|
||||
const view = {
|
||||
name: "an odoo view",
|
||||
viewType: "list",
|
||||
action: {
|
||||
modelName: "partner",
|
||||
views: [[false, "list"]],
|
||||
},
|
||||
};
|
||||
const data = {
|
||||
sheets: [
|
||||
{
|
||||
cells: {
|
||||
A1: "[menu_xml](odoo://ir_menu_xml_id/test_menu)",
|
||||
A2: "[menu_id](odoo://ir_menu_id/12)",
|
||||
A3: `[odoo_view](odoo://view/${JSON.stringify(view)})`,
|
||||
A4: "[external_link](https://odoo.com)",
|
||||
A5: "[internal_link](o-spreadsheet://Sheet1)",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { model } = await createModelWithDataSource({
|
||||
spreadsheetData: data,
|
||||
serverData: getMenuServerData(),
|
||||
});
|
||||
const frozenData = await freezeOdooData(model);
|
||||
expect(frozenData.sheets[0].cells.A1).toBe("menu_xml");
|
||||
expect(frozenData.sheets[0].cells.A2).toBe("menu_id");
|
||||
expect(frozenData.sheets[0].cells.A3).toBe("odoo_view");
|
||||
expect(frozenData.sheets[0].cells.A4).toBe("[external_link](https://odoo.com)");
|
||||
expect(frozenData.sheets[0].cells.A5).toBe("[internal_link](o-spreadsheet://Sheet1)");
|
||||
});
|
||||
|
||||
test("spilled pivot table", async function () {
|
||||
const { model } = await createSpreadsheetWithPivot({
|
||||
arch: /* xml */ `
|
||||
<pivot>
|
||||
<field name="probability" type="measure"/>
|
||||
</pivot>
|
||||
`,
|
||||
});
|
||||
setCellContent(model, "A10", "=PIVOT(1)");
|
||||
setCellStyle(model, "B12", { bold: true });
|
||||
const data = await freezeOdooData(model);
|
||||
const sheet = data.sheets[0];
|
||||
const cells = sheet.cells;
|
||||
expect(cells.A10).toBe("Partner Pivot");
|
||||
expect(cells.A11).toBe('=""');
|
||||
expect(cells.A12).toBe("Total");
|
||||
expect(cells.B10).toBe("Total");
|
||||
expect(cells.B11).toBe("Probability");
|
||||
expect(cells.B12).toBe("131");
|
||||
expect(data.formats[sheet.formats.B12]).toBe("#,##0.00");
|
||||
expect(data.pivots).toEqual({});
|
||||
expect(sheet.styles).toEqual({ B12: 1 });
|
||||
expect(data.styles[sheet.styles["B12"]]).toEqual(
|
||||
{ bold: true },
|
||||
{ message: "style is preserved" }
|
||||
);
|
||||
});
|
||||
|
||||
test("empty string computed measure is exported as =\"\"", async function () {
|
||||
const { model } = await createSpreadsheetWithPivot();
|
||||
setCellContent(model, "A10", "=PIVOT(1)");
|
||||
expect(getEvaluatedCell(model, "B12").value).toBe(""); // empty value
|
||||
const data = await freezeOdooData(model);
|
||||
const cells = data.sheets[0].cells;
|
||||
expect(cells.B12).toBe('=""');
|
||||
});
|
||||
|
||||
test("Lists are purged from the frozen data", async function () {
|
||||
const { model } = await createSpreadsheetWithList();
|
||||
const data = await freezeOdooData(model);
|
||||
expect(data.lists).toEqual({});
|
||||
});
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
import { contains, mockService } from "@web/../tests/web_test_helpers";
|
||||
import { defineSpreadsheetModels } from "@spreadsheet/../tests/helpers/data";
|
||||
import { expect, test } from "@odoo/hoot";
|
||||
|
||||
import { mountPublicSpreadsheet } from "@spreadsheet/../tests/helpers/ui";
|
||||
import { THIS_YEAR_GLOBAL_FILTER } from "@spreadsheet/../tests/helpers/global_filter";
|
||||
import { addGlobalFilter } from "@spreadsheet/../tests/helpers/commands";
|
||||
import { freezeOdooData } from "@spreadsheet/helpers/model";
|
||||
import { createModelWithDataSource } from "@spreadsheet/../tests/helpers/model";
|
||||
|
||||
defineSpreadsheetModels();
|
||||
|
||||
let data;
|
||||
mockService("http", {
|
||||
get: (route, params) => {
|
||||
if (route === "dashboardDataUrl") {
|
||||
return data;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
test("show spreadsheet in readonly mode", async function () {
|
||||
const { model } = await createModelWithDataSource();
|
||||
await addGlobalFilter(model, THIS_YEAR_GLOBAL_FILTER);
|
||||
data = await freezeOdooData(model);
|
||||
const fixture = await mountPublicSpreadsheet("dashboardDataUrl", "spreadsheet");
|
||||
const filterButton = fixture.querySelector(".o-public-spreadsheet-filter-button");
|
||||
expect(filterButton).toBe(null);
|
||||
});
|
||||
|
||||
test("show dashboard in dashboard mode when there are global filters", async function () {
|
||||
const { model } = await createModelWithDataSource();
|
||||
await addGlobalFilter(model, THIS_YEAR_GLOBAL_FILTER);
|
||||
data = await freezeOdooData(model);
|
||||
const fixture = await mountPublicSpreadsheet("dashboardDataUrl", "dashboard");
|
||||
const filterButton = fixture.querySelector(".o-public-spreadsheet-filter-button");
|
||||
expect(filterButton).toBeVisible();
|
||||
});
|
||||
|
||||
test("show dashboard in dashboard mode when there are no global filters", async function () {
|
||||
const { model } = await createModelWithDataSource();
|
||||
data = await freezeOdooData(model);
|
||||
const fixture = await mountPublicSpreadsheet("dashboardDataUrl", "dashboard");
|
||||
const filterButton = fixture.querySelector(".o-public-spreadsheet-filter-button");
|
||||
expect(filterButton).toBe(null);
|
||||
});
|
||||
|
||||
test("click filter button can show all filters", async function () {
|
||||
const { model } = await createModelWithDataSource();
|
||||
await addGlobalFilter(model, THIS_YEAR_GLOBAL_FILTER);
|
||||
data = await freezeOdooData(model);
|
||||
const fixture = await mountPublicSpreadsheet("dashboardDataUrl", "dashboard");
|
||||
await contains(".o-public-spreadsheet-filter-button").click();
|
||||
expect(fixture.querySelector(".o-public-spreadsheet-filters")).toBeVisible();
|
||||
expect(fixture.querySelector(".o-public-spreadsheet-filter-button")).toBe(null);
|
||||
});
|
||||
|
||||
test("click close button in filter panel will close the panel", async function () {
|
||||
const { model } = await createModelWithDataSource();
|
||||
await addGlobalFilter(model, THIS_YEAR_GLOBAL_FILTER);
|
||||
data = await freezeOdooData(model);
|
||||
const fixture = await mountPublicSpreadsheet("dashboardDataUrl", "dashboard");
|
||||
await contains(".o-public-spreadsheet-filter-button").click();
|
||||
await contains(".o-public-spreadsheet-filters-close-button").click();
|
||||
expect(fixture.querySelector(".o-public-spreadsheet-filter-button")).toBeVisible();
|
||||
expect(fixture.querySelector(".o-public-spreadsheet-filters")).toBe(null);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("Hides the download button when the downloadExcelUrl is not provided", async function () {
|
||||
const { model } = await createModelWithDataSource();
|
||||
data = await freezeOdooData(model);
|
||||
const fixture = await mountPublicSpreadsheet("dashboardDataUrl", "spreadsheet", false);
|
||||
await contains(".o-topbar-menu[data-id='file']").click();
|
||||
expect(fixture.querySelector(".o-menu-item[data-name='download_public_excel']")).toBe(null);
|
||||
});
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { nextTick } from "@web/../tests/helpers/utils";
|
||||
import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended";
|
||||
import { createModelWithDataSource } from "./model";
|
||||
const uuidGenerator = new spreadsheet.helpers.UuidGenerator();
|
||||
|
||||
/** @typedef {import("@spreadsheet/o_spreadsheet/o_spreadsheet").Model} Model */
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Model} model
|
||||
*/
|
||||
export function insertChartInSpreadsheet(model, type = "odoo_bar") {
|
||||
const definition = getChartDefinition(type);
|
||||
model.dispatch("CREATE_CHART", {
|
||||
sheetId: model.getters.getActiveSheetId(),
|
||||
id: definition.id,
|
||||
position: {
|
||||
x: 10,
|
||||
y: 10,
|
||||
},
|
||||
definition,
|
||||
});
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {Object} params
|
||||
* @param {function} [params.mockRPC]
|
||||
* @param {object} [params.serverData]
|
||||
* @param {string} [params.type]
|
||||
*
|
||||
* @returns { Promise<{ model: Model, env: Object }>}
|
||||
*/
|
||||
export async function createSpreadsheetWithChart(params = {}) {
|
||||
const model = await createModelWithDataSource({
|
||||
mockRPC: params.mockRPC,
|
||||
serverData: params.serverData,
|
||||
});
|
||||
|
||||
insertChartInSpreadsheet(model, params.type);
|
||||
|
||||
const env = model.config.evalContext.env;
|
||||
env.model = model;
|
||||
await nextTick();
|
||||
return { model, env };
|
||||
}
|
||||
|
||||
function getChartDefinition(type) {
|
||||
return {
|
||||
metaData: {
|
||||
groupBy: ["foo", "bar"],
|
||||
measure: "__count",
|
||||
order: null,
|
||||
resModel: "partner",
|
||||
},
|
||||
searchParams: {
|
||||
comparison: null,
|
||||
context: {},
|
||||
domain: [],
|
||||
groupBy: [],
|
||||
orderBy: [],
|
||||
},
|
||||
stacked: true,
|
||||
title: "Partners",
|
||||
background: "#FFFFFF",
|
||||
legendPosition: "top",
|
||||
verticalAxisPosition: "left",
|
||||
dataSourceId: uuidGenerator.uuidv4(),
|
||||
id: uuidGenerator.uuidv4(),
|
||||
type,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,163 +0,0 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended";
|
||||
import { waitForDataSourcesLoaded } from "@spreadsheet/../tests/utils/model";
|
||||
|
||||
const { toCartesian, toZone } = spreadsheet.helpers;
|
||||
|
||||
/**
|
||||
* @typedef {import("@spreadsheet/global_filters/plugins/global_filters_core_plugin").GlobalFilter} GlobalFilter
|
||||
*/
|
||||
|
||||
/**
|
||||
* Select a cell
|
||||
*/
|
||||
export function selectCell(model, xc) {
|
||||
const { col, row } = toCartesian(xc);
|
||||
return model.selection.selectCell(col, row);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a global filter and ensure the data sources are completely reloaded
|
||||
* @param {Model} model
|
||||
* @param {{filter: GlobalFilter}} filter
|
||||
*/
|
||||
export async function addGlobalFilter(model, filter, fieldMatchings = {}) {
|
||||
const result = model.dispatch("ADD_GLOBAL_FILTER", { ...filter, ...fieldMatchings });
|
||||
await waitForDataSourcesLoaded(model);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a global filter and ensure the data sources are completely reloaded
|
||||
*/
|
||||
export async function removeGlobalFilter(model, id) {
|
||||
const result = model.dispatch("REMOVE_GLOBAL_FILTER", { id });
|
||||
await waitForDataSourcesLoaded(model);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a global filter and ensure the data sources are completely reloaded
|
||||
*/
|
||||
export async function editGlobalFilter(model, filter) {
|
||||
const result = model.dispatch("EDIT_GLOBAL_FILTER", filter);
|
||||
await waitForDataSourcesLoaded(model);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value of a global filter and ensure the data sources are completely
|
||||
* reloaded
|
||||
*/
|
||||
export async function setGlobalFilterValue(model, payload) {
|
||||
const result = model.dispatch("SET_GLOBAL_FILTER_VALUE", payload);
|
||||
await waitForDataSourcesLoaded(model);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the selection
|
||||
*/
|
||||
export function setSelection(model, xc) {
|
||||
const zone = toZone(xc);
|
||||
model.selection.selectZone({ cell: { col: zone.left, row: zone.top }, zone });
|
||||
}
|
||||
|
||||
/**
|
||||
* Autofill from a zone to a cell
|
||||
*/
|
||||
export function autofill(model, from, to) {
|
||||
setSelection(model, from);
|
||||
model.dispatch("AUTOFILL_SELECT", toCartesian(to));
|
||||
model.dispatch("AUTOFILL");
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the content of a cell
|
||||
*/
|
||||
export function setCellContent(model, xc, content, sheetId = model.getters.getActiveSheetId()) {
|
||||
model.dispatch("UPDATE_CELL", { ...toCartesian(xc), sheetId, content });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the format of a cell
|
||||
*/
|
||||
export function setCellFormat(model, xc, format, sheetId = model.getters.getActiveSheetId()) {
|
||||
model.dispatch("UPDATE_CELL", { ...toCartesian(xc), sheetId, format });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the style of a cell
|
||||
*/
|
||||
export function setCellStyle(model, xc, style, sheetId = model.getters.getActiveSheetId()) {
|
||||
model.dispatch("UPDATE_CELL", { ...toCartesian(xc), sheetId, style });
|
||||
}
|
||||
|
||||
/** Create a test chart in the active sheet*/
|
||||
export function createBasicChart(model, chartId, sheetId = model.getters.getActiveSheetId()) {
|
||||
model.dispatch("CREATE_CHART", {
|
||||
id: chartId,
|
||||
position: { x: 0, y: 0 },
|
||||
sheetId: sheetId,
|
||||
definition: {
|
||||
title: "test",
|
||||
dataSets: ["A1"],
|
||||
type: "bar",
|
||||
background: "#fff",
|
||||
verticalAxisPosition: "left",
|
||||
legendPosition: "top",
|
||||
stackedBar: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Create a test scorecard chart in the active sheet*/
|
||||
export function createScorecardChart(model, chartId, sheetId = model.getters.getActiveSheetId()) {
|
||||
model.dispatch("CREATE_CHART", {
|
||||
id: chartId,
|
||||
position: { x: 0, y: 0 },
|
||||
sheetId: sheetId,
|
||||
definition: {
|
||||
title: "test",
|
||||
keyValue: "A1",
|
||||
type: "scorecard",
|
||||
background: "#fff",
|
||||
baselineColorDown: "#DC6965",
|
||||
baselineColorUp: "#00A04A",
|
||||
baselineMode: "absolute",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Create a test scorecard chart in the active sheet*/
|
||||
export function createGaugeChart(model, chartId, sheetId = model.getters.getActiveSheetId()) {
|
||||
model.dispatch("CREATE_CHART", {
|
||||
id: chartId,
|
||||
position: { x: 0, y: 0 },
|
||||
sheetId: sheetId,
|
||||
definition: {
|
||||
title: "test",
|
||||
type: "gauge",
|
||||
background: "#fff",
|
||||
dataRange: "A1",
|
||||
sectionRule: {
|
||||
rangeMin: "0",
|
||||
rangeMax: "100",
|
||||
colors: {
|
||||
lowerColor: "#112233",
|
||||
middleColor: "#445566",
|
||||
upperColor: "#778899",
|
||||
},
|
||||
lowerInflectionPoint: {
|
||||
type: "number",
|
||||
value: "25",
|
||||
},
|
||||
upperInflectionPoint: {
|
||||
type: "number",
|
||||
value: "85",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -1,445 +0,0 @@
|
|||
/** @odoo-module */
|
||||
|
||||
/**
|
||||
* @typedef {object} ServerData
|
||||
* @property {object} models
|
||||
* @property {object} views
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get a basic arch for a pivot, which is compatible with the data given by
|
||||
* getBasicData().
|
||||
*
|
||||
* Here is the pivot created:
|
||||
* A B C D E F
|
||||
* 1 1 2 12 17 Total
|
||||
* 2 Proba Proba Proba Proba Proba
|
||||
* 3 false 15 15
|
||||
* 4 true 11 10 95 116
|
||||
* 5 Total 11 15 10 95 131
|
||||
*/
|
||||
export function getBasicPivotArch() {
|
||||
return /* xml */ `
|
||||
<pivot string="Partners">
|
||||
<field name="foo" type="col"/>
|
||||
<field name="bar" type="row"/>
|
||||
<field name="probability" type="measure"/>
|
||||
</pivot>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a basic arch for a list, which is compatible with the data given by
|
||||
* getBasicData().
|
||||
*
|
||||
* Here is the list created:
|
||||
* A B C D
|
||||
* 1 Foo bar Date Product
|
||||
* 2 12 True 2016-04-14 xphone
|
||||
* 3 1 True 2016-10-26 xpad
|
||||
* 4 17 True 2016-12-15 xpad
|
||||
* 5 2 False 2016-12-11 xpad
|
||||
*/
|
||||
export function getBasicListArch() {
|
||||
return /* xml */ `
|
||||
<tree string="Partners">
|
||||
<field name="foo"/>
|
||||
<field name="bar"/>
|
||||
<field name="date"/>
|
||||
<field name="product_id"/>
|
||||
</tree>
|
||||
`;
|
||||
}
|
||||
|
||||
export function getBasicGraphArch() {
|
||||
return /* xml */ `
|
||||
<graph>
|
||||
<field name="bar" />
|
||||
</graph>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {ServerData}
|
||||
*/
|
||||
export function getBasicServerData() {
|
||||
return {
|
||||
models: getBasicData(),
|
||||
views: {
|
||||
"partner,false,list": getBasicListArch(),
|
||||
"partner,false,pivot": getBasicPivotArch(),
|
||||
"partner,false,graph": getBasicGraphArch(),
|
||||
"partner,false,form": /* xml */ `<Form/>`,
|
||||
"partner,false,search": /* xml */ `<search/>`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} model
|
||||
* @param {Array<string>} columns
|
||||
* @param {Object} data
|
||||
*
|
||||
* @returns { {definition: Object, columns: Array<Object>}}
|
||||
*/
|
||||
export function generateListDefinition(model, columns, data = getBasicData()) {
|
||||
const cols = [];
|
||||
for (const name of columns) {
|
||||
cols.push({
|
||||
name,
|
||||
type: data[model].fields[name].type,
|
||||
});
|
||||
}
|
||||
return {
|
||||
definition: {
|
||||
metaData: {
|
||||
resModel: model,
|
||||
columns,
|
||||
},
|
||||
searchParams: {
|
||||
domain: [],
|
||||
context: {},
|
||||
orderBy: [],
|
||||
},
|
||||
name: "List",
|
||||
},
|
||||
columns: cols,
|
||||
};
|
||||
}
|
||||
|
||||
export function getBasicListArchs() {
|
||||
return {
|
||||
"partner,false,list": getBasicListArch(),
|
||||
"partner,false,search": /* xml */ `<search/>`,
|
||||
"partner,false,form": /* xml */ `<form/>`,
|
||||
};
|
||||
}
|
||||
|
||||
export function getBasicData() {
|
||||
return {
|
||||
"documents.document": {
|
||||
fields: {
|
||||
name: { string: "Name", type: "char" },
|
||||
raw: { string: "Data", type: "text" },
|
||||
thumbnail: { string: "Thumbnail", type: "text" },
|
||||
display_thumbnail: { string: "Thumbnail", type: "text" },
|
||||
favorited_ids: { string: "Name", type: "many2many" },
|
||||
is_favorited: { string: "Name", type: "boolean" },
|
||||
mimetype: { string: "Mimetype", type: "char" },
|
||||
partner_id: { string: "Related partner", type: "many2one", relation: "partner" },
|
||||
owner_id: { string: "Owner", type: "many2one", relation: "partner" },
|
||||
handler: {
|
||||
string: "Handler",
|
||||
type: "selection",
|
||||
selection: [["spreadsheet", "Spreadsheet"]],
|
||||
},
|
||||
previous_attachment_ids: {
|
||||
string: "History",
|
||||
type: "many2many",
|
||||
relation: "ir.attachment",
|
||||
},
|
||||
tag_ids: { string: "Tags", type: "many2many", relation: "documents.tag" },
|
||||
folder_id: { string: "Workspaces", type: "many2one", relation: "documents.folder" },
|
||||
res_model: { string: "Model (technical)", type: "char" },
|
||||
available_rule_ids: {
|
||||
string: "Rules",
|
||||
type: "many2many",
|
||||
relation: "documents.workflow.rule",
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
name: "My spreadsheet",
|
||||
raw: "{}",
|
||||
is_favorited: false,
|
||||
folder_id: 1,
|
||||
handler: "spreadsheet",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "",
|
||||
raw: "{}",
|
||||
is_favorited: true,
|
||||
folder_id: 1,
|
||||
handler: "spreadsheet",
|
||||
},
|
||||
],
|
||||
},
|
||||
"ir.model": {
|
||||
fields: {
|
||||
name: { string: "Model Name", type: "char" },
|
||||
model: { string: "Model", type: "char" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 37,
|
||||
name: "Product",
|
||||
model: "product",
|
||||
},
|
||||
{
|
||||
id: 40,
|
||||
name: "Partner",
|
||||
model: "partner",
|
||||
},
|
||||
],
|
||||
},
|
||||
"documents.folder": {
|
||||
fields: {
|
||||
name: { string: "Name", type: "char" },
|
||||
parent_folder_id: {
|
||||
string: "Parent Workspace",
|
||||
type: "many2one",
|
||||
relation: "documents.folder",
|
||||
},
|
||||
description: { string: "Description", type: "text" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
name: "Workspace1",
|
||||
description: "Workspace",
|
||||
parent_folder_id: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
"documents.tag": {
|
||||
fields: {},
|
||||
records: [],
|
||||
get_tags: () => [],
|
||||
},
|
||||
"documents.workflow.rule": {
|
||||
fields: {},
|
||||
records: [],
|
||||
},
|
||||
"documents.share": {
|
||||
fields: {},
|
||||
records: [],
|
||||
},
|
||||
"spreadsheet.template": {
|
||||
fields: {
|
||||
name: { string: "Name", type: "char" },
|
||||
data: { string: "Data", type: "binary" },
|
||||
thumbnail: { string: "Thumbnail", type: "binary" },
|
||||
display_thumbnail: { string: "Thumbnail", type: "text" },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, name: "Template 1", data: btoa("{}") },
|
||||
{ id: 2, name: "Template 2", data: btoa("{}") },
|
||||
],
|
||||
},
|
||||
"res.currency": {
|
||||
fields: {
|
||||
name: { string: "Code", type: "char" },
|
||||
symbol: { string: "Symbol", type: "char" },
|
||||
position: {
|
||||
string: "Position",
|
||||
type: "selection",
|
||||
selection: [
|
||||
["after", "A"],
|
||||
["before", "B"],
|
||||
],
|
||||
},
|
||||
decimal_places: { string: "decimal", type: "integer" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
name: "EUR",
|
||||
symbol: "€",
|
||||
position: "after",
|
||||
decimal_places: 2,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "USD",
|
||||
symbol: "$",
|
||||
position: "before",
|
||||
decimal_places: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
partner: {
|
||||
fields: {
|
||||
foo: {
|
||||
string: "Foo",
|
||||
type: "integer",
|
||||
store: true,
|
||||
searchable: true,
|
||||
group_operator: "sum",
|
||||
},
|
||||
bar: {
|
||||
string: "Bar",
|
||||
type: "boolean",
|
||||
store: true,
|
||||
sortable: true,
|
||||
searchable: true,
|
||||
},
|
||||
name: {
|
||||
string: "name",
|
||||
type: "char",
|
||||
store: true,
|
||||
sortable: true,
|
||||
searchable: true,
|
||||
},
|
||||
date: {
|
||||
string: "Date",
|
||||
type: "date",
|
||||
store: true,
|
||||
sortable: true,
|
||||
searchable: true,
|
||||
},
|
||||
create_date: {
|
||||
string: "Creation Date",
|
||||
type: "datetime",
|
||||
store: true,
|
||||
sortable: true,
|
||||
},
|
||||
active: { string: "Active", type: "bool", default: true, searchable: true },
|
||||
product_id: {
|
||||
string: "Product",
|
||||
type: "many2one",
|
||||
relation: "product",
|
||||
store: true,
|
||||
sortable: true,
|
||||
searchable: true,
|
||||
},
|
||||
tag_ids: {
|
||||
string: "Tags",
|
||||
type: "many2many",
|
||||
relation: "tag",
|
||||
store: true,
|
||||
sortable: true,
|
||||
searchable: true,
|
||||
},
|
||||
probability: {
|
||||
string: "Probability",
|
||||
type: "float",
|
||||
searchable: true,
|
||||
store: true,
|
||||
group_operator: "avg",
|
||||
},
|
||||
field_with_array_agg: {
|
||||
string: "field_with_array_agg",
|
||||
type: "integer",
|
||||
searchable: true,
|
||||
group_operator: "array_agg",
|
||||
},
|
||||
currency_id: {
|
||||
string: "Currency",
|
||||
type: "many2one",
|
||||
relation: "res.currency",
|
||||
store: true,
|
||||
sortable: true,
|
||||
searchable: true,
|
||||
},
|
||||
pognon: {
|
||||
string: "Money!",
|
||||
type: "monetary",
|
||||
currency_field: "currency_id",
|
||||
store: true,
|
||||
sortable: true,
|
||||
group_operator: "avg",
|
||||
searchable: true,
|
||||
},
|
||||
partner_properties: {
|
||||
string: "Properties",
|
||||
type: "properties",
|
||||
store: true,
|
||||
sortable: true,
|
||||
searchable: true,
|
||||
},
|
||||
jsonField: {
|
||||
string: "Json Field",
|
||||
type: "json",
|
||||
store: true,
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
foo: 12,
|
||||
bar: true,
|
||||
date: "2016-04-14",
|
||||
create_date: "2016-04-03 00:00:00",
|
||||
product_id: 37,
|
||||
probability: 10,
|
||||
field_with_array_agg: 1,
|
||||
tag_ids: [42, 67],
|
||||
currency_id: 1,
|
||||
pognon: 74.4,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
foo: 1,
|
||||
bar: true,
|
||||
date: "2016-10-26",
|
||||
create_date: "2014-04-03 00:05:32",
|
||||
product_id: 41,
|
||||
probability: 11,
|
||||
field_with_array_agg: 2,
|
||||
tag_ids: [42, 67],
|
||||
currency_id: 2,
|
||||
pognon: 74.8,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
foo: 17,
|
||||
bar: true,
|
||||
date: "2016-12-15",
|
||||
create_date: "2006-01-03 11:30:50",
|
||||
product_id: 41,
|
||||
probability: 95,
|
||||
field_with_array_agg: 3,
|
||||
tag_ids: [],
|
||||
currency_id: 1,
|
||||
pognon: 4,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
foo: 2,
|
||||
bar: false,
|
||||
date: "2016-12-11",
|
||||
create_date: "2016-12-10 21:59:59",
|
||||
product_id: 41,
|
||||
probability: 15,
|
||||
field_with_array_agg: 4,
|
||||
tag_ids: [42],
|
||||
currency_id: 2,
|
||||
pognon: 1000,
|
||||
},
|
||||
],
|
||||
},
|
||||
product: {
|
||||
fields: {
|
||||
name: { string: "Product Name", type: "char" },
|
||||
active: { string: "Active", type: "bool", default: true },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 37,
|
||||
display_name: "xphone",
|
||||
},
|
||||
{
|
||||
id: 41,
|
||||
display_name: "xpad",
|
||||
},
|
||||
],
|
||||
},
|
||||
tag: {
|
||||
fields: {
|
||||
name: { string: "Tag Name", type: "char" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 42,
|
||||
display_name: "isCool",
|
||||
},
|
||||
{
|
||||
id: 67,
|
||||
display_name: "Growing",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended";
|
||||
|
||||
const { toCartesian } = spreadsheet.helpers;
|
||||
|
||||
/**
|
||||
* Get the value of the given cell
|
||||
*/
|
||||
export function getCellValue(model, xc, sheetId = model.getters.getActiveSheetId()) {
|
||||
const { col, row } = toCartesian(xc);
|
||||
const cell = model.getters.getCell(sheetId, col, row);
|
||||
if (!cell) {
|
||||
return undefined;
|
||||
}
|
||||
return cell.evaluated.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cell of the given xc
|
||||
*/
|
||||
export function getCell(model, xc, sheetId = model.getters.getActiveSheetId()) {
|
||||
const { col, row } = toCartesian(xc);
|
||||
return model.getters.getCell(sheetId, col, row);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cells of the given sheet (or active sheet if not provided)
|
||||
*/
|
||||
export function getCells(model, sheetId = model.getters.getActiveSheetId()) {
|
||||
return model.getters.getCells(sheetId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the formula of the given xc
|
||||
*/
|
||||
export function getCellFormula(model, xc, sheetId = model.getters.getActiveSheetId()) {
|
||||
const cell = getCell(model, xc, sheetId);
|
||||
return cell && cell.isFormula() ? model.getters.getFormulaCellContent(sheetId, cell) : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content of the given xc
|
||||
*/
|
||||
export function getCellContent(model, xc, sheetId = model.getters.getActiveSheetId()) {
|
||||
const cell = getCell(model, xc, sheetId);
|
||||
return cell ? model.getters.getCellText(cell, true) : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of the merges (["A1:A2"]) of the sheet
|
||||
*/
|
||||
export function getMerges(model, sheetId = model.getters.getActiveSheetId()) {
|
||||
return model.exportData().sheets.find((sheet) => sheet.id === sheetId).merges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the formatted value of the given xc
|
||||
*/
|
||||
export function getCellFormattedValue(model, xc, sheetId = model.getters.getActiveSheetId()) {
|
||||
const cell = getCell(model, xc, sheetId);
|
||||
return cell ? model.getters.getCellText(cell, false) : "";
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
registry
|
||||
.category("mock_server")
|
||||
.add("res.currency/get_currencies_for_spreadsheet", function (route, args) {
|
||||
const currencyNames = args.args[0];
|
||||
const result = [];
|
||||
for (let currencyName of currencyNames) {
|
||||
const curr = this.models["res.currency"].records.find(
|
||||
(curr) => curr.name === currencyName
|
||||
);
|
||||
|
||||
result.push({
|
||||
code: curr.name,
|
||||
symbol: curr.symbol,
|
||||
decimalPlaces: curr.decimal_places || 2,
|
||||
position: curr.position || "after",
|
||||
});
|
||||
}
|
||||
return result;
|
||||
})
|
||||
.add("res.currency/get_company_currency_for_spreadsheet", function (route, args) {
|
||||
return {
|
||||
code: "EUR",
|
||||
symbol: "€",
|
||||
position: "after",
|
||||
decimalPlaces: 2,
|
||||
};
|
||||
});
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { ormService } from "@web/core/orm_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import { nextTick } from "@web/../tests/helpers/utils";
|
||||
|
||||
import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended";
|
||||
import { DataSources } from "@spreadsheet/data_sources/data_sources";
|
||||
import { getBasicServerData } from "./data";
|
||||
|
||||
const { Model } = spreadsheet;
|
||||
|
||||
/**
|
||||
* @typedef {import("@spreadsheet/../tests/utils/data").ServerData} ServerData
|
||||
*/
|
||||
|
||||
export function setupDataSourceEvaluation(model) {
|
||||
model.config.dataSources.addEventListener("data-source-updated", () => {
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
model.dispatch("EVALUATE_CELLS", { sheetId });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a spreadsheet model with a mocked server environnement
|
||||
*
|
||||
* @param {object} params
|
||||
* @param {object} [params.spreadsheetData] Spreadsheet data to import
|
||||
* @param {ServerData} [params.serverData] Data to be injected in the mock server
|
||||
* @param {function} [params.mockRPC] Mock rpc function
|
||||
*/
|
||||
export async function createModelWithDataSource(params = {}) {
|
||||
registry.category("services").add("orm", ormService, { force: true });
|
||||
registry.category("services").add("localization", makeFakeLocalizationService(), { force: true });
|
||||
const env = await makeTestEnv({
|
||||
serverData: params.serverData || getBasicServerData(),
|
||||
mockRPC: params.mockRPC,
|
||||
});
|
||||
const model = new Model(params.spreadsheetData, {
|
||||
evalContext: { env },
|
||||
//@ts-ignore
|
||||
dataSources: new DataSources(env.services.orm),
|
||||
});
|
||||
setupDataSourceEvaluation(model);
|
||||
await nextTick(); // initial async formulas loading
|
||||
return model;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Model} model
|
||||
*/
|
||||
export async function waitForDataSourcesLoaded(model) {
|
||||
function readAllCellsValue() {
|
||||
for (const sheetId of model.getters.getSheetIds()) {
|
||||
const cells = model.getters.getCells(sheetId);
|
||||
for (const cellId in cells) {
|
||||
cells[cellId].evaluated.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Read a first time in order to trigger the RPC
|
||||
readAllCellsValue();
|
||||
//@ts-ignore
|
||||
await model.config.dataSources.waitForAllLoaded();
|
||||
await nextTick();
|
||||
// Read a second time to trigger the compute format (which could trigger a RPC for currency, in list)
|
||||
readAllCellsValue();
|
||||
await nextTick();
|
||||
// Read a third time to trigger the RPC to get the correct currency
|
||||
readAllCellsValue();
|
||||
await nextTick();
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { PivotArchParser } from "@web/views/pivot/pivot_arch_parser";
|
||||
import { nextTick } from "@web/../tests/helpers/utils";
|
||||
|
||||
import PivotDataSource from "@spreadsheet/pivot/pivot_data_source";
|
||||
import { getBasicServerData } from "./data";
|
||||
import { createModelWithDataSource, waitForDataSourcesLoaded } from "./model";
|
||||
|
||||
/** @typedef {import("@spreadsheet/o_spreadsheet/o_spreadsheet").Model} Model */
|
||||
|
||||
/**
|
||||
* @param {Model} model
|
||||
* @param {object} params
|
||||
* @param {string} params.arch
|
||||
* @param {[number, number]} [params.anchor]
|
||||
*/
|
||||
export async function insertPivotInSpreadsheet(model, params) {
|
||||
const archInfo = new PivotArchParser().parse(params.arch);
|
||||
const definition = {
|
||||
metaData: {
|
||||
colGroupBys: archInfo.colGroupBys,
|
||||
rowGroupBys: archInfo.rowGroupBys,
|
||||
activeMeasures: archInfo.activeMeasures,
|
||||
resModel: params.resModel || "partner",
|
||||
},
|
||||
searchParams: {
|
||||
domain: [],
|
||||
context: {},
|
||||
groupBy: [],
|
||||
orderBy: [],
|
||||
},
|
||||
name: "Partner Pivot",
|
||||
};
|
||||
const dataSource = model.config.dataSources.create(PivotDataSource, definition);
|
||||
await dataSource.load();
|
||||
const { cols, rows, measures } = dataSource.getTableStructure().export();
|
||||
const table = {
|
||||
cols,
|
||||
rows,
|
||||
measures,
|
||||
};
|
||||
const [col, row] = params.anchor || [0, 0];
|
||||
model.dispatch("INSERT_PIVOT", {
|
||||
id: model.getters.getNextPivotId(),
|
||||
sheetId: model.getters.getActiveSheetId(),
|
||||
col,
|
||||
row,
|
||||
table,
|
||||
dataSourceId: "pivotData1",
|
||||
definition,
|
||||
});
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} params
|
||||
* @param {string} [params.arch]
|
||||
* @param {object} [params.serverData]
|
||||
* @param {function} [params.mockRPC]
|
||||
* @returns {Promise<{ model: Model, env: object}>}
|
||||
*/
|
||||
export async function createSpreadsheetWithPivot(params = {}) {
|
||||
const serverData = params.serverData || getBasicServerData();
|
||||
const model = await createModelWithDataSource({
|
||||
mockRPC: params.mockRPC,
|
||||
serverData: params.serverData,
|
||||
});
|
||||
const arch = params.arch || serverData.views["partner,false,pivot"];
|
||||
await insertPivotInSpreadsheet(model, { arch });
|
||||
const env = model.config.evalContext.env;
|
||||
env.model = model;
|
||||
await waitForDataSourcesLoaded(model);
|
||||
return { model, env };
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended";
|
||||
import { registerCleanup } from "@web/../tests/helpers/cleanup";
|
||||
import { getFixture } from "@web/../tests/helpers/utils";
|
||||
import { loadJS, templates } from "@web/core/assets";
|
||||
|
||||
const { App } = owl;
|
||||
const { Spreadsheet } = spreadsheet;
|
||||
const { getMenuChildren } = spreadsheet.helpers;
|
||||
|
||||
/** @typedef {import("@spreadsheet/o_spreadsheet/o_spreadsheet").Model} Model */
|
||||
|
||||
/**
|
||||
* Mount o-spreadsheet component with the given spreadsheet model
|
||||
* @param {Model} model
|
||||
* @returns {Promise<HTMLElement>}
|
||||
*/
|
||||
export async function mountSpreadsheet(model) {
|
||||
await loadJS("/web/static/lib/Chart/Chart.js");
|
||||
const app = new App(Spreadsheet, {
|
||||
props: { model },
|
||||
templates: templates,
|
||||
env: model.config.evalContext.env,
|
||||
test: true,
|
||||
});
|
||||
registerCleanup(() => app.destroy());
|
||||
const fixture = getFixture();
|
||||
await app.mount(fixture);
|
||||
return fixture;
|
||||
}
|
||||
|
||||
export async function doMenuAction(registry, path, env) {
|
||||
const root = path[0];
|
||||
let node = registry.get(root);
|
||||
for (const p of path.slice(1)) {
|
||||
const children = getMenuChildren(node, env);
|
||||
node = children.find((child) => child.id === p);
|
||||
}
|
||||
if (!node) {
|
||||
throw new Error(`Cannot find menu with path "${path.join("/")}"`);
|
||||
}
|
||||
await node.action(env);
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { defineModels, fields, models, mountView, onRpc } from "@web/../tests/web_test_helpers";
|
||||
import { defineMailModels } from "@mail/../tests/mail_test_helpers";
|
||||
import { click } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
|
||||
class TestSpreadsheet extends models.Model {
|
||||
_name = "test.spreadsheet";
|
||||
spreadsheet_binary_data = fields.Binary();
|
||||
_records = [{ spreadsheet_binary_data: "R0lGODlhDAMAKIFAF5LAP/zxANyuAP/gaP//wACH5BAEAUALAw" }];
|
||||
}
|
||||
|
||||
defineMailModels();
|
||||
defineModels([TestSpreadsheet]);
|
||||
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
test("Downloading dashboard json file should be disabled in list view", async () => {
|
||||
onRpc("/web/content", () => {
|
||||
expect.step("We shouldn't be getting the file.");
|
||||
});
|
||||
await mountView({
|
||||
resModel: "test.spreadsheet",
|
||||
type: "list",
|
||||
arch: `<list>
|
||||
<field
|
||||
name="spreadsheet_binary_data"
|
||||
widget="binary_spreadsheet"
|
||||
filename="dashboard.json"
|
||||
/>
|
||||
</list>`,
|
||||
});
|
||||
click(`.o_field_widget[name="spreadsheet_binary_data"]`);
|
||||
await animationFrame();
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("Download button for dashboard json file should be hidden in list view", async () => {
|
||||
await mountView({
|
||||
resModel: "test.spreadsheet",
|
||||
type: "list",
|
||||
arch: `<list>
|
||||
<field
|
||||
name="spreadsheet_binary_data"
|
||||
widget="binary_spreadsheet"
|
||||
filename="dashboard.json"
|
||||
/>
|
||||
</list>`,
|
||||
});
|
||||
expect(`.o_field_widget[name="spreadsheet_binary_data"] .fa-download`).toHaveCount(0, {
|
||||
message: "The download button should be hidden",
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue