diff --git a/shopinvader_api_unit_member/security/res_partner.xml b/shopinvader_api_unit_member/security/res_partner.xml index 1e7df244e9..1104199958 100644 --- a/shopinvader_api_unit_member/security/res_partner.xml +++ b/shopinvader_api_unit_member/security/res_partner.xml @@ -14,9 +14,15 @@ name="groups" eval="[(4, ref('shopinvader_api_unit_member.shopinvader_unit_management_user_group'))]" /> - ['|', ('unit_id.manager_ids','=',authenticated_partner_id), ('manager_ids','=',authenticated_partner_id)] + [ + '|', + ('manager_ids','=',authenticated_partner_id), + '|', + ('unit_id.manager_ids','=',authenticated_partner_id), + '|', + ('parent_id.manager_ids','=',authenticated_partner_id), + ('parent_id.unit_id.manager_ids','=',authenticated_partner_id) + ] diff --git a/shopinvader_api_unit_member/tests/test_shopinvader_api_unit_members.py b/shopinvader_api_unit_member/tests/test_shopinvader_api_unit_members.py index 4be49eab64..77aef4716d 100644 --- a/shopinvader_api_unit_member/tests/test_shopinvader_api_unit_members.py +++ b/shopinvader_api_unit_member/tests/test_shopinvader_api_unit_members.py @@ -4,12 +4,12 @@ import json from contextlib import contextmanager -from unittest import mock from fastapi import status from requests import Response from odoo.tests.common import tagged +from odoo.tools import mute_logger from odoo.addons.extendable_fastapi.tests.common import FastAPITransactionCase from odoo.addons.shopinvader_unit_management.tests.common import ( @@ -51,12 +51,10 @@ def setUpClass(cls) -> None: )._get_app() @contextmanager - def _rollback_called_test_client(self): - with self._create_test_client() as test_client, mock.patch.object( - self.env.cr.__class__, "rollback" - ) as mock_rollback: + def _create_test_client(self, **kwargs): + kwargs.setdefault("raise_server_exceptions", False) + with mute_logger("httpx"), super()._create_test_client(**kwargs) as test_client: yield test_client - mock_rollback.assert_called_once() def test_get_manager_unit_members(self): """ @@ -94,7 +92,7 @@ def test_collaborator_unit_members(self): """ self.default_fastapi_authenticated_partner = self.collaborator_1_1 - with self._rollback_called_test_client() as test_client: + with self._create_test_client() as test_client: response: Response = test_client.get("/unit/members") self.assertEqual( response.status_code, @@ -107,7 +105,7 @@ def test_unit_unit_members(self): """ self.default_fastapi_authenticated_partner = self.unit_1 - with self._rollback_called_test_client() as test_client: + with self._create_test_client() as test_client: response: Response = test_client.get("/unit/members") self.assertEqual( response.status_code, @@ -149,7 +147,7 @@ def test_get_manager_unit_members_wrong_unit(self): """ Test that a manager can't access members of another unit """ - with self._rollback_called_test_client() as test_client: + with self._create_test_client() as test_client: response: Response = test_client.get( f"/unit/members/{self.collaborator_2_2.id}" ) @@ -162,13 +160,13 @@ def test_get_manager_unit_members_wrong_type(self): """ Test that a manager can't access a unit """ - with self._rollback_called_test_client() as test_client: + with self._create_test_client() as test_client: response: Response = test_client.get(f"/unit/members/{self.unit_1.id}") self.assertEqual( response.status_code, status.HTTP_404_NOT_FOUND, ) - with self._rollback_called_test_client() as test_client: + with self._create_test_client() as test_client: response: Response = test_client.get(f"/unit/members/{self.unit_2.id}") self.assertEqual( response.status_code, @@ -257,7 +255,7 @@ def test_create_unit_manager(self): ) def test_create_unit_wrong_type(self): - with self._rollback_called_test_client() as test_client: + with self._create_test_client() as test_client: response: Response = test_client.post( "/unit/members", data=json.dumps({"name": "New Unit", "type": "unit"}), @@ -267,7 +265,7 @@ def test_create_unit_wrong_type(self): status.HTTP_403_FORBIDDEN, ) - with self._rollback_called_test_client() as test_client: + with self._create_test_client() as test_client: response: Response = test_client.post( "/unit/members", data=json.dumps({"name": "New Unit", "type": "unknown"}), @@ -279,7 +277,7 @@ def test_create_unit_wrong_type(self): def test_create_unit_wrong_partner(self): self.default_fastapi_authenticated_partner = self.collaborator_1_1 - with self._rollback_called_test_client() as test_client: + with self._create_test_client() as test_client: response: Response = test_client.post( "/unit/members", data=json.dumps({"name": "New Unit", "type": "collaborator"}), @@ -387,7 +385,7 @@ def test_update_unit_manager(self): def test_update_unit_wrong_partner(self): self.default_fastapi_authenticated_partner = self.collaborator_1_1 - with self._rollback_called_test_client() as test_client: + with self._create_test_client() as test_client: response: Response = test_client.post( f"/unit/members/{self.collaborator_1_1.id}", data=json.dumps({"name": "New Unit Name"}), @@ -474,7 +472,7 @@ def test_delete_unit_manager(self): def test_delete_unit_wrong_partner(self): self.default_fastapi_authenticated_partner = self.collaborator_1_1 - with self._rollback_called_test_client() as test_client: + with self._create_test_client() as test_client: response: Response = test_client.delete( f"/unit/members/{self.collaborator_1_1.id}", ) diff --git a/shopinvader_api_unit_request/tests/test_shopinvader_api_unit_request.py b/shopinvader_api_unit_request/tests/test_shopinvader_api_unit_request.py index 484607ea4b..c91ef94110 100644 --- a/shopinvader_api_unit_request/tests/test_shopinvader_api_unit_request.py +++ b/shopinvader_api_unit_request/tests/test_shopinvader_api_unit_request.py @@ -4,12 +4,12 @@ import json from contextlib import contextmanager -from unittest import mock from fastapi import status from requests import Response from odoo.tests.common import tagged +from odoo.tools import mute_logger from odoo.addons.shopinvader_api_cart.tests.common import CommonSaleCart from odoo.addons.shopinvader_api_sale.routers import sale_line_router @@ -112,12 +112,10 @@ def _slice_sol(self, data, *fields): return {tuple(item[field] for field in fields) for item in data["items"]} @contextmanager - def _rollback_called_test_client(self, **kwargs): - with self._create_test_client(**kwargs) as test_client, mock.patch.object( - self.env.cr.__class__, "rollback" - ) as mock_rollback: + def _create_test_client(self, **kwargs): + kwargs.setdefault("raise_server_exceptions", False) + with mute_logger("httpx"), super()._create_test_client(**kwargs) as test_client: yield test_client - mock_rollback.assert_called_once() def test_cart_request_as_collaborator(self): """ @@ -178,7 +176,7 @@ def test_cart_request_as_manager(self): self.env["sale.order"]._create_empty_cart( self.default_fastapi_authenticated_partner.id ) - with self._rollback_called_test_client() as test_client: + with self._create_test_client() as test_client: response: Response = test_client.post("/request") self.assertEqual( response.status_code, @@ -196,7 +194,7 @@ def test_cart_request_as_unit(self): self.env["sale.order"]._create_empty_cart( self.default_fastapi_authenticated_partner.id ) - with self._rollback_called_test_client() as test_client: + with self._create_test_client() as test_client: response: Response = test_client.post("/request") self.assertEqual( response.status_code, @@ -305,7 +303,7 @@ def test_sale_line_requested_flow(self): self.assertEqual(response.json()["count"], 1) def test_sale_line_requested_as_collaborator(self): - with self._rollback_called_test_client( + with self._create_test_client( app=self.sale_line_app, router=unit_request_line_router ) as test_client: response: Response = test_client.get("/unit/request_lines") diff --git a/shopinvader_unit_management/README.rst b/shopinvader_unit_management/README.rst index c8b7b812ca..a14a8fe280 100644 --- a/shopinvader_unit_management/README.rst +++ b/shopinvader_unit_management/README.rst @@ -7,7 +7,7 @@ Shopinvader Unit Management !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:8b84b0911ab2e1dd5bca6a65f2d6a814183bb0ff565d3772f39aa28e24c4223d + !! source digest: sha256:c85b9aaf1e5ec9ff4516960b20e84bae393a2240bd14e62e11d410ca7d6d6028 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png @@ -16,17 +16,11 @@ Shopinvader Unit Management .. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 -.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fodoo--shopinvader-lightgray.png?logo=github - :target: https://github.com/OCA/odoo-shopinvader/tree/16.0/shopinvader_unit_management - :alt: OCA/odoo-shopinvader -.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/odoo-shopinvader-16-0/odoo-shopinvader-16-0-shopinvader_unit_management - :alt: Translate me on Weblate -.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png - :target: https://runboat.odoo-community.org/builds?repo=OCA/odoo-shopinvader&target_branch=16.0 - :alt: Try me on Runboat - -|badge1| |badge2| |badge3| |badge4| |badge5| +.. |badge3| image:: https://img.shields.io/badge/github-shopinvader%2Fodoo--shopinvader-lightgray.png?logo=github + :target: https://github.com/shopinvader/odoo-shopinvader/tree/16.0/shopinvader_unit_management + :alt: shopinvader/odoo-shopinvader + +|badge1| |badge2| |badge3| This module introduces the concept of unit management. The unit is a group of partners with managers and collaborators. @@ -42,10 +36,10 @@ To cater to your needs, you can inherit the res.partner model and make the unit_ Bug Tracker =========== -Bugs are tracked on `GitHub Issues `_. +Bugs are tracked on `GitHub Issues `_. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed -`feedback `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -67,16 +61,6 @@ Contributors Maintainers ~~~~~~~~~~~ -This module is maintained by the OCA. - -.. image:: https://odoo-community.org/logo.png - :alt: Odoo Community Association - :target: https://odoo-community.org - -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. - -This module is part of the `OCA/odoo-shopinvader `_ project on GitHub. +This module is part of the `shopinvader/odoo-shopinvader `_ project on GitHub. -You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. +You are welcome to contribute. diff --git a/shopinvader_unit_management/__manifest__.py b/shopinvader_unit_management/__manifest__.py index d5786b73db..84613e52a0 100644 --- a/shopinvader_unit_management/__manifest__.py +++ b/shopinvader_unit_management/__manifest__.py @@ -12,7 +12,7 @@ "license": "AGPL-3", "author": "Akretion", "website": "https://github.com/shopinvader/odoo-shopinvader", - "depends": [], + "depends": ["shopinvader_address"], "demo": [ "demo/res_partner_demo.xml", ], diff --git a/shopinvader_unit_management/models/res_partner.py b/shopinvader_unit_management/models/res_partner.py index 4fd403ec89..30b1a2254a 100644 --- a/shopinvader_unit_management/models/res_partner.py +++ b/shopinvader_unit_management/models/res_partner.py @@ -87,3 +87,30 @@ def _delete_shopinvader_unit_member(self, member_id): raise AccessError(_("Cannot perform this action on this member")) member.sudo().active = False return member + + # Address overrides + def _get_shopinvader_invoicing_addresses(self) -> "ResPartner": + # A unit member can invoice on unit + addresses = super()._get_shopinvader_invoicing_addresses() + if self.unit_id: + addresses |= self.unit_id._get_shopinvader_invoicing_addresses() + return addresses + + def _get_shopinvader_delivery_addresses(self) -> "ResPartner": + # A unit member can deliver at unit + addresses = super()._get_shopinvader_delivery_addresses() + if self.unit_id: + addresses |= self.unit_id._get_shopinvader_delivery_addresses() + return addresses + + def _get_shopinvader_invoicing_address(self, address_id: int) -> "ResPartner": + address = super()._get_shopinvader_invoicing_address(address_id) + if address in self.unit_id._get_shopinvader_invoicing_addresses(): + raise AccessError(_("Cannot perform this action on this address")) + return address + + def _get_shopinvader_delivery_address(self, address_id: int) -> "ResPartner": + address = super()._get_shopinvader_delivery_address(address_id) + if address in self.unit_id._get_shopinvader_delivery_addresses(): + raise AccessError(_("Cannot perform this action on this address")) + return address diff --git a/shopinvader_unit_management/static/description/index.html b/shopinvader_unit_management/static/description/index.html index 0f2b2323d3..23a5cea897 100644 --- a/shopinvader_unit_management/static/description/index.html +++ b/shopinvader_unit_management/static/description/index.html @@ -9,10 +9,11 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ +:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. +Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -275,7 +276,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: grey; } /* line numbers */ +pre.code .ln { color: gray; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -301,7 +302,7 @@ span.pre { white-space: pre } -span.problematic { +span.problematic, pre.problematic { color: red } span.section-subtitle { @@ -367,9 +368,9 @@

Shopinvader Unit Management

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:8b84b0911ab2e1dd5bca6a65f2d6a814183bb0ff565d3772f39aa28e24c4223d +!! source digest: sha256:c85b9aaf1e5ec9ff4516960b20e84bae393a2240bd14e62e11d410ca7d6d6028 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

Beta License: AGPL-3 OCA/odoo-shopinvader Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 shopinvader/odoo-shopinvader

This module introduces the concept of unit management. The unit is a group of partners with managers and collaborators. This module provides a simple implementation of the unit management concept.

@@ -388,10 +389,10 @@

Shopinvader Unit Management

Bug Tracker

-

Bugs are tracked on GitHub Issues. +

Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed -feedback.

+feedback.

Do not contact contributors directly about support or help with technical issues.

@@ -413,13 +414,8 @@

Contributors

Maintainers

-

This module is maintained by the OCA.

-Odoo Community Association -

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.

-

This module is part of the OCA/odoo-shopinvader project on GitHub.

-

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+

This module is part of the shopinvader/odoo-shopinvader project on GitHub.

+

You are welcome to contribute.

diff --git a/shopinvader_unit_management/tests/common.py b/shopinvader_unit_management/tests/common.py index 81140ced2a..0e58c3b552 100644 --- a/shopinvader_unit_management/tests/common.py +++ b/shopinvader_unit_management/tests/common.py @@ -2,10 +2,10 @@ # @author Florian Mounier # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo.tests.common import SavepointCase +from odoo.tests.common import TransactionCase -class TestUnitManagementCommon(SavepointCase): +class TestUnitManagementCommon(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() diff --git a/shopinvader_unit_management/tests/test_unit_management.py b/shopinvader_unit_management/tests/test_unit_management.py index 536a454a04..7c26484cb9 100644 --- a/shopinvader_unit_management/tests/test_unit_management.py +++ b/shopinvader_unit_management/tests/test_unit_management.py @@ -2,6 +2,7 @@ # @author Florian Mounier # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo.exceptions import AccessError from .common import TestUnitManagementCommon @@ -95,3 +96,152 @@ def test_unit_management_collaborators(self): self.assertFalse(self.collaborator_3_3.member_ids) self.assertFalse(self.collaborator_3_3.manager_ids) self.assertFalse(self.collaborator_3_3.collaborator_ids) + + def test_unit_invoicing_addresses(self): + self.assertEqual( + self.collaborator_1_1._get_shopinvader_invoicing_addresses(), + self.collaborator_1_1 | self.unit_1, + ) + + def test_unit_delivery_addresses_partner(self): + self.assertFalse( + self.collaborator_1_1._get_shopinvader_delivery_addresses(), + ) + + # Create a delivery address for the collaborator + address = self.env["res.partner"].create( + { + "name": "Delivery Address", + "type": "delivery", + "parent_id": self.collaborator_1_1.id, + } + ) + self.assertEqual( + self.unit_1.member_ids, + self.manager_1_1 + | self.collaborator_1_1 + | self.collaborator_1_2 + | self.collaborator_1_3 + | self.collaborator_1_4 + | self.collaborator_1_5, + ) + self.assertEqual( + self.collaborator_1_1._get_shopinvader_delivery_addresses(), + address, + ) + + def test_unit_update_invoicing_address(self): + self.collaborator_1_1._update_shopinvader_invoicing_address( + {"name": "New Name"}, self.collaborator_1_1.id + ) + with self.assertRaises(AccessError): + self.collaborator_1_1._update_shopinvader_invoicing_address( + {"name": "New Name"}, self.unit_1.id + ) + + def test_unit_delivery_addresses_unit(self): + self.assertFalse( + self.collaborator_1_1._get_shopinvader_delivery_addresses(), + ) + + # Create a delivery address for the collaborator + address_unit = self.env["res.partner"].create( + { + "name": "Delivery Address", + "type": "delivery", + "parent_id": self.unit_1.id, + } + ) + self.assertEqual( + self.unit_1.member_ids, + self.manager_1_1 + | self.collaborator_1_1 + | self.collaborator_1_2 + | self.collaborator_1_3 + | self.collaborator_1_4 + | self.collaborator_1_5, + ) + self.assertEqual( + self.collaborator_1_1._get_shopinvader_delivery_addresses(), + address_unit, + ) + + def test_unit_delivery_addresses_both(self): + self.assertFalse( + self.collaborator_1_1._get_shopinvader_delivery_addresses(), + ) + + # Create a delivery address for both the collaborator and the unit + address = self.env["res.partner"].create( + { + "name": "Delivery Address", + "type": "delivery", + "parent_id": self.collaborator_1_1.id, + } + ) + address_unit = self.env["res.partner"].create( + { + "name": "Delivery Address", + "type": "delivery", + "parent_id": self.unit_1.id, + } + ) + self.assertEqual( + self.unit_1.member_ids, + self.manager_1_1 + | self.collaborator_1_1 + | self.collaborator_1_2 + | self.collaborator_1_3 + | self.collaborator_1_4 + | self.collaborator_1_5, + ) + self.assertEqual( + self.collaborator_1_1._get_shopinvader_delivery_addresses(), + address | address_unit, + ) + + def test_unit_update_delivery_address(self): + # Create a delivery address for both the collaborator and the unit + address = self.env["res.partner"].create( + { + "name": "Delivery Address", + "type": "delivery", + "parent_id": self.collaborator_1_1.id, + } + ) + address_unit = self.env["res.partner"].create( + { + "name": "Delivery Address", + "type": "delivery", + "parent_id": self.unit_1.id, + } + ) + + self.collaborator_1_1._update_shopinvader_delivery_address( + {"name": "New Name"}, address.id + ) + with self.assertRaises(AccessError): + self.collaborator_1_1._update_shopinvader_delivery_address( + {"name": "New Name"}, address_unit.id + ) + + def test_unit_delete_delivery_address(self): + # Create a delivery address for both the collaborator and the unit + address = self.env["res.partner"].create( + { + "name": "Delivery Address", + "type": "delivery", + "parent_id": self.collaborator_1_1.id, + } + ) + address_unit = self.env["res.partner"].create( + { + "name": "Delivery Address", + "type": "delivery", + "parent_id": self.unit_1.id, + } + ) + + self.collaborator_1_1._delete_shopinvader_delivery_address(address.id) + with self.assertRaises(AccessError): + self.collaborator_1_1._delete_shopinvader_delivery_address(address_unit.id)