Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[ADD] sales_team_security
==============================
Security rules for sales teams
==============================

This module sets different permissions levels for accessing sales and CRM
records based on the sales team: customers, sales orders, leads, opportunities,
phone calls and sales teams.

It also handles the propagation of the sales team from commercial partners to
the contacts, which standard doesn't make.

Installation
============

At installation time, this module sets int all the contacts that have the sales
team empty the sales team of the parent. If you have a lot of contacts, this
operation can take a while.

Usage
=====

On the user configuration (Configuration > Users > Users), select in the
*Sales Team* section the option "See only own team". Then, the documents
mentioned before will be filtered out to have only those belonging to the
teams the user belongs to.

This is complementary to the Sales level access, but sometimes can be
incoherent depending on the combination chosen. If you chose "See Own Leads"
on _Sales_ section, marking on unmarking the sales team check will be
irrelevant, because the most restricting level, which the sales one, will
prevail.

Known issues/Roadmap
====================

* This module is designed for supporting only sales part, so someone that has
  access to other Odoo parts (for example, an accountant), shouldn't have
  this new permission, or some access errors will be found when seeing invoices
  and other documents. A _sales_team_security_account_ bridge module can be
  done for fixing this case, but not in the case of for example warehouse.
  • Loading branch information
pedrobaeza committed Mar 14, 2017
1 parent a3cca91 commit 17efd97
Show file tree
Hide file tree
Showing 12 changed files with 328 additions and 0 deletions.
79 changes: 79 additions & 0 deletions sales_team_security/README.rst
@@ -0,0 +1,79 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3

==============================
Security rules for sales teams
==============================

This module sets different permissions levels for accessing sales and CRM
records based on the sales team: customers, sales orders, leads, opportunities,
phone calls and sales teams.

It also handles the propagation of the sales team from commercial partners to
the contacts, which standard doesn't make.

Installation
============

At installation time, this module sets in all the contacts that have the sales
team empty the sales team of the parent. If you have a lot of contacts, this
operation can take a while.

Configuration
=============

On the user configuration (Configuration > Users > Users), select in the
*Sales Team* section the option "See only own team". Then, the documents
mentioned before will be filtered out to have only those belonging to the
teams the user belongs to.

This is complementary to the "Sales" level access, but sometimes can be
incoherent depending on the combination chosen. If you chose "See Own Leads"
on *Sales* section, marking or unmarking the sales team check will be
irrelevant, because the most restricting level, which is the sales one, will
prevail.

.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot
:target: https://runbot.odoo-community.org/runbot/167/8.0

Bug Tracker
===========

Bugs are tracked on `GitHub Issues
<https://github.com/OCA/sale-workflow/issues>`_. In case of trouble, please
check there if your issue has already been reported. If you spotted it first,
help us smashing it by providing a detailed and welcomed feedback.

Known issues/Roadmap
====================

* This module is designed for supporting only sales part, so someone that has
access to other Odoo parts (for example, an accountant), shouldn't have
this new permission, or some access errors will be found when seeing invoices
and other documents. A *sales_team_security_account* bridge module can be
done for fixing this case, but not in the case of for example warehouse.

Credits
=======

Contributors
------------

* Pedro M. Baeza <pedro.baeza@serviciosbaeza.com>

Maintainer
----------

.. image:: http://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

This module is maintained by the OCA.

OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.

To contribute to this module, please visit https://odoo-community.org.
5 changes: 5 additions & 0 deletions sales_team_security/__init__.py
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html

from . import models
from .hooks import assign_contacts_team
24 changes: 24 additions & 0 deletions sales_team_security/__openerp__.py
@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Tecnativa - Pedro M. Baeza
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html

{
"name": "Sales teams security",
"version": "8.0.1.0.0",
"license": "AGPL-3",
"depends": [
"sales_team",
"sale",
"crm",
],
"author": "Tecnativa, "
"Odoo Community Association (OCA)",
"website": "https://www.tecnativa.com",
"category": "Sales Management",
"installable": True,
"data": [
'security/sales_team_security.xml',
'views/res_partner_view.xml',
],
"post_init_hook": "assign_contacts_team",
}
19 changes: 19 additions & 0 deletions sales_team_security/hooks.py
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Tecnativa - Pedro M. Baeza
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html


