summaryrefslogtreecommitdiff
path: root/addons/crm/static
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/crm/static
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/crm/static')
-rw-r--r--addons/crm/static/description/icon.pngbin0 -> 10122 bytes
-rw-r--r--addons/crm/static/description/icon.svg24
-rw-r--r--addons/crm/static/src/img/autofill.gifbin0 -> 15971 bytes
-rw-r--r--addons/crm/static/src/img/generate-leads.gifbin0 -> 19967 bytes
-rw-r--r--addons/crm/static/src/img/mapview-toggle.gifbin0 -> 35546 bytes
-rw-r--r--addons/crm/static/src/img/pipeline-progress.gifbin0 -> 42184 bytes
-rw-r--r--addons/crm/static/src/img/probability-rate.gifbin0 -> 33059 bytes
-rw-r--r--addons/crm/static/src/js/crm_form.js91
-rw-r--r--addons/crm/static/src/js/crm_kanban.js52
-rw-r--r--addons/crm/static/src/js/systray_activity_menu.js57
-rw-r--r--addons/crm/static/src/js/tours/crm.js94
-rw-r--r--addons/crm/static/tests/crm_rainbowman_tests.js384
-rw-r--r--addons/crm/static/tests/mock_server.js56
-rw-r--r--addons/crm/static/tests/tours/crm_rainbowman.js77
-rw-r--r--addons/crm/static/xls/crm_lead.xlsbin0 -> 33792 bytes
15 files changed, 835 insertions, 0 deletions
diff --git a/addons/crm/static/description/icon.png b/addons/crm/static/description/icon.png
new file mode 100644
index 00000000..65ebc4f2
--- /dev/null
+++ b/addons/crm/static/description/icon.png
Binary files differ
diff --git a/addons/crm/static/description/icon.svg b/addons/crm/static/description/icon.svg
new file mode 100644
index 00000000..4dc59c77
--- /dev/null
+++ b/addons/crm/static/description/icon.svg
@@ -0,0 +1,24 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="70" height="70" viewBox="0 0 70 70">
+ <defs>
+ <path id="icon-a" d="M4 0h61c4 0 5 1 5 5v60c0 4-1 5-5 5H4c-3 0-4-1-4-5V5c0-4 1-5 4-5z"/>
+ <linearGradient id="icon-c" x1="98.162%" x2="0%" y1="1.838%" y2="100%">
+ <stop offset="0%" stop-color="#797DA5"/>
+ <stop offset="50.799%" stop-color="#6D7194"/>
+ <stop offset="100%" stop-color="#626584"/>
+ </linearGradient>
+ <path id="icon-d" d="M56.583 23.333h-3.5c-.518 0-.983.226-1.304.584h-1.715l-2.27-2.647-.012-.013a7.581 7.581 0 0 0-5.707-2.59h-3.394c-1.294 0-2.545.36-3.623 1.021a8.19 8.19 0 0 0-3.95-1.021h-2.342c-2.107 0-4.2.818-5.775 2.391l-2.857 2.859H18.22a1.745 1.745 0 0 0-1.304-.584h-3.5a1.75 1.75 0 0 0-1.75 1.75v17.5c0 .967.783 1.75 1.75 1.75h3.5a1.75 1.75 0 0 0 1.65-1.166h1.37l5.495 4.927c1.862 1.928 4.37 3.24 7.042 3.24 1.195 0 2.354-.281 3.362-.798 1.818.037 3.726-.756 5.036-2.29a7.109 7.109 0 0 0 3.698-2.523c1.53-.32 2.97-1.202 3.896-2.556h2.967a1.75 1.75 0 0 0 1.65 1.166h3.5a1.75 1.75 0 0 0 1.75-1.75v-17.5a1.75 1.75 0 0 0-1.75-1.75zM15.167 42a1.167 1.167 0 1 1 0-2.333 1.167 1.167 0 0 1 0 2.333zm30.08-.42c-1.12 1.042-2.69.826-2.914.583.103.976-1.331 2.993-3.579 2.835-.404 1.351-2.057 2.467-3.754 1.878-.648.648-1.638.957-2.526.957-1.82 0-3.483-1.06-4.604-2.254l-5.928-5.316a2.332 2.332 0 0 0-1.557-.596h-1.718v-12.25h1.95c.619 0 1.212-.246 1.65-.684l3.2-3.2a4.667 4.667 0 0 1 3.3-1.366h2.34c.424 0 .84.057 1.24.167l-3.155 3.682a5.376 5.376 0 0 0-.048 6.96c2.362 2.833 6.663 2.86 9.077.143l1.894-2.193 5.282 6.99c.98 1.065.799 2.781-.15 3.664zm6.086-1.913H49.55a5.985 5.985 0 0 0-1.441-3.967l-5.693-7.534a1.752 1.752 0 0 0-2.907-1.894l-3.911 4.53a2.49 2.49 0 0 1-3.765-.068 1.885 1.885 0 0 1 .016-2.44l4.224-4.928a3.434 3.434 0 0 1 2.608-1.2h3.394c1.175 0 2.293.507 3.068 1.389l3.312 3.862h2.878v12.25zm3.5 2.333a1.167 1.167 0 1 1 0-2.333 1.167 1.167 0 0 1 0 2.333z"/>
+ </defs>
+ <g fill="none" fill-rule="evenodd">
+ <mask id="icon-b" fill="#fff">
+ <use xlink:href="#icon-a"/>
+ </mask>
+ <g mask="url(#icon-b)">
+ <path fill="url(#icon-c)" d="M0 0H70V70H0z"/>
+ <path fill="#FFF" fill-opacity=".383" d="M4 1h61c2.667 0 4.333.667 5 2V0H0v3c.667-1.333 2-2 4-2z"/>
+ <path fill="#000" d="M33.423 69H4.006C2.003 69 0 68.854 0 64.911V38.065l12.348-13.998 8.686.293L29.375 21h14.023l6.01 4.089L58 24.36v19.558L33.423 69z" opacity=".165"/>
+ <path fill="#000" fill-opacity=".383" d="M4 69h61c2.667 0 4.333-1 5-3v4H0v-4c.667 2 2 3 4 3z"/>
+ <path fill="#000" fill-rule="nonzero" d="M56.583 25.333h-3.5c-.518 0-.983.226-1.304.584h-1.715l-2.27-2.647-.012-.013a7.581 7.581 0 0 0-5.707-2.59h-3.394c-1.294 0-2.545.36-3.623 1.021a8.19 8.19 0 0 0-3.95-1.021h-2.342c-2.107 0-4.2.818-5.775 2.391l-2.857 2.859H18.22a1.745 1.745 0 0 0-1.304-.584h-3.5a1.75 1.75 0 0 0-1.75 1.75v17.5c0 .967.783 1.75 1.75 1.75h3.5a1.75 1.75 0 0 0 1.65-1.166h1.37l5.495 4.927c1.862 1.928 4.37 3.24 7.042 3.24 1.195 0 2.354-.281 3.362-.798 1.818.037 3.726-.756 5.036-2.29a7.109 7.109 0 0 0 3.698-2.523c1.53-.32 2.97-1.202 3.896-2.556h2.967a1.75 1.75 0 0 0 1.65 1.166h3.5a1.75 1.75 0 0 0 1.75-1.75v-17.5a1.75 1.75 0 0 0-1.75-1.75zM15.167 44a1.167 1.167 0 1 1 0-2.333 1.167 1.167 0 0 1 0 2.333zm30.08-.42c-1.12 1.042-2.69.826-2.914.583.103.976-1.331 2.993-3.579 2.835-.404 1.351-2.057 2.467-3.754 1.878-.648.648-1.638.957-2.526.957-1.82 0-3.483-1.06-4.604-2.254l-5.928-5.316a2.332 2.332 0 0 0-1.557-.596h-1.718v-12.25h1.95c.619 0 1.212-.246 1.65-.684l3.2-3.2a4.667 4.667 0 0 1 3.3-1.366h2.34c.424 0 .84.057 1.24.167l-3.155 3.682a5.376 5.376 0 0 0-.048 6.96c2.362 2.833 6.663 2.86 9.077.143l1.894-2.193 5.282 6.99c.98 1.065.799 2.781-.15 3.664zm6.086-1.913H49.55a5.985 5.985 0 0 0-1.441-3.967l-5.693-7.534a1.752 1.752 0 0 0-2.907-1.894l-3.911 4.53a2.49 2.49 0 0 1-3.765-.068 1.885 1.885 0 0 1 .016-2.44l4.224-4.928a3.434 3.434 0 0 1 2.608-1.2h3.394c1.175 0 2.293.507 3.068 1.389l3.312 3.862h2.878v12.25zm3.5 2.333a1.167 1.167 0 1 1 0-2.333 1.167 1.167 0 0 1 0 2.333z" opacity=".372"/>
+ <use fill="#FFF" fill-rule="nonzero" xlink:href="#icon-d"/>
+ </g>
+ </g>
+</svg>
diff --git a/addons/crm/static/src/img/autofill.gif b/addons/crm/static/src/img/autofill.gif
new file mode 100644
index 00000000..d6e6ad6d
--- /dev/null
+++ b/addons/crm/static/src/img/autofill.gif
Binary files differ
diff --git a/addons/crm/static/src/img/generate-leads.gif b/addons/crm/static/src/img/generate-leads.gif
new file mode 100644
index 00000000..1fc56ceb
--- /dev/null
+++ b/addons/crm/static/src/img/generate-leads.gif
Binary files differ
diff --git a/addons/crm/static/src/img/mapview-toggle.gif b/addons/crm/static/src/img/mapview-toggle.gif
new file mode 100644
index 00000000..b611d456
--- /dev/null
+++ b/addons/crm/static/src/img/mapview-toggle.gif
Binary files differ
diff --git a/addons/crm/static/src/img/pipeline-progress.gif b/addons/crm/static/src/img/pipeline-progress.gif
new file mode 100644
index 00000000..f97de03f
--- /dev/null
+++ b/addons/crm/static/src/img/pipeline-progress.gif
Binary files differ
diff --git a/addons/crm/static/src/img/probability-rate.gif b/addons/crm/static/src/img/probability-rate.gif
new file mode 100644
index 00000000..57517833
--- /dev/null
+++ b/addons/crm/static/src/img/probability-rate.gif
Binary files differ
diff --git a/addons/crm/static/src/js/crm_form.js b/addons/crm/static/src/js/crm_form.js
new file mode 100644
index 00000000..40845647
--- /dev/null
+++ b/addons/crm/static/src/js/crm_form.js
@@ -0,0 +1,91 @@
+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({
+ /**
+ * Main method used when saving the record hitting the "Save" button.
+ * We check if the stage_id field was altered and if we need to display a rainbowman
+ * message.
+ *
+ * @override
+ */
+ saveRecord: function () {
+ return this._super(...arguments).then((modifiedFields) => {
+ if (modifiedFields.indexOf('stage_id') !== -1) {
+ this._checkRainbowmanMessage(this.renderer.state.res_id)
+ }
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Apply change may be called with 'event.data.force_save' set to True.
+ * This typically happens when directly clicking in the statusbar widget on a new stage.
+ * If it's the case, we check for a modified stage_id field and if we need to display a
+ * rainbowman message.
+ *
+ * @param {string} dataPointID
+ * @param {Object} changes
+ * @param {OdooEvent} event
+ * @override
+ * @private
+ */
+ _applyChanges: function (dataPointID, changes, event) {
+ return this._super(...arguments).then(() => {
+ if (event.data.force_save && 'stage_id' in changes) {
+ this._checkRainbowmanMessage(parseInt(event.target.res_id));
+ }
+ });
+ },
+
+ /**
+ * When updating a crm.lead, through direct use of the status bar or when saving the
+ * record, we check for a rainbowman message to display.
+ *
+ * (see Widget docstring for more information).
+ *
+ * @param {integer} recordId
+ */
+ _checkRainbowmanMessage: async function(recordId) {
+ const message = await this._rpc({
+ model: 'crm.lead',
+ method : 'get_rainbowman_message',
+ args: [[recordId]],
+ });
+ if (message) {
+ this.trigger_up('show_effect', {
+ message: message,
+ type: 'rainbow_man',
+ });
+ }
+ }
+ });
+
+ var CrmFormView = FormView.extend({
+ config: _.extend({}, FormView.prototype.config, {
+ Controller: CrmFormController,
+ }),
+ });
+
+ viewRegistry.add('crm_form', CrmFormView);
+
+ return {
+ CrmFormController: CrmFormController,
+ CrmFormView: CrmFormView,
+ };
+});
diff --git a/addons/crm/static/src/js/crm_kanban.js b/addons/crm/static/src/js/crm_kanban.js
new file mode 100644
index 00000000..dbc50064
--- /dev/null
+++ b/addons/crm/static/src/js/crm_kanban.js
@@ -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,
+ };
+
+});
diff --git a/addons/crm/static/src/js/systray_activity_menu.js b/addons/crm/static/src/js/systray_activity_menu.js
new file mode 100644
index 00000000..8a1f71b0
--- /dev/null
+++ b/addons/crm/static/src/js/systray_activity_menu.js
@@ -0,0 +1,57 @@
+odoo.define('crm.systray.ActivityMenu', function (require) {
+"use strict";
+
+var ActivityMenu = require('mail.systray.ActivityMenu');
+
+ActivityMenu.include({
+
+ //--------------------------------------------------
+ // Private
+ //--------------------------------------------------
+
+ /**
+ * @override
+ */
+ _getViewsList(model) {
+ if (model === "crm.lead") {
+ return [[false, 'list'], [false, 'kanban'],
+ [false, 'form'], [false, 'calendar'],
+ [false, 'pivot'], [false, 'graph'],
+ [false, 'activity']
+ ];
+ }
+ return this._super(...arguments);
+ },
+
+ //-----------------------------------------
+ // Handlers
+ //-----------------------------------------
+
+ /**
+ * @private
+ * @override
+ */
+ _onActivityFilterClick: function (event) {
+ // fetch the data from the button otherwise fetch the ones from the parent (.o_mail_preview).
+ var data = _.extend({}, $(event.currentTarget).data(), $(event.target).data());
+ var context = {};
+ if (data.res_model === "crm.lead") {
+ if (data.filter === 'my') {
+ context['search_default_activities_overdue'] = 1;
+ context['search_default_activities_today'] = 1;
+ } else {
+ context['search_default_activities_' + data.filter] = 1;
+ }
+ // Necessary because activity_ids of mail.activity.mixin has auto_join
+ // So, duplicates are faking the count and "Load more" doesn't show up
+ context['force_search_count'] = 1;
+ this.do_action('crm.crm_lead_action_my_activities', {
+ additional_context: context,
+ clear_breadcrumbs: true,
+ });
+ } else {
+ this._super.apply(this, arguments);
+ }
+ },
+});
+});
diff --git a/addons/crm/static/src/js/tours/crm.js b/addons/crm/static/src/js/tours/crm.js
new file mode 100644
index 00000000..8c2e1350
--- /dev/null
+++ b/addons/crm/static/src/js/tours/crm.js
@@ -0,0 +1,94 @@
+odoo.define('crm.tour', function(require) {
+"use strict";
+
+var core = require('web.core');
+var tour = require('web_tour.tour');
+
+var _t = core._t;
+
+tour.register('crm_tour', {
+ url: "/web",
+ rainbowManMessage: _t("Congrats, best of luck catching such big fish! :)"),
+ sequence: 10,
+}, [tour.stepUtils.showAppsMenuItem(), {
+ trigger: '.o_app[data-menu-xmlid="crm.crm_menu_root"]',
+ content: _t('Ready to boost your sales? Let\'s have a look at your <b>Pipeline</b>.'),
+ position: 'bottom',
+ edition: 'community',
+}, {
+ trigger: '.o_app[data-menu-xmlid="crm.crm_menu_root"]',
+ content: _t('Ready to boost your sales? Let\'s have a look at your <b>Pipeline</b>.'),
+ position: 'bottom',
+ edition: 'enterprise',
+}, {
+ trigger: '.o-kanban-button-new',
+ extra_trigger: '.o_opportunity_kanban',
+ content: _t("<b>Create your first opportunity.</b>"),
+ position: 'bottom',
+}, {
+ trigger: ".o_kanban_quick_create .o_field_widget[name='partner_id']",
+ content: _t('<b>Write a few letters</b> to look for a company, or create a new one.'),
+ position: "top",
+ run: function (actions) {
+ actions.text("Brandon Freeman", this.$anchor.find("input"));
+ },
+}, {
+ trigger: ".ui-menu-item > a",
+ auto: true,
+ in_modal: false,
+}, {
+ trigger: ".o_kanban_quick_create .o_kanban_add",
+ content: _t("Now, <b>add your Opportunity</b> to your Pipeline."),
+ position: "bottom",
+}, {
+ trigger: ".o_opportunity_kanban .o_kanban_group:first-child .o_kanban_record:last-child .oe_kanban_content",
+ extra_trigger: ".o_opportunity_kanban",
+ content: _t("<b>Drag &amp; drop opportunities</b> between columns as you progress in your sales cycle."),
+ position: "right",
+ run: "drag_and_drop .o_opportunity_kanban .o_kanban_group:eq(2) ",
+}, {
+ trigger: ".o_kanban_record:not(.o_updating) .o_activity_color_default",
+ extra_trigger: ".o_opportunity_kanban",
+ content: _t("Looks like nothing is planned. :(<br><br><i>Tip : Schedule activities to keep track of everything you have to do!</i>"),
+ position: "bottom",
+}, {
+ trigger: ".o_schedule_activity",
+ extra_trigger: ".o_opportunity_kanban",
+ content: _t("Let's <b>Schedule an Activity.</b>"),
+ position: "bottom",
+ width: 200,
+}, {
+ trigger: '.modal-footer button[name="action_close_dialog"]',
+ content: _t("All set. Let’s <b>Schedule</b> it."),
+ position: "top", // dot NOT move to bottom, it would cause a resize flicker, see task-2476595
+ run: function (actions) {
+ actions.auto('.modal-footer button[special=cancel]');
+ },
+}, {
+ id: "drag_opportunity_to_won_step",
+ trigger: ".o_opportunity_kanban .o_kanban_record:last-child",
+ content: _t("Drag your opportunity to <b>Won</b> when you get the deal. Congrats !"),
+ position: "bottom",
+ run: "drag_and_drop .o_opportunity_kanban .o_kanban_group:eq(3) ",
+}, {
+ trigger: ".o_kanban_record",
+ extra_trigger: ".o_opportunity_kanban",
+ content: _t("Let’s have a look at an Opportunity."),
+ position: "right",
+ run: function (actions) {
+ actions.auto(".o_kanban_record");
+ },
+}, {
+ trigger: ".o_lead_opportunity_form .o_statusbar_status",
+ content: _t("This bar also allows you to switch stage."),
+ position: "bottom"
+}, {
+ trigger: ".breadcrumb-item:not(.active):first",
+ content: _t("Click on the breadcrumb to go back to the Pipeline."),
+ position: "bottom",
+ run: function (actions) {
+ actions.auto(".breadcrumb-item:not(.active):last");
+ }
+}]);
+
+});
diff --git a/addons/crm/static/tests/crm_rainbowman_tests.js b/addons/crm/static/tests/crm_rainbowman_tests.js
new file mode 100644
index 00000000..3a0158d1
--- /dev/null
+++ b/addons/crm/static/tests/crm_rainbowman_tests.js
@@ -0,0 +1,384 @@
+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("first lead won, click on statusbar in edit mode then save", async function (assert) {
+ assert.expect(3);
+
+ const form = await createView(_.extend(this.testFormView, {
+ res_id: 6,
+ 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;
+ },
+ viewOptions: {mode: 'edit'}
+ }));
+
+ await testUtils.dom.click(form.$(".o_statusbar_status button[data-value='3']"));
+ assert.verifySteps([]); // no message displayed yet
+
+ await testUtils.form.clickSave(form);
+ 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();
+ });
+ });
+});
diff --git a/addons/crm/static/tests/mock_server.js b/addons/crm/static/tests/mock_server.js
new file mode 100644
index 00000000..0c6c0fbb
--- /dev/null
+++ b/addons/crm/static/tests/mock_server.js
@@ -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);
+ },
+ });
+});
diff --git a/addons/crm/static/tests/tours/crm_rainbowman.js b/addons/crm/static/tests/tours/crm_rainbowman.js
new file mode 100644
index 00000000..3cfed895
--- /dev/null
+++ b/addons/crm/static/tests/tours/crm_rainbowman.js
@@ -0,0 +1,77 @@
+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=expected_revenue] > input",
+ content: "complete expected 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_reward_rainbow",
+ extra_trigger: ".o_reward_rainbow",
+ run: function () {} // check rainbowman is properly displayed
+ }, {
+ trigger: ".o-kanban-button-new",
+ content: "create second lead",
+ }, {
+ trigger: "input[name=name]",
+ content: "complete name",
+ run: "text Test Lead 2",
+ }, {
+ trigger: "div[name=expected_revenue] > input",
+ content: "complete expected revenue",
+ run: "text 999999998",
+ }, {
+ trigger: "button.o_kanban_add",
+ content: "create lead",
+ }, {
+ trigger: ".o_kanban_record .o_kanban_record_title:contains('Test Lead 2')",
+ run: function () {} // wait for the record to be properly created
+ }, {
+ // move first test back to new stage to be able to test rainbowman a second time
+ trigger: ".o_kanban_record .o_kanban_record_title:contains('Test Lead 1')",
+ content: "move back to new stage",
+ run: "drag_and_drop .o_opportunity_kanban .o_kanban_group:eq(0) "
+ }, {
+ trigger: ".o_kanban_record .o_kanban_record_title:contains('Test Lead 2')",
+ content: "click on second 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",
+ }
+ ]);
+});
diff --git a/addons/crm/static/xls/crm_lead.xls b/addons/crm/static/xls/crm_lead.xls
new file mode 100644
index 00000000..a8db450d
--- /dev/null
+++ b/addons/crm/static/xls/crm_lead.xls
Binary files differ