[IMP] crm: harmonize won triggers with rainbowman

PURPOSE

Make sure the Rainbowman animation is rightly triggered whichever way the user
marks the opportunity as won. Especially relevant for the onboarding as we want
to show the first rainbowman to users completing the tour.

SPECIFICATIONS

Now the rainbowman and its finely-tuned message are displayed to the user when
he uses the statusbar, mark lead as won using action buttons or drag and drop
a lead in the won stage in kanban view.

LINKS

Task ID-2287758
PR #54553
This commit is contained in:
Jérémy Hennecart
2020-08-17 06:42:28 +00:00
committed by Thibault Delavallée
parent decfd753f9
commit a895cfcded
10 changed files with 626 additions and 16 deletions
+2 -1
View File
@@ -4,6 +4,7 @@ import logging
from odoo.addons.mail.controllers.main import MailController
from odoo import http
from odoo.http import request
_logger = logging.getLogger(__name__)
@@ -15,7 +16,7 @@ class CrmController(http.Controller):
comparison, record, redirect = MailController._check_token_and_record_or_redirect('crm.lead', int(res_id), token)
if comparison and record:
try:
record.action_set_won()
record.action_set_won_rainbowman()
except Exception:
_logger.exception("Could not mark crm.lead as won")
return MailController._redirect_to_messaging()
+21 -12
View File
@@ -739,6 +739,26 @@ class Lead(models.Model):
self.ensure_one()
self.action_set_won()
message = self._get_rainbowman_message()
if message:
return {
'effect': {
'fadeout': 'slow',
'message': message,
'img_url': '/web/image/%s/%s/image_1024' % (self.team_id.user_id._name, self.team_id.user_id.id) if self.team_id.user_id.image_1024 else '/web/static/src/img/smile.svg',
'type': 'rainbow_man',
}
}
return True
def get_rainbowman_message(self):
self.ensure_one()
if self.stage_id.is_won:
return self._get_rainbowman_message()
return False
def _get_rainbowman_message(self):
message = False
if self.user_id and self.team_id and self.expected_revenue:
query = """
SELECT
@@ -763,7 +783,6 @@ class Lead(models.Model):
'team_id': self.team_id.id})
query_result = self.env.cr.dictfetchone()
message = False
if query_result['total_won'] == 1:
message = _('Go, go, go! Congrats for your first deal.')
elif query_result['max_team_30'] == self.expected_revenue:
@@ -774,17 +793,7 @@ class Lead(models.Model):
message = _('You just beat your personal record for the past 30 days.')
elif query_result['max_user_7'] == self.expected_revenue:
message = _('You just beat your personal record for the past 7 days.')
if message:
return {
'effect': {
'fadeout': 'slow',
'message': message,
'img_url': '/web/image/%s/%s/image_1024' % (self.team_id.user_id._name, self.team_id.user_id.id) if self.team_id.user_id.image_1024 else '/web/static/src/img/smile.svg',
'type': 'rainbow_man',
}
}
return True
return message
def action_schedule_meeting(self):
""" Open meeting's calendar view to schedule meeting on current opportunity.
+53
View File
@@ -0,0 +1,53 @@
odoo.define("crm.crm_form", function (require) {
"use strict";
/**
* This From Controller makes sure we display a rainbowman message
* when the stage is won, even when we click on the statusbar.
* When the stage of a lead is changed and data are saved, we check
* if the lead is won and if a message should be displayed to the user
* with a rainbowman like when the user click on the button "Mark Won".
*/
var FormController = require('web.FormController');
var FormView = require('web.FormView');
var viewRegistry = require('web.view_registry');
var CrmFormController = FormController.extend({
/**
* After data are saved we display a possible rainbowman
* message when the stage is won.
* @override
*/
_applyChanges: async function (dataPointID, changes, event) {
var result = await this._super(...arguments);
if ('stage_id' in changes) {
const message = await this._rpc({
model: 'crm.lead',
method : 'get_rainbowman_message',
args: [[parseInt(event.target.res_id)]],
});
if (message) {
this.trigger_up('show_effect', {
message: message,
type: 'rainbow_man',
});
}
}
return result;
},
});
var CrmFormView = FormView.extend({
config: _.extend({}, FormView.prototype.config, {
Controller: CrmFormController,
}),
});
viewRegistry.add('crm_form', CrmFormView);
return {
CrmFormController: CrmFormController,
CrmFormView: CrmFormView,
};
});
+52
View File
@@ -0,0 +1,52 @@
odoo.define('crm.crm_kanban', function (require) {
"use strict";
/**
* This Kanban Model make sure we display a rainbowman
* message when a lead is won after we moved it in the
* correct column and when it's grouped by stage_id (default).
*/
var KanbanModel = require('web.KanbanModel');
var KanbanView = require('web.KanbanView');
var viewRegistry = require('web.view_registry');
var CrmKanbanModel = KanbanModel.extend({
/**
* Check if the kanban view is grouped by "stage_id" before checking if the lead is won
* and displaying a possible rainbowman message.
* @override
*/
moveRecord: async function (recordID, groupID, parentID) {
var result = await this._super(...arguments);
if (this.localData[parentID].groupedBy[0] === this.defaultGroupedBy[0]) {
const message = await this._rpc({
model: 'crm.lead',
method : 'get_rainbowman_message',
args: [[parseInt(this.localData[recordID].res_id)]],
});
if (message) {
this.trigger_up('show_effect', {
message: message,
type: 'rainbow_man',
});
}
}
return result;
},
});
var CrmKanbanView = KanbanView.extend({
config: _.extend({}, KanbanView.prototype.config, {
Model: CrmKanbanModel,
}),
});
viewRegistry.add('crm_kanban', CrmKanbanView);
return {
CrmKanbanModel: CrmKanbanModel,
CrmKanbanView: CrmKanbanView,
};
});
@@ -0,0 +1,360 @@
odoo.define('crm.form_rainbowman_tests', function (require) {
"use strict";
var CrmFormView = require('crm.crm_form').CrmFormView;
var CrmKanbanView = require('crm.crm_kanban').CrmKanbanView;
var testUtils = require('web.test_utils');
var createView = testUtils.createView;
QUnit.module('Crm Rainbowman Triggers', {
beforeEach: function () {
const format = "YYYY-MM-DD HH:mm:ss";
this.data = {
'res.users': {
fields: {
display_name: { string: 'Name', type: 'char' },
},
records: [
{ id: 1, name: 'Mario' },
{ id: 2, name: 'Luigi' },
{ id: 3, name: 'Link' },
{ id: 4, name: 'Zelda' },
],
},
'crm.team': {
fields: {
display_name: { string: 'Name', type: 'char' },
member_ids: { string: 'Members', type: 'many2many', relation: 'res.users' },
},
records: [
{ id: 1, name: 'Mushroom Kingdom', member_ids: [1, 2] },
{ id: 2, name: 'Hyrule', member_ids: [3, 4] },
],
},
'crm.stage': {
fields: {
display_name: { string: 'Name', type: 'char' },
is_won: { string: 'Is won', type: 'boolean' },
},
records: [
{ id: 1, name: 'Start' },
{ id: 2, name: 'Middle' },
{ id: 3, name: 'Won', is_won: true},
],
},
'crm.lead': {
fields: {
display_name: { string: 'Name', type: 'char' },
planned_revenue: { string: 'Revenue', type: 'float' },
stage_id: { string: 'Stage', type: 'many2one', relation: 'crm.stage' },
team_id: { string: 'Sales Team', type: 'many2one', relation: 'crm.team' },
user_id: { string: 'Salesperson', type: 'many2one', relation: 'res.users' },
date_closed: { string: 'Date closed', type: 'datetime' },
},
records : [
{ id: 1, name: 'Lead 1', planned_revenue: 5.0, stage_id: 1, team_id: 1, user_id: 1 },
{ id: 2, name: 'Lead 2', planned_revenue: 5.0, stage_id: 2, team_id: 2, user_id: 4 },
{ id: 3, name: 'Lead 3', planned_revenue: 3.0, stage_id: 3, team_id: 1, user_id: 1, date_closed: moment().subtract(5, 'days').format(format) },
{ id: 4, name: 'Lead 4', planned_revenue: 4.0, stage_id: 3, team_id: 2, user_id: 4, date_closed: moment().subtract(23, 'days').format(format) },
{ id: 5, name: 'Lead 5', planned_revenue: 7.0, stage_id: 3, team_id: 1, user_id: 1, date_closed: moment().subtract(20, 'days').format(format) },
{ id: 6, name: 'Lead 6', planned_revenue: 4.0, stage_id: 2, team_id: 1, user_id: 2 },
{ id: 7, name: 'Lead 7', planned_revenue: 1.8, stage_id: 3, team_id: 2, user_id: 3, date_closed: moment().subtract(23, 'days').format(format) },
{ id: 8, name: 'Lead 8', planned_revenue: 1.9, stage_id: 1, team_id: 2, user_id: 3 },
{ id: 9, name: 'Lead 9', planned_revenue: 1.5, stage_id: 3, team_id: 2, user_id: 3, date_closed: moment().subtract(5, 'days').format(format) },
{ id: 10, name: 'Lead 10', planned_revenue: 1.7, stage_id: 2, team_id: 2, user_id: 3 },
{ id: 11, name: 'Lead 11', planned_revenue: 2.0, stage_id: 3, team_id: 2, user_id: 4, date_closed: moment().subtract(5, 'days').format(format) },
],
},
};
this.testFormView = {
arch: `
<form js_class="crm_form">
<header><field name="stage_id" widget="statusbar" options="{'clickable': '1'}"/></header>
<field name="name"/>
<field name="planned_revenue"/>
<field name="team_id"/>
<field name="user_id"/>
</form>`,
data: this.data,
model: 'crm.lead',
View: CrmFormView,
};
this.testKanbanView = {
arch: `
<kanban js_class="crm_kanban">
<templates>
<t t-name="kanban-box">
<div><field name="name"/></div>
</t>
</templates>
</kanban>`,
data: this.data,
model: 'crm.lead',
View: CrmKanbanView,
groupBy: ['stage_id'],
};
},
}, function () {
QUnit.test("first lead won, click on statusbar", async function (assert) {
assert.expect(2);
this.testFormView.res_id = 6;
this.testFormView.mockRPC = async function (route, args) {
const result = await this._super(...arguments);
if (args.model === 'crm.lead' && args.method === 'get_rainbowman_message') {
assert.step(result || "no rainbowman");
}
return result;
};
const form = await createView(this.testFormView);
await testUtils.dom.click(form.$(".o_statusbar_status button[data-value='3']"));
assert.verifySteps(['Go, go, go! Congrats for your first deal.']);
form.destroy();
});
QUnit.test("team record 30 days, click on statusbar", async function (assert) {
assert.expect(2);
this.testFormView.res_id = 2;
this.testFormView.mockRPC = async function (route, args) {
const result = await this._super(...arguments);
if (args.model === 'crm.lead' && args.method === 'get_rainbowman_message') {
assert.step(result || "no rainbowman");
}
return result;
};
const form = await createView(this.testFormView);
await testUtils.dom.click(form.$(".o_statusbar_status button[data-value='3']"));
assert.verifySteps(['Boom! Team record for the past 30 days.']);
form.destroy();
});
QUnit.test("team record 7 days, click on statusbar", async function (assert) {
assert.expect(2);
this.testFormView.res_id = 1;
this.testFormView.mockRPC = async function (route, args) {
const result = await this._super(...arguments);
if (args.model === 'crm.lead' && args.method === 'get_rainbowman_message') {
assert.step(result || "no rainbowman");
}
return result;
};
const form = await createView(this.testFormView);
await testUtils.dom.click(form.$(".o_statusbar_status button[data-value='3']"));
assert.verifySteps(['Yeah! Deal of the last 7 days for the team.']);
form.destroy();
});
QUnit.test("user record 30 days, click on statusbar", async function (assert) {
assert.expect(2);
this.testFormView.res_id = 8;
this.testFormView.mockRPC = async function (route, args) {
const result = await this._super(...arguments);
if (args.model === 'crm.lead' && args.method === 'get_rainbowman_message') {
assert.step(result || "no rainbowman");
}
return result;
};
const form = await createView(this.testFormView);
await testUtils.dom.click(form.$(".o_statusbar_status button[data-value='3']"));
assert.verifySteps(['You just beat your personal record for the past 30 days.']);
form.destroy();
});
QUnit.test("user record 7 days, click on statusbar", async function (assert) {
assert.expect(2);
this.testFormView.res_id = 10;
this.testFormView.mockRPC = async function (route, args) {
const result = await this._super(...arguments);
if (args.model === 'crm.lead' && args.method === 'get_rainbowman_message') {
assert.step(result || "no rainbowman");
}
return result;
};
const form = await createView(this.testFormView);
await testUtils.dom.click(form.$(".o_statusbar_status button[data-value='3']"));
assert.verifySteps(['You just beat your personal record for the past 7 days.']);
form.destroy();
});
QUnit.test("click on stage (not won) on statusbar", async function (assert) {
assert.expect(2);
this.testFormView.res_id = 1;
this.testFormView.mockRPC = async function (route, args) {
const result = await this._super(...arguments);
if (args.model === 'crm.lead' && args.method === 'get_rainbowman_message') {
assert.step(result || "no rainbowman");
}
return result;
};
const form = await createView(this.testFormView);
await testUtils.dom.click(form.$(".o_statusbar_status button[data-value='2']"));
assert.verifySteps(['no rainbowman']);
form.destroy();
});
QUnit.test("first lead won, drag & drop kanban", async function (assert) {
assert.expect(2);
this.testKanbanView.mockRPC = async function (route, args) {
const result = await this._super(...arguments);
if (args.model === 'crm.lead' && args.method === 'get_rainbowman_message') {
assert.step(result || "no rainbowman");
}
return result;
};
const kanban = await createView(this.testKanbanView);
kanban.model.defaultGroupedBy = ['stage_id'];
await kanban.reload();
await testUtils.dom.dragAndDrop(kanban.$('.o_kanban_record:contains("Lead 6")'), kanban.$('.o_kanban_group:eq(2)'));
assert.verifySteps(['Go, go, go! Congrats for your first deal.']);
kanban.destroy();
});
QUnit.test("team record 30 days, drag & drop kanban", async function (assert) {
assert.expect(2);
this.testKanbanView.mockRPC = async function (route, args) {
const result = await this._super(...arguments);
if (args.model === 'crm.lead' && args.method === 'get_rainbowman_message') {
assert.step(result || "no rainbowman");
}
return result;
};
const kanban = await createView(this.testKanbanView);
kanban.model.defaultGroupedBy = ['stage_id'];
await kanban.reload();
await testUtils.dom.dragAndDrop(kanban.$('.o_kanban_record:contains("Lead 2")'), kanban.$('.o_kanban_group:eq(2)'));
assert.verifySteps(['Boom! Team record for the past 30 days.']);
kanban.destroy();
});
QUnit.test("team record 7 days, drag & drop kanban", async function (assert) {
assert.expect(2);
this.testKanbanView.mockRPC = async function (route, args) {
const result = await this._super(...arguments);
if (args.model === 'crm.lead' && args.method === 'get_rainbowman_message') {
assert.step(result || "no rainbowman");
}
return result;
};
const kanban = await createView(this.testKanbanView);
kanban.model.defaultGroupedBy = ['stage_id'];
await kanban.reload();
await testUtils.dom.dragAndDrop(kanban.$('.o_kanban_group:eq(0) .o_kanban_record:contains("Lead 1")'), kanban.$('.o_kanban_group:eq(2)'));
assert.verifySteps(['Yeah! Deal of the last 7 days for the team.']);
kanban.destroy();
});
QUnit.test("user record 30 days, drag & drop kanban", async function (assert) {
assert.expect(2);
this.testKanbanView.mockRPC = async function (route, args) {
const result = await this._super(...arguments);
if (args.model === 'crm.lead' && args.method === 'get_rainbowman_message') {
assert.step(result || "no rainbowman");
}
return result;
};
const kanban = await createView(this.testKanbanView);
kanban.model.defaultGroupedBy = ['stage_id'];
await kanban.reload();
await testUtils.dom.dragAndDrop(kanban.$('.o_kanban_record:contains("Lead 8")'), kanban.$('.o_kanban_group:eq(2)'));
assert.verifySteps(['You just beat your personal record for the past 30 days.']);
kanban.destroy();
});
QUnit.test("user record 7 days, drag & drop kanban", async function (assert) {
assert.expect(2);
this.testKanbanView.mockRPC = async function (route, args) {
const result = await this._super(...arguments);
if (args.model === 'crm.lead' && args.method === 'get_rainbowman_message') {
assert.step(result || "no rainbowman");
}
return result;
};
const kanban = await createView(this.testKanbanView);
kanban.model.defaultGroupedBy = ['stage_id'];
await kanban.reload();
await testUtils.dom.dragAndDrop(kanban.$('.o_kanban_record:contains("Lead 10")'), kanban.$('.o_kanban_group:eq(2)'));
assert.verifySteps(['You just beat your personal record for the past 7 days.']);
kanban.destroy();
});
QUnit.test("drag & drop record kanban in stage not won", async function (assert) {
assert.expect(2);
this.testKanbanView.mockRPC = async function (route, args) {
const result = await this._super(...arguments);
if (args.model === 'crm.lead' && args.method === 'get_rainbowman_message') {
assert.step(result || "no rainbowman");
}
return result;
};
const kanban = await createView(this.testKanbanView);
kanban.model.defaultGroupedBy = ['stage_id'];
await kanban.reload();
await testUtils.dom.dragAndDrop(kanban.$('.o_kanban_record:contains("Lead 8")'), kanban.$('.o_kanban_group:eq(1)'));
assert.verifySteps(["no rainbowman"]);
kanban.destroy();
});
QUnit.test("drag & drop record in kanban not grouped by stage_id", async function (assert) {
assert.expect(1);
this.testKanbanView.mockRPC = async function (route, args) {
const result = await this._super(...arguments);
if (args.model === 'crm.lead' && args.method === 'get_rainbowman_message') {
assert.step(result || "no rainbowman");
}
return result;
};
this.testKanbanView.groupBy = ['user_id'];
const kanban = await createView(this.testKanbanView);
kanban.model.defaultGroupedBy = ['stage_id'];
await kanban.reload();
await testUtils.dom.dragAndDrop(kanban.$('.o_kanban_group:eq(0) .o_kanban_record:first'), kanban.$('.o_kanban_group:eq(1)'));
assert.verifySteps([]); // Should never pass by the rpc
kanban.destroy();
});
});
});
+56
View File
@@ -0,0 +1,56 @@
odoo.define('crm.MockServer', function (require) {
'use strict';
var MockServer = require('web.MockServer');
MockServer.include({
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
* @private
*/
async _performRpc(route, args) {
if (args.model === 'crm.lead' && args.method === 'get_rainbowman_message') {
let message = false;
const records = this.data['crm.lead'].records;
const record = records.find(r => r.id === args.args[0][0]);
const won_stage = this.data['crm.stage'].records.find(s => s.is_won);
if (record.stage_id === won_stage.id && record.user_id && record.team_id && record.planned_revenue > 0) {
const now = moment();
let query_result = {};
// Total won
query_result['total_won'] = records.filter(r => r.stage_id === won_stage.id && r.user_id === record.user_id).length;
// Max team 30 days
const recordsTeam30 = records.filter(r => r.stage_id === won_stage.id && r.team_id === record.team_id && (!r.date_closed || moment.duration(now.diff(moment(r.date_closed))).days() <= 30));
query_result['max_team_30'] = Math.max(...recordsTeam30.map(r => r.planned_revenue));
// Max team 7 days
const recordsTeam7 = records.filter(r => r.stage_id === won_stage.id && r.team_id === record.team_id && (!r.date_closed || moment.duration(now.diff(moment(r.date_closed))).days() <= 7));
query_result['max_team_7'] = Math.max(...recordsTeam7.map(r => r.planned_revenue));
// Max User 30 days
const recordsUser30 = records.filter(r => r.stage_id === won_stage.id && r.user_id === record.user_id && (!r.date_closed || moment.duration(now.diff(moment(r.date_closed))).days() <= 30));
query_result['max_user_30'] = Math.max(...recordsUser30.map(r => r.planned_revenue));
// Max User 7 days
const recordsUser7 = records.filter(r => r.stage_id === won_stage.id && r.user_id === record.user_id && (!r.date_closed || moment.duration(now.diff(moment(r.date_closed))).days() <= 7));
query_result['max_user_7'] = Math.max(...recordsUser7.map(r => r.planned_revenue));
if (query_result.total_won === 1) {
message = "Go, go, go! Congrats for your first deal.";
} else if (query_result.max_team_30 === record.planned_revenue) {
message = "Boom! Team record for the past 30 days.";
} else if (query_result.max_team_7 === record.planned_revenue) {
message = "Yeah! Deal of the last 7 days for the team.";
} else if (query_result.max_user_30 === record.planned_revenue) {
message = "You just beat your personal record for the past 30 days.";
} else if (query_result.max_user_7 === record.planned_revenue) {
message = "You just beat your personal record for the past 7 days.";
}
}
return message;
}
return this._super(...arguments);
},
});
});
@@ -0,0 +1,66 @@
odoo.define('crm.tour_crm_rainbowman', function (require) {
"use strict";
var tour = require('web_tour.tour');
tour.register('crm_rainbowman', {
test: true,
url: "/web",
}, [
tour.stepUtils.showAppsMenuItem(),
{
trigger: ".o_app[data-menu-xmlid='crm.crm_menu_root']",
content: "open crm app",
}, {
trigger: ".o-kanban-button-new",
content: "click create",
}, {
trigger: "input[name=name]",
content: "complete name",
run: "text Test Lead 1",
}, {
trigger: "div[name=planned_revenue] > input",
content: "complete planned revenue",
run: "text 999999997",
}, {
trigger: "button.o_kanban_add",
content: "create lead",
}, {
trigger: ".o_kanban_record .o_kanban_record_title:contains('Test Lead 1')",
content: "move to won stage",
run: "drag_and_drop .o_opportunity_kanban .o_kanban_group:eq(3) "
}, {
trigger: ".o-kanban-button-new",
extra_trigger: ".o_reward_rainbow",
content: "click create",
}, {
trigger: "input[name=name]",
content: "complete name",
run: "text Test Lead 2",
}, {
trigger: "div[name=planned_revenue] > input",
content: "complete planned revenue",
run: "text 999999998",
}, {
trigger: "button.o_kanban_add",
content: "create lead",
}, {
trigger: ".o_kanban_record .o_kanban_record_title:contains('Test Lead 2')",
content: "click on lead",
}, {
trigger: ".o_statusbar_status button[data-value='4']",
content: "move lead to won stage",
}, {
trigger: ".o_statusbar_status button[data-value='1']",
extra_trigger: ".o_reward_rainbow",
content: "move lead to previous stage & rainbowman appears",
}, {
trigger: "button[name=action_set_won_rainbowman]",
content: "click button mark won",
}, {
trigger: ".o_menu_brand",
extra_trigger: ".o_reward_rainbow",
content: "last rainbowman appears",
}
]);
});
+13
View File
@@ -2,8 +2,21 @@
<odoo>
<template id="assets_backend" name="CRM assets backend" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<script type="text/javascript" src="/crm/static/src/js/crm_form.js"/>
<script type="text/javascript" src="/crm/static/src/js/crm_kanban.js"/>
<script type="text/javascript" src="/crm/static/src/js/systray_activity_menu.js"/>
<script type="text/javascript" src="/crm/static/src/js/tours/crm.js"></script>
</xpath>
</template>
<template id="assets_tests" name="CRM Assets Tests" inherit_id="web.assets_tests">
<xpath expr="." position="inside">
<script type="text/javascript" src="/crm/static/tests/tours/crm_rainbowman.js"></script>
</xpath>
</template>
<template id="qunit_suite" name="crm tests" inherit_id="web.qunit_suite_tests">
<xpath expr="." position="inside">
<script type="text/javascript" src="/crm/static/tests/mock_server.js"></script>
<script type="text/javascript" src="/crm/static/tests/crm_rainbowman_tests.js"></script>
</xpath>
</template>
</odoo>
+2 -2
View File
@@ -4,7 +4,7 @@
<field name="name">crm.lead.form</field>
<field name="model">crm.lead</field>
<field name="arch" type="xml">
<form class="o_lead_opportunity_form">
<form class="o_lead_opportunity_form" js_class="crm_form">
<header>
<button name="action_set_won_rainbowman" string="Mark Won"
type="object" class="oe_highlight"
@@ -355,7 +355,7 @@
<field name="model">crm.lead</field>
<field name="priority" eval="100"/>
<field name="arch" type="xml">
<kanban class="o_kanban_mobile" archivable="false" sample="1">
<kanban class="o_kanban_mobile" archivable="false" js_class="crm_kanban" sample="1">
<field name="name"/>
<field name="contact_name"/>
<field name="priority"/>
@@ -4,7 +4,7 @@ odoo.define('crm.leads.tree', function (require) {
var ListView = require('web.ListView');
var KanbanController = require('web.KanbanController');
var KanbanView = require('web.KanbanView');
var KanbanView = require('crm.crm_kanban').CrmKanbanView;
var viewRegistry = require('web.view_registry');