def assign_contacts_team(cr, registry):
"""At installation time, propagate the parent sales team to the children
contacts that have this field empty, as it's supposed that the intention
is to have the same.
"""
cr.execute(
"""
UPDATE res_partner
SET section_id=parent.section_id
FROM res_partner AS parent
WHERE parent.section_id IS NOT NULL
AND res_partner.parent_id = parent.id
AND res_partner.section_id IS NULL
""")
5 changes: 5 additions & 0 deletions sales_team_security/models/__init__.py
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html

from . import res_partner
from . import res_users
39 changes: 39 additions & 0 deletions sales_team_security/models/res_partner.py
@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Tecnativa - Pedro M. Baeza
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html

from openerp import api, models
from lxml import etree


class ResPartner(models.Model):
_inherit = 'res.partner'

@api.model
def fields_view_get(self, view_id=None, view_type='form', toolbar=False,
submenu=False):
"""Patch view to inject the default value for the section_id."""
res = super(ResPartner, self).fields_view_get(
view_id=view_id, view_type=view_type, toolbar=toolbar,
submenu=submenu)
if view_type == 'form':
eview = etree.fromstring(res['arch'])
xml_fields = eview.xpath("//field[@name='child_ids']")
if xml_fields:
context_str = xml_fields[0].get('context', '{}').replace(
'{', "{'default_section_id': section_id, ", 1,
)
xml_fields[0].set('context', context_str)
res['arch'] = etree.tostring(eview)
return res

@api.multi
def onchange_address(self, use_parent_address, parent_id):
res = super(ResPartner, self).onchange_address(
use_parent_address, parent_id)
if parent_id:
parent = self.browse(parent_id)
if parent.section_id:
value = res.setdefault('value', {})
value['section_id'] = parent.section_id.id
return res
13 changes: 13 additions & 0 deletions sales_team_security/models/res_users.py
@@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Tecnativa - Pedro M. Baeza
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html

from openerp import fields, models


class ResUsers(models.Model):
_inherit = 'res.users'

sale_team_ids = fields.Many2many(
comodel_name="crm.case.section", string="Sales teams",
relation='sale_member_rel', column1='member_id', column2='section_id')
82 changes: 82 additions & 0 deletions sales_team_security/security/sales_team_security.xml
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="0">

<record model="ir.module.category" id="module_category_sales_team">
<field name="name">Sales Teams</field>
<field name="sequence">2</field>
</record>

<record id="group_see_only_own_team" model="res.groups">
<field name="name">See only own team</field>
<field name="category_id" ref="sales_team_security.module_category_sales_team"/>
</record>

</data>
<data noupdate="1">

<record id="sale_order_team_rule" model="ir.rule">
<field name="name">Sales Team Orders</field>
<field ref="sale.model_sale_order" name="model_id"/>
<field name="domain_force">['|', ('message_follower_ids', 'in', user.partner_id.ids), '|', ('section_id', 'in', user.sale_team_ids.ids), ('section_id', '=', False)]</field>
<field name="groups" eval="[(4, ref('sales_team_security.group_see_only_own_team'))]"/>
</record>

<record id="sale_order_report_team_rule" model="ir.rule">
<field name="name">Sales Team Orders Analysis</field>
<field ref="sale.model_sale_report" name="model_id"/>
<field name="domain_force">['|', ('message_follower_ids', 'in', user.partner_id.ids), '|', ('section_id', 'in', user.sale_team_ids.ids), ('section_id', '=', False)]</field>
<field name="groups" eval="[(4, ref('sales_team_security.group_see_only_own_team'))]"/>
</record>

<record id="sale_order_line_team_rule" model="ir.rule">
<field name="name">Sales Team Order Lines</field>
<field ref="sale.model_sale_order_line" name="model_id"/>
<field name="domain_force">['|', ('message_follower_ids', 'in', user.partner_id.ids), '|', ('section_id', 'in', user.sale_team_ids.ids), ('section_id', '=', False)]</field>
<field name="groups" eval="[(4, ref('sales_team_security.group_see_only_own_team'))]"/>
</record>

<record id="res_partner_team_rule" model="ir.rule">
<field name="name">Sales Team Partners</field>
<field ref="base.model_res_partner" name="model_id"/>
<field name="domain_force">['|', ('message_follower_ids', 'in', user.partner_id.ids), '|', ('section_id', 'in', user.sale_team_ids.ids), ('section_id', '=', False)]</field>
<field name="groups" eval="[(4, ref('sales_team_security.group_see_only_own_team'))]"/>
</record>

