diff --git a/addons/sale/__manifest__.py b/addons/sale/__manifest__.py
index bd902063075..8026aeacf9b 100644
--- a/addons/sale/__manifest__.py
+++ b/addons/sale/__manifest__.py
@@ -60,7 +60,7 @@ This module contains all the common features of Sales Management and eCommerce.
'web.assets_backend': [
'sale/static/src/scss/sale_onboarding.scss',
'sale/static/src/scss/product_configurator.scss',
- 'sale/static/src/js/sale.js',
+ 'sale/static/src/js/sale_progressbar_field.js',
'sale/static/src/js/tours/sale.js',
'sale/static/src/js/sale_order_line_mixin.js',
'sale/static/src/js/product_configurator_widget.js',
@@ -83,6 +83,9 @@ This module contains all the common features of Sales Management and eCommerce.
'web.report_assets_common': [
'sale/static/src/scss/sale_report.scss',
],
+ 'web.assets_qweb': [
+ 'sale/static/src/xml/**/*',
+ ],
},
'post_init_hook': '_synchronize_cron',
'license': 'LGPL-3',
diff --git a/addons/sale/static/src/js/sale.js b/addons/sale/static/src/js/sale.js
deleted file mode 100644
index e46e368f93b..00000000000
--- a/addons/sale/static/src/js/sale.js
+++ /dev/null
@@ -1,34 +0,0 @@
-odoo.define('sale.sales_team_dashboard', function (require) {
-"use strict";
-
-const { FieldProgressBar } = require('web.basic_fields');
-const fieldRegistry = require('web.field_registry');
-
-var core = require('web.core');
-var _t = core._t;
-
-const SalesTeamProgressBar = FieldProgressBar.extend({
- _render() {
- this._super.apply(this, arguments);
- const isUnset = !this.recordData[this.nodeOptions.max_value];
- if (isUnset) {
- const msg = document.createElement('a');
- msg.innerText = _t("Click to define an invoicing target");
- msg.setAttribute('href', '#')
- msg.addEventListener("click", (ev) => {
- ev.preventDefault();
- msg.parentElement.removeChild(msg);
- for (let child of this.el.children) {
- child.classList.remove('d-none')
- }
- });
- for (let child of this.el.children) {
- child.classList.add('d-none');
- }
- this.el.insertBefore(msg, this.el.firstChild)
- }
- },
-});
-
-fieldRegistry.add("sales_team_progressbar", SalesTeamProgressBar);
-});
diff --git a/addons/sale/static/src/js/sale_progressbar_field.js b/addons/sale/static/src/js/sale_progressbar_field.js
new file mode 100644
index 00000000000..49491c3e4c6
--- /dev/null
+++ b/addons/sale/static/src/js/sale_progressbar_field.js
@@ -0,0 +1,45 @@
+/** @odoo-module **/
+
+import { registry } from "@web/core/registry";
+import { useService } from "@web/core/utils/hooks";
+import { KanbanProgressBarField } from "@web/views/fields/progress_bar/kanban_progress_bar_field";
+
+const { useEffect } = owl;
+
+/**
+ * A custom Component for the view of sales teams on the kanban view in the CRM app.
+ *
+ * The wanted behavior is to show a progress bar when an invoicing target is defined or show
+ * a link redirecting to the record's form view otherwise.
+ */
+export class SaleProgressBarField extends KanbanProgressBarField {
+ /**
+ * Anything used by the component is defined on the setup method.
+ */
+ setup() {
+ super.setup();
+
+ this.actionService = useService("action");
+ this.orm = useService("orm");
+
+ useEffect(() => {
+ this.state.isInvoicingTargetDefined = this.props.record.data[this.props.maxValueField];
+ });
+ }
+
+ /**
+ * Display the form view of the record on click.
+ */
+ async defineInvoicingTarget() {
+ const { resId, resModel } = this.props.record;
+ const action = await this.orm.call(resModel, "get_formview_action", [[resId]]);
+ this.actionService.doAction(action, { props: { mode: "edit" } });
+ }
+}
+
+/**
+ * Define the template name used on the component.
+ */
+SaleProgressBarField.template = "sale.SaleProgressBarField";
+
+registry.category("fields").add("sales_team_progressbar", SaleProgressBarField);
diff --git a/addons/sale/static/src/xml/sales_team_progress_bar_template.xml b/addons/sale/static/src/xml/sales_team_progress_bar_template.xml
new file mode 100644
index 00000000000..5d049e9fee9
--- /dev/null
+++ b/addons/sale/static/src/xml/sales_team_progress_bar_template.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+ Click to define an invoicing target
+
+
+
+
+
diff --git a/addons/sale/static/tests/sales_team_dashboard_tests.js b/addons/sale/static/tests/sales_team_dashboard_tests.js
index dadea52f230..ca97efd57c8 100644
--- a/addons/sale/static/tests/sales_team_dashboard_tests.js
+++ b/addons/sale/static/tests/sales_team_dashboard_tests.js
@@ -1,117 +1,90 @@
-odoo.define('sale.dashboard_tests', function (require) {
-"use strict";
+/** @odoo-module **/
-var KanbanView = require('web.KanbanView');
-var testUtils = require('web.test_utils');
+import { click, getFixture } from "@web/../tests/helpers/utils";
+import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
+import { registry } from "@web/core/registry";
-var createView = testUtils.createView;
+const serviceRegistry = registry.category("services");
-QUnit.module('Sales Team Dashboard', {
- beforeEach: function () {
- this.data = {
- 'crm.team': {
- fields: {
- foo: {string: "Foo", type: 'char'},
- invoiced: {string: "Invoiced", type: 'integer'},
- invoiced_target: {string: "Invoiced_target", type: 'integer'},
+let serverData;
+let target;
+
+QUnit.module("Sales Team Dashboard", {
+ beforeEach() {
+ target = getFixture();
+ serverData = {
+ models: {
+ "crm.team": {
+ fields: {
+ foo: { string: "Foo", type: "char" },
+ invoiced: { string: "Invoiced", type: "integer" },
+ invoiced_target: { string: "Invoiced_target", type: "integer" },
+ },
+ records: [{ id: 1, foo: "yop", invoiced: 0, invoiced_target: 0 }],
},
- records: [
- {id: 1, foo: "yop"},
- ],
},
};
- }
+
+ setupViewRegistries();
+ },
});
-QUnit.test('edit target with several o_kanban_primary_bottom divs [REQUIRE FOCUS]', async function (assert) {
- assert.expect(7);
+QUnit.test("edit target with several o_kanban_primary_bottom divs", async (assert) => {
+ assert.expect(4);
- var kanban = await createView({
- View: KanbanView,
- model: 'crm.team',
- data: this.data,
- arch: '' +
- '' +
- '' +
- '' +
- '' +
- `
` +
- '
' +
- '
' +
- '
' +
- '' +
- '' +
- '',
- mockRPC: function (route, args) {
- if (args.method === 'write') {
- assert.strictEqual(args.args[1].invoiced_target, 123,
- "new value is correctly saved");
+ const fakeActionService = {
+ start: () => ({
+ async doAction(action) {
+ assert.deepEqual(
+ action,
+ {
+ res_model: "crm.team",
+ target: "current",
+ type: "ir.actions.act_window",
+ method: "get_formview_action",
+ },
+ "should trigger do_action with the correct args"
+ );
+ return true;
+ },
+ }),
+ };
+ serviceRegistry.add("action", fakeActionService, { force: true });
+
+ await makeView({
+ serverData,
+ type: "kanban",
+ resModel: "crm.team",
+ arch: /* xml */`
+
+
+
+
+
+ `,
+ resId: 1,
+ async mockRPC(route, { method, model }) {
+ if (route === "/web/dataset/call_kw/crm.team/get_formview_action") {
+ return {
+ method,
+ res_model: model,
+ target: "current",
+ type: "ir.actions.act_window",
+ };
}
- if (args.method === 'read') { // Read happens after the write
- assert.deepEqual(args.args[1], ['invoiced_target', 'invoiced', 'display_name'],
- 'the read (after write) should ask for invoiced_target');
- }
- return this._super.apply(this, arguments);
},
});
- assert.containsOnce(kanban, '.o_legacy_kanban_view .o_progressbar:contains(Click to define an invoicing target)')
- assert.containsN(kanban, '.o_kanban_primary_bottom', 2,
- "should have two divs with classname 'o_kanban_primary_bottom'");
-
- assert.containsNone(kanban, '.o_progressbar input')
- await testUtils.dom.click(kanban.$('.o_progressbar a'));
- assert.containsOnce(kanban, '.o_progressbar input')
-
- kanban.$('.o_progressbar input').focus();
- kanban.$('.o_progressbar input').val('123');
- kanban.$('.o_progressbar input').trigger('blur');
- await testUtils.nextTick();
- assert.strictEqual(kanban.$('.o_kanban_record').text().trim(), "0 / 123");
-
- kanban.destroy();
-});
-
-QUnit.test('edit target supports push Enter', async function (assert) {
- assert.expect(3);
-
- var kanban = await createView({
- View: KanbanView,
- model: 'crm.team',
- data: this.data,
- arch: '' +
- '' +
- '' +
- '' +
- '' +
- `
` +
- '
' +
- '
' +
- '
' +
- '' +
- '' +
- '',
- mockRPC: function (route, args) {
- if (args.method === 'write') {
- assert.strictEqual(args.args[1].invoiced_target, 123,
- "new value is correctly saved");
- }
- if (args.method === 'read') { // Read happens after the write
- assert.deepEqual(args.args[1], ['invoiced_target', 'invoiced', 'display_name'],
- 'the read (after write) should ask for invoiced_target');
- }
- return this._super.apply(this, arguments);
- },
- });
-
- await testUtils.dom.click(kanban.$('.o_progressbar a'));
- kanban.$('.o_progressbar input').focus();
- kanban.$('.o_progressbar input').val('123');
- kanban.$('.o_progressbar input').trigger($.Event('keyup', {which: $.ui.keyCode.ENTER, keyCode: $.ui.keyCode.ENTER}));
- await testUtils.nextTick();
- assert.strictEqual(kanban.$('.o_kanban_record').text().trim(), "0 / 123");
-
- kanban.destroy();
-});
+ assert.containsOnce(
+ target,
+ ".o_field_sales_team_progressbar:contains(Click to define an invoicing target)"
+ );
+ assert.containsN(target, ".o_kanban_primary_bottom", 2);
+ assert.containsNone(target, ".o_progressbar input");
+ await click(target, ".sale_progressbar_form_link"); // should trigger a do_action
});
diff --git a/addons/web/static/src/views/fields/progress_bar/kanban_progress_bar_field.js b/addons/web/static/src/views/fields/progress_bar/kanban_progress_bar_field.js
index fc3a812e7f9..97c39bf4aa1 100644
--- a/addons/web/static/src/views/fields/progress_bar/kanban_progress_bar_field.js
+++ b/addons/web/static/src/views/fields/progress_bar/kanban_progress_bar_field.js
@@ -3,7 +3,7 @@
import { registry } from "@web/core/registry";
import { ProgressBarField } from "./progress_bar_field";
-class KanbanProgressBarField extends ProgressBarField {
+export class KanbanProgressBarField extends ProgressBarField {
onClick() {
if (this.props.isEditable && !this.props.record.isReadonly(this.props.name)) {
this.state.isEditing = true;