From 55259a6b94cd8af56b203c65428c619a260e30df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 3 Apr 2020 14:33:40 +0000 Subject: [PATCH 01/16] [IMP] sales_team: clean some views xml-id naming GLOBAL PURPOSE Ability to have salesmen belonging to several sales team is a core requirement of CRM. It is therefore moved from website_crm_score to crm along with cleaning and behavior improvement. Automatic lead assignment is also moved and cleaned. SPECIFICATIONS Rename views according to guidelines in order to better understand what they are. As there are few views let us take a few minutes naming them correctly. Also prepare sales team enhancements and ease migration scripts addition by bumping version numbers. LINKS Task ID-2086889 (main task) Task ID-2357969 (scoring migration task) Community PR odoo/odoo#48422 Enterprise PR odoo/enterprise#499 Upgrade PR odoo/upgrade#996 --- addons/crm/__manifest__.py | 1 + addons/crm/views/crm_menu_views.xml | 4 ++-- addons/crm/views/crm_team_views.xml | 8 ++++---- addons/pos_sale/__manifest__.py | 2 +- addons/pos_sale/views/sales_team_views.xml | 6 +++--- addons/sale/__manifest__.py | 3 ++- addons/sale/views/sale_views.xml | 4 ++-- addons/sale/views/sales_team_views.xml | 6 +++--- addons/sale_crm/views/crm_team_views.xml | 5 ----- addons/sales_team/__manifest__.py | 2 +- addons/sales_team/views/crm_team_views.xml | 19 ++++++++----------- addons/website_sale/__manifest__.py | 5 ++++- addons/website_sale/views/crm_team_views.xml | 6 +++--- 13 files changed, 34 insertions(+), 37 deletions(-) diff --git a/addons/crm/__manifest__.py b/addons/crm/__manifest__.py index d74210125f3..c5d31ed7477 100644 --- a/addons/crm/__manifest__.py +++ b/addons/crm/__manifest__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. + { 'name': 'CRM', 'version': '1.4', diff --git a/addons/crm/views/crm_menu_views.xml b/addons/crm/views/crm_menu_views.xml index 9db695e5e06..20fb5503662 100644 --- a/addons/crm/views/crm_menu_views.xml +++ b/addons/crm/views/crm_menu_views.xml @@ -35,7 +35,7 @@ id="sales_team_menu_team_pipeline" name="Teams" parent="crm_menu_sales" - action="sales_team.crm_team_salesteams_pipelines_act" + action="sales_team.crm_team_action_pipeline" groups="sales_team.group_sale_manager" sequence="4"/> - + [('use_opportunities', '=', True)] - - crm.team.kanban + + crm.team.view.kanban.dashboard.inherit.crm crm.team - + diff --git a/addons/pos_sale/__manifest__.py b/addons/pos_sale/__manifest__.py index 45724cb9d56..d4d9bc5a08d 100644 --- a/addons/pos_sale/__manifest__.py +++ b/addons/pos_sale/__manifest__.py @@ -4,7 +4,7 @@ { 'name': 'pos_sale', - 'version': '1.0', + 'version': '1.1', 'category': 'Hidden', 'sequence': 6, 'summary': 'Link module between Point of Sale and Sales', diff --git a/addons/pos_sale/views/sales_team_views.xml b/addons/pos_sale/views/sales_team_views.xml index 4a0b3b34f33..9496346b0a0 100644 --- a/addons/pos_sale/views/sales_team_views.xml +++ b/addons/pos_sale/views/sales_team_views.xml @@ -30,10 +30,10 @@ - - crm.team.kanban + + crm.team.view.kanban.dashboard.inherit.pos crm.team - + diff --git a/addons/sale/__manifest__.py b/addons/sale/__manifest__.py index 8008e5afa26..acc73155900 100644 --- a/addons/sale/__manifest__.py +++ b/addons/sale/__manifest__.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. + { 'name': 'Sales', - 'version': '1.1', + 'version': '1.2', 'category': 'Sales/Sales', 'summary': 'Sales internal machinery', 'description': """ diff --git a/addons/sale/views/sale_views.xml b/addons/sale/views/sale_views.xml index 0c069240fd3..5df852f8124 100644 --- a/addons/sale/views/sale_views.xml +++ b/addons/sale/views/sale_views.xml @@ -17,7 +17,7 @@ name="Sales Teams" parent="sale_order_menu" groups="sales_team.group_sale_manager" - action="sales_team.crm_team_salesteams_act" + action="sales_team.crm_team_action_sales" sequence="3"/> - - crm.team.kanban + + crm.team.view.kanban.dashboard.inherit.sale crm.team - + diff --git a/addons/sale_crm/views/crm_team_views.xml b/addons/sale_crm/views/crm_team_views.xml index c3b4089e275..e207770a99c 100644 --- a/addons/sale_crm/views/crm_team_views.xml +++ b/addons/sale_crm/views/crm_team_views.xml @@ -12,9 +12,4 @@ - - - {'in_sales_app': True} - - diff --git a/addons/sales_team/__manifest__.py b/addons/sales_team/__manifest__.py index cbab6a39496..918c64a71e4 100644 --- a/addons/sales_team/__manifest__.py +++ b/addons/sales_team/__manifest__.py @@ -3,7 +3,7 @@ { 'name': 'Sales Teams', - 'version': '1.0', + 'version': '1.1', 'category': 'Sales/Sales', 'summary': 'Sales Teams', 'description': """ diff --git a/addons/sales_team/views/crm_team_views.xml b/addons/sales_team/views/crm_team_views.xml index ed50186104c..f373dd02882 100644 --- a/addons/sales_team/views/crm_team_views.xml +++ b/addons/sales_team/views/crm_team_views.xml @@ -1,8 +1,8 @@ - - Sales Teams - Search + + crm.team.view.search crm.team @@ -89,7 +89,7 @@ - crm.team.kanban + crm.team.view.kanban crm.team @@ -112,8 +112,8 @@ - - crm.team.dashboard + + crm.team.view.kanban.dashboard crm.team @@ -193,11 +193,10 @@ - + Sales Teams crm.team kanban,form - {'in_sales_app': True}

