Rodney Enciso Arias 8 lat temu
commit
493dbcb5fa

+ 79 - 0
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 - 0
__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

BIN
__init__.pyc


+ 24 - 0
__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 - 0
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
+        """)

BIN
hooks.pyc


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

BIN
models/__init__.pyc


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

BIN
models/res_partner.pyc


+ 13 - 0
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')

BIN
models/res_users.pyc


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

BIN
static/description/icon.png


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