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