Initial commit: Report packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:51 +02:00
commit bc5e1e9efa
604 changed files with 474102 additions and 0 deletions

View file

@ -0,0 +1,73 @@
/** @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,
};
}

View file

@ -0,0 +1,163 @@
/** @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",
},
},
},
});
}

View file

@ -0,0 +1,445 @@
/** @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",
},
],
},
};
}

View file

@ -0,0 +1,52 @@
/** @odoo-module */
const { DateTime } = luxon;
import { Domain } from "@web/core/domain";
function getDateDomainBounds(domain) {
const startDateStr = domain[1][2];
const endDateStr = domain[2][2];
const isDateTime = startDateStr.includes(":");
if (isDateTime) {
const dateTimeFormat = "yyyy-MM-dd HH:mm:ss";
const start = DateTime.fromFormat(startDateStr, dateTimeFormat);
const end = DateTime.fromFormat(endDateStr, dateTimeFormat);
return { start, end };
}
const start = DateTime.fromISO(startDateStr);
const end = DateTime.fromISO(endDateStr);
const startIsIncluded = domain[1][1] === ">=";
const endIsIncluded = domain[2][1] === "<=";
return {
start: startIsIncluded ? start.startOf("day") : start.endOf("day"),
end: endIsIncluded ? end.endOf("day") : end.startOf("day"),
};
}
/**
* @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) {
domain = new Domain(domain).toList();
assert.deepEqual(domain[0], "&");
assert.deepEqual(domain[1], [field, ">=", start]);
assert.deepEqual(domain[2], [field, "<=", end]);
}
/**
* @param {import("@web/core/domain").DomainRepr} domain
* @returns {number}
*/
export function getDateDomainDurationInDays(domain) {
domain = new Domain(domain).toList();
const bounds = getDateDomainBounds(domain);
const diff = bounds.end.diff(bounds.start, ["days"]);
return Math.round(diff.days);
}

View file

@ -0,0 +1,63 @@
/** @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) : "";
}

View file

@ -0,0 +1,69 @@
/** @odoo-module */
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 */
/**
* Insert a list in a spreadsheet model.
*
* @param {Model} model
* @param {Object} params
* @param {string} params.model
* @param {Array<string>} params.columns
* @param {number} [params.linesNumber]
* @param {[number, number]} [params.position]
* @param {string} [params.sheetId]
*/
export function insertListInSpreadsheet(model, params) {
const { definition, columns } = generateListDefinition(params.model, params.columns);
const [col, row] = params.position || [0, 0];
model.dispatch("INSERT_ODOO_LIST", {
sheetId: params.sheetId || model.getters.getActiveSheetId(),
definition,
linesNumber: params.linesNumber || 10,
columns,
id: model.getters.getNextListId(),
col,
row,
dataSourceId: uuidGenerator.uuidv4(),
});
}
/**
*
* @param {Object} params
* @param {string} [params.model]
* @param {Array<string>} [params.columns]
* @param {Object} [params.serverData]
* @param {function} [params.mockRPC]
* @param {number} [params.linesNumber]
* @param {[number, number]} [params.position]
* @param {string} [params.sheetId]
*
* @returns { Promise<{ model: Model, env: Object }>}
*/
export async function createSpreadsheetWithList(params = {}) {
const model = await createModelWithDataSource({
mockRPC: params.mockRPC,
serverData: params.serverData,
});
insertListInSpreadsheet(model, {
columns: params.columns || ["foo", "bar", "date", "product_id"],
model: params.model || "partner",
linesNumber: params.linesNumber,
position: params.position,
sheetId: params.sheetId,
});
const env = model.config.evalContext.env;
env.model = model;
await waitForDataSourcesLoaded(model);
return { model, env };
}

View file

@ -0,0 +1,31 @@
/** @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,
};
});

View file

@ -0,0 +1,74 @@
/** @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();
}

View file

@ -0,0 +1,75 @@
/** @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 };
}

View file

@ -0,0 +1,15 @@
/** @odoo-module */
import { nextTick } from "@web/../tests/helpers/utils";
import { createSpreadsheetWithPivot } from "./pivot";
import { insertListInSpreadsheet } from "./list";
export async function createSpreadsheetWithPivotAndList() {
const { model, env } = await createSpreadsheetWithPivot();
insertListInSpreadsheet(model, {
model: "partner",
columns: ["foo", "bar", "date", "product_id"],
});
await nextTick();
return { env, model };
}

View file

@ -0,0 +1,44 @@
/** @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);
}