mirror of
https://github.com/bringout/oca-ocb-report.git
synced 2026-04-22 11:22:02 +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,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);
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
const { DateTime } = luxon;
|
||||
import { Domain } from "@web/core/domain";
|
||||
import { expect } from "@odoo/hoot";
|
||||
|
||||
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 {string} field
|
||||
* @param {string} start
|
||||
* @param {string} end
|
||||
* @param {import("@web/core/domain").DomainRepr} domain
|
||||
*/
|
||||
export function assertDateDomainEqual(field, start, end, domain) {
|
||||
domain = new Domain(domain).toList();
|
||||
expect(domain[0]).toBe("&");
|
||||
expect(domain[1]).toEqual([field, ">=", start]);
|
||||
expect(domain[2]).toEqual([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);
|
||||
}
|
||||
|
|
@ -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",
|
||||
};
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
import { waitForDataLoaded } from "@spreadsheet/helpers/model";
|
||||
import { generateListDefinition } from "@spreadsheet/../tests/helpers/data";
|
||||
import { createModelWithDataSource } from "@spreadsheet/../tests/helpers/model";
|
||||
|
||||
/**
|
||||
* @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]
|
||||
* @param {{name: string, asc: boolean}[]} [params.orderBy]
|
||||
*/
|
||||
export function insertListInSpreadsheet(model, params) {
|
||||
const { definition, columns } = generateListDefinition(
|
||||
params.model,
|
||||
params.columns,
|
||||
params.actionXmlId,
|
||||
params.orderBy
|
||||
);
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @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]
|
||||
* @param {object} [params.modelConfig]
|
||||
* @param {{name: string, asc: boolean}[]} [params.orderBy]
|
||||
*
|
||||
* @returns { Promise<{ model: OdooSpreadsheetModel, env: Object }>}
|
||||
*/
|
||||
export async function createSpreadsheetWithList(params = {}) {
|
||||
const { model, env } = await createModelWithDataSource({
|
||||
mockRPC: params.mockRPC,
|
||||
serverData: params.serverData,
|
||||
modelConfig: params.modelConfig,
|
||||
});
|
||||
|
||||
insertListInSpreadsheet(model, {
|
||||
columns: params.columns || ["foo", "bar", "date", "product_id"],
|
||||
model: params.model || "partner",
|
||||
linesNumber: params.linesNumber,
|
||||
position: params.position,
|
||||
sheetId: params.sheetId,
|
||||
orderBy: params.orderBy,
|
||||
});
|
||||
|
||||
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 };
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
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();
|
||||
insertListInSpreadsheet(model, {
|
||||
model: "partner",
|
||||
columns: ["foo", "bar", "date", "product_id"],
|
||||
});
|
||||
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 };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue