diff --git a/addons/crm/static/src/views/forecast_kanban/forecast_kanban_model.js b/addons/crm/static/src/views/forecast_kanban/forecast_kanban_model.js index c98bf26ba6e..8d99ad08770 100644 --- a/addons/crm/static/src/views/forecast_kanban/forecast_kanban_model.js +++ b/addons/crm/static/src/views/forecast_kanban/forecast_kanban_model.js @@ -8,6 +8,8 @@ export class ForecastKanbanModel extends CrmKanbanModel { super.setup(...arguments); this.fillTemporalService = fillTemporalService; this.forceNextRecompute = !params.state?.groups; + this.originalDomain = null; + this.fillTemporalDomain = null; } async _webReadGroup(config, firstGroupByName, orderBy) { @@ -15,10 +17,20 @@ export class ForecastKanbanModel extends CrmKanbanModel { config.context = this.fillTemporalPeriod(config).getContext({ context: config.context, }); - config.domain = this.fillTemporalPeriod(config).getDomain({ - domain: config.domain, - forceStartBound: false, - }); + // Domain leaves added by the fillTemporalPeriod should be replaced + // between 2 _webReadGroup calls, not added on top of each other. + // Keep track of the modified domain, and if encountered in the + // future, modify the original domain instead. It is not robust + // against external modification of `config.domain`, but currently + // there are only replacements except this case. + if (!this.originalDomain || this.fillTemporalDomain !== config.domain) { + this.originalDomain = config.domain || []; + } + this.fillTemporalDomain = this.fillTemporalPeriod(config).getDomain({ + domain: this.originalDomain, + forceStartBound: false, + }); + config.domain = this.fillTemporalDomain; } return super._webReadGroup(...arguments); } diff --git a/addons/crm/static/src/views/forecast_kanban/forecast_kanban_renderer.js b/addons/crm/static/src/views/forecast_kanban/forecast_kanban_renderer.js index a5658b59ab3..716dc6c02ef 100644 --- a/addons/crm/static/src/views/forecast_kanban/forecast_kanban_renderer.js +++ b/addons/crm/static/src/views/forecast_kanban/forecast_kanban_renderer.js @@ -42,6 +42,7 @@ export class ForecastKanbanRenderer extends CrmKanbanRenderer { }) .expand(); await this.props.list.load(); + await this.props.progressBarState?._updateProgressBar(); } } diff --git a/addons/crm/static/tests/forecast_kanban_tests.js b/addons/crm/static/tests/forecast_kanban_tests.js index 16845a028b3..e1f927cd484 100644 --- a/addons/crm/static/tests/forecast_kanban_tests.js +++ b/addons/crm/static/tests/forecast_kanban_tests.js @@ -5,6 +5,7 @@ import { fillTemporalService } from "@crm/views/fill_temporal_service"; import { makeView, setupViewRegistries } from "@web/../tests/views/helpers"; import { click, + dragAndDrop, getFixture, patchDate, } from '@web/../tests/helpers/utils'; @@ -263,3 +264,118 @@ QUnit.module('Crm Fill Temporal Service', { }).end.toFormat('yyyy-MM-dd'), '2023-01-01'); }); }); + +QUnit.module('Crm Forecast main flow with progressBars', (hooks) => { + function getCounters() { + return [...target.querySelectorAll(".o_animated_number")].map((counter) => counter.innerText); + } + + function getProgressBarsColors() { + return [...target.querySelectorAll(".o_column_progress")].map(columnProgressEl => { + return [...columnProgressEl.querySelectorAll(".progress-bar")].map(progressBarEl => { + return [...progressBarEl.classList.values()].filter(htmlClass => { + return htmlClass.startsWith('bg-'); + })[0]; + }) + }); + } + + hooks.beforeEach(() => { + serviceRegistry.add("fillTemporalService", fillTemporalService); + this.testKanbanView = { + arch: ` + + + + + +
+ +
+
+
+
`, + searchViewArch: ` + + + + ` + , + serverData: { + models: { + 'crm.lead': { + fields: { + color: {string: 'Color', type: 'string'}, + date_deadline: {string: 'Expected Closing', type: 'date'}, + int_field: {string: 'Value', type: 'integer', sortable: true}, + name: {string: 'Name', type: 'char'}, + }, + }, + }, + views: {}, + }, + resModel: 'crm.lead', + type: "kanban", + context: { + search_default_forecast: true, + search_default_groupby_date_deadline: true, + forecast_field: 'date_deadline', + }, + groupBy: ['date_deadline'], + }; + target = getFixture(); + setupViewRegistries(); + // first september 2023 + patchDate(2023, 8, 1, 0, 0, 0); + }); + + QUnit.test("Forecast drag&drop and add column", async (assert) => { + this.testKanbanView.serverData.models['crm.lead'].records = [ + {id: 1, int_field: 7, color: 'd', name: 'Lead 1', date_deadline: '2023-09-03'}, + {id: 2, int_field: 20, color: 'w', name: 'Lead 2', date_deadline: '2023-09-05'}, + {id: 3, int_field: 300, color: 's', name: 'Lead 3', date_deadline: '2023-10-10'}, + ]; + + await makeView({ + ...this.testKanbanView, + async mockRPC(route, args) { + assert.step(args.method || route); + }, + }); + + assert.deepEqual(getCounters(), ["27", "300"]); + assert.deepEqual(getProgressBarsColors(), [["bg-warning", "bg-danger"], ["bg-success"]]); + + await dragAndDrop( + ".o_kanban_group:first-child .o_kanban_record", + ".o_kanban_group:nth-child(2)" + ); + + assert.deepEqual(getCounters(), ["20", "307"]); + assert.deepEqual(getProgressBarsColors(), [["bg-warning"], ["bg-success", "bg-danger"]]); + + await click(target, ".o_kanban_add_column"); + + // Counters and progressbars should be unchanged after adding a column. + assert.deepEqual(getCounters(), ["20", "307"]); + assert.deepEqual(getProgressBarsColors(), [["bg-warning"], ["bg-success", "bg-danger"]]); + + assert.verifySteps([ + // makeView + "get_views", + "web_read_group", + "read_progress_bar", + "web_search_read", + "web_search_read", + // drag&drop + "web_save", + "read_progress_bar", + "web_read_group", + // add column + "web_read_group", + "web_search_read", + "web_search_read", + "read_progress_bar" + ]); + }); +}); diff --git a/addons/web/static/src/views/kanban/progress_bar_hook.js b/addons/web/static/src/views/kanban/progress_bar_hook.js index 6c91c5a11d9..b4ffb2ddd0e 100644 --- a/addons/web/static/src/views/kanban/progress_bar_hook.js +++ b/addons/web/static/src/views/kanban/progress_bar_hook.js @@ -263,6 +263,7 @@ class ProgressBarState { if (groupsId !== this.model.root.groups.map((g) => g.id).join()) { return; } + this._pbCounts = res; for (const group of this.model.root.groups) { if (!group.isFolded) { const groupInfo = this.getGroupInfo(group);