[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
This commit is contained in:
Thibault Delavallée
2021-01-21 10:56:19 +00:00
parent 55259a6b94
commit 9790d7dcfb
17 changed files with 336 additions and 45 deletions
+2
View File
@@ -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',
],
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo><data noupdate="1">
<record id="sales_team.crm_team_member_admin_sales" model="crm.team.member">
<field name="maximum_user_leads">1</field>
</record>
<record id="sales_team.crm_team_member_demo_team_1" model="crm.team.member">
<field name="maximum_user_leads">2</field>
<field name="team_user_domain">['&amp;', ['lang_id', '=', 'English'], ['country_id', '=', 'Belgium']]</field>
</record>
</data></odoo>
+1
View File
@@ -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
+10 -4
View File
@@ -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)
+39
View File
@@ -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'))
+7
View File
@@ -96,6 +96,13 @@
parent="crm_menu_config"
action="sales_team.crm_team_action_config"
sequence="5"/>
<menuitem
id="crm_team_member_config"
name="Teams Members"
parent="crm_menu_config"
action="sales_team.crm_team_member_action"
sequence="6"
groups="base.group_no_one"/>
<menuitem
id="crm_team_menu_config_activity_types"
name="Activity Types"
@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo><data>
<record id="crm_team_member_view_tree" model="ir.ui.view">
<field name="name">crm.team.member.view.tree</field>
<field name="model">crm.team.member</field>
<field name="inherit_id" ref="sales_team.crm_team_member_view_tree"/>
<field name="arch" type="xml">
<field name="user_id" position="after">
<field name="maximum_user_leads"/>
<field name="lead_month_count"/>
</field>
</field>
</record>
<record id="crm_team_member_view_kanban" model="ir.ui.view">
<field name="name">crm.team.member.view.kanban.inherit.crm</field>
<field name="model">crm.team.member</field>
<field name="inherit_id" ref="sales_team.crm_team_member_view_kanban"/>
<field name="arch" type="xml">
<xpath expr="//li[hasclass('o_salesteam_member_name')]" position="after">
<li class="nav-item"><field name="team_user_domain" widget="filters"/></li>
</xpath>
<xpath expr="//ul[hasclass('o_assignation_body')]" position="after">
<div class="o_assignation_leads_count">
<field name="maximum_user_leads" invisible="1"/>
<field name="lead_month_count" widget="gauge"
options="{'max_field': 'maximum_user_leads'}"/>
</div>
</xpath>
</field>
</record>
<record id="crm_team_member_view_form" model="ir.ui.view">
<field name="name">crm.team.member.view.form.inherit.crm</field>
<field name="model">crm.team.member</field>
<field name="inherit_id" ref="sales_team.crm_team_member_view_form"/>
<field name="arch" type="xml">
<field name="user_id" position="after">
<field name="lead_month_count" invisible="1"/>
</field>
<xpath expr="//group" position="inside">
<label for="maximum_user_leads" string="Maximum Leads / 30 days" />
<div>
<field name="maximum_user_leads" class="oe_inline"/>
</div>
<field name="team_user_domain" widget="domain" options="{'model': 'crm.lead'}"/>
</xpath>
</field>
</record>
<record id="sales_team.crm_team_member_action" model="ir.actions.act_window">
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new salesman
</p><p>
Link users to salesteam, set a per 30 days lead capacity for each
of them and set filters to auto assign your leads.
</p>
</field>
</record>
</data></odoo>
+1
View File
@@ -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',
],
+4 -1
View File
@@ -4,9 +4,12 @@
<record id="team_sales_department" model="crm.team">
<field name="name">Sales</field>
<field name="sequence">0</field>
<field name="member_ids" eval="[(4, ref('base.user_admin'))]"/>
<field name="company_id" eval="False"/>
</record>
<record id="crm_team_member_admin_sales" model="crm.team.member">
<field name="crm_team_id" ref="team_sales_department"/>
<field name="user_id" ref="base.user_admin"/>
</record>
<record id="salesteam_website_sales" model="crm.team">
<field name="name">Website</field>
+9 -11
View File
@@ -1,24 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- rename for demo-data -->
<record model="crm.team" id="team_sales_department">
<field name="name">Europe</field>
<field name="member_ids" eval="[(4, ref('base.user_demo'))]"/>
<field name="company_id" eval="False"/>
</record>
<record id="base.user_demo" model="res.users">
<record model="res.users" id="base.user_demo">
<field name="groups_id" eval="[(4,ref('sales_team.group_sale_salesman'))]"/>
</record>
<record id="team_sales_department" model="crm.team">
<field name="member_ids" eval="[(4, ref('base.user_demo'))]"/>
<record model="crm.team" id="team_sales_department">
<field name="name">Sales</field>
</record>
<record model="crm.team" id="crm_team_1">
<field name="name">America</field>
<field name="name">Pre-Sales</field>
<field name="company_id" eval="False"/>
<field name="member_ids" eval="[(4, ref('base.user_demo'))]"/>
</record>
<record id="crm_team_member_demo_team_1" model="crm.team.member">
<field name="user_id" ref="base.user_demo"/>
<field name="crm_team_id" ref="sales_team.crm_team_1"/>
</record>
</data>
</odoo>
+1
View File
@@ -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
+11 -8
View File
@@ -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(
@@ -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)
+10 -2
View File
@@ -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')
'crm.team', string='User Sales Team',
related='crm_team_member_ids.crm_team_id',
readonly=False, store=True)
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_crm_team crm.team model_crm_team base.group_user 1 0 0 0
3 access_crm_team_user crm.team.user model_crm_team sales_team.group_sale_salesman 1 0 0 0
4 access_crm_team_manager crm.team.manager model_crm_team sales_team.group_sale_manager 1 1 1 1
5 access_crm_team_member_all access.crm.team.member.all model_crm_team_member 0 0 0 0
6 access_crm_team_member_user access.crm.team.member.user model_crm_team_member base.group_user 1 0 0 0
7 access_crm_team_member_salesman access.crm.team.member.salesman model_crm_team_member sales_team.group_sale_salesman 1 1 1 1
8 access_crm_tag crm_tag model_crm_tag base.group_user 0 0 0 0
9 access_crm_tag_salesman crm_tag salesman model_crm_tag sales_team.group_sale_salesman 1 1 1 0
10 access_crm_tag_manager crm_tag manager model_crm_tag sales_team.group_sale_manager 1 1 1 1
@@ -0,0 +1,138 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo><data>
<record id="crm_team_member_view_search" model="ir.ui.view">
<field name="name">crm.team.member.view.search</field>
<field name="model">crm.team.member</field>
<field name="arch" type="xml">
<search string="Sales Person">
<field name="name"/>
<field name="crm_team_id"/>
<separator/>
<filter name="archived" string="Archived" domain="[('active', '=', False)]"/>
<group expand="0" string="Group By">
<filter string="Sales Team" name="groupby_crm_team_id" context="{'group_by': 'crm_team_id'}"/>
</group>
</search>
</field>
</record>
<record id="crm_team_member_view_tree" model="ir.ui.view">
<field name="name">crm.team.member.view.tree</field>
<field name="model">crm.team.member</field>
<field name="arch" type="xml">
<tree string="Sales Men" sample="1">
<field name="crm_team_id"/>
<field name="user_id"/>
</tree>
</field>
</record>
<record id="crm_team_member_view_tree_from_team" model="ir.ui.view">
<field name="name">crm.team.member.view.tree.from.team</field>
<field name="model">crm.team.member</field>
<field name="inherit_id" ref="sales_team.crm_team_member_view_tree"/>
<field name="mode">primary</field>
<field name="priority">32</field>
<field name="arch" type="xml">
<xpath expr="//field[@name='crm_team_id']" position="replace">
</xpath>
</field>
</record>
<record id="crm_team_member_view_kanban" model="ir.ui.view">
<field name="name">crm.team.member.view.kanban</field>
<field name="model">crm.team.member</field>
<field name="arch" type="xml">
<kanban quick_create="false"
create="true"
sample="1">
<field name="user_id"/>
<field name="active"/>
<templates>
<t t-name="kanban-box">
<t t-set="active" t-value="record.active.raw_value"/>
<t t-set="button_state" t-value="active ? 'o_assignation_button_active' : 'o_assignation_button_inactive'"/>
<div class="o_assignation_kanban_record oe_kanban_global_click pb-0">
<div class="o_kanban_record_top mb-0">
<ul class="nav">
<li class="nav-item">
<a type="open" class="nav-link">
<img t-att-src="kanban_image('res.users', 'image_128', record.user_id.raw_value)" class="o_kanban_image o_image_64_cover" alt="Avatar"/>
</a>
</li>
</ul>
<ul class="nav o_assignation_body">
<li class="nav-item o_salesteam_member_name"><a type="open" class="nav-link"><b><field name="name"/></b></a></li>
<li class="nav-item o_salesteam_member_crm_team_id w-100"><a type="open" class="nav-link"><field name="crm_team_id"/></a></li>
<li t-attf-class="nav-item #{button_state}">
<a type="object" name="toggle_active" t-attf-class="nav-link fa fa-check-square" title="Toggle active"/>
</li>
</ul>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="crm_team_member_view_kanban_from_team" model="ir.ui.view">
<field name="name">crm.team.member.view.kanban.from.team</field>
<field name="model">crm.team.member</field>
<field name="inherit_id" ref="sales_team.crm_team_member_view_kanban"/>
<field name="mode">primary</field>
<field name="priority">32</field>
<field name="arch" type="xml">
<xpath expr="//li[hasclass('o_salesteam_member_crm_team_id')]" position="replace">
</xpath>
</field>
</record>
<record id="crm_team_member_view_form" model="ir.ui.view">
<field name="name">crm.team.member.view.form</field>
<field name="model">crm.team.member</field>
<field name="arch" type="xml">
<form string="Sales Men">
<field name="active" invisible="1"/>
<sheet>
<h1>
<field name="user_id" domain="[('share', '=', False)]"/>
</h1>
<group>
<field name="crm_team_id" kanban_view_ref="%(sales_team.crm_team_view_kanban)s" readonly="context.get('ro_team', False)"/>
</group>
</sheet>
<div class="oe_chatter">
<field name="message_ids" widget="mail_thread"/>
</div>
</form>
</field>
</record>
<record id="crm_team_member_view_form_from_team" model="ir.ui.view">
<field name="name">crm.team.member.view.form.from.team</field>
<field name="model">crm.team.member</field>
<field name="inherit_id" ref="sales_team.crm_team_member_view_form"/>
<field name="mode">primary</field>
<field name="priority">32</field>
<field name="arch" type="xml">
<field name="crm_team_id" position="replace"/>
<xpath expr="//div[hasclass('oe_chatter')]" position="replace"/>
</field>
</record>
<record id="crm_team_member_action" model="ir.actions.act_window">
<field name="name">Team Members</field>
<field name="res_model">crm.team.member</field>
<field name="view_mode">kanban,tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new salesman
</p><p>
Link users to salesteam.
</p>
</field>
</record>
</data></odoo>
+9 -19
View File
@@ -42,25 +42,15 @@
</group>
</group>
<notebook>
<page name="members" string="Team Members" >
<field name="member_ids" widget="many2many" options="{'not_delete': True}">
<kanban quick_create="false" create="true" delete="true">
<field name="id"/>
<field name="name"/>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click" style="max-width: 200px">
<div class="o_kanban_record_top">
<img t-att-src="kanban_image('res.users', 'image_128', record.id.raw_value)" class="oe_avatar oe_kanban_avatar_smallbox o_image_40_cover mb0" alt="Avatar"/>
<div class="o_kanban_record_headings ml8">
<strong class="o_kanban_record_title"><field name="name"/></strong>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
<page string="Assignment" name="assignment">
<field name="crm_team_member_ids" mode="kanban"
class="w-100"
context="{
'kanban_view_ref': 'sales_team.crm_team_member_view_kanban_from_team',
'form_view_ref': 'sales_team.crm_team_member_view_form_from_team',
'tree_view_ref': 'sales_team.crm_team_member_view_tree_from_team',
'default_crm_team_id': active_id,
}"/>
</page>
</notebook>
</sheet>