diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/crm/static | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/crm/static')
| -rw-r--r-- | addons/crm/static/description/icon.png | bin | 0 -> 10122 bytes | |||
| -rw-r--r-- | addons/crm/static/description/icon.svg | 24 | ||||
| -rw-r--r-- | addons/crm/static/src/img/autofill.gif | bin | 0 -> 15971 bytes | |||
| -rw-r--r-- | addons/crm/static/src/img/generate-leads.gif | bin | 0 -> 19967 bytes | |||
| -rw-r--r-- | addons/crm/static/src/img/mapview-toggle.gif | bin | 0 -> 35546 bytes | |||
| -rw-r--r-- | addons/crm/static/src/img/pipeline-progress.gif | bin | 0 -> 42184 bytes | |||
| -rw-r--r-- | addons/crm/static/src/img/probability-rate.gif | bin | 0 -> 33059 bytes | |||
| -rw-r--r-- | addons/crm/static/src/js/crm_form.js | 91 | ||||
| -rw-r--r-- | addons/crm/static/src/js/crm_kanban.js | 52 | ||||
| -rw-r--r-- | addons/crm/static/src/js/systray_activity_menu.js | 57 | ||||
| -rw-r--r-- | addons/crm/static/src/js/tours/crm.js | 94 | ||||
| -rw-r--r-- | addons/crm/static/tests/crm_rainbowman_tests.js | 384 | ||||
| -rw-r--r-- | addons/crm/static/tests/mock_server.js | 56 | ||||
| -rw-r--r-- | addons/crm/static/tests/tours/crm_rainbowman.js | 77 | ||||
| -rw-r--r-- | addons/crm/static/xls/crm_lead.xls | bin | 0 -> 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 Binary files differnew file mode 100644 index 00000000..65ebc4f2 --- /dev/null +++ b/addons/crm/static/description/icon.png 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 Binary files differnew file mode 100644 index 00000000..d6e6ad6d --- /dev/null +++ b/addons/crm/static/src/img/autofill.gif diff --git a/addons/crm/static/src/img/generate-leads.gif b/addons/crm/static/src/img/generate-leads.gif Binary files differnew file mode 100644 index 00000000..1fc56ceb --- /dev/null +++ b/addons/crm/static/src/img/generate-leads.gif diff --git a/addons/crm/static/src/img/mapview-toggle.gif b/addons/crm/static/src/img/mapview-toggle.gif Binary files differnew file mode 100644 index 00000000..b611d456 --- /dev/null +++ b/addons/crm/static/src/img/mapview-toggle.gif diff --git a/addons/crm/static/src/img/pipeline-progress.gif b/addons/crm/static/src/img/pipeline-progress.gif Binary files differnew file mode 100644 index 00000000..f97de03f --- /dev/null +++ b/addons/crm/static/src/img/pipeline-progress.gif diff --git a/addons/crm/static/src/img/probability-rate.gif b/addons/crm/static/src/img/probability-rate.gif Binary files differnew file mode 100644 index 00000000..57517833 --- /dev/null +++ b/addons/crm/static/src/img/probability-rate.gif 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 & 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 Binary files differnew file mode 100644 index 00000000..a8db450d --- /dev/null +++ b/addons/crm/static/xls/crm_lead.xls |
