From ed8c6acd92ae7f8a94b811a069cdb451e7f3bde3 Mon Sep 17 00:00:00 2001 From: snt Date: Sat, 21 Feb 2026 14:09:57 +0100 Subject: [PATCH] [FIX] website_sale_aplicoop: Add portal user support for sale.order creation Portal users don't have write/create permissions on sale.order by default. This causes errors when trying to create orders during checkout or draft save. Changes: - Add _get_salesperson_for_order() helper to retrieve partner's salesperson - Use sudo() for all sale.order create() operations - Automatically assign user_id (salesperson) when creating orders - Use sudo() for order updates and line modifications - Add fallback to commercial_partner_id.user_id for salesperson This ensures orders are created with proper permissions while maintaining traceability through the assigned salesperson. Test coverage: - Add test_portal_sale_order_creation.py with 3 tests - Test portal user creates sale.order - Test salesperson fallback logic - Test portal user updates order lines --- .../controllers/website_sale.py | 114 +++++++++---- website_sale_aplicoop/tests/__init__.py | 1 + .../tests/test_portal_sale_order_creation.py | 156 ++++++++++++++++++ 3 files changed, 243 insertions(+), 28 deletions(-) create mode 100644 website_sale_aplicoop/tests/test_portal_sale_order_creation.py diff --git a/website_sale_aplicoop/controllers/website_sale.py b/website_sale_aplicoop/controllers/website_sale.py index 9593adc..a8e64b1 100644 --- a/website_sale_aplicoop/controllers/website_sale.py +++ b/website_sale_aplicoop/controllers/website_sale.py @@ -831,6 +831,30 @@ class AplicoopWebsiteSale(WebsiteSale): ) return sale_order_lines + def _get_salesperson_for_order(self, partner): + """Get the salesperson (user_id) for creating sale orders. + + For portal users without write access to sale.order, we need to create + the order as the assigned salesperson or with sudo(). + + Args: + partner: res.partner record + + Returns: + res.users record (salesperson) or False + """ + # First check if partner has an assigned salesperson + if partner.user_id and not partner.user_id._is_public(): + return partner.user_id + + # Fallback to commercial partner's salesperson + commercial_partner = partner.commercial_partner_id + if commercial_partner.user_id and not commercial_partner.user_id._is_public(): + return commercial_partner.user_id + + # No salesperson found + return False + def _create_or_update_sale_order( self, group_order, @@ -846,14 +870,16 @@ class AplicoopWebsiteSale(WebsiteSale): """ if existing_order: # Update existing order with new lines and propagate fields - existing_order.order_line = sale_order_lines - if not existing_order.group_order_id: - existing_order.group_order_id = group_order.id - existing_order.pickup_day = group_order.pickup_day - existing_order.pickup_date = group_order.pickup_date - existing_order.home_delivery = is_delivery + # Use sudo() to avoid permission issues with portal users + existing_order_sudo = existing_order.sudo() + existing_order_sudo.order_line = sale_order_lines + if not existing_order_sudo.group_order_id: + existing_order_sudo.group_order_id = group_order.id + existing_order_sudo.pickup_day = group_order.pickup_day + existing_order_sudo.pickup_date = group_order.pickup_date + existing_order_sudo.home_delivery = is_delivery if commitment_date: - existing_order.commitment_date = commitment_date + existing_order_sudo.commitment_date = commitment_date _logger.info( "Updated existing sale.order %d: commitment_date=%s, home_delivery=%s", existing_order.id, @@ -874,7 +900,18 @@ class AplicoopWebsiteSale(WebsiteSale): if commitment_date: order_vals["commitment_date"] = commitment_date - sale_order = request.env["sale.order"].create(order_vals) + # Get salesperson for order creation (portal users need this) + salesperson = self._get_salesperson_for_order(current_user.partner_id) + if salesperson: + order_vals["user_id"] = salesperson.id + _logger.info( + "Creating sale.order with salesperson %s (%d)", + salesperson.name, + salesperson.id, + ) + + # Create order with sudo to avoid permission issues with portal users + sale_order = request.env["sale.order"].sudo().create(order_vals) _logger.info( "sale.order created successfully: %d with group_order_id=%d, pickup_day=%s, home_delivery=%s", sale_order.id, @@ -912,7 +949,18 @@ class AplicoopWebsiteSale(WebsiteSale): elif group_order.pickup_date: order_vals["commitment_date"] = group_order.pickup_date - sale_order = request.env["sale.order"].create(order_vals) + # Get salesperson for order creation (portal users need this) + salesperson = self._get_salesperson_for_order(current_user.partner_id) + if salesperson: + order_vals["user_id"] = salesperson.id + _logger.info( + "Creating draft sale.order with salesperson %s (%d)", + salesperson.name, + salesperson.id, + ) + + # Create order with sudo to avoid permission issues with portal users + sale_order = request.env["sale.order"].sudo().create(order_vals) # Ensure the order has a name (sequence) try: @@ -1158,7 +1206,8 @@ class AplicoopWebsiteSale(WebsiteSale): lambda line, pid=product_id: line.product_id.id == pid ) if existing_line: - existing_line.write( + # Use sudo() to avoid permission issues with portal users + existing_line.sudo().write( { "product_uom_qty": existing_line.product_uom_qty + new_quantity @@ -1170,7 +1219,8 @@ class AplicoopWebsiteSale(WebsiteSale): existing_line.product_uom_qty, ) else: - existing_draft.order_line.create( + # Use sudo() to avoid permission issues with portal users + existing_draft.order_line.sudo().create( { "order_id": existing_draft.id, "product_id": product_id, @@ -1193,22 +1243,7 @@ class AplicoopWebsiteSale(WebsiteSale): "Deleted existing draft(s) for replace: %s", existing_drafts.mapped("id"), ) - sale_order = request.env["sale.order"].create( - { - "partner_id": current_user.partner_id.id, - "order_line": sale_order_lines, - "state": "draft", - "group_order_id": order_id, - "pickup_day": group_order.pickup_day, - "pickup_date": group_order.pickup_date, - "home_delivery": group_order.home_delivery, - } - ) - return sale_order, False - - # Default: create new draft - sale_order = request.env["sale.order"].create( - { + order_vals = { "partner_id": current_user.partner_id.id, "order_line": sale_order_lines, "state": "draft", @@ -1217,7 +1252,30 @@ class AplicoopWebsiteSale(WebsiteSale): "pickup_date": group_order.pickup_date, "home_delivery": group_order.home_delivery, } - ) + # Get salesperson for order creation (portal users need this) + salesperson = self._get_salesperson_for_order(current_user.partner_id) + if salesperson: + order_vals["user_id"] = salesperson.id + + sale_order = request.env["sale.order"].sudo().create(order_vals) + return sale_order, False + + # Default: create new draft + order_vals = { + "partner_id": current_user.partner_id.id, + "order_line": sale_order_lines, + "state": "draft", + "group_order_id": order_id, + "pickup_day": group_order.pickup_day, + "pickup_date": group_order.pickup_date, + "home_delivery": group_order.home_delivery, + } + # Get salesperson for order creation (portal users need this) + salesperson = self._get_salesperson_for_order(current_user.partner_id) + if salesperson: + order_vals["user_id"] = salesperson.id + + sale_order = request.env["sale.order"].sudo().create(order_vals) return sale_order, False def _decode_json_body(self): diff --git a/website_sale_aplicoop/tests/__init__.py b/website_sale_aplicoop/tests/__init__.py index a7c925e..52ca371 100644 --- a/website_sale_aplicoop/tests/__init__.py +++ b/website_sale_aplicoop/tests/__init__.py @@ -11,3 +11,4 @@ from . import test_multi_company from . import test_save_order_endpoints from . import test_date_calculations from . import test_pricing_with_pricelist +from . import test_portal_sale_order_creation diff --git a/website_sale_aplicoop/tests/test_portal_sale_order_creation.py b/website_sale_aplicoop/tests/test_portal_sale_order_creation.py new file mode 100644 index 0000000..1fadb52 --- /dev/null +++ b/website_sale_aplicoop/tests/test_portal_sale_order_creation.py @@ -0,0 +1,156 @@ +# Copyright 2026 Criptomart +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +"""Test portal users can create sale orders with proper permissions.""" + +import logging +from datetime import datetime +from datetime import timedelta + +from odoo.tests import tagged +from odoo.tests.common import TransactionCase + +_logger = logging.getLogger(__name__) + + +@tagged("post_install", "-at_install") +class TestPortalSaleOrderCreation(TransactionCase): + """Test that portal users can create sale orders through the controller.""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + + # Create a portal user + cls.portal_user = cls.env["res.users"].create( + { + "name": "Portal Test User", + "login": "portal_test_user", + "email": "portal@test.com", + "groups_id": [(6, 0, [cls.env.ref("base.group_portal").id])], + } + ) + + # Create a salesperson + cls.salesperson = cls.env["res.users"].create( + { + "name": "Salesperson Test", + "login": "salesperson_test", + "email": "sales@test.com", + "groups_id": [ + (6, 0, [cls.env.ref("sales_team.group_sale_salesman").id]) + ], + } + ) + + # Assign salesperson to portal user's partner + cls.portal_user.partner_id.user_id = cls.salesperson + + # Create a group order for testing + cls.group_order = cls.env["group.order"].create( + { + "name": "Test Group Order", + "state": "confirmed", + "pickup_day": "0", # Monday + "pickup_date": datetime.now().date() + timedelta(days=7), + "cutoff_date": datetime.now().date() + timedelta(days=3), + } + ) + + # Create a test product + cls.product = cls.env["product.product"].create( + { + "name": "Test Product", + "list_price": 100.0, + "type": "product", + } + ) + + def test_portal_user_can_create_sale_order(self): + """Test that portal users can create sale orders with sudo().""" + # Create sale order as portal user + order_vals = { + "partner_id": self.portal_user.partner_id.id, + "order_line": [ + ( + 0, + 0, + { + "product_id": self.product.id, + "product_uom_qty": 2, + "price_unit": 100.0, + "name": self.product.name, + }, + ) + ], + "state": "draft", + "group_order_id": self.group_order.id, + "user_id": self.salesperson.id, # Assign salesperson + } + + # This should work with sudo() + sale_order = self.env["sale.order"].sudo().create(order_vals) + + self.assertTrue(sale_order.exists()) + self.assertEqual(sale_order.partner_id, self.portal_user.partner_id) + self.assertEqual(sale_order.user_id, self.salesperson) + self.assertEqual(len(sale_order.order_line), 1) + self.assertEqual(sale_order.group_order_id, self.group_order) + + def test_get_salesperson_fallback(self): + """Test salesperson fallback to commercial partner.""" + # Create commercial partner with salesperson + commercial_partner = self.env["res.partner"].create( + { + "name": "Commercial Partner", + "is_company": True, + "user_id": self.salesperson.id, + } + ) + + # Create child contact without salesperson + child_partner = self.env["res.partner"].create( + { + "name": "Child Contact", + "parent_id": commercial_partner.id, + } + ) + + # Child should fallback to commercial partner's salesperson + self.assertEqual( + child_partner.commercial_partner_id.user_id, self.salesperson + ) + + def test_portal_user_can_update_order_lines(self): + """Test that portal users can update existing order lines with sudo().""" + # Create initial order + sale_order = ( + self.env["sale.order"] + .sudo() + .create( + { + "partner_id": self.portal_user.partner_id.id, + "state": "draft", + "group_order_id": self.group_order.id, + "user_id": self.salesperson.id, + "order_line": [ + ( + 0, + 0, + { + "product_id": self.product.id, + "product_uom_qty": 1, + "price_unit": 100.0, + "name": self.product.name, + }, + ) + ], + } + ) + ) + + # Update order line as portal user (with sudo) + existing_line = sale_order.order_line[0] + existing_line.sudo().write({"product_uom_qty": 5}) + + self.assertEqual(existing_line.product_uom_qty, 5)