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);