From 55259a6b94cd8af56b203c65428c619a260e30df Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
@@ -209,11 +208,10 @@
@@ -225,12 +223,11 @@
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 @@
]]>
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. + Link salespersons to sales teams. Set their monthly lead capacity + and configure automatic lead assignment.
diff --git a/addons/sale/tests/test_sale_order.py b/addons/sale/tests/test_sale_order.py index 42cbc604541..77055cb9939 100644 --- a/addons/sale/tests/test_sale_order.py +++ b/addons/sale/tests/test_sale_order.py @@ -29,8 +29,8 @@ class TestSaleOrder(TestSaleCommon): 'email': 'team0user@example.com', 'login': 'team0user', 'name': 'User in Team 0', - 'sale_team_id': cls.crm_team0.id }) + cls.crm_team0.sudo().write({'member_ids': [4, cls.user_in_team.id]}) cls.user_not_in_team = cls.env['res.users'].sudo().create({ 'email': 'noteamuser@example.com', 'login': 'noteamuser', diff --git a/addons/sales_team/models/crm_team.py b/addons/sales_team/models/crm_team.py index a1b231c0e86..d96aa6c6992 100644 --- a/addons/sales_team/models/crm_team.py +++ b/addons/sales_team/models/crm_team.py @@ -25,7 +25,7 @@ class CrmTeam(models.Model): team = self.env['crm.team'].search([ ('company_id', 'in', [False, self.env.company.id]), - '|', ('user_id', '=', user_id), ('member_ids', 'in', user_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')) @@ -36,6 +36,7 @@ class CrmTeam(models.Model): def _get_default_favorite_user_ids(self): return [(6, 0, [self.env.uid])] + # description name = fields.Char('Sales Team', required=True, translate=True) sequence = fields.Integer('Sequence', default=10) active = fields.Boolean(default=True, help="If the active field is set to false, it will allow you to hide the Sales Team without removing it.") @@ -48,10 +49,15 @@ class CrmTeam(models.Model): user_id = fields.Many2one('res.users', string='Team Leader', check_company=True) # memberships member_ids = fields.Many2many( - 'res.users', 'crm_team_member', 'crm_team_id', 'user_id', string='Channel Members', - check_company=True, domain=[('share', '=', False)], + 'res.users', string='Salespersons', check_company=True, domain=[('share', '=', False)], + compute='_compute_member_ids', inverse='_inverse_member_ids', search='_search_member_ids', + help="Users assigned to this team.") + crm_team_member_ids = fields.One2many( + 'crm.team.member', 'crm_team_id', string='Sales Team Members', 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') + crm_team_member_all_ids = fields.One2many( + 'crm.team.member', 'crm_team_id', string='Sales Team Members (incl. inactive)', + context={'active_test': False}) # UX options color = fields.Integer(string='Color Index', help="The color of the channel") favorite_user_ids = fields.Many2many( @@ -63,6 +69,29 @@ class CrmTeam(models.Model): dashboard_button_name = fields.Char(string="Dashboard Button", compute='_compute_dashboard_button_name') dashboard_graph_data = fields.Text(compute='_compute_dashboard_graph') + @api.depends('crm_team_member_ids.active') + def _compute_member_ids(self): + for team in self: + team.member_ids = team.crm_team_member_ids.user_id + + def _inverse_member_ids(self): + for team in self: + # pre-save value to avoid having _compute_member_ids interfering + # while building membership status + memberships = team.crm_team_member_ids + users_current = team.member_ids + users_new = users_current - memberships.user_id + + # add missing memberships + self.env['crm.team.member'].create([{'crm_team_id': team.id, 'user_id': user.id} for user in users_new]) + + # activate or deactivate other memberships depending on members + for membership in memberships: + membership.active = membership.user_id in users_current + + def _search_member_ids(self, operator, value): + return [('crm_team_member_ids.user_id', operator, value)] + def _compute_is_favorite(self): for team in self: team.is_favorite = self.env.user in team.favorite_user_ids diff --git a/addons/sales_team/models/crm_team_member.py b/addons/sales_team/models/crm_team_member.py index 878611840fa..937446d9a12 100644 --- a/addons/sales_team/models/crm_team_member.py +++ b/addons/sales_team/models/crm_team_member.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import fields, models +from odoo import api, fields, models class CrmTeamMember(models.Model): @@ -9,8 +9,42 @@ class CrmTeamMember(models.Model): _inherit = ['mail.thread'] _description = 'Sales Team Member' _rec_name = 'user_id' + _order = 'create_date ASC' - 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) + crm_team_id = fields.Many2one( + 'crm.team', string='Sales Team', + default=False, # TDE: temporary fix to activate depending computed fields + index=True, required=True) + user_id = fields.Many2one( + 'res.users', string='Salesman', # check responsible field + index=True, ondelete='cascade', required=True, + domain="['&', ('share', '=', False), ('id', 'not in', user_in_teams_ids)]") + user_in_teams_ids = fields.Many2many( + 'res.users', compute='_compute_user_in_teams_ids', + help='UX: Give users not to add in the currently chosen team to avoid duplicates') + active = fields.Boolean(string='Active', default=True) + # salesman information + image_1920 = fields.Image("Image", related="user_id.image_1920", max_width=1920, max_height=1920) + image_128 = fields.Image("Image (128)", related="user_id.image_128", max_width=128, max_height=128) + name = fields.Char(string='Name', related='user_id.display_name', readonly=False) + email = fields.Char(string='Email', related='user_id.email') + phone = fields.Char(string='Phone', related='user_id.phone') + mobile = fields.Char(string='Mobile', related='user_id.mobile') + company_id = fields.Many2one('res.company', string='Company', related='user_id.company_id') + + _sql_constraints = [ + ('crm_team_member_unique', + 'UNIQUE(crm_team_id,user_id)', + 'Error, team / user memberships should not be duplicated.'), + ] + + @api.depends('crm_team_id') + @api.depends_context('default_crm_team_id') + def _compute_user_in_teams_ids(self): + for member in self: + if member.crm_team_id: + member.user_in_teams_ids = member.crm_team_id.member_ids + elif self.env.context.get('default_crm_team_id'): + member.user_in_teams_ids = self.env['crm.team'].browse(self.env.context['default_crm_team_id']).member_ids + else: + member.user_in_teams_ids = self.env['res.users'] diff --git a/addons/sales_team/models/res_users.py b/addons/sales_team/models/res_users.py index 213561dcf7e..f89a744efbf 100644 --- a/addons/sales_team/models/res_users.py +++ b/addons/sales_team/models/res_users.py @@ -1,20 +1,35 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import fields, models +from odoo import api, 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. + crm_team_ids = fields.Many2many( + 'crm.team', 'crm_team_member', 'user_id', 'crm_team_id', string='Sales Teams', + check_company=True, copy=False, readonly=True, + compute='_compute_crm_team_ids', search='_search_crm_team_ids') + crm_team_member_ids = fields.One2many('crm.team.member', 'user_id', string='Sales Team Members') sale_team_id = fields.Many2one( - 'crm.team', string='User Sales Team', - related='crm_team_member_ids.crm_team_id', - readonly=False, store=True) + 'crm.team', string='User Sales Team', compute='_compute_sale_team_id', + readonly=True, store=True, + help="Main user sales team. Used notably for pipeline, or to set sales team in invoicing or subscription.") + + @api.depends('crm_team_member_ids.active') + def _compute_crm_team_ids(self): + for user in self: + user.crm_team_ids = user.crm_team_member_ids.crm_team_id + + def _search_crm_team_ids(self, operator, value): + return [('crm_team_member_ids.crm_team_id', operator, value)] + + @api.depends('crm_team_member_ids.crm_team_id', 'crm_team_member_ids.create_date', 'crm_team_member_ids.active') + def _compute_sale_team_id(self): + for user in self: + if not user.crm_team_member_ids.ids: + user.sale_team_id = False + else: + sorted_memberships = user.crm_team_member_ids # sorted by create date + user.sale_team_id = sorted_memberships[0].crm_team_id if sorted_memberships else False diff --git a/addons/sales_team/security/ir.model.access.csv b/addons/sales_team/security/ir.model.access.csv index 447f7daa188..b6e225ec29e 100644 --- a/addons/sales_team/security/ir.model.access.csv +++ b/addons/sales_team/security/ir.model.access.csv @@ -4,7 +4,7 @@ access_crm_team_user,crm.team.user,model_crm_team,sales_team.group_sale_salesman 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_team_member_manager,access.crm.team.member.manager,model_crm_team_member,sales_team.group_sale_manager,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/static/src/scss/crm_team_views.scss b/addons/sales_team/static/src/scss/crm_team_views.scss new file mode 100644 index 00000000000..ac451496af2 --- /dev/null +++ b/addons/sales_team/static/src/scss/crm_team_views.scss @@ -0,0 +1,40 @@ +$o-kanban-large-record-width: 400px; +$o-kanban-medium-record-width: 350px; + +.o_kanban_view.o_crm_team_kanban { + .o_kanban_group:not(.o_column_folded) { + width: $o-kanban-large-record-width + 2*$o-kanban-group-padding; + } + .o_kanban_record { + width: $o-kanban-large-record-width; + } +} + +.o_kanban_view.o_crm_team_member_kanban { + .o_kanban_group:not(.o_column_folded) { + width: $o-kanban-medium-record-width + 2*$o-kanban-group-padding; + } + .o_kanban_record { + width: $o-kanban-medium-record-width; + } + .ribbon { + &::before, &::after { + display: none; + } + + span { + background-color: $o-brand-odoo; + padding: 5px; + font-size: small; + height: auto; + } + } + .ribbon-top-right { + margin-top: -$o-kanban-dashboard-vpadding; + + span { + left: 0px; + right: 30px; + } + } +} diff --git a/addons/sales_team/static/src/scss/sales_team_dashboard.scss b/addons/sales_team/static/src/scss/sales_team_dashboard.scss deleted file mode 100644 index 163d11138d2..00000000000 --- a/addons/sales_team/static/src/scss/sales_team_dashboard.scss +++ /dev/null @@ -1,8 +0,0 @@ - -.o_kanban_view.o_kanban_dashboard.o_salesteam_kanban { - &.o_kanban_ungrouped { - .o_kanban_record { - width: 450px; - } - } -} diff --git a/addons/sales_team/tests/__init__.py b/addons/sales_team/tests/__init__.py index d3d022e9d99..cf596552de4 100644 --- a/addons/sales_team/tests/__init__.py +++ b/addons/sales_team/tests/__init__.py @@ -3,3 +3,5 @@ from . import common from . import test_sales_team +from . import test_sales_team_internals +from . import test_sales_team_membership diff --git a/addons/sales_team/tests/common.py b/addons/sales_team/tests/common.py index fd9b366d80a..7b2ee4a4d9a 100644 --- a/addons/sales_team/tests/common.py +++ b/addons/sales_team/tests/common.py @@ -56,7 +56,14 @@ class TestSalesCommon(TransactionCase): 'sequence': 5, 'company_id': False, 'user_id': cls.user_sales_manager.id, - 'member_ids': [(4, cls.user_sales_leads.id), (4, cls.user_admin.id)], + }) + cls.sales_team_1_m1 = cls.env['crm.team.member'].create({ + 'user_id': cls.user_sales_leads.id, + 'crm_team_id': cls.sales_team_1.id, + }) + cls.sales_team_1_m2 = cls.env['crm.team.member'].create({ + 'user_id': cls.user_admin.id, + 'crm_team_id': cls.sales_team_1.id, }) diff --git a/addons/sales_team/tests/test_sales_team.py b/addons/sales_team/tests/test_sales_team.py index c8b7f965f8e..2808aadf2bd 100644 --- a/addons/sales_team/tests/test_sales_team.py +++ b/addons/sales_team/tests/test_sales_team.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo.addons.sales_team.tests.common import TestSalesCommon +from odoo.tests.common import users class TestDefaultTeam(TestSalesCommon): @@ -47,6 +48,7 @@ class TestDefaultTeam(TestSalesCommon): def test_default_team_fallback(self): """ Test fallback: domain, order """ self.sales_team_1.member_ids = [(5,)] + self.sales_team_1.flush() with self.with_user('user_sales_leads'): team = self.env['crm.team']._get_default_team_id() diff --git a/addons/sales_team/tests/test_sales_team_internals.py b/addons/sales_team/tests/test_sales_team_internals.py new file mode 100644 index 00000000000..500b0bcdf5b --- /dev/null +++ b/addons/sales_team/tests/test_sales_team_internals.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import exceptions +from odoo.addons.sales_team.tests.common import TestSalesMC +from odoo.tests.common import users + + +class TestSecurity(TestSalesMC): + + @users('user_sales_leads') + def test_team_access(self): + sales_team = self.sales_team_1.with_user(self.env.user) + + sales_team.read(['name']) + for member in sales_team.member_ids: + member.read(['name']) + + with self.assertRaises(exceptions.AccessError): + sales_team.write({'name': 'Trolling'}) + + for membership in sales_team.crm_team_member_ids: + membership.read(['name']) + with self.assertRaises(exceptions.AccessError): + membership.write({'active': False}) + + with self.assertRaises(exceptions.AccessError): + sales_team.write({'member_ids': [(5, 0)]}) + + @users('user_sales_leads') + def test_team_multi_company(self): + self.sales_team_1.with_user(self.env.user).read(['name']) + with self.assertRaises(exceptions.AccessError): + self.team_c2.with_user(self.env.user).read(['name']) diff --git a/addons/sales_team/tests/test_sales_team_membership.py b/addons/sales_team/tests/test_sales_team_membership.py new file mode 100644 index 00000000000..72376f44d9d --- /dev/null +++ b/addons/sales_team/tests/test_sales_team_membership.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.sales_team.tests.common import TestSalesCommon +from odoo.tests.common import users + + +class TestMembership(TestSalesCommon): + """Tests to ensure membership behavior """ + + @classmethod + def setUpClass(cls): + super(TestMembership, cls).setUpClass() + cls.new_team = cls.env['crm.team'].create({ + 'name': 'Test Specific', + 'sequence': 10, + }) + + @users('user_sales_manager') + def test_membership_sync(self): + self.assertEqual(self.new_team.crm_team_member_ids, self.env['crm.team.member']) + self.assertEqual(self.new_team.crm_team_member_all_ids, self.env['crm.team.member']) + self.assertEqual(self.new_team.member_ids, self.env['res.users']) + + # creating memberships correctly updates m2m without any refresh + new_member = self.env['crm.team.member'].create({ + 'user_id': self.env.user.id, + 'crm_team_id': self.new_team.id, + }) + self.assertEqual(self.new_team.crm_team_member_ids, new_member) + self.assertEqual(self.new_team.crm_team_member_all_ids, new_member) + self.assertEqual(self.new_team.member_ids, self.env.user) + + # adding members correctly update o2m with right values + self.new_team.write({ + 'member_ids': [(4, self.user_sales_leads.id)] + }) + added = self.env['crm.team.member'].search([('crm_team_id', '=', self.new_team.id), ('user_id', '=', self.user_sales_leads.id)]) + self.assertEqual(self.new_team.crm_team_member_ids, new_member + added) + self.assertEqual(self.new_team.crm_team_member_all_ids, new_member + added) + self.assertEqual(self.new_team.member_ids, self.env.user | self.user_sales_leads) + + # archiving membership correctly updates m2m and o2m + added.write({'active': False}) + self.assertEqual(self.new_team.crm_team_member_ids, new_member) + self.assertEqual(self.new_team.crm_team_member_all_ids, new_member + added) + self.assertEqual(self.new_team.member_ids, self.env.user) + + # reactivating correctly updates m2m and o2m + added.write({'active': True}) + self.assertEqual(self.new_team.crm_team_member_ids, new_member + added) + self.assertEqual(self.new_team.crm_team_member_all_ids, new_member + added) + self.assertEqual(self.new_team.member_ids, self.env.user | self.user_sales_leads) + + # send to db as errors may pop at that step (like trying to set NULL on a m2o inverse of o2m) + self.new_team.flush() + + def test_users_sale_team_id(self): + self.assertTrue(self.sales_team_1.sequence < self.new_team.sequence) + + self.assertEqual(self.user_sales_leads.crm_team_ids, self.sales_team_1) + self.assertEqual(self.user_sales_leads.sale_team_id, self.sales_team_1) + + # subscribe to new team -> default team is still the old one + self.new_team.write({ + 'member_ids': [(4, self.user_sales_leads.id)] + }) + self.assertEqual(self.user_sales_leads.crm_team_ids, self.sales_team_1 | self.new_team) + self.assertEqual(self.user_sales_leads.sale_team_id, self.sales_team_1) + + # archive membership to first team -> second one becomes default + self.sales_team_1_m1.write({'active': False}) + self.assertEqual(self.user_sales_leads.crm_team_ids, self.new_team) + self.assertEqual(self.user_sales_leads.sale_team_id, self.new_team) + + # activate membership to first team -> first one becomes default again + self.sales_team_1_m1.write({'active': True}) + self.assertEqual(self.user_sales_leads.crm_team_ids, self.sales_team_1 | self.new_team) + self.assertEqual(self.user_sales_leads.sale_team_id, self.sales_team_1) + + # keep only one membership -> default team + self.sales_team_1_m1.unlink() + self.assertEqual(self.user_sales_leads.crm_team_ids, self.new_team) + self.assertEqual(self.user_sales_leads.sale_team_id, self.new_team) diff --git a/addons/sales_team/views/assets.xml b/addons/sales_team/views/assets.xml index 3331b7cf4c1..34290b8bab7 100644 --- a/addons/sales_team/views/assets.xml +++ b/addons/sales_team/views/assets.xml @@ -1,8 +1,8 @@Create a new salesman
- Link users to salesteam. + Link salespersons to sales teams.
diff --git a/addons/sales_team/views/crm_team_views.xml b/addons/sales_team/views/crm_team_views.xml index 474ba15db30..5a362e6a4b8 100644 --- a/addons/sales_team/views/crm_team_views.xml +++ b/addons/sales_team/views/crm_team_views.xml @@ -9,8 +9,9 @@%s
%s