diff --git a/README.md b/README.md index abe73b04831..110fb5d9d47 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ addon | version | maintainers | summary [purchase_request](purchase_request/) | 16.0.1.0.3 | | Use this module to have notification of requirements of materials and/or external services and keep track of such requirements. [purchase_request_tier_validation](purchase_request_tier_validation/) | 16.0.1.0.0 | | Extends the functionality of Purchase Requests to support a tier validation process. [purchase_requisition_tier_validation](purchase_requisition_tier_validation/) | 16.0.1.0.0 | | Extends the functionality of Purchase Agreements to support a tier validation process. -[purchase_security](purchase_security/) | 16.0.1.0.0 | [![pilarvargas-tecnativa](https://github.com/pilarvargas-tecnativa.png?size=30px)](https://github.com/pilarvargas-tecnativa) | See only your purchase orders +[purchase_security](purchase_security/) | 16.0.2.0.0 | [![pilarvargas-tecnativa](https://github.com/pilarvargas-tecnativa.png?size=30px)](https://github.com/pilarvargas-tecnativa) | See only your purchase orders [purchase_stock_packaging](purchase_stock_packaging/) | 16.0.1.0.0 | [![rousseldenis](https://github.com/rousseldenis.png?size=30px)](https://github.com/rousseldenis) | Allows to transmit the product packaging from the procurement values to the generated purchase order line [purchase_tag](purchase_tag/) | 16.0.1.1.0 | | Allows to add multiple tags to purchase orders [purchase_tier_validation](purchase_tier_validation/) | 16.0.1.1.0 | | Extends the functionality of Purchase Orders to support a tier validation process. diff --git a/purchase_security/README.rst b/purchase_security/README.rst index 902edf22830..c388628141a 100644 --- a/purchase_security/README.rst +++ b/purchase_security/README.rst @@ -7,7 +7,7 @@ Purchase Order security !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:2644bb2f51446a5ee8a417a4cc70af517724fe84f7858e0ad32227d1e805e8d1 + !! source digest: sha256:c16cac2ffcc0b82500b7eacd7f41279805c1df3dacd03c09ca1ee90726358a5e !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png @@ -28,10 +28,15 @@ Purchase Order security |badge1| |badge2| |badge3| |badge4| |badge5| -This addon creates a new group called "Purchase (own orders)" in Purchase. +This addon creates new groups in Purchase. -For users in this group, the only Purchase Orders they can see are those where -they are the representative, or all of them if they are managers. +Visibility of purchase orders is restricted for users in these groups. +You can only see the purchase order: + +- User (own orders): If you are a follower of the partner or there is no user + or you are the partner's user. +- User (team orders): If you are a follower of the partner or there is no user + or you are a user of your purchasing team. **Table of contents** @@ -78,6 +83,8 @@ Contributors * João Marques * Pilar Vargas + * Stefan Ungureanu + * Pedro M. Baeza * `Solvos `_: * David Alonso diff --git a/purchase_security/__manifest__.py b/purchase_security/__manifest__.py index 9db0f140446..b3449db5f91 100644 --- a/purchase_security/__manifest__.py +++ b/purchase_security/__manifest__.py @@ -2,7 +2,7 @@ { "name": "Purchase Order security", - "version": "16.0.1.0.0", + "version": "16.0.2.0.0", "category": "Purchase", "development_status": "Production/Stable", "author": "Tecnativa, Odoo Community Association (OCA)", @@ -11,7 +11,13 @@ "license": "AGPL-3", "depends": ["purchase"], "maintainers": ["pilarvargas-tecnativa"], - "data": ["security/security.xml", "views/purchase_order_views.xml"], + "data": [ + "security/security.xml", + "security/ir.model.access.csv", + "views/purchase_order_views.xml", + "views/purchase_team_views.xml", + "views/res_partner_views.xml", + ], "installable": True, "auto_install": False, } diff --git a/purchase_security/i18n/es.po b/purchase_security/i18n/es.po index 1e47ede510f..17e65158f3a 100644 --- a/purchase_security/i18n/es.po +++ b/purchase_security/i18n/es.po @@ -16,17 +16,144 @@ msgstr "" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 4.17\n" +#. module: purchase_security +#: model_terms:ir.ui.view,arch_db:purchase_security.purchase_team_form +msgid "" +"" +msgstr "" + +#. module: purchase_security +#: model_terms:ir.ui.view,arch_db:purchase_security.purchase_team_form +msgid "Avatar" +msgstr "" + +#. module: purchase_security +#: model:ir.model,name:purchase_security.model_res_partner +msgid "Contact" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_purchase_team__create_uid +msgid "Created by" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_purchase_team__create_date +msgid "Created on" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_purchase_team__display_name +msgid "Display Name" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_purchase_team__id +msgid "ID" +msgstr "" + #. module: purchase_security #: model:ir.model.fields,field_description:purchase_security.field_purchase_order__is_user_id_editable msgid "Is User Id Editable" msgstr "Se puede editar el ID de usuario" +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_purchase_team____last_update +msgid "Last Modified on" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_purchase_team__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_purchase_team__write_date +msgid "Last Updated on" +msgstr "" + +#. module: purchase_security +#: model_terms:ir.ui.view,arch_db:purchase_security.purchase_team_form +msgid "Members" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_purchase_team__name +msgid "Name" +msgstr "" + #. module: purchase_security #: model:ir.model,name:purchase_security.model_purchase_order msgid "Purchase Order" -msgstr "orden de compra" +msgstr "Pedido de compra" + +#. module: purchase_security +#: model:ir.model,name:purchase_security.model_purchase_team +#: model_terms:ir.ui.view,arch_db:purchase_security.purchase_team_form +msgid "Purchase Team" +msgstr "" + +#. module: purchase_security +#: model:ir.actions.act_window,name:purchase_security.action_purchase_team_display +#: model:ir.ui.menu,name:purchase_security.menu_purchase_team_tree +msgid "Purchase Teams" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_purchase_team__user_ids +msgid "Purchase Users" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_res_partner__purchase_user_id +#: model:ir.model.fields,field_description:purchase_security.field_res_users__purchase_user_id +#: model_terms:ir.ui.view,arch_db:purchase_security.view_res_partner_filter +msgid "Purchase representative" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_res_partner__purchase_team_id +#: model:ir.model.fields,field_description:purchase_security.field_res_users__purchase_team_id +msgid "Purchase team" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_res_users__purchase_team_ids +msgid "Purchases Teams" +msgstr "" + +#. module: purchase_security +#: model:ir.model,name:purchase_security.model_ir_rule +msgid "Record Rule" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_purchase_team__sequence +msgid "Sequence" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_purchase_order__team_id +msgid "Team" +msgstr "" + +#. module: purchase_security +#: model:ir.model,name:purchase_security.model_res_users +msgid "User" +msgstr "" #. module: purchase_security #: model:res.groups,name:purchase_security.group_purchase_own_orders msgid "User (own orders)" -msgstr "Usuario (sus propios pedidos)" +msgstr "Usuario (pedidos propios)" + +#. module: purchase_security +#: model:res.groups,name:purchase_security.group_purchase_group_orders +msgid "User (team orders)" +msgstr "" + +#. module: purchase_security +#: model_terms:ir.ui.view,arch_db:purchase_security.purchase_team_form +msgid "e.g. Europe" +msgstr "" diff --git a/purchase_security/i18n/it.po b/purchase_security/i18n/it.po index 05cf30ad207..23b7fa38d70 100644 --- a/purchase_security/i18n/it.po +++ b/purchase_security/i18n/it.po @@ -16,17 +16,144 @@ msgstr "" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 4.17\n" +#. module: purchase_security +#: model_terms:ir.ui.view,arch_db:purchase_security.purchase_team_form +msgid "" +"" +msgstr "" + +#. module: purchase_security +#: model_terms:ir.ui.view,arch_db:purchase_security.purchase_team_form +msgid "Avatar" +msgstr "" + +#. module: purchase_security +#: model:ir.model,name:purchase_security.model_res_partner +msgid "Contact" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_purchase_team__create_uid +msgid "Created by" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_purchase_team__create_date +msgid "Created on" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_purchase_team__display_name +msgid "Display Name" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_purchase_team__id +msgid "ID" +msgstr "" + #. module: purchase_security #: model:ir.model.fields,field_description:purchase_security.field_purchase_order__is_user_id_editable msgid "Is User Id Editable" msgstr "L'ID utente è modificabile" +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_purchase_team____last_update +msgid "Last Modified on" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_purchase_team__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_purchase_team__write_date +msgid "Last Updated on" +msgstr "" + +#. module: purchase_security +#: model_terms:ir.ui.view,arch_db:purchase_security.purchase_team_form +msgid "Members" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_purchase_team__name +msgid "Name" +msgstr "" + #. module: purchase_security #: model:ir.model,name:purchase_security.model_purchase_order msgid "Purchase Order" msgstr "Ordine di acquisto" +#. module: purchase_security +#: model:ir.model,name:purchase_security.model_purchase_team +#: model_terms:ir.ui.view,arch_db:purchase_security.purchase_team_form +msgid "Purchase Team" +msgstr "" + +#. module: purchase_security +#: model:ir.actions.act_window,name:purchase_security.action_purchase_team_display +#: model:ir.ui.menu,name:purchase_security.menu_purchase_team_tree +msgid "Purchase Teams" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_purchase_team__user_ids +msgid "Purchase Users" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_res_partner__purchase_user_id +#: model:ir.model.fields,field_description:purchase_security.field_res_users__purchase_user_id +#: model_terms:ir.ui.view,arch_db:purchase_security.view_res_partner_filter +msgid "Purchase representative" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_res_partner__purchase_team_id +#: model:ir.model.fields,field_description:purchase_security.field_res_users__purchase_team_id +msgid "Purchase team" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_res_users__purchase_team_ids +msgid "Purchases Teams" +msgstr "" + +#. module: purchase_security +#: model:ir.model,name:purchase_security.model_ir_rule +msgid "Record Rule" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_purchase_team__sequence +msgid "Sequence" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_purchase_order__team_id +msgid "Team" +msgstr "" + +#. module: purchase_security +#: model:ir.model,name:purchase_security.model_res_users +msgid "User" +msgstr "" + #. module: purchase_security #: model:res.groups,name:purchase_security.group_purchase_own_orders msgid "User (own orders)" msgstr "Utente (solo propri ordini)" + +#. module: purchase_security +#: model:res.groups,name:purchase_security.group_purchase_group_orders +msgid "User (team orders)" +msgstr "" + +#. module: purchase_security +#: model_terms:ir.ui.view,arch_db:purchase_security.purchase_team_form +msgid "e.g. Europe" +msgstr "" diff --git a/purchase_security/i18n/purchase_security.pot b/purchase_security/i18n/purchase_security.pot index c0a819e1bd4..0e37b1a514f 100644 --- a/purchase_security/i18n/purchase_security.pot +++ b/purchase_security/i18n/purchase_security.pot @@ -13,17 +13,142 @@ msgstr "" "Content-Transfer-Encoding: \n" "Plural-Forms: \n" +#. module: purchase_security +#: model_terms:ir.ui.view,arch_db:purchase_security.purchase_team_form +msgid "" +msgstr "" + +#. module: purchase_security +#: model_terms:ir.ui.view,arch_db:purchase_security.purchase_team_form +msgid "Avatar" +msgstr "" + +#. module: purchase_security +#: model:ir.model,name:purchase_security.model_res_partner +msgid "Contact" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_purchase_team__create_uid +msgid "Created by" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_purchase_team__create_date +msgid "Created on" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_purchase_team__display_name +msgid "Display Name" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_purchase_team__id +msgid "ID" +msgstr "" + #. module: purchase_security #: model:ir.model.fields,field_description:purchase_security.field_purchase_order__is_user_id_editable msgid "Is User Id Editable" msgstr "" +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_purchase_team____last_update +msgid "Last Modified on" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_purchase_team__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_purchase_team__write_date +msgid "Last Updated on" +msgstr "" + +#. module: purchase_security +#: model_terms:ir.ui.view,arch_db:purchase_security.purchase_team_form +msgid "Members" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_purchase_team__name +msgid "Name" +msgstr "" + #. module: purchase_security #: model:ir.model,name:purchase_security.model_purchase_order msgid "Purchase Order" msgstr "" +#. module: purchase_security +#: model:ir.model,name:purchase_security.model_purchase_team +#: model_terms:ir.ui.view,arch_db:purchase_security.purchase_team_form +msgid "Purchase Team" +msgstr "" + +#. module: purchase_security +#: model:ir.actions.act_window,name:purchase_security.action_purchase_team_display +#: model:ir.ui.menu,name:purchase_security.menu_purchase_team_tree +msgid "Purchase Teams" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_purchase_team__user_ids +msgid "Purchase Users" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_res_partner__purchase_user_id +#: model:ir.model.fields,field_description:purchase_security.field_res_users__purchase_user_id +#: model_terms:ir.ui.view,arch_db:purchase_security.view_res_partner_filter +msgid "Purchase representative" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_res_partner__purchase_team_id +#: model:ir.model.fields,field_description:purchase_security.field_res_users__purchase_team_id +msgid "Purchase team" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_res_users__purchase_team_ids +msgid "Purchases Teams" +msgstr "" + +#. module: purchase_security +#: model:ir.model,name:purchase_security.model_ir_rule +msgid "Record Rule" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_purchase_team__sequence +msgid "Sequence" +msgstr "" + +#. module: purchase_security +#: model:ir.model.fields,field_description:purchase_security.field_purchase_order__team_id +msgid "Team" +msgstr "" + +#. module: purchase_security +#: model:ir.model,name:purchase_security.model_res_users +msgid "User" +msgstr "" + #. module: purchase_security #: model:res.groups,name:purchase_security.group_purchase_own_orders msgid "User (own orders)" msgstr "" + +#. module: purchase_security +#: model:res.groups,name:purchase_security.group_purchase_group_orders +msgid "User (team orders)" +msgstr "" + +#. module: purchase_security +#: model_terms:ir.ui.view,arch_db:purchase_security.purchase_team_form +msgid "e.g. Europe" +msgstr "" diff --git a/purchase_security/models/__init__.py b/purchase_security/models/__init__.py index 9f03530643d..38e405adafa 100644 --- a/purchase_security/models/__init__.py +++ b/purchase_security/models/__init__.py @@ -1 +1,5 @@ +from . import ir_rule from . import purchase_order +from . import purchase_team +from . import res_partner +from . import res_users diff --git a/purchase_security/models/ir_rule.py b/purchase_security/models/ir_rule.py new file mode 100644 index 00000000000..fd6467715ec --- /dev/null +++ b/purchase_security/models/ir_rule.py @@ -0,0 +1,57 @@ +# Copyright 2024 Tecnativa - Víctor Martínez +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html + +from odoo import api, models, tools +from odoo.osv import expression +from odoo.tools import config + + +class IrRule(models.Model): + _inherit = "ir.rule" + + @api.model + @tools.conditional( + "xml" not in config["dev_mode"], + tools.ormcache( + "self.env.uid", + "self.env.su", + "model_name", + "mode", + "tuple(self._compute_domain_context_values())", + ), + ) + def _compute_domain(self, model_name, mode="read"): + """Inject extra domain for restricting partners when the user + has the group 'Purchase / User (own orders).""" + res = super()._compute_domain(model_name, mode=mode) + user = self.env.user + group1 = "purchase_security.group_purchase_own_orders" + group2 = "purchase_security.group_purchase_group_orders" + group3 = "purchase.group_purchase_manager" + if model_name == "res.partner" and not self.env.su: + if user.has_group(group1) and not user.has_group(group3): + extra_domain = [ + "|", + ("message_partner_ids", "in", user.partner_id.ids), + "|", + ("id", "=", user.partner_id.id), + ] + if user.has_group(group2): + extra_domain += [ + "|", + ("purchase_team_id", "=", user.purchase_team_ids[:1].id), + ("purchase_team_id", "=", False), + ] + else: + extra_domain += [ + "|", + ("purchase_user_id", "=", user.id), + "&", + ("purchase_user_id", "=", False), + "|", + ("purchase_team_id", "=", False), + ("purchase_team_id", "=", user.purchase_team_ids[:1].id), + ] + extra_domain = expression.normalize_domain(extra_domain) + res = expression.AND([extra_domain] + [res]) + return res diff --git a/purchase_security/models/purchase_order.py b/purchase_security/models/purchase_order.py index b312c05eb0d..4e72337e093 100644 --- a/purchase_security/models/purchase_order.py +++ b/purchase_security/models/purchase_order.py @@ -1,7 +1,10 @@ # © 2023 Solvos Consultoría Informática () -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +# Copyright 2023 Tecnativa - Stefan Ungureanu +# Copyright 2023 Tecnativa - Pedro M. Baeza +# Copyright 2024 Tecnativa - Víctor Martínez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo import fields, models +from odoo import api, fields, models class PurchaseOrder(models.Model): @@ -10,9 +13,41 @@ class PurchaseOrder(models.Model): is_user_id_editable = fields.Boolean( compute="_compute_is_user_id_editable", ) + team_id = fields.Many2one( + "purchase.team", + string="Team", + index=True, + auto_join=True, + compute="_compute_team_id", + store=True, + readonly=False, + ) def _compute_is_user_id_editable(self): is_user_id_editable = self.env.user.has_group( "purchase.group_purchase_manager" ) or not self.env.user.has_group("purchase_security.group_purchase_own_orders") self.write({"is_user_id_editable": is_user_id_editable}) + + @api.depends("user_id") + def _compute_team_id(self): + """When a user is assigned, the first team which the user belongs to is + assigned, and if no one, the first purchase team. + """ + first_team = self.env["purchase.team"].search([], limit=1) + for record in self: + record.team_id = record.user_id.purchase_team_ids[:1] or first_team + + def onchange_partner_id(self): + res = super().onchange_partner_id() + if self.partner_id: + partner = self.partner_id.commercial_partner_id + if not self.env.context.get("default_user_id"): + self.user_id = partner.purchase_user_id or self.env.user + if not self.env.context.get("default_team_id"): + self.team_id = ( + partner.purchase_team_id + or self.user_id.purchase_team_ids[:1] + or self.env["purchase.team"].search([], limit=1) + ) + return res diff --git a/purchase_security/models/purchase_team.py b/purchase_security/models/purchase_team.py new file mode 100644 index 00000000000..13126178e79 --- /dev/null +++ b/purchase_security/models/purchase_team.py @@ -0,0 +1,21 @@ +# Copyright 2023 Tecnativa - Stefan Ungureanu +# Copyright 2023 Tecnativa - Pedro M. Baeza +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class PurchaseTeam(models.Model): + _name = "purchase.team" + _description = "Purchase Team" + _order = "sequence,id" + + name = fields.Char(required=True) + sequence = fields.Integer(default=10) + user_ids = fields.Many2many( + comodel_name="res.users", + relation="purchase_team_res_users_rel", + column1="purchase_team_id", + column2="res_users_id", + string="Purchase Users", + ) diff --git a/purchase_security/models/res_partner.py b/purchase_security/models/res_partner.py new file mode 100644 index 00000000000..9e889e487de --- /dev/null +++ b/purchase_security/models/res_partner.py @@ -0,0 +1,20 @@ +# Copyright 2024 Tecnativa - Víctor Martínez +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html + +from odoo import fields, models + + +class ResPartner(models.Model): + _inherit = "res.partner" + + purchase_user_id = fields.Many2one( + comodel_name="res.users", + domain="[('share', '=', False)]", + string="Purchase representative", + index=True, + ) + purchase_team_id = fields.Many2one( + comodel_name="purchase.team", + string="Purchase team", + index=True, + ) diff --git a/purchase_security/models/res_users.py b/purchase_security/models/res_users.py new file mode 100644 index 00000000000..663ce99e091 --- /dev/null +++ b/purchase_security/models/res_users.py @@ -0,0 +1,19 @@ +# Copyright 2024 Tecnativa - Víctor Martínez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResUsers(models.Model): + _inherit = "res.users" + + purchase_team_ids = fields.Many2many( + comodel_name="purchase.team", + relation="purchase_team_res_users_rel", + column1="res_users_id", + column2="purchase_team_id", + string="Purchases Teams", + check_company=True, + copy=False, + readonly=True, + ) diff --git a/purchase_security/readme/CONTRIBUTORS.rst b/purchase_security/readme/CONTRIBUTORS.rst index 0d457e9bd24..eccebd01a91 100644 --- a/purchase_security/readme/CONTRIBUTORS.rst +++ b/purchase_security/readme/CONTRIBUTORS.rst @@ -2,6 +2,8 @@ * João Marques * Pilar Vargas + * Stefan Ungureanu + * Pedro M. Baeza * `Solvos `_: * David Alonso diff --git a/purchase_security/readme/DESCRIPTION.rst b/purchase_security/readme/DESCRIPTION.rst index 040e02e5d7d..ec67d54b8d0 100644 --- a/purchase_security/readme/DESCRIPTION.rst +++ b/purchase_security/readme/DESCRIPTION.rst @@ -1,4 +1,9 @@ -This addon creates a new group called "Purchase (own orders)" in Purchase. +This addon creates new groups in Purchase. -For users in this group, the only Purchase Orders they can see are those where -they are the representative, or all of them if they are managers. +Visibility of purchase orders is restricted for users in these groups. +You can only see the purchase order: + +- User (own orders): If you are a follower of the partner or there is no user + or you are the partner's user. +- User (team orders): If you are a follower of the partner or there is no user + or you are a user of your purchasing team. diff --git a/purchase_security/security/ir.model.access.csv b/purchase_security/security/ir.model.access.csv new file mode 100644 index 00000000000..6daeaddfd8e --- /dev/null +++ b/purchase_security/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +purchase_security.access_purchase_team_manager,access_purchase_team,purchase_security.model_purchase_team,purchase.group_purchase_manager,1,1,1,1 +purchase_security.access_purchase_team_user,access_purchase_team,purchase_security.model_purchase_team,purchase.group_purchase_user,1,0,0,0 diff --git a/purchase_security/security/security.xml b/purchase_security/security/security.xml index 2a58930cc43..f8976f42d2f 100644 --- a/purchase_security/security/security.xml +++ b/purchase_security/security/security.xml @@ -8,10 +8,19 @@ eval="[(4, ref('purchase.group_purchase_user'))]" /> + + User (team orders) + + + @@ -27,9 +36,15 @@ ['|',('user_id','=',user.id),('user_id','=',False)] + >['|',('user_id','=',user.id),'&',('user_id','=',False),('team_id','=',False)] + + View purchase orders (purchase team member) + + [('team_id.user_ids', '=', user.id)] + + View purchase order lines (manager) @@ -44,8 +59,16 @@ ['|',('order_id.user_id','=',user.id),('order_id.user_id','=',False)] + >['|',('order_id.user_id','=',user.id),'&',('order_id.user_id','=',False),('order_id.team_id','=',False)] + + View purchase order lines (purchase responsible) + + [('order_id.team_id.user_ids', '=', user.id)] + + diff --git a/purchase_security/static/description/index.html b/purchase_security/static/description/index.html index f4efb30aab2..4daabf2e435 100644 --- a/purchase_security/static/description/index.html +++ b/purchase_security/static/description/index.html @@ -1,4 +1,3 @@ - @@ -367,12 +366,18 @@

Purchase Order security

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:2644bb2f51446a5ee8a417a4cc70af517724fe84f7858e0ad32227d1e805e8d1 +!! source digest: sha256:c16cac2ffcc0b82500b7eacd7f41279805c1df3dacd03c09ca1ee90726358a5e !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Production/Stable License: AGPL-3 OCA/purchase-workflow Translate me on Weblate Try me on Runboat

-

This addon creates a new group called “Purchase (own orders)” in Purchase.

-

For users in this group, the only Purchase Orders they can see are those where -they are the representative, or all of them if they are managers.

+

This addon creates new groups in Purchase.

+

Visibility of purchase orders is restricted for users in these groups. +You can only see the purchase order:

+
    +
  • User (own orders): If you are a follower of the partner or there is no user +or you are the partner’s user.
  • +
  • User (team orders): If you are a follower of the partner or there is no user +or you are a user of your purchasing team.
  • +

Table of contents

    @@ -423,6 +428,8 @@

    Contributors

  • Tecnativa:
    • João Marques
    • Pilar Vargas
    • +
    • Stefan Ungureanu
    • +
    • Pedro M. Baeza
  • Solvos:
      diff --git a/purchase_security/tests/test_access_rights.py b/purchase_security/tests/test_access_rights.py index 44a09eb3c5c..3d0cee899f5 100644 --- a/purchase_security/tests/test_access_rights.py +++ b/purchase_security/tests/test_access_rights.py @@ -1,60 +1,70 @@ # Copyright 2020 Tecnativa - Víctor Martínez +# Copyright 2023 Tecnativa - Stefan Ungureanu +# Copyright 2023 Tecnativa - Pedro M. Baeza +# Copyright 2024 Tecnativa - Víctor Martínez # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). -import logging +from odoo.tests import Form, common, new_test_user +from odoo.tests.common import users -from odoo.tests.common import TransactionCase -_logger = logging.getLogger(__name__) - - -class TestPurchaseOrderSecurity(TransactionCase): +class TestPurchaseOrderSecurity(common.TransactionCase): @classmethod def setUpClass(cls): - super(TestPurchaseOrderSecurity, cls).setUpClass() + super().setUpClass() + cls.env = cls.env( + context=dict( + cls.env.context, + mail_create_nolog=True, + mail_create_nosubscribe=True, + mail_notrack=True, + no_reset_password=True, + tracking_disable=True, + ) + ) + # Teams + cls.team1 = cls.env["purchase.team"].create({"name": "Team1"}) + cls.team2 = cls.env["purchase.team"].create({"name": "Team2"}) # Users - users = cls.env["res.users"].with_context(no_reset_password=True) - group_name = "group_purchase_own_orders" # User in group_purchase_own_orders - cls.user_group_purchase_own_orders = users.create( - { - "name": "group_purchase_own_orders", - "login": "group_purchase_own_orders", - "email": "group_purchase_own_orders@example.com", - "groups_id": [ - (6, 0, [cls.env.ref("purchase_security.%s" % group_name).id]) - ], - } + cls.user_group_purchase_own_orders = new_test_user( + cls.env, + login="group_purchase_own_orders", + groups="purchase_security.group_purchase_own_orders", + ) + # User 1 in group_purchase_group_orders + cls.user_group_team_1 = new_test_user( + cls.env, + login="group_purchase_team_1_orders", + groups="purchase_security.group_purchase_group_orders", + ) + # Adding user 1 to both teams + cls.team1.write({"user_ids": [(4, cls.user_group_team_1.id)]}) + cls.team2.write({"user_ids": [(4, cls.user_group_team_1.id)]}) + # User 2 in group_purchase_group_orders + cls.user_group_team_2 = new_test_user( + cls.env, + login="group_purchase_team_2_orders", + groups="purchase_security.group_purchase_group_orders", + ) + # Adding user 2 to only one team + cls.team1.write({"user_ids": [(4, cls.user_group_team_2.id)]}) + # User with group permission but without being assigned to any team + cls.user_group_team_3 = new_test_user( + cls.env, + login="group_purchase_team_3_orders", + groups="purchase_security.group_purchase_group_orders", ) # Purchase order user - cls.user_po_user = users.create( - { - "name": "po_user", - "login": "po_user", - "email": "po_user@example.com", - "groups_id": [(6, 0, [cls.env.ref("purchase.group_purchase_user").id])], - } + cls.user_po_user = new_test_user( + cls.env, login="po_user", groups="purchase.group_purchase_user" ) # Purchase order manager - cls.user_po_manager = users.create( - { - "name": "po_manager", - "login": "po_manager", - "email": "po_manager@example.com", - "groups_id": [ - (6, 0, [cls.env.ref("purchase.group_purchase_manager").id]) - ], - } + cls.user_po_manager = new_test_user( + cls.env, login="po_manager", groups="purchase.group_purchase_manager" ) # User without groups - cls.user_without_groups = users.create( - { - "name": "without_groups", - "login": "without_groups", - "email": "without_groups@example.com", - "groups_id": False, - } - ) + cls.user_without_groups = new_test_user(cls.env, login="without_groups") # Partner for the POs cls.partner_po = cls.env["res.partner"].create({"name": "PO Partner"}) # Purchase Order @@ -64,6 +74,7 @@ def setUpClass(cls): "name": "po_security_1", "partner_id": cls.partner_po.id, "user_id": False, # No Purchase Representative + "team_id": False, # No automatic team }, { "name": "po_security_2", @@ -74,15 +85,65 @@ def setUpClass(cls): "name": "po_security_3", "user_id": cls.user_po_manager.id, "partner_id": cls.partner_po.id, + "team_id": cls.team1.id, }, { "name": "po_security_4", "user_id": cls.user_group_purchase_own_orders.id, "partner_id": cls.partner_po.id, + "team_id": cls.team2.id, }, ) ) + @users("group_purchase_team_1_orders") + def test_new_purchase_order(self): + order_form_1 = Form(self.env["purchase.order"]) + self.assertEqual(order_form_1.user_id, self.user_group_team_1) + self.assertEqual(order_form_1.team_id, self.team1) + order_form_1.partner_id = self.partner_po + self.assertEqual(order_form_1.user_id, self.user_group_team_1) + self.assertEqual(order_form_1.team_id, self.team1) + # order_form with default_user_id (user_group_team_2 > team_2) + self.team1.write({"user_ids": [(3, self.user_group_team_2.id)]}) + self.team2.write({"user_ids": [(4, self.user_group_team_2.id)]}) + order_form_2 = Form( + self.env["purchase.order"].with_context( + default_user_id=self.user_group_team_2.id + ) + ) + self.assertEqual(order_form_2.user_id, self.user_group_team_2) + self.assertEqual(order_form_2.team_id, self.team2) + order_form_2.partner_id = self.partner_po + self.assertEqual(order_form_2.user_id, self.user_group_team_2) + self.assertEqual(order_form_2.team_id, self.team2) + # order_form with default_user_id (user_group_team_3 > without team) + order_form_2 = Form( + self.env["purchase.order"].with_context( + default_user_id=self.user_group_team_3.id + ) + ) + self.assertEqual(order_form_2.user_id, self.user_group_team_3) + self.assertEqual(order_form_2.team_id, self.team1) + order_form_2.partner_id = self.partner_po + self.assertEqual(order_form_2.user_id, self.user_group_team_3) + self.assertEqual(order_form_2.team_id, self.team1) + + def _check_permission(self, user, team, expected): + self.partner_po.write( + { + "purchase_user_id": user.id if user else user, + "purchase_team_id": team.id if team else team, + } + ) + domain = [("id", "=", self.partner_po.id)] + obj = self.env[self.partner_po._name] + self.assertEqual(bool(obj.search(domain)), expected) + + def test_po_auto_team(self): + order = self.env["purchase.order"].search([("name", "=", "po_security_2")]) + self.assertEqual(order.team_id, self.team1) + def test_access_user_user_group_purchase_own_orders(self): # User in group should have access to it's own PO # and to those w/o Purchase Representative @@ -91,7 +152,6 @@ def test_access_user_user_group_purchase_own_orders(self): self.env["purchase.order"] .with_user(self.user_group_purchase_own_orders) .search([]) - .ids ), 2, ) @@ -111,7 +171,6 @@ def test_access_user_po_user(self): self.env["purchase.order"] .with_user(self.user_po_user) .search([("name", "like", "po_security")]) - .ids ), 4, ) @@ -124,7 +183,6 @@ def test_access_user_po_manager(self): self.env["purchase.order"] .with_user(self.user_po_manager) .search([("name", "like", "po_security")]) - .ids ), 4, ) @@ -138,3 +196,182 @@ def test_access_user_without_groups(self): len(self.env["purchase.order"].with_user(self.user_without_groups).read()), 0, ) + + def test_access_user_user_group_purchase_group_orders_1(self): + # User in group should have access PO's without any team assigned, + # and to those to whose team he belongs. In this case, it belongs to + # both teams + self.assertEqual( + len( + self.env["purchase.order"] + .with_user(self.user_group_team_1) + .search([("name", "like", "po_security")]) + ), + 4, + ) + + def test_access_user_user_group_purchase_group_orders_2(self): + # User in group should have access PO's without any team assigned, + # and to those to whose team he belongs. In this case, it belongs to + # only one team, so the other order won't be seen + self.assertEqual( + len( + self.env["purchase.order"] + .with_user(self.user_group_team_2) + .search([("name", "like", "po_security")]) + ), + 3, + ) + + def test_access_user_user_group_purchase_group_orders_3(self): + # User in group should have access PO's without any team assigned, + # and to those to whose team they belongs. In this case, it does not + # belongs to any team, so the other orders won't be seen + self.assertEqual( + len( + self.env["purchase.order"] + .with_user(self.user_group_team_3) + .search([("name", "like", "po_security")]) + ), + 1, + ) + + @users("po_user") + def test_partner_permissions_01(self): + """User with purchase.group_purchase_user group.""" + self._check_permission(False, False, True) + self._check_permission(False, self.team1, True) + self._check_permission(False, self.team2, True) + self._check_permission(self.user_group_purchase_own_orders, False, True) + self._check_permission(self.user_group_purchase_own_orders, self.team1, True) + self._check_permission(self.user_group_purchase_own_orders, self.team2, True) + self._check_permission(self.user_group_team_1, False, True) + self._check_permission(self.user_group_team_1, self.team1, True) + self._check_permission(self.user_group_team_1, self.team2, True) + self._check_permission(self.user_group_team_2, False, True) + self._check_permission(self.user_group_team_2, self.team1, True) + self._check_permission(self.user_group_team_2, self.team2, True) + self._check_permission(self.user_group_team_3, False, True) + self._check_permission(self.user_group_team_3, self.team1, True) + self._check_permission(self.user_group_team_3, self.team2, True) + self._check_permission(self.user_po_user, False, True) + self._check_permission(self.user_po_user, self.team1, True) + self._check_permission(self.user_po_user, self.team2, True) + self._check_permission(self.user_po_manager, False, True) + self._check_permission(self.user_po_manager, self.team1, True) + self._check_permission(self.user_po_manager, self.team2, True) + self._check_permission(self.user_without_groups, False, True) + self._check_permission(self.user_without_groups, self.team1, True) + self._check_permission(self.user_without_groups, self.team2, True) + + @users("group_purchase_own_orders") + def test_partner_permissions_02(self): + """User with purchase_security.group_purchase_own_orders group.""" + self._check_permission(False, False, True) + self._check_permission(False, self.team1, False) + self._check_permission(False, self.team2, False) + self._check_permission(self.user_group_purchase_own_orders, False, True) + self._check_permission(self.user_group_purchase_own_orders, self.team1, True) + self._check_permission(self.user_group_purchase_own_orders, self.team2, True) + self._check_permission(self.user_group_team_1, False, False) + self._check_permission(self.user_group_team_1, self.team1, False) + self._check_permission(self.user_group_team_1, self.team2, False) + self._check_permission(self.user_group_team_2, False, False) + self._check_permission(self.user_group_team_2, self.team1, False) + self._check_permission(self.user_group_team_2, self.team2, False) + self._check_permission(self.user_group_team_3, False, False) + self._check_permission(self.user_group_team_3, self.team1, False) + self._check_permission(self.user_group_team_3, self.team2, False) + self._check_permission(self.user_po_user, False, False) + self._check_permission(self.user_po_user, self.team1, False) + self._check_permission(self.user_po_user, self.team2, False) + self._check_permission(self.user_po_manager, False, False) + self._check_permission(self.user_po_manager, self.team1, False) + self._check_permission(self.user_po_manager, self.team2, False) + self._check_permission(self.user_without_groups, False, False) + self._check_permission(self.user_without_groups, self.team1, False) + self._check_permission(self.user_without_groups, self.team2, False) + + @users("group_purchase_team_1_orders") + def test_partner_permissions_03(self): + """User with purchase_security.group_purchase_group_orders group.""" + self._check_permission(False, False, True) + self._check_permission(False, self.team1, True) + self._check_permission(False, self.team2, False) + self._check_permission(self.user_group_purchase_own_orders, False, True) + self._check_permission(self.user_group_purchase_own_orders, self.team1, True) + self._check_permission(self.user_group_purchase_own_orders, self.team2, False) + self._check_permission(self.user_group_team_1, False, True) + self._check_permission(self.user_group_team_1, self.team1, True) + self._check_permission(self.user_group_team_1, self.team2, False) + self._check_permission(self.user_group_team_2, False, True) + self._check_permission(self.user_group_team_2, self.team1, True) + self._check_permission(self.user_group_team_2, self.team2, False) + self._check_permission(self.user_group_team_3, False, True) + self._check_permission(self.user_group_team_3, self.team1, True) + self._check_permission(self.user_group_team_3, self.team2, False) + self._check_permission(self.user_po_user, False, True) + self._check_permission(self.user_po_user, self.team1, True) + self._check_permission(self.user_po_user, self.team2, False) + self._check_permission(self.user_po_manager, False, True) + self._check_permission(self.user_po_manager, self.team1, True) + self._check_permission(self.user_po_manager, self.team2, False) + self._check_permission(self.user_without_groups, False, True) + self._check_permission(self.user_without_groups, self.team1, True) + self._check_permission(self.user_without_groups, self.team2, False) + + @users("po_manager") + def test_partner_permissions_04(self): + """User with purchase.group_purchase_manager group.""" + self._check_permission(False, False, True) + self._check_permission(False, self.team1, True) + self._check_permission(False, self.team2, True) + self._check_permission(self.user_group_purchase_own_orders, False, True) + self._check_permission(self.user_group_purchase_own_orders, self.team1, True) + self._check_permission(self.user_group_purchase_own_orders, self.team2, True) + self._check_permission(self.user_group_team_1, False, True) + self._check_permission(self.user_group_team_1, self.team1, True) + self._check_permission(self.user_group_team_1, self.team2, True) + self._check_permission(self.user_group_team_2, False, True) + self._check_permission(self.user_group_team_2, self.team1, True) + self._check_permission(self.user_group_team_2, self.team2, True) + self._check_permission(self.user_group_team_3, False, True) + self._check_permission(self.user_group_team_3, self.team1, True) + self._check_permission(self.user_group_team_3, self.team2, True) + self._check_permission(self.user_po_user, False, True) + self._check_permission(self.user_po_user, self.team1, True) + self._check_permission(self.user_po_user, self.team2, True) + self._check_permission(self.user_po_manager, False, True) + self._check_permission(self.user_po_manager, self.team1, True) + self._check_permission(self.user_po_manager, self.team2, True) + self._check_permission(self.user_without_groups, False, True) + self._check_permission(self.user_without_groups, self.team1, True) + self._check_permission(self.user_without_groups, self.team2, True) + + @users("without_groups") + def test_partner_permissions_05(self): + """User witout groups""" + self._check_permission(False, False, True) + self._check_permission(False, self.team1, True) + self._check_permission(False, self.team2, True) + self._check_permission(self.user_group_purchase_own_orders, False, True) + self._check_permission(self.user_group_purchase_own_orders, self.team1, True) + self._check_permission(self.user_group_purchase_own_orders, self.team2, True) + self._check_permission(self.user_group_team_1, False, True) + self._check_permission(self.user_group_team_1, self.team1, True) + self._check_permission(self.user_group_team_1, self.team2, True) + self._check_permission(self.user_group_team_2, False, True) + self._check_permission(self.user_group_team_2, self.team1, True) + self._check_permission(self.user_group_team_2, self.team2, True) + self._check_permission(self.user_group_team_3, False, True) + self._check_permission(self.user_group_team_3, self.team1, True) + self._check_permission(self.user_group_team_3, self.team2, True) + self._check_permission(self.user_po_user, False, True) + self._check_permission(self.user_po_user, self.team1, True) + self._check_permission(self.user_po_user, self.team2, True) + self._check_permission(self.user_po_manager, False, True) + self._check_permission(self.user_po_manager, self.team1, True) + self._check_permission(self.user_po_manager, self.team2, True) + self._check_permission(self.user_without_groups, False, True) + self._check_permission(self.user_without_groups, self.team1, True) + self._check_permission(self.user_without_groups, self.team2, True) diff --git a/purchase_security/views/purchase_order_views.xml b/purchase_security/views/purchase_order_views.xml index 8d226de3b82..9558a7f4f5d 100644 --- a/purchase_security/views/purchase_order_views.xml +++ b/purchase_security/views/purchase_order_views.xml @@ -14,6 +14,18 @@ } 1 + + + + diff --git a/purchase_security/views/purchase_team_views.xml b/purchase_security/views/purchase_team_views.xml new file mode 100644 index 00000000000..36fe514ceac --- /dev/null +++ b/purchase_security/views/purchase_team_views.xml @@ -0,0 +1,99 @@ + + + + purchase.team.form + purchase.team + +
      + +
      +
      + + + + + + + + + + +
      +
      +
      + Avatar +
      +
      + +
      + +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      + +
      +
      + + purchase.team.form + purchase.team + + + + + + + + + + Purchase Teams + ir.actions.act_window + purchase.team + tree,form + + +
      diff --git a/purchase_security/views/res_partner_views.xml b/purchase_security/views/res_partner_views.xml new file mode 100644 index 00000000000..19f40faf49f --- /dev/null +++ b/purchase_security/views/res_partner_views.xml @@ -0,0 +1,59 @@ + + + + res.partner.select + res.partner + + + + + + + + + + + + res.partner.tree + res.partner + + + + + + + + + res.partner.property.form.inherit + res.partner + + + + + + + + + +