<record id="crm_lead_team_rule" model="ir.rule">
<field name="name">Sales Team Leads/Opportunities</field>
<field ref="crm.model_crm_lead" name="model_id"/>
<field name="domain_force">['|', ('message_follower_ids', 'in', user.partner_id.ids), '|', ('section_id', 'in', user.sale_team_ids.ids), ('section_id', '=', False)]</field>
<field name="groups" eval="[(4, ref('sales_team_security.group_see_only_own_team'))]"/>
</record>

<record id="crm_lead_report_team" model="ir.rule">
<field name="name">Sales Team Leads Analysis</field>
<field ref="crm.model_crm_lead_report" name="model_id"/>
<field name="domain_force">['|', ('message_follower_ids', 'in', user.partner_id.ids), '|', ('section_id', 'in', user.sale_team_ids.ids), ('section_id', '=', False)]</field>
<field name="groups" eval="[(4, ref('sales_team_security.group_see_only_own_team'))]"/>
</record>

<record id="crm_phonecall_team_rule" model="ir.rule">
<field name="name">Sales Team Phone Calls</field>
<field ref="crm.model_crm_phonecall" name="model_id"/>
<field name="domain_force">['|', ('message_follower_ids', 'in', user.partner_id.ids), '|', ('section_id', 'in', user.sale_team_ids.ids), ('section_id', '=', False)]</field>
<field name="groups" eval="[(4, ref('sales_team_security.group_see_only_own_team'))]"/>
</record>

<record id="crm_phonecall_report_team" model="ir.rule">
<field name="name">Sales Team Phone Calls Analysis</field>
<field ref="crm.model_crm_phonecall_report" name="model_id"/>
<field name="domain_force">['|', ('message_follower_ids', 'in', user.partner_id.ids), '|', ('section_id', 'in', user.sale_team_ids.ids), ('section_id', '=', False)]</field>
<field name="groups" eval="[(4, ref('sales_team_security.group_see_only_own_team'))]"/>
</record>

<record id="sales_team_team_rule" model="ir.rule">
<field name="name">Own Sales Teams</field>
<field ref="sales_team.model_crm_case_section" name="model_id"/>
<field name="domain_force">['|', ('message_follower_ids', 'in', user.partner_id.ids), ('id', 'in', user.sale_team_ids.ids)]</field>
<field name="groups" eval="[(4, ref('sales_team_security.group_see_only_own_team'))]"/>
</record>

</data>
</openerp>
Binary file added sales_team_security/static/description/icon.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions sales_team_security/tests/__init__.py
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html

from . import test_sales_team_security
43 changes: 43 additions & 0 deletions sales_team_security/tests/test_sales_team_security.py
@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Tecnativa - Pedro M. Baeza
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html

from openerp.tests import common
from ..hooks import assign_contacts_team
from lxml import etree


class TestSalesTeamSecurity(common.SavepointCase):
@classmethod
def setUpClass(cls):
super(TestSalesTeamSecurity, cls).setUpClass()
cls.section = cls.env['crm.case.section'].create({
'name': 'Test section',
})
cls.partner = cls.env['res.partner'].create({
'name': 'Test partner',
'section_id': cls.section.id,
})

def test_onchange_parent_id(self):
res = self.env['res.partner'].onchange_address(True, self.partner.id)
self.assertEqual(res['value']['section_id'], self.section.id)

def test_assign_contacts_team(self):
contact = self.env['res.partner'].create({
'name': 'Test contact',
'parent_id': self.partner.id,
'section_id': False,
})
assign_contacts_team(self.env.cr, self.env.registry)
contact.refresh()
self.assertEqual(contact.section_id, self.partner.section_id)

def test_partner_fields_view_get(self):
res = self.env['res.partner'].fields_view_get(
view_id=self.ref('base.view_partner_form'))
eview = etree.fromstring(res['arch'])
xml_fields = eview.xpath("//field[@name='child_ids']")
self.assertTrue(xml_fields)
self.assertTrue(
'default_section_id' in xml_fields[0].get('context', ''))
15 changes: 15 additions & 0 deletions sales_team_security/views/res_partner_view.xml
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="view_partner_form" model="ir.ui.view">
<field name="name">Partner form (with sales team in contacts)</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='child_ids']/form//field[@name='function']" position="before">
<field name="section_id" invisible="1" groups="base.group_multi_salesteams"/>
</xpath>
</field>
</record>
</data>
</openerp>

0 comments on commit 17efd97

Please sign in to comment.