From 2a48b989c109c652d2bb301f33eeb4b2886388fc Mon Sep 17 00:00:00 2001 From: abd-msyukyu-odoo Date: Fri, 7 May 2021 08:37:09 +0000 Subject: [PATCH] [IMP] crm: implement the forecast feature for opportunities Add a new submenu "Forecast" in "Reporting" for CRM The Forecast shows 4 views: 1. Kanban 2. Graph 3. Pivot 4. List Objective Shows the short term forecast for opportunities (crm_lead) based on their expected closing (date_deadline) and have an overview of the prorated revenues for each period (defaults to month). Implementation 1. Kanban - the progressbar is using the prorated_revenue - the forecast_field is date_deadline - the default groupby is date_deadline 2.3. Graph, Pivot - the measure defaults to the prorated_revenue - the row defaults to date_deadline 4. List - shows the prorated_revenue The special filter "Forecast" is defined to be used by the forecastModelExtension (JS). (define the forecast_filter context key) The forecast_field context key is defined in the action to be used by the forecastModelExtension and the forecastService (JS). The demo data for opportunities is updated to spread date_deadline over 4 months Enable drag&drop and quickCreate features for the kanban view with the use of the allow_group_range_value xml attribute Add a "Won" flag for the forecast view to better distinguish opportunities that need to be worked on Test tour Task-ID: 2243913 PR odoo/odoo#69380 ENT PR odoo/enterprise#18547 --- addons/crm/__manifest__.py | 1 + addons/crm/data/crm_lead_demo.xml | 22 ++- addons/crm/data/ir_action_data.xml | 12 ++ addons/crm/models/crm_team.py | 9 + .../crm/report/crm_activity_report_views.xml | 2 +- addons/crm/static/src/scss/crm.scss | 2 +- .../static/tests/tours/crm_forecast_tour.js | 98 ++++++++++ addons/crm/tests/test_crm_ui.py | 2 + addons/crm/views/crm_lead_views.xml | 182 +++++++++++++++++- addons/crm/views/crm_menu_views.xml | 9 +- 10 files changed, 324 insertions(+), 15 deletions(-) create mode 100644 addons/crm/data/ir_action_data.xml create mode 100644 addons/crm/static/tests/tours/crm_forecast_tour.js diff --git a/addons/crm/__manifest__.py b/addons/crm/__manifest__.py index 9282c7991f0..ea636571ffa 100644 --- a/addons/crm/__manifest__.py +++ b/addons/crm/__manifest__.py @@ -33,6 +33,7 @@ 'data/crm_stage_data.xml', 'data/crm_team_data.xml', 'data/digest_data.xml', + 'data/ir_action_data.xml', 'data/ir_cron_data.xml', 'data/mail_data.xml', 'data/crm_recurring_plan_data.xml', diff --git a/addons/crm/data/crm_lead_demo.xml b/addons/crm/data/crm_lead_demo.xml index b831a9dda15..006d2f9cdd8 100644 --- a/addons/crm/data/crm_lead_demo.xml +++ b/addons/crm/data/crm_lead_demo.xml @@ -353,7 +353,7 @@ Andrew

]]>
Kensington Road 189 1 - + @@ -379,7 +379,7 @@ Andrew

]]>
+32 10 588 558 2 - + @@ -402,7 +402,7 @@ Andrew

]]>
+32 10 588 558 1 - + @@ -495,7 +495,7 @@ Andrew

]]>
Union Road 2 - + @@ -520,7 +520,7 @@ Andrew

]]>
Rue des Palais 51, bte 33 1 - + @@ -553,7 +553,7 @@ Andrew

]]>
1 - + @@ -628,6 +628,7 @@ Andrew

]]>
+1 312 349 2324 2 + @@ -684,6 +685,7 @@ Andrew

]]>
0 + @@ -702,6 +704,7 @@ Andrew

]]>
Lima 0 + @@ -750,7 +753,6 @@ Andrew

]]>
1300 2 - @@ -774,7 +776,7 @@ Andrew

]]>
Rue de Namur 69 2 - + @@ -865,7 +867,7 @@ Andrew

]]>
1920 Del Dew Drive 1 - + @@ -887,7 +889,7 @@ Andrew

]]>
1920 Del Dew Drive 1 - + diff --git a/addons/crm/data/ir_action_data.xml b/addons/crm/data/ir_action_data.xml new file mode 100644 index 00000000000..afd5fa06514 --- /dev/null +++ b/addons/crm/data/ir_action_data.xml @@ -0,0 +1,12 @@ + + + + + Crm: Forecast + + code + + action = model.action_opportunity_forecast() + + + diff --git a/addons/crm/models/crm_team.py b/addons/crm/models/crm_team.py index 3c18597b34c..521c2630721 100644 --- a/addons/crm/models/crm_team.py +++ b/addons/crm/models/crm_team.py @@ -587,6 +587,15 @@ class Team(models.Model): @api.model def action_your_pipeline(self): action = self.env["ir.actions.actions"]._for_xml_id("crm.crm_lead_action_pipeline") + return self._action_update_to_pipeline(action) + + @api.model + def action_opportunity_forecast(self): + action = self.env['ir.actions.actions']._for_xml_id('crm.crm_lead_action_forecast') + return self._action_update_to_pipeline(action) + + @api.model + def _action_update_to_pipeline(self, action): user_team_id = self.env.user.sale_team_id.id if user_team_id: # To ensure that the team is readable in multi company diff --git a/addons/crm/report/crm_activity_report_views.xml b/addons/crm/report/crm_activity_report_views.xml index 6b9f1efe030..b76e34d3661 100644 --- a/addons/crm/report/crm_activity_report_views.xml +++ b/addons/crm/report/crm_activity_report_views.xml @@ -104,7 +104,7 @@ name="Activities" parent="crm_menu_report" action="crm_activity_report_action" - sequence="3"/> + sequence="4"/> Pipeline Activities diff --git a/addons/crm/static/src/scss/crm.scss b/addons/crm/static/src/scss/crm.scss index f3970e4ba69..e731507557b 100644 --- a/addons/crm/static/src/scss/crm.scss +++ b/addons/crm/static/src/scss/crm.scss @@ -21,7 +21,7 @@ } } - .oe_kanban_card_lost_ribbon { + .oe_kanban_card_ribbon { min-height: 105px; .o_kanban_record_title { max-width: calc(100% - 65px); diff --git a/addons/crm/static/tests/tours/crm_forecast_tour.js b/addons/crm/static/tests/tours/crm_forecast_tour.js new file mode 100644 index 00000000000..df08fea7171 --- /dev/null +++ b/addons/crm/static/tests/tours/crm_forecast_tour.js @@ -0,0 +1,98 @@ +/** @odoo-module */ +import tour from 'web_tour.tour'; +const today = moment(); + +tour.register('crm_forecast', { + test: true, + url: "/web", +}, [ + tour.stepUtils.showAppsMenuItem(), + { + trigger: ".o_app[data-menu-xmlid='crm.crm_menu_root']", + content: "open crm app", + }, { + trigger: '.o_dropdown_toggler[data-menu-xmlid="crm.crm_menu_report"]', + content: 'Open Reporting menu', + run: 'click', + }, { + trigger: '.o_dropdown_item[data-menu-xmlid="crm.crm_menu_forecast"]', + content: 'Open Forecast menu', + run: 'click', + }, { + trigger: '.o_column_quick_create:contains(Add next month)', + content: 'Wait page loading' + }, { + trigger: ".o-kanban-button-new", + content: "click create", + run: 'click', + }, { + trigger: "input[name=name]", + content: "complete name", + run: "text Test Opportunity 1", + }, { + trigger: "div[name=expected_revenue] > input", + content: "complete expected revenue", + run: "text 999999", + }, { + trigger: "button.o_kanban_edit", + content: "edit lead", + }, { + trigger: "input[name=date_deadline]", + content: "complete expected closing", + run: `text ${today.format("MM/DD/YYYY")}`, + }, { + trigger: "input[name=date_deadline]", + content: "click to make the datepicker disappear", + run: "click" + }, { + trigger: "body:not(:has(div.bootstrap-datetimepicker-widget))", + content: "wait for date_picker to disappear", + run: function () {}, + }, { + trigger: '.o_back_button', + content: 'navigate back to the kanban view', + position: "bottom", + run: "click" + }, { + trigger: ".o_kanban_record .o_kanban_record_title:contains('Test Opportunity 1')", + content: "move to the next month", + run: function (actions) { + const undefined_groups = $('.o_column_title:contains("Undefined")').length; + actions.drag_and_drop(` .o_opportunity_kanban .o_kanban_group:eq(${1 + undefined_groups})`, this.$anchor); + } + }, { + trigger: ".o_kanban_record .o_kanban_record_title:contains('Test Opportunity 1')", + content: "edit lead", + run: "click" + }, { + trigger: `span[name=date_deadline]:contains("${moment(today).add(2, 'months').startOf('month').subtract(1, 'days').format("MM/DD/YYYY")}")`, + content: "edit datetime", + position: "bottom", + run: "click" + }, { + trigger: "input[name=date_deadline]", + content: "complete expected closing", + run: `text ${moment(today).add(5, 'months').startOf('month').subtract(1, 'days').format("MM/DD/YYYY")}` + }, { + trigger: "body:not(:has(div.bootstrap-datetimepicker-widget))", + content: "wait for date_picker to disappear", + run: function () {}, + }, { + trigger: "input[name=probability]", + content: "max out probability", + run: "text 100" + }, { + trigger: '.o_back_button', + content: 'navigate back to the kanban view', + position: "bottom", + run: "click" + }, { + trigger: '.o_kanban_add_column', + content: "add next month", + run: "click" + }, { + trigger: ".o_kanban_record:contains('Test Opportunity 1'):contains('Won')", + content: "assert that the opportunity has the Won banner", + run: function () {}, + } +]); diff --git a/addons/crm/tests/test_crm_ui.py b/addons/crm/tests/test_crm_ui.py index 04aa368bc1a..4cca0e9fb73 100644 --- a/addons/crm/tests/test_crm_ui.py +++ b/addons/crm/tests/test_crm_ui.py @@ -25,6 +25,8 @@ class TestUi(HttpCase): }) self.start_tour("/web", 'crm_rainbowman', login="temp_crm_user") + def test_03_crm_tour_forecast(self): + self.start_tour("/web", 'crm_forecast', login="admin") class TestCRMLeadMisc(TestCrmCommon): diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml index 19c39c3fa52..5d5913539a0 100644 --- a/addons/crm/views/crm_lead_views.xml +++ b/addons/crm/views/crm_lead_views.xml @@ -535,7 +535,7 @@ -
+
Lost @@ -591,6 +591,59 @@ + + crm.lead.view.kanban.forecast + crm.lead + + primary + 32 + + + date_deadline + forecast_kanban + + + + + + + + + prorated_revenue + + + + + + + #{!selection_mode ? kanban_color(record.color.raw_value) : ''} + #{lost_ribbon || won_ribbon ? 'oe_kanban_card_ribbon' : ''} + oe_kanban_global_click oe_kanban_card d-flex flex-column + + + +
+ Won + Lost +
+
+ +
+ + + + + + + + + +
+
+
+
+ crm.lead.search.lead crm.lead @@ -738,6 +791,25 @@ + + crm.lead.view.tree.forecast + crm.lead + + primary + 32 + + + forecast_list + + + hide + + + + + + + crm.lead.list.activities crm.lead @@ -797,6 +869,24 @@ + + crm.lead.view.graph.forecast + crm.lead + 32 + + + + + + + + + + + + + + crm.lead.view.pivot crm.lead @@ -811,6 +901,25 @@ + + crm.lead.view.pivot.forecast + crm.lead + 32 + + + + + + + + + + + + + + + crm.lead.search.opportunity crm.lead @@ -887,6 +996,24 @@ + + crm.lead.view.search.forecast + crm.lead + + primary + 32 + + + + + + + + + + + + @@ -1095,9 +1222,62 @@ if record: + + Forecast + crm.lead + kanban,graph,pivot,tree,form + [('type', '=', 'opportunity')] + { + 'default_type': 'opportunity', + 'search_default_assigned_to_me': 1, + 'search_default_forecast': 1, + 'search_default_date_deadline': 1, + 'forecast_field': 'date_deadline' + } + +

+ No opportunity to display! +

+ Easily set expected closing dates and overview your revenue streams. +

+
+ +
+ + + + kanban + + + + + + + graph + + + + + + + pivot + + + + + + + tree + + + + + + + diff --git a/addons/crm/views/crm_menu_views.xml b/addons/crm/views/crm_menu_views.xml index 1f0aca1f5af..14182af8d87 100644 --- a/addons/crm/views/crm_menu_views.xml +++ b/addons/crm/views/crm_menu_views.xml @@ -60,17 +60,22 @@ parent="crm_menu_root" sequence="20" groups="sales_team.group_sale_salesman"/> + + sequence="2"/> + sequence="3"/>