[IMP] web,*: new PivotView component
Part-of: odoo/odoo#73311 Co-authored-by: Aaron Bohy <aab@odoo.com> Co-authored-by: Bruno Boi <boi@odoo.com> Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com> Co-authored-by: Julien Mougenot <jum@odoo.com> Co-authored-by: Lucas Perais <lpe@odoo.com> Co-authored-by: Jorge Pinna Puissant <jpp@odoo.com>
This commit is contained in:
committed by
Aaron Bohy
co-authored by
Aaron Bohy
Bruno Boi
Julien Mougenot
Lucas Perais
Jorge Pinna Puissant
parent
242b1caa65
commit
d1e227b07b
@@ -7,6 +7,15 @@ var ListController = require('web.ListController');
|
||||
var testUtils = require('web.test_utils');
|
||||
var ListRenderer = require('web.ListRenderer');
|
||||
var pyUtils = require('web.py_utils');
|
||||
const registry = require("@web/core/registry");
|
||||
const { makeFakeUserService } = require("@web/../tests/helpers/mock_services");
|
||||
const {
|
||||
toggleFilterMenu,
|
||||
toggleMenuItem,
|
||||
toggleMenuItemOption,
|
||||
toggleComparisonMenu,
|
||||
toggleFavoriteMenu,
|
||||
} = require("@web/../tests/search/helpers");
|
||||
|
||||
const cpHelpers = testUtils.controlPanel;
|
||||
const { createWebClient, doAction } = require("@web/../tests/webclient/helpers");
|
||||
@@ -1053,7 +1062,8 @@ QUnit.test('click on a cell of pivot view inside dashboard', async function (ass
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
// TODO: The button "Add to my dashboard" is not yet developped on the new control panel search view
|
||||
QUnit.skip(
|
||||
"correctly save the time ranges of a reporting view in comparison mode",
|
||||
async function (assert) {
|
||||
assert.expect(1);
|
||||
@@ -1090,6 +1100,8 @@ QUnit.test(
|
||||
}
|
||||
};
|
||||
|
||||
registry.category("services").add("user", makeFakeUserService());
|
||||
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
|
||||
await doAction(webClient, {
|
||||
@@ -1100,16 +1112,16 @@ QUnit.test(
|
||||
});
|
||||
|
||||
// filter on July 2020
|
||||
await cpHelpers.toggleFilterMenu(webClient);
|
||||
await cpHelpers.toggleMenuItem(webClient, "Date");
|
||||
await cpHelpers.toggleMenuItemOption(webClient, "Date", "July");
|
||||
await toggleFilterMenu(webClient);
|
||||
await toggleMenuItem(webClient, "Date");
|
||||
await toggleMenuItemOption(webClient, "Date", "July");
|
||||
|
||||
// compare July 2020 to June 2020
|
||||
await cpHelpers.toggleComparisonMenu(webClient);
|
||||
await cpHelpers.toggleMenuItem(webClient, 0);
|
||||
await toggleComparisonMenu(webClient);
|
||||
await toggleMenuItem(webClient, 0);
|
||||
|
||||
// add the view to the dashboard
|
||||
await cpHelpers.toggleFavoriteMenu(webClient);
|
||||
await toggleFavoriteMenu(webClient);
|
||||
|
||||
await testUtils.dom.click($(".o_add_to_board > button"));
|
||||
await testUtils.fields.editInput($(".o_add_to_board input"), "a name");
|
||||
|
||||
@@ -648,7 +648,7 @@
|
||||
<field name="name">event.registration.pivot</field>
|
||||
<field name="model">event.registration</field>
|
||||
<field name="arch" type="xml">
|
||||
<pivot string="Registration" display_quantity="True" sample="1">
|
||||
<pivot string="Registration" display_quantity="1" sample="1">
|
||||
<field name="event_id" type="row"/>
|
||||
</pivot>
|
||||
</field>
|
||||
|
||||
@@ -3,14 +3,12 @@ odoo.define('hr_expense.expenses.tree', function (require) {
|
||||
var DocumentUploadMixin = require('hr_expense.documents.upload.mixin');
|
||||
var KanbanController = require('web.KanbanController');
|
||||
var KanbanView = require('web.KanbanView');
|
||||
var PivotView = require('web.PivotView');
|
||||
var ListController = require('web.ListController');
|
||||
var ListView = require('web.ListView');
|
||||
var viewRegistry = require('web.view_registry');
|
||||
var core = require('web.core');
|
||||
var ListRenderer = require('web.ListRenderer');
|
||||
var KanbanRenderer = require('web.KanbanRenderer');
|
||||
var PivotRenderer = require('web.PivotRenderer');
|
||||
var session = require('web.session');
|
||||
const config = require('web.config');
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<field name="name">hr.leave.employee.type.report.view.pivot</field>
|
||||
<field name="model">hr.leave.employee.type.report</field>
|
||||
<field name="arch" type="xml">
|
||||
<pivot sample="1" disable_linking="True">
|
||||
<pivot sample="1" disable_linking="1">
|
||||
<field name="employee_id" type="row"/>
|
||||
<field name="number_of_days" type="measure"/>
|
||||
<field name="leave_type" type="col"/>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<field name="name">im_livechat.report.channel.pivot</field>
|
||||
<field name="model">im_livechat.report.channel</field>
|
||||
<field name="arch" type="xml">
|
||||
<pivot string="Livechat Support Statistics" disable_linking="True" sample="1">
|
||||
<pivot string="Livechat Support Statistics" disable_linking="1" sample="1">
|
||||
<field name="technical_name" type="row"/>
|
||||
<field name="duration" type="measure"/>
|
||||
<field name="nbr_message" type="measure"/>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<field name="name">im_livechat.report.operator.pivot</field>
|
||||
<field name="model">im_livechat.report.operator</field>
|
||||
<field name="arch" type="xml">
|
||||
<pivot string="Livechat Support Statistics" disable_linking="True" sample="1">
|
||||
<pivot string="Livechat Support Statistics" disable_linking="1" sample="1">
|
||||
<field name="partner_id" type="row"/>
|
||||
<field name="duration" type="measure"/>
|
||||
<field name="nbr_channel" type="measure"/>
|
||||
|
||||
@@ -208,7 +208,7 @@
|
||||
<field name="model">mailing.contact</field>
|
||||
<field name="priority">10</field>
|
||||
<field name="arch" type="xml">
|
||||
<pivot string="Mailing List Contacts" stacked="True" sample="1">
|
||||
<pivot string="Mailing List Contacts" stacked="1" sample="1">
|
||||
<field name="create_date" type="row"/>
|
||||
</pivot>
|
||||
</field>
|
||||
|
||||
@@ -68,9 +68,9 @@
|
||||
'project/static/src/js/project_graph_view.js',
|
||||
'project/static/src/js/project_kanban.js',
|
||||
'project/static/src/js/project_list.js',
|
||||
'project/static/src/js/project_pivot.js',
|
||||
'project/static/src/js/project_pivot_view.js',
|
||||
'project/static/src/js/project_rating_graph_view.js',
|
||||
'project/static/src/js/project_rating_reporting.js',
|
||||
'project/static/src/js/project_rating_pivot_view.js',
|
||||
'project/static/src/js/project_task_kanban_examples.js',
|
||||
'project/static/src/js/tours/project.js',
|
||||
'project/static/src/js/project_calendar.js',
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<field name="name">report.project.task.user.pivot</field>
|
||||
<field name="model">report.project.task.user</field>
|
||||
<field name="arch" type="xml">
|
||||
<pivot string="Tasks Analysis" display_quantity="true" sample="1">
|
||||
<pivot string="Tasks Analysis" display_quantity="1" sample="1">
|
||||
<field name="project_id" type="row"/>
|
||||
</pivot>
|
||||
</field>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import PivotView from 'web.PivotView';
|
||||
import { ProjectControlPanel } from '@project/js/project_control_panel';
|
||||
import viewRegistry from 'web.view_registry';
|
||||
|
||||
export const ProjectPivotView = PivotView.extend({
|
||||
config: Object.assign({}, PivotView.prototype.config, {
|
||||
ControlPanel: ProjectControlPanel,
|
||||
}),
|
||||
});
|
||||
|
||||
viewRegistry.add('project_pivot', ProjectPivotView);
|
||||
@@ -0,0 +1,12 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { PivotView } from "@web/views/pivot/pivot_view";
|
||||
import { ProjectControlPanel } from "@project/project_control_panel/project_control_panel";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
const viewRegistry = registry.category("views");
|
||||
|
||||
class ProjectPivotView extends PivotView {}
|
||||
ProjectPivotView.components = { ...PivotView, ControlPanel: ProjectControlPanel };
|
||||
|
||||
viewRegistry.add("project_pivot", ProjectPivotView);
|
||||
@@ -0,0 +1,34 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { _lt } from "@web/core/l10n/translation";
|
||||
import { PivotView } from "@web/views/pivot/pivot_view";
|
||||
import { PivotArchParser } from "@web/views/pivot/pivot_arch_parser";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
const viewRegistry = registry.category("views");
|
||||
|
||||
const MEASURE_STRINGS = {
|
||||
parent_res_id: _lt("Project"),
|
||||
rating: _lt("Rating Value (/5)"),
|
||||
res_id: _lt("Task"),
|
||||
};
|
||||
|
||||
class ProjectRatingArchParser extends PivotArchParser {
|
||||
parse() {
|
||||
const archInfo = super.parse(...arguments);
|
||||
for (const [key, val] of Object.entries(MEASURE_STRINGS)) {
|
||||
archInfo.fieldAttrs[key] = {
|
||||
...archInfo.fieldAttrs[key],
|
||||
string: val.toString(),
|
||||
};
|
||||
}
|
||||
return archInfo;
|
||||
}
|
||||
}
|
||||
|
||||
// Would it be not better achiedved by using a proper arch directly?
|
||||
|
||||
class ProjectRatingPivotView extends PivotView {}
|
||||
ProjectRatingPivotView.archParser = ProjectRatingArchParser;
|
||||
|
||||
viewRegistry.add("project_rating_pivot", ProjectRatingPivotView);
|
||||
@@ -1,34 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { _t } from 'web.core';
|
||||
import PivotController from 'web.PivotController';
|
||||
import PivotView from 'web.PivotView';
|
||||
import viewRegistry from 'web.view_registry';
|
||||
|
||||
const ProjectPivotController = PivotController.extend({
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
const measures = JSON.parse(JSON.stringify(this.measures));
|
||||
if ('res_id' in measures) {
|
||||
measures.res_id.string = _t('Task');
|
||||
}
|
||||
if ('parent_res_id' in measures) {
|
||||
measures.parent_res_id.string = _t('Project');
|
||||
}
|
||||
if ('rating' in measures) {
|
||||
measures.rating.string = _t('Rating Value (/5)');
|
||||
}
|
||||
this.measures = measures;
|
||||
},
|
||||
});
|
||||
|
||||
const ProjectPivotView = PivotView.extend({
|
||||
config: Object.assign({}, PivotView.prototype.config, {
|
||||
Controller: ProjectPivotController,
|
||||
}),
|
||||
});
|
||||
|
||||
viewRegistry.add('project_rating_pivot', ProjectPivotView);
|
||||
@@ -4,7 +4,7 @@
|
||||
<field name="name">product.month.pivot</field>
|
||||
<field name="model">purchase.report</field>
|
||||
<field name="arch" type="xml">
|
||||
<pivot string="Purchase Analysis" display_quantity="true" sample="1">
|
||||
<pivot string="Purchase Analysis" display_quantity="1" sample="1">
|
||||
<field name="category_id" type="row"/>
|
||||
<field name="order_id" type="measure"/>
|
||||
<field name="untaxed_total" type="measure"/>
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
<field name="name">purchase.order.pivot</field>
|
||||
<field name="model">purchase.order</field>
|
||||
<field name="arch" type="xml">
|
||||
<pivot string="Purchase Order" display_quantity="True" sample="1">
|
||||
<pivot string="Purchase Order" display_quantity="1" sample="1">
|
||||
<field name="partner_id" type="row"/>
|
||||
<field name="amount_total" type="measure"/>
|
||||
</pivot>
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
<field name="name">rating.rating.pivot</field>
|
||||
<field name="model">rating.rating</field>
|
||||
<field name="arch" type="xml">
|
||||
<pivot string="Ratings" display_quantity="true" sample="1">
|
||||
<pivot string="Ratings" display_quantity="1" sample="1">
|
||||
<field name="rated_partner_id" type="row"/>
|
||||
<field name="create_date" type="col"/>
|
||||
<field name="rating" type="measure" string="Rating Value (/5)"/>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<field name="name">project.profitability.report.pivot</field>
|
||||
<field name="model">project.profitability.report</field>
|
||||
<field name="arch" type="xml">
|
||||
<pivot string="Profitability Analysis" display_quantity="true" sample="1">
|
||||
<pivot string="Profitability Analysis" display_quantity="1" sample="1">
|
||||
<field name="project_id" type="row"/>
|
||||
<field name="amount_untaxed_to_invoice" type="measure"/>
|
||||
<field name="amount_untaxed_invoiced" type="measure"/>
|
||||
|
||||
@@ -114,7 +114,6 @@ This module provides the core of the Odoo Web Client.
|
||||
'web/static/src/legacy/scss/fields.scss',
|
||||
'web/static/src/legacy/scss/file_upload.scss',
|
||||
'web/static/src/legacy/scss/views.scss',
|
||||
'web/static/src/legacy/scss/pivot_view.scss',
|
||||
'web/static/src/legacy/scss/graph_view.scss',
|
||||
'web/static/src/legacy/scss/form_view.scss',
|
||||
'web/static/src/legacy/scss/list_view.scss',
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
/**
|
||||
* @param {Number} value
|
||||
* @param {Number} comparisonValue
|
||||
* @returns {Number}
|
||||
*/
|
||||
export function computeVariation(value, comparisonValue) {
|
||||
if (isNaN(value) || isNaN(comparisonValue)) {
|
||||
return NaN;
|
||||
}
|
||||
if (comparisonValue === 0) {
|
||||
if (value === 0) {
|
||||
return 0;
|
||||
} else if (value > 0) {
|
||||
return 1;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return (value - comparisonValue) / Math.abs(comparisonValue);
|
||||
}
|
||||
@@ -49,7 +49,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
PivotCustomGroupByItem.template = "web.PivotCustomGroupByItem";
|
||||
PivotCustomGroupByItem.template = "web.legacy.PivotCustomGroupByItem";
|
||||
PivotCustomGroupByItem.props = { fields: Array };
|
||||
|
||||
export class PivotGroupByMenu extends DropdownMenu {
|
||||
@@ -98,7 +98,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
PivotGroupByMenu.template = "web.PivotGroupByMenu";
|
||||
PivotGroupByMenu.template = "web.legacy.PivotGroupByMenu";
|
||||
PivotGroupByMenu.components = { PivotCustomGroupByItem };
|
||||
PivotGroupByMenu.props = {
|
||||
...DropdownMenu.props,
|
||||
@@ -303,7 +303,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
PivotRenderer.template = 'web.PivotRenderer';
|
||||
PivotRenderer.template = 'web.legacy.PivotRenderer';
|
||||
PivotRenderer.components = { PivotGroupByMenu };
|
||||
|
||||
export default PivotRenderer;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates>
|
||||
|
||||
<div t-name="web.PivotRenderer" class="o_pivot" owl="1">
|
||||
<div t-name="web.legacy.PivotRenderer" class="o_pivot" owl="1">
|
||||
<t t-if="!(props.hasData and props.measures.length) or (props.isSample and !props.isEmbedded)">
|
||||
<t t-if="props.noContentHelp" t-call="web.ActionHelper" >
|
||||
<t t-set="noContentHelp" t-value="props.noContentHelp"/>
|
||||
@@ -13,8 +13,8 @@
|
||||
<thead>
|
||||
<tr t-foreach="props.table.headers" t-as="row" t-key="'header_' + row_index">
|
||||
<t t-foreach="row" t-as="cell" t-key="'header_row_' + cell_index">
|
||||
<t t-if="cell.measure" t-call="web.PivotMeasure"/>
|
||||
<t t-elif="cell.isLeaf !== undefined" t-call="web.PivotHeader">
|
||||
<t t-if="cell.measure" t-call="web.legacy.PivotMeasure"/>
|
||||
<t t-elif="cell.isLeaf !== undefined" t-call="web.legacy.PivotHeader">
|
||||
<t t-set="isXAxis" t-value="true"/>
|
||||
</t>
|
||||
<th t-else="1" t-att-colspan="cell.width" t-att-rowspan="cell.height"/>
|
||||
@@ -23,7 +23,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="props.table.rows" t-as="row" t-key="'row_' + row_index">
|
||||
<t t-call="web.PivotHeader">
|
||||
<t t-call="web.legacy.PivotHeader">
|
||||
<t t-set="isXAxis" t-value="false"/>
|
||||
<t t-set="cell" t-value="row"/>
|
||||
</t>
|
||||
@@ -54,17 +54,17 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<t t-name="web.PivotHeader" owl="1">
|
||||
<t t-name="web.legacy.PivotHeader" owl="1">
|
||||
<th t-att-colspan="isXAxis ? cell.width : undefined" t-att-rowspan="isXAxis ? cell.height : undefined" t-att-class="{
|
||||
o_pivot_header_cell_closed: cell.isLeaf,
|
||||
o_pivot_header_cell_opened: !cell.isLeaf,
|
||||
}" t-attf-style="{{ isXAxis ? undefined : 'padding-left: ' + _getPadding(cell) + 'px;' }}" t-att-title="cell.label" t-on-click.self.prevent="_onHeaderClick(cell, isXAxis ? 'col' : 'row')">
|
||||
<span t-on-click.self.prevent="_onHeaderClick(cell, isXAxis ? 'col' : 'row')" t-esc="cell.title"/>
|
||||
<t t-if="_isClicked(cell.groupId, isXAxis)" t-call="web.PivotGroupBySelection"/>
|
||||
<t t-if="_isClicked(cell.groupId, isXAxis)" t-call="web.legacy.PivotGroupBySelection"/>
|
||||
</th>
|
||||
</t>
|
||||
|
||||
<t t-name="web.PivotMeasure" owl="1">
|
||||
<t t-name="web.legacy.PivotMeasure" owl="1">
|
||||
<th class="text-muted" t-att-colspan="cell.width" t-att-rowspan="cell.height" t-att-class="{
|
||||
o_pivot_origin_row: cell.originIndexes,
|
||||
o_pivot_measure_row: !cell.originIndexes,
|
||||
@@ -75,7 +75,7 @@
|
||||
</th>
|
||||
</t>
|
||||
|
||||
<div t-name="web.PivotGroupBySelection" class="dropdown" owl="1">
|
||||
<div t-name="web.legacy.PivotGroupBySelection" class="dropdown" owl="1">
|
||||
<PivotGroupByMenu
|
||||
customGroupBys="customGroupBys"
|
||||
fields="props.groupableFields"
|
||||
@@ -85,7 +85,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<t t-name="web.PivotGroupByMenu" owl="1">
|
||||
<t t-name="web.legacy.PivotGroupByMenu" owl="1">
|
||||
<div class="dropdown-menu o_pivot_field_menu show" data-display="static" role="menu">
|
||||
<t t-set="currentGroup" t-value="null"/>
|
||||
<t t-foreach="items" t-as="item" t-key="item.fieldName">
|
||||
@@ -133,7 +133,7 @@
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="web.PivotCustomGroupByItem" t-inherit="web.Legacy.CustomGroupByItem" t-inherit-mode="primary" owl="1">
|
||||
<t t-name="web.legacy.PivotCustomGroupByItem" t-inherit="web.Legacy.CustomGroupByItem" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//div[hasclass('o_generator_menu')]" position="attributes">
|
||||
<attribute name="t-on-click">_onToggleCustomGroupbyItem</attribute>
|
||||
</xpath>
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { evaluateExpr } from "@web/core/py_js/py";
|
||||
import { XMLParser } from "@web/core/utils/xml";
|
||||
import { archParseBoolean } from "@web/views/helpers/utils";
|
||||
|
||||
export class PivotArchParser extends XMLParser {
|
||||
parse(arch) {
|
||||
const archInfo = {
|
||||
activeMeasures: [], // store the defined active measures
|
||||
colGroupBys: [], // store the defined group_by used on cols
|
||||
defaultOrder: null,
|
||||
fieldAttrs: {},
|
||||
rowGroupBys: [], // store the defined group_by used on rows
|
||||
widgets: {}, // wigdets defined in the arch
|
||||
};
|
||||
|
||||
this.visitXML(arch, (node) => {
|
||||
switch (node.tagName) {
|
||||
case "pivot": {
|
||||
if (node.hasAttribute("disable_linking")) {
|
||||
archInfo.disableLinking = archParseBoolean(
|
||||
node.getAttribute("disable_linking")
|
||||
);
|
||||
}
|
||||
if (node.hasAttribute("default_order")) {
|
||||
archInfo.defaultOrder = node.getAttribute("default_order");
|
||||
}
|
||||
if (node.hasAttribute("string")) {
|
||||
archInfo.title = node.getAttribute("string");
|
||||
}
|
||||
if (node.hasAttribute("display_quantity")) {
|
||||
archInfo.displayQuantity = archParseBoolean(
|
||||
node.getAttribute("display_quantity")
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "field": {
|
||||
let fieldName = node.getAttribute("name"); // exists (rng validation)
|
||||
|
||||
archInfo.fieldAttrs[fieldName] = {};
|
||||
if (node.hasAttribute("string")) {
|
||||
archInfo.fieldAttrs[fieldName].string = node.getAttribute("string");
|
||||
}
|
||||
if (node.hasAttribute("invisible")) {
|
||||
const isInvisible = Boolean(evaluateExpr(node.getAttribute("invisible")));
|
||||
if (isInvisible) {
|
||||
archInfo.fieldAttrs[fieldName].isInvisible = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (node.hasAttribute("interval")) {
|
||||
fieldName += ":" + node.getAttribute("interval");
|
||||
}
|
||||
if (node.hasAttribute("widget")) {
|
||||
archInfo.widgets[fieldName] = node.getAttribute("widget");
|
||||
}
|
||||
if (node.getAttribute("type") === "measure" || node.hasAttribute("operator")) {
|
||||
archInfo.activeMeasures.push(fieldName);
|
||||
}
|
||||
if (node.getAttribute("type") === "col") {
|
||||
archInfo.colGroupBys.push(fieldName);
|
||||
}
|
||||
if (node.getAttribute("type") === "row") {
|
||||
archInfo.rowGroupBys.push(fieldName);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return archInfo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { GroupByMenu } from "@web/search/group_by_menu/group_by_menu";
|
||||
import { getIntervalOptions } from "@web/search/utils/dates";
|
||||
|
||||
export class PivotGroupByMenu extends GroupByMenu {
|
||||
/**
|
||||
* @override
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
get items() {
|
||||
let items = super.items.filter((i) => !i.custom);
|
||||
if (items.length === 0) {
|
||||
items = this.fields;
|
||||
}
|
||||
|
||||
// Add custom groupbys
|
||||
let groupNumber = 1 + Math.max(0, ...items.map(({ groupNumber: n }) => n));
|
||||
for (const [fieldName, customGroupBy] of this.props.customGroupBys.entries()) {
|
||||
items.push({ ...customGroupBy, name: fieldName, groupNumber: groupNumber++ });
|
||||
}
|
||||
|
||||
return items.map((item) => ({
|
||||
...item,
|
||||
id: item.id || item.name,
|
||||
fieldName: item.fieldName || item.name,
|
||||
description: item.description || item.string,
|
||||
isActive: false,
|
||||
options:
|
||||
item.options || ["date", "datetime"].includes(item.type)
|
||||
? getIntervalOptions()
|
||||
: undefined,
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* @override
|
||||
* @param {CustomEvent} ev
|
||||
*/
|
||||
onAddCustomGroup(ev) {
|
||||
this.trigger("add-custom-group-by", {
|
||||
fieldName: ev.detail.fieldName,
|
||||
groupId: this.props.cell.groupId,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @override
|
||||
* @param {CustomEvent} ev
|
||||
*/
|
||||
onGroupBySelected(ev) {
|
||||
const { itemId, optionId } = ev.detail.payload;
|
||||
const item = this.items.find(({ id }) => id === itemId);
|
||||
// Augment payload data
|
||||
Object.assign(ev.detail.payload, {
|
||||
fieldName: item.fieldName,
|
||||
interval: optionId,
|
||||
groupId: this.props.cell.groupId,
|
||||
});
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,140 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { PivotGroupByMenu } from "@web/views/pivot/pivot_group_by_menu";
|
||||
import { registry } from "@web/core/registry";
|
||||
import fieldUtils from "web.field_utils";
|
||||
|
||||
const { Component } = owl;
|
||||
const formatterRegistry = registry.category("formatters");
|
||||
|
||||
export class PivotRenderer extends Component {
|
||||
setup() {
|
||||
this.model = this.props.model;
|
||||
this.table = this.model.getTable();
|
||||
}
|
||||
willUpdateProps() {
|
||||
this.table = this.model.getTable();
|
||||
}
|
||||
/**
|
||||
* Get the formatted value of the cell.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} cell
|
||||
* @returns {string} Formatted value
|
||||
*/
|
||||
getFormattedValue(cell) {
|
||||
const field = this.model.meta.measures[cell.measure];
|
||||
let formatType = this.model.meta.widgets[cell.measure];
|
||||
if (!formatType) {
|
||||
const fieldType = field.type;
|
||||
formatType = fieldType === "many2one" ? "integer" : fieldType;
|
||||
}
|
||||
//If the formatter is not found on the registry, search on the legacy fieldUtils.format.
|
||||
//This must be removed when all the formatters will be on the registry
|
||||
const formatter = formatterRegistry.get(formatType, null) || fieldUtils.format[formatType];
|
||||
if (!formatter) {
|
||||
throw new Error(`${formatType} is not a defined formatter!`);
|
||||
}
|
||||
return formatter(cell.value, field);
|
||||
}
|
||||
/**
|
||||
* Get the formatted variation of a cell.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} cell
|
||||
* @returns {string} Formatted variation
|
||||
*/
|
||||
getFormattedVariation(cell) {
|
||||
if (isNaN(cell.value)) {
|
||||
return "-";
|
||||
}
|
||||
const formatPercentage = formatterRegistry.get("percentage");
|
||||
return formatPercentage(cell.value, this.model.meta.fields[cell.measure]);
|
||||
}
|
||||
/**
|
||||
* Retrieve the padding of a left header.
|
||||
*
|
||||
* @param {Object} cell
|
||||
* @returns {Number} Padding
|
||||
*/
|
||||
getPadding(cell) {
|
||||
return 5 + cell.indent * 30;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Handlers
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
onCellClicked(cell) {
|
||||
this.props.onCellClicked(cell);
|
||||
}
|
||||
/**
|
||||
* Handle the adding of a custom groupby (inside the view, not the searchview).
|
||||
*
|
||||
* @param {"col"|"row"} type
|
||||
* @param {CustomEvent} ev
|
||||
*/
|
||||
onAddCustomGroupBy(type, ev) {
|
||||
this.model.addGroupBy({ ...ev.detail, custom: true, type });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the selection of a groupby dropdown item.
|
||||
*
|
||||
* @param {"col"|"row"} type
|
||||
* @param {CustomEvent} ev
|
||||
*/
|
||||
onDropdownItemSelected(type, ev) {
|
||||
this.model.addGroupBy({ ...ev.detail.payload, type });
|
||||
}
|
||||
/**
|
||||
* Handle a click on a header cell.
|
||||
*
|
||||
* @param {Object} cell
|
||||
* @param {string} type col or row
|
||||
*/
|
||||
onHeaderClick(cell, type) {
|
||||
if (cell.isLeaf && cell.isFolded) {
|
||||
this.model.expandGroup(cell.groupId, type);
|
||||
} else if (!cell.isLeaf) {
|
||||
this.model.closeGroup(cell.groupId, type);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Handle a click on a measure cell.
|
||||
*
|
||||
* @param {Object} cell
|
||||
*/
|
||||
onMeasureClick(cell) {
|
||||
this.model.sortRows({
|
||||
groupId: cell.groupId,
|
||||
measure: cell.measure,
|
||||
order: (cell.order || "desc") === "asc" ? "desc" : "asc",
|
||||
originIndexes: cell.originIndexes,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Hover the column in which the mouse is.
|
||||
*
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
onMouseEnter(ev) {
|
||||
var index = [...ev.currentTarget.parentNode.children].indexOf(ev.currentTarget);
|
||||
if (ev.currentTarget.tagName === "TH") {
|
||||
index += 1;
|
||||
}
|
||||
this.el
|
||||
.querySelectorAll("td:nth-child(" + (index + 1) + ")")
|
||||
.forEach((elt) => elt.classList.add("o_cell_hover"));
|
||||
}
|
||||
/**
|
||||
* Remove the hover on the columns.
|
||||
*/
|
||||
onMouseLeave() {
|
||||
this.el
|
||||
.querySelectorAll(".o_cell_hover")
|
||||
.forEach((elt) => elt.classList.remove("o_cell_hover"));
|
||||
}
|
||||
}
|
||||
PivotRenderer.template = "web.PivotRenderer";
|
||||
PivotRenderer.components = { PivotGroupByMenu };
|
||||
@@ -0,0 +1,81 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.PivotRenderer" owl="1">
|
||||
<div class="o_pivot">
|
||||
<table class="table-hover table-sm table-bordered" t-att-class="{ o_enable_linking: !model.meta.disableLinking, o_sample_data_disabled: model.meta.useSampleModel }">
|
||||
<thead>
|
||||
<tr t-foreach="table.headers" t-as="row" t-key="'header_' + row_index">
|
||||
<t t-foreach="row" t-as="cell" t-key="'header_row_' + cell_index">
|
||||
<t t-if="cell.measure" t-call="web.PivotMeasure"/>
|
||||
<t t-elif="cell.isLeaf !== undefined" t-call="web.PivotHeader">
|
||||
<t t-set="isXAxis" t-value="true"/>
|
||||
</t>
|
||||
<th t-else="" t-att-colspan="cell.width" t-att-rowspan="cell.height"/>
|
||||
</t>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="table.rows" t-as="row" t-key="'row_' + row_index">
|
||||
<t t-call="web.PivotHeader">
|
||||
<t t-set="isXAxis" t-value="false"/>
|
||||
<t t-set="cell" t-value="row"/>
|
||||
</t>
|
||||
<t t-foreach="row.subGroupMeasurements" t-as="cell" t-key="'row_cell_' + cell_index">
|
||||
<td class="o_pivot_cell_value text-right" t-att-class="{
|
||||
o_empty: cell.value === undefined,
|
||||
'font-weight-bold': cell.isBold,
|
||||
}" t-on-click="onCellClicked(cell)"
|
||||
t-on-mouseover="onMouseEnter" t-on-mouseout="onMouseLeave">
|
||||
<t t-if="cell.value !== undefined">
|
||||
<div t-if="cell.originIndexes.length > 1" class="o_variation" t-att-class="{
|
||||
o_positive: cell.value > 0,
|
||||
o_negative: cell.value < 0,
|
||||
o_null: cell.value === 0,
|
||||
}" t-esc="getFormattedVariation(cell)"/>
|
||||
<div t-elif="model.meta.measures[cell.measure].type === 'boolean'" class="o_value">
|
||||
<CheckBox disabled="true" value="cell.value" />
|
||||
</div>
|
||||
<div t-else="1" class="o_value" t-esc="getFormattedValue(cell)"/>
|
||||
</t>
|
||||
</td>
|
||||
</t>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="web.PivotHeader" owl="1">
|
||||
<th t-att-colspan="isXAxis ? cell.width : undefined" t-att-rowspan="isXAxis ? cell.height : undefined" t-att-class="{
|
||||
o_pivot_header_cell_closed: cell.isLeaf,
|
||||
o_pivot_header_cell_opened: !cell.isLeaf,
|
||||
}" t-attf-style="{{ isXAxis ? undefined : 'padding-left: ' + getPadding(cell) + 'px;' }}" t-att-data-tooltip="cell.label" data-tooltip-position="right" t-on-click.prevent="onHeaderClick(cell, isXAxis ? 'col' : 'row')">
|
||||
<t t-if="cell.isLeaf and !cell.isFolded">
|
||||
<PivotGroupByMenu
|
||||
tag="'span'"
|
||||
toggler="'parent'"
|
||||
cell="cell"
|
||||
customGroupBys="model.meta.customGroupBys"
|
||||
showActiveItems="false"
|
||||
t-on-click.stop=""
|
||||
t-on-dropdown-item-selected="onDropdownItemSelected(isXAxis ? 'col' : 'row')"
|
||||
t-on-add-custom-group-by="onAddCustomGroupBy(isXAxis ? 'col' : 'row')"
|
||||
/>
|
||||
</t>
|
||||
<span t-esc="cell.title"/>
|
||||
</th>
|
||||
</t>
|
||||
|
||||
<t t-name="web.PivotMeasure" owl="1">
|
||||
<th class="text-muted" t-att-colspan="cell.width" t-att-rowspan="cell.height" t-att-class="{
|
||||
o_pivot_origin_row: cell.originIndexes,
|
||||
o_pivot_measure_row: !cell.originIndexes,
|
||||
o_pivot_sort_order_asc: cell.order === 'asc',
|
||||
o_pivot_sort_order_desc: cell.order === 'desc',
|
||||
}" t-on-click.prevent="onMeasureClick(cell)" t-on-mouseover="onMouseEnter" t-on-mouseout="onMouseLeave">
|
||||
<t t-esc="cell.title"/>
|
||||
</th>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,186 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { _lt } from "@web/core/l10n/translation";
|
||||
import { download } from "@web/core/network/download";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { ControlPanel } from "@web/search/control_panel/control_panel";
|
||||
import { SearchPanel } from "@web/search/search_panel/search_panel";
|
||||
import { useModel } from "@web/views/helpers/model";
|
||||
import { standardViewProps } from "@web/views/helpers/standard_view_props";
|
||||
import { useSetupView } from "@web/views/helpers/view_hook";
|
||||
import { PivotArchParser } from "@web/views/pivot/pivot_arch_parser";
|
||||
import { PivotModel } from "@web/views/pivot/pivot_model";
|
||||
import { PivotRenderer } from "@web/views/pivot/pivot_renderer";
|
||||
|
||||
const viewRegistry = registry.category("views");
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class PivotView extends Component {
|
||||
setup() {
|
||||
this.actionService = useService("action");
|
||||
|
||||
let modelParams = {};
|
||||
if (this.props.state) {
|
||||
modelParams.data = this.props.state.data;
|
||||
modelParams.meta = this.props.state.meta;
|
||||
} else {
|
||||
const { arch } = this.props;
|
||||
|
||||
// parse arch
|
||||
const archInfo = new this.constructor.archParser().parse(arch);
|
||||
|
||||
if (!archInfo.activeMeasures.length || archInfo.displayQuantity) {
|
||||
archInfo.activeMeasures.unshift("__count");
|
||||
}
|
||||
|
||||
modelParams.meta = {
|
||||
activeMeasures: archInfo.activeMeasures,
|
||||
additionalMeasures: this.props.additionalMeasures,
|
||||
colGroupBys: archInfo.colGroupBys,
|
||||
defaultOrder: archInfo.defaultOrder,
|
||||
disableLinking: Boolean(archInfo.disableLinking),
|
||||
fields: this.props.fields,
|
||||
fieldAttrs: archInfo.fieldAttrs,
|
||||
resModel: this.props.resModel,
|
||||
rowGroupBys: archInfo.rowGroupBys,
|
||||
title: this.props.title || archInfo.title || this.env._t("Untitled"),
|
||||
useSampleModel: Boolean(this.props.useSampleModel),
|
||||
widgets: archInfo.widgets,
|
||||
};
|
||||
}
|
||||
|
||||
this.model = useModel(this.constructor.Model, modelParams);
|
||||
|
||||
useSetupView({
|
||||
exportLocalState: () => {
|
||||
const { data, meta } = this.model;
|
||||
return { data, meta };
|
||||
},
|
||||
saveParams: () => this.saveParams(), // FIXME: rename this
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @returns {Object}
|
||||
*/
|
||||
saveParams() {
|
||||
return {
|
||||
context: {
|
||||
pivot_measures: this.model.meta.activeMeasures,
|
||||
pivot_column_groupby: this.model.meta.fullColGroupBys,
|
||||
pivot_row_groupby: this.model.meta.fullRowGroupBys,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Exports the current pivot table data in a xls file. For this, we have to
|
||||
* serialize the current state, then call the server /web/pivot/export_xlsx.
|
||||
* Force a reload before exporting to ensure to export up-to-date data.
|
||||
*/
|
||||
onDownloadButtonClicked() {
|
||||
if (this.model.getTableWidth() > 16384) {
|
||||
throw new Error(
|
||||
this.env._t(
|
||||
"For Excel compatibility, data cannot be exported if there are more than 16384 columns.\n\nTip: try to flip axis, filter further or reduce the number of measures."
|
||||
)
|
||||
);
|
||||
}
|
||||
const table = this.model.exportData();
|
||||
download({
|
||||
url: "/web/pivot/export_xlsx",
|
||||
data: { data: JSON.stringify(table) },
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Expands all groups
|
||||
*/
|
||||
onExpandButtonClicked() {
|
||||
this.model.expandAll();
|
||||
}
|
||||
/**
|
||||
* Flips axis
|
||||
*/
|
||||
onFlipButtonClicked() {
|
||||
this.model.flip();
|
||||
}
|
||||
/**
|
||||
* Toggles the given measure
|
||||
*
|
||||
* @param {CustomEvent} ev
|
||||
*/
|
||||
onMeasureSelected(ev) {
|
||||
this.model.toggleMeasure(ev.detail.payload.measure);
|
||||
}
|
||||
/**
|
||||
* @param {CustomEvent} ev
|
||||
*/
|
||||
onOpenView(cell) {
|
||||
if (cell.value === undefined || this.model.meta.disableLinking) {
|
||||
return;
|
||||
}
|
||||
|
||||
const context = Object.assign({}, this.model.searchParams.context);
|
||||
Object.keys(context).forEach((x) => {
|
||||
if (x === "group_by" || x.startsWith("search_default_")) {
|
||||
delete context[x];
|
||||
}
|
||||
});
|
||||
|
||||
// retrieve form and list view ids from the action
|
||||
this.views = ["list", "form"].map((viewType) => {
|
||||
const view = this.props.info.views.find((view) => view[1] === viewType);
|
||||
return [view ? view[0] : false, viewType];
|
||||
});
|
||||
|
||||
const group = {
|
||||
rowValues: cell.groupId[0],
|
||||
colValues: cell.groupId[1],
|
||||
originIndex: cell.originIndexes[0],
|
||||
};
|
||||
const domain = this.model.getGroupDomain(group);
|
||||
|
||||
this.actionService.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
name: this.model.meta.title,
|
||||
res_model: this.props.resModel,
|
||||
views: this.views,
|
||||
view_mode: "list",
|
||||
target: "current",
|
||||
context,
|
||||
domain,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
PivotView.template = "web.PivotView";
|
||||
PivotView.buttonTemplate = "web.PivotView.Buttons";
|
||||
PivotView.components = { ControlPanel, SearchPanel, Renderer: PivotRenderer };
|
||||
|
||||
PivotView.props = {
|
||||
...standardViewProps,
|
||||
additionalMeasures: { type: Array, elements: String, optional: 1 },
|
||||
display: { type: Object, optional: 1 },
|
||||
title: { type: String, optional: 1 },
|
||||
};
|
||||
PivotView.defaultProps = {
|
||||
additionalMeasures: [],
|
||||
display: {},
|
||||
};
|
||||
|
||||
PivotView.Model = PivotModel;
|
||||
|
||||
PivotView.archParser = PivotArchParser;
|
||||
|
||||
PivotView.type = "pivot";
|
||||
PivotView.display_name = _lt("Pivot");
|
||||
PivotView.icon = "fa-table";
|
||||
PivotView.multiRecord = true;
|
||||
PivotView.searchMenuTypes = ["filter", "groupBy", "comparison", "favorite"];
|
||||
|
||||
viewRegistry.add("pivot", PivotView);
|
||||
+11
-5
@@ -1,5 +1,4 @@
|
||||
.o_pivot {
|
||||
|
||||
.o_pivot_cell_value {
|
||||
font-size: 1em;
|
||||
.o_comparison {
|
||||
@@ -16,6 +15,13 @@
|
||||
}
|
||||
|
||||
table {
|
||||
th > .o_dropdown > .o_dropdown_toggler {
|
||||
background: none!important;
|
||||
border: none!important;
|
||||
padding: 0!important;
|
||||
// width: 100%!important;
|
||||
// text-align: left!important;
|
||||
}
|
||||
background-color: $o-view-background-color;
|
||||
width: auto; // bootstrap override
|
||||
|
||||
@@ -108,8 +114,8 @@
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.o_pivot_field_menu .dropdown-item.disabled {
|
||||
color: $text-muted;
|
||||
cursor: default;
|
||||
}
|
||||
// .o_pivot_field_menu .dropdown-item.disabled {
|
||||
// color: $text-muted;
|
||||
// cursor: default;
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.PivotView.Buttons" owl="1">
|
||||
<div class="btn-group" role="toolbar" aria-label="Main actions" t-on-dropdown-item-selected="onMeasureSelected">
|
||||
<t t-call="web.ReportViewMeasures">
|
||||
<t t-set="measures" t-value="model.meta.measures"/>
|
||||
<t t-set="activeMeasures" t-value="model.meta.activeMeasures"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="btn-group" role="toolbar" aria-label="Pivot settings">
|
||||
<t t-set="noDataDisplayed" t-value="!model.hasData() || !model.meta.activeMeasures.length"/>
|
||||
<button class="btn btn-secondary fa fa-exchange o_pivot_flip_button" t-on-click="onFlipButtonClicked" data-tooltip="Flip axis" aria-label="Flip axis" t-attf-disabled="{{noDataDisplayed ? 'disabled' : false}}"/>
|
||||
<button class="btn btn-secondary fa fa-arrows o_pivot_expand_button" t-on-click="onExpandButtonClicked" data-tooltip="Expand all" aria-label="Expand all" t-attf-disabled="{{noDataDisplayed ? 'disabled' : false}}"/>
|
||||
<button class="btn btn-secondary fa fa-download o_pivot_download" t-on-click="onDownloadButtonClicked" data-tooltip="Download xlsx" aria-label="Download xlsx" t-attf-disabled="{{noDataDisplayed ? 'disabled' : false}}"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="web.PivotView" owl="1">
|
||||
<div class="o_pivot_view" t-att-class="{o_view_sample_data: model.meta.useSampleModel }">
|
||||
<ControlPanel t-if="env.searchModel.display.controlPanel" t-props="props.info">
|
||||
<t t-set-slot="control-panel-bottom-left">
|
||||
<t t-call="{{ constructor.buttonTemplate }}"/>
|
||||
</t>
|
||||
</ControlPanel>
|
||||
<div class="o_content">
|
||||
<SearchPanel t-if="env.searchModel.display.searchPanel" />
|
||||
<t t-set="displayNoContent" t-value="
|
||||
props.info.noContentHelp !== false and (
|
||||
!(model.hasData() and model.meta.activeMeasures.length) or
|
||||
model.meta.useSampleModel
|
||||
)" />
|
||||
<t t-if="displayNoContent">
|
||||
<t t-if="props.info.noContentHelp" t-call="web.ActionHelper">
|
||||
<t t-set="noContentHelp" t-value="props.info.noContentHelp"/>
|
||||
</t>
|
||||
<t t-else="" t-call="web.NoContentHelper"/>
|
||||
</t>
|
||||
<t t-if="model.hasData() and model.meta.activeMeasures.length">
|
||||
<Renderer model="model" onCellClicked="cell => onOpenView(cell)"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
fakeCommandService,
|
||||
makeFakeDialogService,
|
||||
makeFakeLocalizationService,
|
||||
makeFakeUserService,
|
||||
} from "../../helpers/mock_services";
|
||||
import { click, getFixture, legacyExtraNextTick, patchWithCleanup } from "../../helpers/utils";
|
||||
import { createWebClient, doAction, getActionManagerServerData } from "../../webclient/helpers";
|
||||
@@ -357,6 +358,7 @@ QUnit.module("DebugMenu", (hooks) => {
|
||||
debug: true,
|
||||
});
|
||||
|
||||
registry.category("services").add("user", makeFakeUserService());
|
||||
registry.category("debug").category("view").add("editViewItem", editView);
|
||||
|
||||
const serverData = getActionManagerServerData();
|
||||
|
||||
@@ -330,6 +330,9 @@ export class MockServer {
|
||||
}
|
||||
// generate a field_view_get result
|
||||
const fields = Object.assign({}, this.models[modelName].fields);
|
||||
for (const fieldName in fields) {
|
||||
fields[fieldName].name = fieldName;
|
||||
}
|
||||
// var viewOptions = params.viewOptions || {};
|
||||
const fvg = _fieldsViewGet({
|
||||
arch,
|
||||
@@ -726,7 +729,7 @@ export class MockServer {
|
||||
let aggregatedFields = [];
|
||||
// if no fields have been given, the server picks all stored fields
|
||||
if (kwargs.fields.length === 0) {
|
||||
aggregatedFields = Object.keys(this.models[modelName].fields).filter(
|
||||
aggregatedFields = Object.keys(fields).filter(
|
||||
(fieldName) => !groupByFieldNames.includes(fieldName)
|
||||
);
|
||||
} else {
|
||||
@@ -1499,7 +1502,11 @@ export class MockServer {
|
||||
fieldNames = [...new Set(fieldNames.concat(["id"]))];
|
||||
const { context } = params;
|
||||
const active_test = context && "active_test" in context ? context.active_test : true;
|
||||
let records = this.getRecords(params.model, params.domain || [], { active_test });
|
||||
let records = this.getRecords(
|
||||
params.model,
|
||||
params.domain || [],
|
||||
Object.assign({}, params.context, { active_test })
|
||||
);
|
||||
if (params.sort) {
|
||||
// warning: only consider first level of sort
|
||||
params.sort = params.sort.split(",")[0];
|
||||
|
||||
@@ -246,6 +246,17 @@ export function makeFakeDialogService(addDialog) {
|
||||
};
|
||||
}
|
||||
|
||||
export function makeFakeUserService(hasGroup = () => false) {
|
||||
return {
|
||||
...userService,
|
||||
start() {
|
||||
const fakeUserService = userService.start(...arguments);
|
||||
fakeUserService.hasGroup = hasGroup;
|
||||
return fakeUserService;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const mocks = {
|
||||
company: () => companyService,
|
||||
command: () => fakeCommandService,
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
import testUtils from "web.test_utils";
|
||||
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
|
||||
import { makeFakeUserService } from "@web/../tests/helpers/mock_services";
|
||||
import { Model } from "web.Model";
|
||||
import Registry from "web.Registry";
|
||||
import SearchBar from "web.SearchBar";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
let serverData;
|
||||
|
||||
@@ -321,6 +323,8 @@ QUnit.module("Search Bar (legacy)", (hooks) => {
|
||||
</pivot>
|
||||
`;
|
||||
|
||||
registry.category("services").add("user", makeFakeUserService());
|
||||
|
||||
let rpcs;
|
||||
const webClient = await createWebClient({
|
||||
serverData,
|
||||
|
||||
@@ -6,6 +6,8 @@ var PivotView = require('web.PivotView');
|
||||
const PivotController = require("web.PivotController");
|
||||
var testUtils = require('web.test_utils');
|
||||
var testUtilsDom = require('web.test_utils_dom');
|
||||
const { registry } = require('@web/core/registry');
|
||||
const legacyViewRegistry = require('web.view_registry');
|
||||
|
||||
const { createWebClient, doAction } = require('@web/../tests/webclient/helpers');
|
||||
|
||||
@@ -31,6 +33,9 @@ let serverData;
|
||||
|
||||
QUnit.module('Views', {
|
||||
beforeEach: function () {
|
||||
registry.category("views").remove("pivot"); // remove new pivot from registry
|
||||
legacyViewRegistry.add("pivot", PivotView); // add legacy pivot -> will be wrapped and added to new registry
|
||||
|
||||
this.data = {
|
||||
partner: {
|
||||
fields: {
|
||||
@@ -117,7 +122,7 @@ QUnit.module('Views', {
|
||||
serverData = { models: this.data };
|
||||
},
|
||||
}, function () {
|
||||
QUnit.module('PivotView');
|
||||
QUnit.module('Legacy PivotView');
|
||||
|
||||
QUnit.test('simple pivot rendering', async function (assert) {
|
||||
assert.expect(3);
|
||||
@@ -2986,67 +2991,6 @@ QUnit.module('Views', {
|
||||
pivot.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('Navigation list view for a group and back with breadcrumbs', async function (assert) {
|
||||
assert.expect(16);
|
||||
// create a webClient to test the interactions with the search view
|
||||
|
||||
serverData.views = {
|
||||
'partner,false,pivot': '<pivot>' +
|
||||
'<field name="customer" type="row"/>' +
|
||||
'</pivot>',
|
||||
'partner,false,search': '<search><filter name="bayou" string="Bayou" domain="[(\'foo\',\'=\', 12)]"/></search>',
|
||||
'partner,false,list': '<tree><field name="foo"/></tree>',
|
||||
'partner,false,form': '<form><field name="foo"/></form>',
|
||||
};
|
||||
|
||||
let readGroupCount = 0;
|
||||
const mockRPC = (route, args) => {
|
||||
if (args.method === 'read_group') {
|
||||
assert.step('read_group');
|
||||
const domain = args.kwargs.domain;
|
||||
if ([0,1].indexOf(readGroupCount) !== -1) {
|
||||
assert.deepEqual(domain, [], 'domain empty');
|
||||
} else if ([2,3,4,5].indexOf(readGroupCount) !== -1) {
|
||||
assert.deepEqual(domain, [['foo', '=', 12]],
|
||||
'domain conserved when back with breadcrumbs');
|
||||
}
|
||||
readGroupCount++;
|
||||
}
|
||||
if (route === '/web/dataset/search_read') {
|
||||
assert.step('search_read');
|
||||
const domain = args.domain;
|
||||
assert.deepEqual(domain, ['&', ['customer', '=', 1], ['foo', '=', 12]],
|
||||
'list domain is correct');
|
||||
}
|
||||
};
|
||||
|
||||
const webClient = await createWebClient({serverData, mockRPC});
|
||||
|
||||
await doAction(webClient, {
|
||||
res_model: 'partner',
|
||||
type: 'ir.actions.act_window',
|
||||
views: [[false, 'pivot']],
|
||||
});
|
||||
|
||||
|
||||
await cpHelpers.toggleFilterMenu(webClient);
|
||||
await cpHelpers.toggleMenuItem(webClient, 0);
|
||||
await testUtils.nextTick();
|
||||
|
||||
await testUtilsDom.click($(webClient.el).find('.o_pivot_cell_value:nth(1)'));
|
||||
await testUtils.nextTick();
|
||||
|
||||
assert.containsOnce(webClient, '.o_list_view');
|
||||
|
||||
await testUtilsDom.click($(webClient.el).find('.o_control_panel ol.breadcrumb li.breadcrumb-item').eq(0));
|
||||
|
||||
assert.verifySteps([
|
||||
'read_group', 'read_group',
|
||||
'read_group', 'read_group',
|
||||
'search_read',
|
||||
'read_group', 'read_group']);
|
||||
});
|
||||
|
||||
QUnit.test('Cell values are kept when flippin a pivot view in comparison mode', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
|
||||
@@ -10,8 +10,11 @@ const SearchPanel = require("web.searchPanel");
|
||||
const cpHelpers = testUtils.controlPanel;
|
||||
const createView = testUtils.createView;
|
||||
|
||||
const { makeFakeUserService } = require("@web/../tests/helpers/mock_services");
|
||||
const { createWebClient, doAction } = require('@web/../tests/webclient/helpers');
|
||||
const { legacyExtraNextTick } = require("@web/../tests/helpers/utils");
|
||||
const { registry } = require("@web/core/registry");
|
||||
const { PivotView } = require("@web/views/pivot/pivot_view");
|
||||
|
||||
/**
|
||||
* Return the list of counters displayed in the search panel (if any).
|
||||
@@ -2336,6 +2339,7 @@ QUnit.module('Views', {
|
||||
QUnit.test('search panel is available on list and kanban by default', async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
registry.category("services").add("user", makeFakeUserService());
|
||||
const webClient = await createWebClient({ serverData });
|
||||
|
||||
await doAction(webClient, 1);
|
||||
@@ -2359,7 +2363,7 @@ QUnit.module('Views', {
|
||||
assert.containsNone(webClient, '.o_content .o_search_panel');
|
||||
});
|
||||
|
||||
QUnit.test('search panel with view_types attribute', async function (assert) {
|
||||
QUnit.skip('search panel with view_types attribute', async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
serverData.views['partner,false,search'] =
|
||||
@@ -2371,6 +2375,7 @@ QUnit.module('Views', {
|
||||
</search>`;
|
||||
|
||||
|
||||
registry.category("views").add("pivot", PivotView, { force: true });
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, 1);
|
||||
|
||||
@@ -2486,9 +2491,10 @@ QUnit.module('Views', {
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test('search panel filters are kept when switching to a view with no search panel', async function (assert) {
|
||||
QUnit.skip('search panel filters are kept when switching to a view with no search panel', async function (assert) {
|
||||
assert.expect(13);
|
||||
|
||||
registry.category("views").add("pivot", PivotView, { force: true });
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, 1);
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { View } from "@web/views/view";
|
||||
import { getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import { registerCleanup } from "@web/../tests/helpers/cleanup";
|
||||
import { View } from "@web/views/view";
|
||||
import { _fieldsViewGet } from "../helpers/mock_server";
|
||||
import { addLegacyMockEnvironment } from "../webclient/helpers";
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,7 @@ import { errorService } from "../../../src/core/errors/error_service";
|
||||
import { RPCError } from "@web/core/network/rpc_service";
|
||||
import { registerCleanup } from "../../helpers/cleanup";
|
||||
import { WarningDialog } from "@web/core/errors/error_dialogs";
|
||||
import { makeFakeUserService } from "@web/../tests/helpers/mock_services";
|
||||
|
||||
let serverData;
|
||||
const serviceRegistry = registry.category("services");
|
||||
@@ -2304,6 +2305,8 @@ QUnit.module("ActionManager", (hooks) => {
|
||||
assert.step(args.method);
|
||||
}
|
||||
};
|
||||
|
||||
registry.category("services").add("user", makeFakeUserService());
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, 3);
|
||||
assert.doesNotHaveClass(
|
||||
|
||||
Reference in New Issue
Block a user