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] [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 -
- -
-
-
-
-
-
-
+ +