@@ -209,11 +208,10 @@ - + Teams crm.team kanban,form - {}

@@ -225,12 +223,11 @@ - + Sales Teams crm.team tree,form {} -

Define a new sales team diff --git a/addons/website_sale/__manifest__.py b/addons/website_sale/__manifest__.py index d4cdee530ed..55a229302fb 100644 --- a/addons/website_sale/__manifest__.py +++ b/addons/website_sale/__manifest__.py @@ -1,10 +1,13 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + { 'name': 'eCommerce', 'category': 'Website/Website', 'sequence': 50, 'summary': 'Sell your products online', 'website': 'https://www.odoo.com/page/e-commerce', - 'version': '1.0', + 'version': '1.1', 'description': "", 'depends': ['website', 'sale', 'website_payment', 'website_mail', 'website_form', 'portal_rating', 'digest'], 'data': [ diff --git a/addons/website_sale/views/crm_team_views.xml b/addons/website_sale/views/crm_team_views.xml index f3070f10c92..f6cfa662e8a 100644 --- a/addons/website_sale/views/crm_team_views.xml +++ b/addons/website_sale/views/crm_team_views.xml @@ -1,10 +1,10 @@ - - crm.team.kanban + + crm.team.view.kanban.dashboard.inherit.website.sale crm.team - + From 9790d7dcfbf1e22377f5555b275bf328216ae2ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 26 Mar 2020 11:10:29 +0000 Subject: [PATCH 02/16] [REF][MOV] sales_team, crm: move team membership model to sales_team GLOBAL PURPOSE Ability to have salesmen belonging to several sales team is a core requirement of CRM. It is therefore moved from website_crm_score to crm along with cleaning and behavior improvement. Automatic lead assignment is also moved and cleaned. SPECIFICIATIONS Move sales team membership model from website_crm_score (enterprise) directly into sales_team. It allows to have the base requirements to correctly handle mono and multi sales team for salesperson. Membership is modeled using a decorated many2many relationship. Members of a sales team are res.users. Subscriptions are crm.team.member linking a team and an user. It is a rename of team.user model coming from website_crm_score as well as some other relational fields renaming. Part of old team.user model is moved directly to sales team, part of it to crm module (notably assignment limits). Its support will be improved in the upcoming commits. Assignment related fields will be cleaned and used when moving assignment directly into CRM. LINKS Task ID-2086889 (main task) Task ID-2357969 (scoring migration task) Community PR odoo/odoo#48422 Enterprise PR odoo/enterprise#499 Upgrade PR odoo/upgrade#996996 --- addons/crm/__manifest__.py | 2 + addons/crm/data/crm_team_member_demo.xml | 12 ++ addons/crm/models/__init__.py | 1 + addons/crm/models/crm_lead.py | 14 +- addons/crm/models/crm_team_member.py | 39 +++++ addons/crm/views/crm_menu_views.xml | 7 + addons/crm/views/crm_team_member_views.xml | 63 ++++++++ addons/sales_team/__manifest__.py | 1 + addons/sales_team/data/crm_team_data.xml | 5 +- addons/sales_team/data/crm_team_demo.xml | 20 ++- addons/sales_team/models/__init__.py | 1 + addons/sales_team/models/crm_team.py | 19 ++- addons/sales_team/models/crm_team_member.py | 16 ++ addons/sales_team/models/res_users.py | 12 +- .../sales_team/security/ir.model.access.csv | 3 + .../views/crm_team_member_views.xml | 138 ++++++++++++++++++ addons/sales_team/views/crm_team_views.xml | 28 ++-- 17 files changed, 336 insertions(+), 45 deletions(-) create mode 100644 addons/crm/data/crm_team_member_demo.xml create mode 100644 addons/crm/models/crm_team_member.py create mode 100644 addons/crm/views/crm_team_member_views.xml create mode 100644 addons/sales_team/models/crm_team_member.py create mode 100644 addons/sales_team/views/crm_team_member_views.xml diff --git a/addons/crm/__manifest__.py b/addons/crm/__manifest__.py index c5d31ed7477..559e317f66c 100644 --- a/addons/crm/__manifest__.py +++ b/addons/crm/__manifest__.py @@ -47,6 +47,7 @@ 'views/crm_lost_reason_views.xml', 'views/crm_stage_views.xml', 'views/crm_lead_views.xml', + 'views/crm_team_member_views.xml', 'views/digest_views.xml', 'views/mail_activity_views.xml', 'views/res_config_settings_views.xml', @@ -59,6 +60,7 @@ 'demo': [ 'data/crm_team_demo.xml', 'data/mail_template_demo.xml', + 'data/crm_team_member_demo.xml', 'data/mail_activity_demo.xml', 'data/crm_lead_demo.xml', ], diff --git a/addons/crm/data/crm_team_member_demo.xml b/addons/crm/data/crm_team_member_demo.xml new file mode 100644 index 00000000000..00472c3a1c7 --- /dev/null +++ b/addons/crm/data/crm_team_member_demo.xml @@ -0,0 +1,12 @@ + + + + + 1 + + + 2 + ['&', ['lang_id', '=', 'English'], ['country_id', '=', 'Belgium']] + + + diff --git a/addons/crm/models/__init__.py b/addons/crm/models/__init__.py index 7b1a7fdcd7a..2282f96b1d3 100644 --- a/addons/crm/models/__init__.py +++ b/addons/crm/models/__init__.py @@ -7,6 +7,7 @@ from . import crm_lead from . import crm_lost_reason from . import crm_stage from . import crm_team +from . import crm_team_member from . import ir_config_parameter from . import res_config_settings from . import res_partner diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 1c8cf04fe38..07738d4f1ed 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -3,16 +3,16 @@ import logging import threading +from collections import OrderedDict, defaultdict from datetime import date, datetime, timedelta from psycopg2 import sql from odoo import api, fields, models, tools, SUPERUSER_ID from odoo.addons.phone_validation.tools import phone_validation +from odoo.exceptions import UserError, AccessError from odoo.osv import expression from odoo.tools.translate import _ -from odoo.tools import email_re, email_split -from odoo.exceptions import UserError, AccessError -from collections import OrderedDict, defaultdict +from odoo.tools import email_re, email_split, safe_eval from . import crm_stage @@ -71,6 +71,12 @@ PARTNER_ADDRESS_FIELDS_TO_SYNC = [ PLS_COMPUTE_BATCH_STEP = 50000 # odoo.models.PREFETCH_MAX = 1000 but larger cluster can speed up global computation PLS_UPDATE_BATCH_STEP = 5000 +# Evaluation context for all domain-based lead auto assignment +LEAD_ASSIGN_EVAL_CONTEXT = { + 'datetime': safe_eval.datetime, + 'context_today': datetime.now, +} + class Lead(models.Model): _name = "crm.lead" @@ -249,7 +255,7 @@ class Lead(models.Model): if not lead.user_id: continue user = lead.user_id - if lead.team_id and user in lead.team_id.member_ids | lead.team_id.user_id: + if lead.team_id and user in (lead.team_id.member_ids | lead.team_id.user_id): continue team_domain = [('use_leads', '=', True)] if lead.type == 'lead' else [('use_opportunities', '=', True)] team = self.env['crm.team']._get_default_team_id(user_id=user.id, domain=team_domain) diff --git a/addons/crm/models/crm_team_member.py b/addons/crm/models/crm_team_member.py new file mode 100644 index 00000000000..b8d5573fec5 --- /dev/null +++ b/addons/crm/models/crm_team_member.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import datetime + +from odoo import api, exceptions, fields, models, _ +from odoo.addons.crm.models.crm_lead import LEAD_ASSIGN_EVAL_CONTEXT +from odoo.tools import safe_eval + + +class Team(models.Model): + _inherit = 'crm.team.member' + + # assignment + team_user_domain = fields.Char('Domain', tracking=True) + maximum_user_leads = fields.Integer('Leads Per Month') + lead_month_count = fields.Integer( + 'Assigned Leads', compute='_compute_lead_month_count', + help='Lead assigned to this member those last 30 days') + + def _compute_lead_month_count(self): + for member in self: + if member.id: + limit_date = fields.Datetime.now() - datetime.timedelta(days=30) + domain = [('user_id', '=', member.user_id.id), + ('team_id', '=', member.crm_team_id.id), + ('date_open', '>=', limit_date)] + member.lead_month_count = self.env['crm.lead'].search_count(domain) + else: + member.lead_month_count = 0 + + @api.constrains('team_user_domain') + def _constrains_team_user_domain(self): + for member in self: + try: + domain = safe_eval.safe_eval(member.team_user_domain or '[]', LEAD_ASSIGN_EVAL_CONTEXT) + self.env['crm.lead'].search(domain, limit=1) + except Exception: + raise exceptions.UserError(_('Team membership assign domain is incorrectly formatted')) diff --git a/addons/crm/views/crm_menu_views.xml b/addons/crm/views/crm_menu_views.xml index 20fb5503662..eda5f6583b5 100644 --- a/addons/crm/views/crm_menu_views.xml +++ b/addons/crm/views/crm_menu_views.xml @@ -96,6 +96,13 @@ parent="crm_menu_config" action="sales_team.crm_team_action_config" sequence="5"/> + + + + + crm.team.member.view.tree + crm.team.member + + + + + + + + + + + crm.team.member.view.kanban.inherit.crm + crm.team.member + + + +

+ + +
+ + +
+
+
+
+ + + crm.team.member.view.form.inherit.crm + crm.team.member + + + + + + + + + + + + +

+ Create a new salesman +

+ Link users to salesteam, set a per 30 days lead capacity for each + of them and set filters to auto assign your leads. +

+
+
+ +
diff --git a/addons/sales_team/__manifest__.py b/addons/sales_team/__manifest__.py index 918c64a71e4..4087b69a040 100644 --- a/addons/sales_team/__manifest__.py +++ b/addons/sales_team/__manifest__.py @@ -19,6 +19,7 @@ Using this application you can manage Sales Teams with CRM and/or Sales 'views/assets.xml', 'views/crm_tag_views.xml', 'views/crm_team_views.xml', + 'views/crm_team_member_views.xml', 'views/mail_activity_views.xml', 'views/res_partner_views.xml', ], diff --git a/addons/sales_team/data/crm_team_data.xml b/addons/sales_team/data/crm_team_data.xml index 53d006f2324..524f3406ee0 100644 --- a/addons/sales_team/data/crm_team_data.xml +++ b/addons/sales_team/data/crm_team_data.xml @@ -4,9 +4,12 @@ Sales 0 - + + + + Website diff --git a/addons/sales_team/data/crm_team_demo.xml b/addons/sales_team/data/crm_team_demo.xml index cbe77090d97..98141b5dac3 100644 --- a/addons/sales_team/data/crm_team_demo.xml +++ b/addons/sales_team/data/crm_team_demo.xml @@ -1,24 +1,22 @@ - - - Europe - - - - + - - + + Sales - America + Pre-Sales - + + + + + diff --git a/addons/sales_team/models/__init__.py b/addons/sales_team/models/__init__.py index 2fa2d9bcbd2..3f71a03d30e 100644 --- a/addons/sales_team/models/__init__.py +++ b/addons/sales_team/models/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import crm_team_member from . import crm_team from . import crm_tag from . import res_partner diff --git a/addons/sales_team/models/crm_team.py b/addons/sales_team/models/crm_team.py index afe2da480b5..a1b231c0e86 100644 --- a/addons/sales_team/models/crm_team.py +++ b/addons/sales_team/models/crm_team.py @@ -20,16 +20,18 @@ class CrmTeam(models.Model): _check_company_auto = True def _get_default_team_id(self, user_id=None, domain=None): - user_id = user_id or self.env.uid - user_salesteam_id = self.env['res.users'].browse(user_id).sale_team_id.id - # Avoid searching on member_ids (+1 query) when we may have the user salesteam already in cache. + if user_id is None: + user_id = self.env.user.id + team = self.env['crm.team'].search([ ('company_id', 'in', [False, self.env.company.id]), - '|', ('user_id', '=', user_id), ('id', '=', user_salesteam_id), + '|', ('user_id', '=', user_id), ('member_ids', 'in', user_id), ], limit=1) if not team and 'default_team_id' in self.env.context: team = self.env['crm.team'].browse(self.env.context.get('default_team_id')) - return team or self.env['crm.team'].search(domain or [], limit=1) + if not team: + team = self.env['crm.team'].search(domain or [], limit=1) + return team def _get_default_favorite_user_ids(self): return [(6, 0, [self.env.uid])] @@ -45,10 +47,11 @@ class CrmTeam(models.Model): related='company_id.currency_id', readonly=True) user_id = fields.Many2one('res.users', string='Team Leader', check_company=True) # memberships - member_ids = fields.One2many( - 'res.users', 'sale_team_id', string='Channel Members', + member_ids = fields.Many2many( + 'res.users', 'crm_team_member', 'crm_team_id', 'user_id', string='Channel Members', check_company=True, domain=[('share', '=', False)], - help="Add members to automatically assign their documents to this sales team. You can only be member of one team.") + help="Add members to automatically assign their documents to this sales team.") + crm_team_member_ids = fields.One2many('crm.team.member', 'crm_team_id', string='Sales Team Memberships') # UX options color = fields.Integer(string='Color Index', help="The color of the channel") favorite_user_ids = fields.Many2many( diff --git a/addons/sales_team/models/crm_team_member.py b/addons/sales_team/models/crm_team_member.py new file mode 100644 index 00000000000..878611840fa --- /dev/null +++ b/addons/sales_team/models/crm_team_member.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class CrmTeamMember(models.Model): + _name = 'crm.team.member' + _inherit = ['mail.thread'] + _description = 'Sales Team Member' + _rec_name = 'user_id' + + crm_team_id = fields.Many2one('crm.team', string='Sales Team', required=True) + user_id = fields.Many2one('res.users', string='Saleman', required=True) # check responsible field + name = fields.Char(string="Name", related='user_id.partner_id.display_name', readonly=False) + active = fields.Boolean(string='Running', default=True) diff --git a/addons/sales_team/models/res_users.py b/addons/sales_team/models/res_users.py index f2fdd18337a..213561dcf7e 100644 --- a/addons/sales_team/models/res_users.py +++ b/addons/sales_team/models/res_users.py @@ -7,6 +7,14 @@ from odoo import fields, models class ResUsers(models.Model): _inherit = 'res.users' + crm_team_ids = fields.Many2many('crm.team', 'crm_team_member', 'user_id', 'crm_team_id', string='Sales Teams') + crm_team_member_ids = fields.One2many('crm.team.member', 'user_id', string='Sales Team Memberships') + # mov of the field defined in website_crm_score. The field is now computed + # based on the new modeling introduced in this module. It is stored to avoid + # breaking the member_ids inverse field. As the relationship between users + # and sales team is a one2many / many2one relationship we take the first of + # the crm.team.member record to find the user's sales team. sale_team_id = fields.Many2one( - 'crm.team', "User's Sales Team", - help='Sales Team the user is member of. Used to compute the members of a Sales Team through the inverse one2many') \ No newline at end of file + 'crm.team', string='User Sales Team', + related='crm_team_member_ids.crm_team_id', + readonly=False, store=True) diff --git a/addons/sales_team/security/ir.model.access.csv b/addons/sales_team/security/ir.model.access.csv index bbef30be361..447f7daa188 100644 --- a/addons/sales_team/security/ir.model.access.csv +++ b/addons/sales_team/security/ir.model.access.csv @@ -2,6 +2,9 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_crm_team,crm.team,model_crm_team,base.group_user,1,0,0,0 access_crm_team_user,crm.team.user,model_crm_team,sales_team.group_sale_salesman,1,0,0,0 access_crm_team_manager,crm.team.manager,model_crm_team,sales_team.group_sale_manager,1,1,1,1 +access_crm_team_member_all,access.crm.team.member.all,model_crm_team_member,,0,0,0,0 +access_crm_team_member_user,access.crm.team.member.user,model_crm_team_member,base.group_user,1,0,0,0 +access_crm_team_member_salesman,access.crm.team.member.salesman,model_crm_team_member,sales_team.group_sale_salesman,1,1,1,1 access_crm_tag,crm_tag,model_crm_tag,base.group_user,0,0,0,0 access_crm_tag_salesman,crm_tag salesman,model_crm_tag,sales_team.group_sale_salesman,1,1,1,0 access_crm_tag_manager,crm_tag manager,model_crm_tag,sales_team.group_sale_manager,1,1,1,1 diff --git a/addons/sales_team/views/crm_team_member_views.xml b/addons/sales_team/views/crm_team_member_views.xml new file mode 100644 index 00000000000..9fa82428477 --- /dev/null +++ b/addons/sales_team/views/crm_team_member_views.xml @@ -0,0 +1,138 @@ + + + + + crm.team.member.view.search + crm.team.member + + + + + + + + + + + + + + + crm.team.member.view.tree + crm.team.member + + + + + + + + + + crm.team.member.view.tree.from.team + crm.team.member + + primary + 32 + + + + + + + + crm.team.member.view.kanban + crm.team.member + + + + + + + + +
+
+ + +
+
+
+
+
+
+
+ + + crm.team.member.view.kanban.from.team + crm.team.member + + primary + 32 + + + + + + + + crm.team.member.view.form + crm.team.member + +
+ + +

+ +

+ + + +
+
+ +
+ +
+
+ + + crm.team.member.view.form.from.team + crm.team.member + + primary + 32 + + + + + + + + Team Members + crm.team.member + kanban,tree,form + +

+ Create a new salesman +

+ Link users to salesteam. +

+
+
+ +
diff --git a/addons/sales_team/views/crm_team_views.xml b/addons/sales_team/views/crm_team_views.xml index f373dd02882..474ba15db30 100644 --- a/addons/sales_team/views/crm_team_views.xml +++ b/addons/sales_team/views/crm_team_views.xml @@ -42,25 +42,15 @@ - - - - - - - -
-
- Avatar -
- -
-
-
-
-
-
-
+ +
From e1355dae7a775237a735847b5f5f426e08642c0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 3 Apr 2020 14:43:34 +0000 Subject: [PATCH 03/16] [REF] sales_team: improve crm.team.member model and data GLOBAL PURPOSE Ability to have salesmen belonging to several sales team is a core requirement of CRM. It is therefore moved from website_crm_score to crm along with cleaning and behavior improvement. Automatic lead assignment is also moved and cleaned. SPECIFICATIONS: MODEL CLEANING In this commit we improve team member model to make it more usable and aligned with current Odoo views and usability. This commit contains notably * add some related fields on the user: phone, mobile, company, email; * rename "team_user_domain" to "assignment_domain"; * rename "maximum_user_leads" to "assignment_max"; * improve member views globally; We also introduce dynamic domain computation for users and teams when seeing team members. This allows to limit selection of users or teams depending on already existing configuration. Purpose is to avoid having people trying to create wrong configuration (duplicates, ...) and receiving error messages. Also * clean some data and demo about teams and members; * fix some views glitches; * improve views: add some fields in search views, improve wording of fields or actions, ... globally improve usability and US as well as feedback given to users; * group sales team members by team by default as it help getting an overview of team members distribution; * update tests to create team members instead of writing directly on m2m towards users; SPECIFICATIONS: SALE_TEAM_ID FIELD ON RES.USERS In this commit we also rewrite sale_team_id field of res.users. Previously in crm it was simply a many2one field linking to a crm.team. It was used to compute team membership as a standard many2one / one2many relationship. In enterprise module ``website_crm_score`` this field was set as a related on first user teams. Indeed scoring changed relationship between users and teams to a many2many. This field is now directly a computed many2one field that links to the "main" team of the user even in multi memberships mode. As memberships can now be de-activated we changed this field to a computed stored field. It is based on oldest active membership of user. That way default team of documents will be the user's oldest team considered as its "main team". SPECIFICATIONS: LEAD_ALL_ASSIGNED_MONTH_COUNT Improve crm_team ``lead_all_assigned_month_count`` field computation. It is now simply the sum of assigned leads of team members. Differences may happen if a lead in teamA is assigned to an user working in teamB. This should not happen if team / user is correctly set at lead level which is generally the case. However if that happens, old field counted it. New field will not count it as it is not directly linked to a team's member. SPECIFICATIONS: DEMO DATA Make crm.lead demo data coherent with user_id / team_id . As demo data should be using a mono-membership behavior for users and teams, we have to ensure user_id and team_id effectively matches. It allows to have coherent reporting and assignment demo flows. Also avoid having Odoobot responsible of leads. Instead of undefined user_id and team_id leading to Odoobot being responsible of some leads, force it to be False. Those demo data allow to test automatic assignment of lead LINKS Task ID-2086889 (main task) Task ID-2357969 (scoring migration task) Community PR odoo/odoo#48422 Enterprise PR odoo/enterprise#499 Upgrade PR odoo/upgrade#996 --- addons/crm/__manifest__.py | 1 + addons/crm/data/crm_lead_demo.xml | 505 +++++++++--------- addons/crm/data/crm_team_member_demo.xml | 7 +- addons/crm/models/crm_team.py | 10 +- addons/crm/models/crm_team_member.py | 15 +- .../src/scss/crm_team_member_views.scss | 6 + addons/crm/tests/common.py | 24 +- addons/crm/tests/test_crm_ui.py | 15 +- addons/crm/views/assets.xml | 4 + addons/crm/views/crm_team_member_views.xml | 38 +- addons/sale/tests/test_sale_order.py | 2 +- addons/sales_team/models/crm_team.py | 37 +- addons/sales_team/models/crm_team_member.py | 44 +- addons/sales_team/models/res_users.py | 37 +- .../sales_team/security/ir.model.access.csv | 2 +- .../static/src/scss/crm_team_views.scss | 40 ++ .../static/src/scss/sales_team_dashboard.scss | 8 - addons/sales_team/tests/__init__.py | 2 + addons/sales_team/tests/common.py | 9 +- addons/sales_team/tests/test_sales_team.py | 2 + .../tests/test_sales_team_internals.py | 34 ++ .../tests/test_sales_team_membership.py | 84 +++ addons/sales_team/views/assets.xml | 4 +- .../views/crm_team_member_views.xml | 78 +-- addons/sales_team/views/crm_team_views.xml | 11 +- 25 files changed, 660 insertions(+), 359 deletions(-) create mode 100644 addons/crm/static/src/scss/crm_team_member_views.scss create mode 100644 addons/sales_team/static/src/scss/crm_team_views.scss delete mode 100644 addons/sales_team/static/src/scss/sales_team_dashboard.scss create mode 100644 addons/sales_team/tests/test_sales_team_internals.py create mode 100644 addons/sales_team/tests/test_sales_team_membership.py diff --git a/addons/crm/__manifest__.py b/addons/crm/__manifest__.py index 559e317f66c..469ead4d4e1 100644 --- a/addons/crm/__manifest__.py +++ b/addons/crm/__manifest__.py @@ -19,6 +19,7 @@ 'fetchmail', 'utm', 'web_tour', + 'web_kanban_gauge', 'contacts', 'digest', 'phone_validation', diff --git a/addons/crm/data/crm_lead_demo.xml b/addons/crm/data/crm_lead_demo.xml index 144ed8e5789..b831a9dda15 100644 --- a/addons/crm/data/crm_lead_demo.xml +++ b/addons/crm/data/crm_lead_demo.xml @@ -86,7 +86,7 @@ +1 555 754 3010 2 - + @@ -108,7 +108,7 @@ United Street 68 2 - + @@ -132,7 +132,7 @@ +1 813 494 5005 2 - + @@ -171,7 +171,7 @@ Contact: +1 813 494 5005

]]>
00118 2 - + @@ -249,7 +249,7 @@ ESM Expert
]]>
OX1 1RQ 2 - + @@ -305,7 +305,7 @@ Andrew

]]>
1300 2 - + @@ -604,7 +604,7 @@ Andrew

]]>
1 - + @@ -667,7 +667,7 @@ Andrew

]]>
1 - + @@ -703,7 +703,7 @@ Andrew

]]>
Lima 0 - + @@ -775,7 +775,7 @@ Andrew

]]>
2 - + @@ -799,7 +799,7 @@ Andrew

]]>
0 - +
@@ -820,6 +820,8 @@ Andrew

]]> 1 + + @@ -842,6 +844,8 @@ Andrew

]]>
1 + + @@ -892,246 +896,267 @@ Andrew

]]>
- - -lead -Recurring delivery contract -36221 -20 -Max Johnson -max123@itconsult.com - -Milan - -1 - - - - - - + + + lead + Recurring delivery contract + 36221 + 20 + Max Johnson + max123@itconsult.com + + Milan + + 1 + + + + + + + + - - -lead -Need info about pricing -28443 -80 -Aloysius Akred -aakreda@theglobeandmail.com - -London -1 Russel square - -1 - - - - - - + + + lead + Need info about pricing + 28443 + 80 + Aloysius Akred + aakreda@theglobeandmail.com + + London + 1 Russel square + + 1 + + + + + + + + - - -lead -Trelian New Offices -88715 -12 -Rosalynd Oxshott -Photobug -roxshott9@trellian.com - -Canguaretama - -1 - - - - - - + + + lead + Trelian New Offices + 88715 + 12 + Rosalynd Oxshott + Photobug + roxshott9@trellian.com + + Canguaretama + + 1 + + + + + + + + - - -lead -Branded Furniture -7784 -20 -Myrna Limprecht -mlimprecht8@fastcompany.com - -Valejas - -1 - - - - - - + + + lead + Branded Furniture + 7784 + 20 + Myrna Limprecht + mlimprecht8@fastcompany.com + + Valejas + + 1 + + + + + + + + - - -lead -Design New Shelves -54587 -81 -Alys Kalinovich -Jaloo -akalinovich7@tinypic.com - -Boafeo - -1 - - - - - - + + + lead + Design New Shelves + 54587 + 81 + Alys Kalinovich + Jaloo + akalinovich7@tinypic.com + + Boafeo + + 1 + + + + + + + + - - -lead -Office chairs -5474 -20 -Jennine Jobbins -Shufflebeat -jjobbins6@simplemachines.org - -Gvardeysk - -1 - - - - - - + + + lead + Office chairs + 5474 + 20 + Jennine Jobbins + Shufflebeat + jjobbins6@simplemachines.org + + Gvardeysk + + 1 + + + + + + + + - - -lead -Cleaning subscription -11475 -25 -Elmo Espinazo -Realblab -eespinazo5@reuters.com - -Amsterdam - -1 - - - - - - + + + lead + Cleaning subscription + 11475 + 25 + Elmo Espinazo + Realblab + eespinazo5@reuters.com + + Amsterdam + + 1 + + + + + + + + - - -lead -Custom Desks (100 pieces) -9987 -20 -Chalmers Redford -Muxo -credford4@salon.com - -Odoyev - -1 - - - - - - - - + + + lead + Custom Desks (100 pieces) + 9987 + 20 + Chalmers Redford + Muxo + credford4@salon.com + + Odoyev + + 1 + + + + + + + + - - -lead -Need a price: urgent -4482 -90 -Itch Kirvell -Skibox -ikirvell3@gnu.org - -Dahu Satu - -1 - - - - - - + + + lead + Need a price: urgent + 4482 + 90 + Itch Kirvell + Skibox + ikirvell3@gnu.org + + Dahu Satu + + 1 + + + + + + + + - - -lead -Furnitures for new location -22500 -20 -Napoleon Grabert -Twinte -ngrabert2@dailymail.co.uk - -Tokyo - -1 - - - - - - + + + lead + Furnitures for new location + 22500 + 20 + Napoleon Grabert + Twinte + ngrabert2@dailymail.co.uk + + Tokyo + + 1 + + + + + + + + - - -lead -Modernize old offices -99755 -20 -Franny Seiller -Yozio -fseiller1@theglobeandmail.com - -Wurigelebur - -1 - - - - - - + + + lead + Modernize old offices + 99755 + 20 + Franny Seiller + Yozio + fseiller1@theglobeandmail.com + + Wurigelebur + + 1 + + + + + + + + - - - -lead -Quote for 35 windows -41421 -20 -Tommi Brockhouse -Gabcube -tbrockhouse0@google.pl - -Brussels - -1 - - - - - - + + + lead + Quote for 35 windows + 41421 + 20 + Tommi Brockhouse + Gabcube + tbrockhouse0@google.pl + + Brussels + + 1 + + + + + + + + diff --git a/addons/crm/data/crm_team_member_demo.xml b/addons/crm/data/crm_team_member_demo.xml index 00472c3a1c7..7112f49297b 100644 --- a/addons/crm/data/crm_team_member_demo.xml +++ b/addons/crm/data/crm_team_member_demo.xml @@ -2,11 +2,12 @@ - 1 + 15 + [['probability', '>=', 20]] - 2 - ['&', ['lang_id', '=', 'English'], ['country_id', '=', 'Belgium']] + 30 + [] diff --git a/addons/crm/models/crm_team.py b/addons/crm/models/crm_team.py index cbdad2f0400..9bf4f75cd1d 100644 --- a/addons/crm/models/crm_team.py +++ b/addons/crm/models/crm_team.py @@ -47,16 +47,10 @@ class Team(models.Model): for team in self: team.lead_unassigned_count = counts.get(team.id, 0) + @api.depends('crm_team_member_ids.lead_month_count') def _compute_lead_all_assigned_month_count(self): - limit_date = datetime.datetime.now() - datetime.timedelta(days=30) - leads_data = self.env['crm.lead'].read_group([ - ('team_id', 'in', self.ids), - ('date_open', '>=', fields.Datetime.to_string(limit_date)), - ('user_id', '!=', False), - ], ['team_id'], ['team_id']) - counts = {datum['team_id'][0]: datum['team_id_count'] for datum in leads_data} for team in self: - team.lead_all_assigned_month_count = counts.get(team.id, 0) + team.lead_all_assigned_month_count = sum(s.lead_month_count for s in team.crm_team_member_ids) def _compute_opportunities_data(self): opportunity_data = self.env['crm.lead'].read_group([ diff --git a/addons/crm/models/crm_team_member.py b/addons/crm/models/crm_team_member.py index b8d5573fec5..46548d3b810 100644 --- a/addons/crm/models/crm_team_member.py +++ b/addons/crm/models/crm_team_member.py @@ -12,15 +12,16 @@ class Team(models.Model): _inherit = 'crm.team.member' # assignment - team_user_domain = fields.Char('Domain', tracking=True) - maximum_user_leads = fields.Integer('Leads Per Month') + assignment_domain = fields.Char('Assignment Domain', tracking=True) + assignment_max = fields.Integer('Max Leads (last 30 days)') lead_month_count = fields.Integer( - 'Assigned Leads', compute='_compute_lead_month_count', + 'Leads (30 days)', compute='_compute_lead_month_count', help='Lead assigned to this member those last 30 days') + @api.depends('user_id', 'crm_team_id') def _compute_lead_month_count(self): for member in self: - if member.id: + if member.user_id.id and member.crm_team_id.id: limit_date = fields.Datetime.now() - datetime.timedelta(days=30) domain = [('user_id', '=', member.user_id.id), ('team_id', '=', member.crm_team_id.id), @@ -29,11 +30,11 @@ class Team(models.Model): else: member.lead_month_count = 0 - @api.constrains('team_user_domain') - def _constrains_team_user_domain(self): + @api.constrains('assignment_domain') + def _constrains_assignment_domain(self): for member in self: try: - domain = safe_eval.safe_eval(member.team_user_domain or '[]', LEAD_ASSIGN_EVAL_CONTEXT) + domain = safe_eval.safe_eval(member.assignment_domain or '[]', LEAD_ASSIGN_EVAL_CONTEXT) self.env['crm.lead'].search(domain, limit=1) except Exception: raise exceptions.UserError(_('Team membership assign domain is incorrectly formatted')) diff --git a/addons/crm/static/src/scss/crm_team_member_views.scss b/addons/crm/static/src/scss/crm_team_member_views.scss new file mode 100644 index 00000000000..4999b609e41 --- /dev/null +++ b/addons/crm/static/src/scss/crm_team_member_views.scss @@ -0,0 +1,6 @@ +.o_kanban_view.o_crm_team_member_kanban { + .o_member_assignment div.oe_gauge { + width: 100px; + height: 80px; + } +} diff --git a/addons/crm/tests/common.py b/addons/crm/tests/common.py index f46e1fd265f..7e2f99c52e1 100644 --- a/addons/crm/tests/common.py +++ b/addons/crm/tests/common.py @@ -246,10 +246,10 @@ class TestLeadConvertCommon(TestCrmCommon): # Sales Team organization # Role: M (team member) R (team manager) # SALESMAN---------------sales_team_1-----sales_team_convert - # admin------------------M----------------/ + # admin------------------M----------------/ (sales_team_1_m2) # user_sales_manager-----R----------------R - # user_sales_leads-------M----------------/ - # user_sales_salesman----/----------------M + # user_sales_leads-------M----------------/ (sales_team_1_m1) + # user_sales_salesman----/----------------M (sales_team_convert_m1) # Stages Team organization # Name-------------------ST-------------------Sequ @@ -268,7 +268,10 @@ class TestLeadConvertCommon(TestCrmCommon): 'use_opportunities': True, 'company_id': False, 'user_id': cls.user_sales_manager.id, - 'member_ids': [(4, cls.user_sales_salesman.id)], + }) + cls.sales_team_convert_m1 = cls.env['crm.team.member'].create({ + 'user_id': cls.user_sales_salesman.id, + 'crm_team_id': cls.sales_team_convert.id, }) cls.stage_team_convert_1 = cls.env['crm.stage'].create({ 'name': 'New', @@ -295,11 +298,11 @@ class TestLeadConvertMassCommon(TestLeadConvertCommon): # Sales Team organization # Role: M (team member) R (team manager) # SALESMAN-------------------sales_team_1-----sales_team_convert - # admin----------------------M----------------/ - # user_sales_manager---------R----------------R + # admin----------------------M----------------/ (sales_team_1_m2) + # user_sales_manager---------R----------------R (sales_team_1_m1) # user_sales_leads-----------M----------------/ - # user_sales_leads_convert---/----------------M <-- NEW - # user_sales_salesman--------/----------------M + # user_sales_leads_convert---/----------------M <-- NEW (sales_team_convert_m2) + # user_sales_salesman--------/----------------M (sales_team_convert_m1) cls.user_sales_leads_convert = mail_new_test_user( cls.env, login='user_sales_leads_convert', @@ -308,8 +311,9 @@ class TestLeadConvertMassCommon(TestLeadConvertCommon): notification_type='inbox', groups='sales_team.group_sale_salesman_all_leads,base.group_partner_manager,crm.group_use_lead', ) - cls.sales_team_convert.write({ - 'member_ids': [(4, cls.user_sales_leads_convert.id)] + cls.sales_team_convert_m2 = cls.env['crm.team.member'].create({ + 'user_id': cls.user_sales_leads_convert.id, + 'crm_team_id': cls.sales_team_convert.id, }) cls.lead_w_partner = cls.env['crm.lead'].create({ diff --git a/addons/crm/tests/test_crm_ui.py b/addons/crm/tests/test_crm_ui.py index 116163f28ac..04aa368bc1a 100644 --- a/addons/crm/tests/test_crm_ui.py +++ b/addons/crm/tests/test_crm_ui.py @@ -1,10 +1,12 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. -import odoo.tests +from odoo.addons.crm.tests.common import TestCrmCommon +from odoo.tests import HttpCase +from odoo.tests.common import tagged, users -@odoo.tests.tagged('post_install','-at_install') -class TestUi(odoo.tests.HttpCase): +@tagged('post_install', '-at_install') +class TestUi(HttpCase): def test_01_crm_tour(self): self.start_tour("/web", 'crm_tour', login="admin") @@ -22,3 +24,10 @@ class TestUi(odoo.tests.HttpCase): ])] }) self.start_tour("/web", 'crm_rainbowman', login="temp_crm_user") + + +class TestCRMLeadMisc(TestCrmCommon): + + @users('user_sales_leads') + def test_team_my_pipeline(self): + action = self.env['crm.team'].action_your_pipeline() diff --git a/addons/crm/views/assets.xml b/addons/crm/views/assets.xml index 1ffed45b2a8..76faa781844 100644 --- a/addons/crm/views/assets.xml +++ b/addons/crm/views/assets.xml @@ -7,7 +7,11 @@ + + + +