[FIX] crm, web: forecast views
After the conversion of the graph and pivot views done in https://github.com/odoo/odoo/pull/73311, it was no more possible to open the forecast_graph and forecast_pivot views. This is due to the fact that the new View component cannot manage a legacy view: every extension of a converted view must be converted. We thus convert forecast_graph and forecast_pivot. closes odoo/odoo#76793 X-original-commit: ff28db335fa77dadc3198e754f1e0649811fba7b Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
@@ -91,6 +91,7 @@
|
||||
'web.qunit_suite_tests': [
|
||||
'crm/static/tests/mock_server.js',
|
||||
'crm/static/tests/forecast_kanban_tests.js',
|
||||
'crm/static/tests/forecast_view_tests.js',
|
||||
'crm/static/tests/crm_rainbowman_tests.js',
|
||||
],
|
||||
},
|
||||
|
||||
@@ -41,7 +41,6 @@ class ForecastModelExtension extends ActionModel.Extension {
|
||||
super.prepareState(...arguments);
|
||||
Object.assign(this.state, {
|
||||
forecastField: null, // {string} forecast_field from the context (name)
|
||||
forecastFieldType: null, // {string} forecast_field type (date or datetime)
|
||||
forecastFilter: null, // {boolean} if the "Forecast" filter is active
|
||||
forecastStart: null, // {string} limiting bound of the filter (if active)
|
||||
// -> starting date before which records are filtered out
|
||||
@@ -55,6 +54,8 @@ class ForecastModelExtension extends ActionModel.Extension {
|
||||
* @returns {Promise}
|
||||
*/
|
||||
callLoad() {
|
||||
// this is bad to do this always: if a state has been received and the view is loaded the
|
||||
// first time, the state won't be used! to fix!
|
||||
this._computeState();
|
||||
return super.callLoad(...arguments);
|
||||
}
|
||||
@@ -111,6 +112,6 @@ class ForecastModelExtension extends ActionModel.Extension {
|
||||
}
|
||||
}
|
||||
|
||||
ActionModel.registry.add("Forecast", ForecastModelExtension, 20);
|
||||
ActionModel.registry.add("forecast", ForecastModelExtension, 20);
|
||||
|
||||
export default ForecastModelExtension;
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { Domain } from "@web/core/domain";
|
||||
import { makeContext } from "@web/core/context";
|
||||
import { SearchModel } from "@web/search/search_model";
|
||||
|
||||
/**
|
||||
* This is the conversion of ForecastModelExtension. See there for more
|
||||
* explanations of what is done here.
|
||||
*/
|
||||
const DATE_FORMAT = {
|
||||
datetime: "YYYY-MM-DD HH:mm:ss",
|
||||
date: "YYYY-MM-DD",
|
||||
};
|
||||
|
||||
export class ForecastSearchModel extends SearchModel {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
exportState() {
|
||||
const state = super.exportState();
|
||||
state.forecast = {
|
||||
forecastField: this.forecastField,
|
||||
forecastFilter: this.forecastFilter,
|
||||
forecastStart: this.forecastStart,
|
||||
};
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
* @returns {string}
|
||||
*/
|
||||
_getForecastStart() {
|
||||
/** @todo stop using moment */
|
||||
const type = this.searchViewFields[this.forecastField].type;
|
||||
let startMoment;
|
||||
const groupBy = this.groupBy;
|
||||
const firstForecastGroupBy = groupBy.find((gb) => gb.includes(this.forecastField));
|
||||
let granularity = "month";
|
||||
if (firstForecastGroupBy) {
|
||||
granularity = firstForecastGroupBy.split(":")[1] || "month";
|
||||
} else if (groupBy.length) {
|
||||
granularity = "day";
|
||||
}
|
||||
startMoment = moment().startOf(granularity);
|
||||
if (type === "datetime") {
|
||||
startMoment = moment.utc(startMoment);
|
||||
}
|
||||
const format = DATE_FORMAT[type];
|
||||
return startMoment.format(format);
|
||||
}
|
||||
|
||||
_getSearchItemDomain(activeItem) {
|
||||
const domain = super._getSearchItemDomain(...arguments);
|
||||
|
||||
const { forecast_field: forecastField } = this.globalContext;
|
||||
const searchItem = this.searchItems[activeItem.searchItemId];
|
||||
|
||||
if (forecastField && searchItem.type === "filter") {
|
||||
const context = makeContext(searchItem.context || {});
|
||||
if (!context.forecast_filter) {
|
||||
return domain;
|
||||
}
|
||||
|
||||
this.forecastField = forecastField;
|
||||
this.forecastFilter = true;
|
||||
if (!this.forecastStart) {
|
||||
this.forecastStart = this._getForecastStart();
|
||||
}
|
||||
|
||||
const forecastDomain = [
|
||||
"|",
|
||||
[this.forecastField, "=", false],
|
||||
[this.forecastField, ">=", this.forecastStart],
|
||||
];
|
||||
return Domain.and([domain, forecastDomain]);
|
||||
}
|
||||
|
||||
return domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_importState(state) {
|
||||
super._importState(...arguments);
|
||||
if (state.Forecast) {
|
||||
const { forecastField, forecastFilter, forecastStart } = state.forecast;
|
||||
this.forecastField = forecastField;
|
||||
this.forecastFilter = forecastFilter;
|
||||
this.forecastStart = forecastStart;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_reset() {
|
||||
super._reset();
|
||||
this.forecastField = null;
|
||||
this.forecastFilter = false;
|
||||
this.forecastStart = null;
|
||||
}
|
||||
}
|
||||
@@ -2,29 +2,24 @@
|
||||
import { ForecastKanbanController } from './forecast_controllers';
|
||||
import { ForecastKanbanModel } from './forecast_models';
|
||||
import { ForecastKanbanRenderer } from './forecast_renderers';
|
||||
import GraphView from 'web.GraphView';
|
||||
import { ForecastSearchModel } from "./forecast_search_model";
|
||||
import { GraphView } from "@web/views/graph/graph_view";
|
||||
import KanbanView from 'web.KanbanView';
|
||||
import ListView from 'web.ListView';
|
||||
import PivotView from 'web.PivotView';
|
||||
import { PivotView } from "@web/views/pivot/pivot_view";
|
||||
import { registry } from "@web/core/registry";
|
||||
import viewRegistry from 'web.view_registry';
|
||||
|
||||
/**
|
||||
* Graph view to be used for a Forecast @see ForecastModelExtension
|
||||
* Graph view to be used for a Forecast @see ForecastSearchModel
|
||||
* requires:
|
||||
* - context key `forecast_field` on a date/datetime field
|
||||
* - special filter "Forecast" (which must set the `forecast_filter:1` context key)
|
||||
*/
|
||||
const ForecastGraphView = GraphView.extend({
|
||||
/**
|
||||
* @private
|
||||
* @override
|
||||
*/
|
||||
_createSearchModel(params, extraExtensions = {}) {
|
||||
Object.assign(extraExtensions, { Forecast: {} });
|
||||
return this._super(params, extraExtensions);
|
||||
},
|
||||
});
|
||||
viewRegistry.add('forecast_graph', ForecastGraphView);
|
||||
class ForecastGraphView extends GraphView {}
|
||||
ForecastGraphView.SearchModel = ForecastSearchModel;
|
||||
|
||||
registry.category("views").add("forecast_graph", ForecastGraphView);
|
||||
|
||||
/**
|
||||
* Kanban view to be used for a Forecast
|
||||
@@ -46,7 +41,7 @@ const ForecastKanbanView = KanbanView.extend({
|
||||
* @override
|
||||
*/
|
||||
_createSearchModel(params, extraExtensions={}) {
|
||||
Object.assign(extraExtensions, { Forecast: {} });
|
||||
Object.assign(extraExtensions, { forecast: {} });
|
||||
return this._super(params, extraExtensions);
|
||||
},
|
||||
});
|
||||
@@ -64,29 +59,22 @@ const ForecastListView = ListView.extend({
|
||||
* @override
|
||||
*/
|
||||
_createSearchModel(params, extraExtensions = {}) {
|
||||
Object.assign(extraExtensions, { Forecast: {} });
|
||||
Object.assign(extraExtensions, { forecast: {} });
|
||||
return this._super(params, extraExtensions);
|
||||
},
|
||||
});
|
||||
viewRegistry.add('forecast_list', ForecastListView);
|
||||
|
||||
/**
|
||||
* Pivot view to be used for a Forecast @see ForecastModelExtension
|
||||
* Pivot view to be used for a Forecast @see ForecastSearchModel
|
||||
* requires:
|
||||
* - context key `forecast_field` on a date/datetime field
|
||||
* - special filter "Forecast" (which must set the `forecast_filter:1` context key)
|
||||
*/
|
||||
const ForecastPivotView = PivotView.extend({
|
||||
/**
|
||||
* @private
|
||||
* @override
|
||||
*/
|
||||
_createSearchModel(params, extraExtensions = {}) {
|
||||
Object.assign(extraExtensions, { Forecast: {} });
|
||||
return this._super(params, extraExtensions);
|
||||
},
|
||||
});
|
||||
viewRegistry.add('forecast_pivot', ForecastPivotView);
|
||||
class ForecastPivotView extends PivotView {}
|
||||
ForecastPivotView.SearchModel = ForecastSearchModel;
|
||||
|
||||
registry.category("views").add("forecast_pivot", ForecastPivotView);
|
||||
|
||||
export {
|
||||
ForecastGraphView,
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { _lt } from "@web/core/l10n/translation";
|
||||
import AbstractView from "web.AbstractView";
|
||||
import AbstractModel from "web.AbstractModel";
|
||||
import { controlPanel as cpHelpers } from "web.test_utils";
|
||||
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
|
||||
import { dialogService } from "@web/core/dialog/dialog_service";
|
||||
import { legacyExtraNextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import legacyViewRegistry from "web.view_registry";
|
||||
import { makeView } from "@web/../tests/views/helpers";
|
||||
import { mock } from "web.test_utils";
|
||||
import { registry } from "@web/core/registry";
|
||||
import {
|
||||
setupControlPanelServiceRegistry,
|
||||
switchView,
|
||||
toggleFilterMenu,
|
||||
toggleGroupByMenu,
|
||||
toggleMenuItem,
|
||||
toggleMenuItemOption,
|
||||
} from "@web/../tests/search/helpers";
|
||||
|
||||
const patchDate = mock.patchDate;
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
const forecastDomain = (forecastStart) => {
|
||||
return ["|", ["date_field", "=", false], ["date_field", ">=", forecastStart]];
|
||||
};
|
||||
|
||||
let serverData;
|
||||
QUnit.module("Views", (hooks) => {
|
||||
hooks.beforeEach(async () => {
|
||||
serverData = {
|
||||
models: {
|
||||
foo: {
|
||||
fields: {
|
||||
date_field: {
|
||||
string: "Date Field",
|
||||
type: "date",
|
||||
store: true,
|
||||
sortable: true,
|
||||
},
|
||||
bar: {
|
||||
string: "Bar",
|
||||
type: "many2one",
|
||||
relation: "partner",
|
||||
store: true,
|
||||
sortable: true,
|
||||
},
|
||||
},
|
||||
records: [],
|
||||
},
|
||||
},
|
||||
views: {
|
||||
"foo,false,legacy_toy": `<legacy_toy/>`,
|
||||
"foo,false,graph": `<graph js_class="forecast_graph"/>`,
|
||||
"foo,false,search": `
|
||||
<search>
|
||||
<filter name="forecast_filter" string="Forecast Filter" context="{ 'forecast_filter': 1 }"/>
|
||||
<filter name="group_by_bar" string="Bar" context="{ 'group_by': 'bar' }"/>
|
||||
<filter name="group_by_date_field" string="Date Field" context="{ 'group_by': 'date_field' }"/>
|
||||
</search>
|
||||
`,
|
||||
},
|
||||
};
|
||||
setupControlPanelServiceRegistry();
|
||||
serviceRegistry.add("dialog", dialogService);
|
||||
});
|
||||
|
||||
QUnit.module("Forecast views");
|
||||
|
||||
QUnit.test("Forecast graph view", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const unpatchDate = patchDate(2021, 8, 16, 16, 54, 0);
|
||||
|
||||
const expectedDomains = [
|
||||
forecastDomain("2021-09-01"), // month granularity due to no groupby
|
||||
forecastDomain("2021-09-16"), // day granularity due to simple bar groupby
|
||||
// quarter granularity due to date field groupby activated with quarter interval option
|
||||
forecastDomain("2021-07-01"),
|
||||
// quarter granularity due to date field groupby activated with quarter and year interval option
|
||||
forecastDomain("2021-01-01"),
|
||||
// forecast filter no more active
|
||||
[],
|
||||
];
|
||||
|
||||
const forecastGraph = await makeView({
|
||||
resModel: "foo",
|
||||
type: "forecast_graph",
|
||||
serverData,
|
||||
searchViewId: false,
|
||||
context: {
|
||||
search_default_forecast_filter: 1,
|
||||
forecast_field: "date_field",
|
||||
},
|
||||
mockRPC(_, args) {
|
||||
if (args.method === "web_read_group") {
|
||||
const { domain } = args.kwargs;
|
||||
assert.deepEqual(domain, expectedDomains.shift());
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await toggleGroupByMenu(forecastGraph);
|
||||
await toggleMenuItem(forecastGraph, "Bar");
|
||||
|
||||
await toggleMenuItem(forecastGraph, "Date Field");
|
||||
await toggleMenuItemOption(forecastGraph, "Date Field", "Quarter");
|
||||
|
||||
await toggleMenuItemOption(forecastGraph, "Date Field", "Year");
|
||||
|
||||
await toggleFilterMenu(forecastGraph);
|
||||
await toggleMenuItem(forecastGraph, "Forecast Filter");
|
||||
|
||||
unpatchDate();
|
||||
});
|
||||
|
||||
/** @todo remove this legacy test when conversion of all forecast views is done */
|
||||
QUnit.test(
|
||||
"legacy and new forecast views can share search model state",
|
||||
async function (assert) {
|
||||
assert.expect(16);
|
||||
|
||||
const unpatchDate = patchDate(2021, 8, 16, 16, 54, 0);
|
||||
|
||||
const expectedDomains = [
|
||||
// first doAction
|
||||
forecastDomain("2021-09-01"), // initial load of forecast_graph
|
||||
forecastDomain("2021-07-01"), // toggle date field groupby with quarter option
|
||||
forecastDomain("2021-07-01"), // initial load of legacy_toy
|
||||
forecastDomain("2021-01-01"), // toggle date field groupby with year option
|
||||
forecastDomain("2021-01-01"), // switch back to forecast_graph
|
||||
|
||||
// second doAction
|
||||
|
||||
forecastDomain("2021-09-01"), // initial load of legacy_toy
|
||||
forecastDomain("2021-07-01"), // toggle date field groupby with quarter option
|
||||
forecastDomain("2021-07-01"), // switch to forecast_graph
|
||||
forecastDomain("2021-01-01"), // toggle date field groupby with year option
|
||||
forecastDomain("2021-01-01"), // switch back to legacy_toy
|
||||
];
|
||||
|
||||
patchWithCleanup(AbstractModel.prototype, {
|
||||
async load(params) {
|
||||
this._super(...arguments);
|
||||
assert.deepEqual(params.domain, expectedDomains.shift());
|
||||
},
|
||||
async reload(_, params) {
|
||||
this._super(...arguments);
|
||||
assert.deepEqual(params.domain, expectedDomains.shift());
|
||||
},
|
||||
});
|
||||
|
||||
const LegacyForecastView = AbstractView.extend({
|
||||
display_name: _lt("Legacy toy view"),
|
||||
icon: "fa fa-bars",
|
||||
multiRecord: true,
|
||||
viewType: "legacy_toy",
|
||||
searchMenuTypes: ["filter", "groupBy"],
|
||||
|
||||
_createSearchModel(params, extraExtensions = {}) {
|
||||
Object.assign(extraExtensions, { forecast: {} });
|
||||
return this._super(params, extraExtensions);
|
||||
},
|
||||
});
|
||||
|
||||
legacyViewRegistry.add("legacy_toy", LegacyForecastView);
|
||||
|
||||
const webClient = await createWebClient({
|
||||
serverData,
|
||||
mockRPC(_, args) {
|
||||
if (args.method === "web_read_group") {
|
||||
const { domain } = args.kwargs;
|
||||
assert.deepEqual(domain, expectedDomains.shift());
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// first doAction forecast_graph -> legacy_toy -> forecast_graph
|
||||
|
||||
await doAction(webClient, {
|
||||
name: "Action name",
|
||||
res_model: "foo",
|
||||
type: "ir.actions.act_window",
|
||||
views: [
|
||||
[false, "graph"],
|
||||
[false, "legacy_toy"],
|
||||
],
|
||||
context: {
|
||||
search_default_forecast_filter: 1,
|
||||
forecast_field: "date_field",
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsOnce(webClient, ".o_switch_view.o_graph.active");
|
||||
|
||||
await toggleGroupByMenu(webClient);
|
||||
await toggleMenuItem(webClient, "Date Field");
|
||||
await toggleMenuItemOption(webClient, "Date Field", "Quarter");
|
||||
|
||||
await switchView(webClient, "legacy_toy");
|
||||
await legacyExtraNextTick();
|
||||
|
||||
assert.containsOnce(webClient, ".o_switch_view.o_legacy_toy.active");
|
||||
|
||||
await cpHelpers.toggleGroupByMenu(webClient);
|
||||
await cpHelpers.toggleMenuItem(webClient, "Date Field");
|
||||
await cpHelpers.toggleMenuItemOption(webClient, "Date Field", "Year");
|
||||
|
||||
await switchView(webClient, "graph");
|
||||
|
||||
assert.containsOnce(webClient, ".o_switch_view.o_graph.active");
|
||||
|
||||
// second doAction legacy_toy -> forecast_graph -> legacy_toy
|
||||
|
||||
await doAction(webClient, {
|
||||
name: "Action name",
|
||||
res_model: "foo",
|
||||
type: "ir.actions.act_window",
|
||||
views: [
|
||||
[false, "legacy_toy"],
|
||||
[false, "graph"],
|
||||
],
|
||||
context: {
|
||||
search_default_forecast_filter: 1,
|
||||
forecast_field: "date_field",
|
||||
},
|
||||
});
|
||||
await legacyExtraNextTick();
|
||||
|
||||
assert.containsOnce(webClient, ".o_switch_view.o_legacy_toy.active");
|
||||
|
||||
await cpHelpers.toggleGroupByMenu(webClient);
|
||||
await cpHelpers.toggleMenuItem(webClient, "Date Field");
|
||||
await cpHelpers.toggleMenuItemOption(webClient, "Date Field", "Quarter");
|
||||
|
||||
await switchView(webClient, "graph");
|
||||
|
||||
assert.containsOnce(webClient, ".o_switch_view.o_graph.active");
|
||||
|
||||
await toggleGroupByMenu(webClient);
|
||||
await toggleMenuItem(webClient, "Date Field");
|
||||
await toggleMenuItemOption(webClient, "Date Field", "Year");
|
||||
|
||||
await switchView(webClient, "legacy_toy");
|
||||
await legacyExtraNextTick();
|
||||
|
||||
assert.containsOnce(webClient, ".o_switch_view.o_legacy_toy.active");
|
||||
|
||||
unpatchDate();
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import ActionModel from "@web/legacy/js/views/action_model";
|
||||
import { makeContext } from "@web/core/context";
|
||||
|
||||
/**
|
||||
* @param {string} state
|
||||
@@ -165,6 +166,12 @@ export function searchModelStateToLegacy(state) {
|
||||
filter.orderedBy = item.orderBy;
|
||||
delete filter.orderBy;
|
||||
break;
|
||||
case "filter":
|
||||
let context = item.context;
|
||||
try {
|
||||
context = makeContext(context);
|
||||
} catch (e) {}
|
||||
filter.context = context;
|
||||
}
|
||||
filters[item.id] = filter;
|
||||
}
|
||||
|
||||
@@ -219,11 +219,7 @@ export class SearchModel extends EventBus {
|
||||
this.view = view || { id: false };
|
||||
|
||||
// used to avoid useless recomputations
|
||||
// this._comparison is left undefined (we have to distinguish it from null)
|
||||
this._context = null;
|
||||
this._domain = null;
|
||||
this._groupBy = null;
|
||||
this._orderBy = null;
|
||||
this._reset();
|
||||
|
||||
const { comparison, context, domain, groupBy, orderBy } = config;
|
||||
|
||||
@@ -391,12 +387,7 @@ export class SearchModel extends EventBus {
|
||||
* @param {string[]} [config.orderBy=[]]
|
||||
*/
|
||||
async reload(config = {}) {
|
||||
// used to avoid useless recomputations
|
||||
delete this._comparison;
|
||||
this._context = null;
|
||||
this._domain = null;
|
||||
this._groupBy = null;
|
||||
this._orderBy = null;
|
||||
this._reset();
|
||||
|
||||
const { comparison, context, domain, groupBy, orderBy } = config;
|
||||
|
||||
@@ -2015,11 +2006,8 @@ export class SearchModel extends EventBus {
|
||||
if (this.blockNotification) {
|
||||
return;
|
||||
}
|
||||
delete this._comparison;
|
||||
this._context = null;
|
||||
this._domain = null;
|
||||
this._groupBy = null;
|
||||
this._orderBy = null;
|
||||
|
||||
this._reset();
|
||||
|
||||
await this._reloadSections();
|
||||
|
||||
@@ -2066,6 +2054,14 @@ export class SearchModel extends EventBus {
|
||||
this.blockNotification = false;
|
||||
}
|
||||
|
||||
_reset() {
|
||||
delete this._comparison;
|
||||
this._context = null;
|
||||
this._domain = null;
|
||||
this._groupBy = null;
|
||||
this._orderBy = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the query informations should be considered as ready
|
||||
* before or after having (re-)fetched the sections data.
|
||||
|
||||
Reference in New Issue
Block a user