# Copyright 2025 Criptomart # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) """ Test suite for product discovery logic in website_sale_aplicoop. The discovery mechanism uses 3 sources: 1. product_ids: Directly linked products 2. category_ids: Products from linked categories (recursive) 3. supplier_ids: Products from linked suppliers Coverage: - Correct union of all 3 sources (no duplicates) - Deep category hierarchies (nested categories) - Empty sources (empty categories/suppliers) - Product filters (is_published, sale_ok) - Ordering and deduplication """ from datetime import datetime from datetime import timedelta from odoo.tests.common import TransactionCase class TestProductDiscoveryUnion(TransactionCase): """Test that product discovery returns correct union of 3 sources.""" def setUp(self): super().setUp() self.group = self.env["res.partner"].create( { "name": "Test Group", "is_company": True, } ) # Create a supplier self.supplier = self.env["res.partner"].create( { "name": "Test Supplier", "is_supplier": True, } ) # Create categories self.category1 = self.env["product.category"].create( { "name": "Category 1", } ) self.category2 = self.env["product.category"].create( { "name": "Category 2", } ) # Create products # Direct product self.direct_product = self.env["product.product"].create( { "name": "Direct Product", "type": "consu", "list_price": 10.0, "is_published": True, "sale_ok": True, } ) # Category 1 product self.cat1_product = self.env["product.product"].create( { "name": "Category 1 Product", "type": "consu", "list_price": 20.0, "categ_id": self.category1.id, "is_published": True, "sale_ok": True, } ) # Category 2 product self.cat2_product = self.env["product.product"].create( { "name": "Category 2 Product", "type": "consu", "list_price": 30.0, "categ_id": self.category2.id, "is_published": True, "sale_ok": True, } ) # Supplier product self.supplier_product = self.env["product.product"].create( { "name": "Supplier Product", "type": "consu", "list_price": 40.0, "categ_id": self.category1.id, # Also in category "seller_ids": [ ( 0, 0, { "partner_id": self.supplier.id, "product_name": "Supplier Product", }, ) ], "is_published": True, "sale_ok": True, } ) start_date = datetime.now().date() self.group_order = self.env["group.order"].create( { "name": "Test Order", "group_ids": [(6, 0, [self.group.id])], "type": "regular", "start_date": start_date, "end_date": start_date + timedelta(days=7), "period": "weekly", "pickup_day": "3", "cutoff_day": "0", } ) def test_discovery_from_direct_products(self): """Test discovery returns directly linked products.""" self.group_order.product_ids = [(4, self.direct_product.id)] discovered = self.group_order.product_ids self.assertIn(self.direct_product, discovered) def test_discovery_from_categories(self): """Test discovery includes products from linked categories.""" self.group_order.category_ids = [(4, self.category1.id)] # Computed placeholder to ensure discovery logic is exercised during test setup _ = self.group_order.product_ids # Should include cat1_product and supplier_product (both in category1) # Note: depends on how discovery is computed def test_discovery_from_suppliers(self): """Test discovery includes products from linked suppliers.""" self.group_order.supplier_ids = [(4, self.supplier.id)] # Should include supplier_product # Note: depends on how supplier link is implemented def test_discovery_union_no_duplicates(self): """Test that union doesn't include same product twice.""" # Add supplier_product via: # 1. Direct link # 2. Category link (cat1) # 3. Supplier link self.group_order.product_ids = [(4, self.supplier_product.id)] self.group_order.category_ids = [(4, self.category1.id)] self.group_order.supplier_ids = [(4, self.supplier.id)] discovered = self.group_order.product_ids # Count occurrences of supplier_product count = sum(1 for p in discovered if p == self.supplier_product) # Should appear only once self.assertEqual(count, 1) def test_discovery_filters_unpublished(self): """Test that unpublished products are excluded from discovery.""" unpublished = self.env["product.product"].create( { "name": "Unpublished Product", "type": "consu", "list_price": 50.0, "categ_id": self.category1.id, "is_published": False, "sale_ok": True, } ) self.group_order.category_ids = [(4, self.category1.id)] discovered = self.group_order.product_ids # Unpublished should not be in discovered self.assertNotIn(unpublished, discovered) def test_discovery_filters_not_for_sale(self): """Test that non-sellable products are excluded.""" not_for_sale = self.env["product.product"].create( { "name": "Not For Sale", "type": "consu", "list_price": 60.0, "categ_id": self.category1.id, "is_published": True, "sale_ok": False, } ) self.group_order.category_ids = [(4, self.category1.id)] discovered = self.group_order.product_ids # Not for sale should not be in discovered self.assertNotIn(not_for_sale, discovered) class TestDeepCategoryHierarchies(TransactionCase): """Test product discovery with nested category structures.""" def setUp(self): super().setUp() self.group = self.env["res.partner"].create( { "name": "Test Group", "is_company": True, } ) # Create nested category structure: # Root -> L1 -> L2 -> L3 -> L4 self.cat_l1 = self.env["product.category"].create( { "name": "Level 1", } ) self.cat_l2 = self.env["product.category"].create( { "name": "Level 2", "parent_id": self.cat_l1.id, } ) self.cat_l3 = self.env["product.category"].create( { "name": "Level 3", "parent_id": self.cat_l2.id, } ) self.cat_l4 = self.env["product.category"].create( { "name": "Level 4", "parent_id": self.cat_l3.id, } ) self.cat_l5 = self.env["product.category"].create( { "name": "Level 5", "parent_id": self.cat_l4.id, } ) # Create products at each level self.product_l2 = self.env["product.product"].create( { "name": "Product L2", "type": "consu", "list_price": 10.0, "categ_id": self.cat_l2.id, "is_published": True, "sale_ok": True, } ) self.product_l4 = self.env["product.product"].create( { "name": "Product L4", "type": "consu", "list_price": 20.0, "categ_id": self.cat_l4.id, "is_published": True, "sale_ok": True, } ) self.product_l5 = self.env["product.product"].create( { "name": "Product L5", "type": "consu", "list_price": 30.0, "categ_id": self.cat_l5.id, "is_published": True, "sale_ok": True, } ) start_date = datetime.now().date() self.group_order = self.env["group.order"].create( { "name": "Test Order", "group_ids": [(6, 0, [self.group.id])], "type": "regular", "start_date": start_date, "end_date": start_date + timedelta(days=7), "period": "weekly", "pickup_day": "3", "cutoff_day": "0", } ) def test_discovery_root_category_includes_all_descendants(self): """Test that linking root category discovers all nested products.""" self.group_order.category_ids = [(4, self.cat_l1.id)] discovered = self.group_order.product_ids # Should include products from L2, L4, L5 (all descendants) self.assertIn(self.product_l2, discovered) self.assertIn(self.product_l4, discovered) self.assertIn(self.product_l5, discovered) def test_discovery_mid_level_category_includes_descendants(self): """Test discovery from middle of hierarchy.""" self.group_order.category_ids = [(4, self.cat_l3.id)] discovered = self.group_order.product_ids # Should include L4 and L5 (descendants of L3) self.assertIn(self.product_l4, discovered) self.assertIn(self.product_l5, discovered) # Should not include L2 (ancestor) self.assertNotIn(self.product_l2, discovered) def test_discovery_leaf_category_only_own_products(self): """Test discovery from leaf (deepest) category.""" self.group_order.category_ids = [(4, self.cat_l5.id)] discovered = self.group_order.product_ids # Should only include products directly in L5 self.assertIn(self.product_l5, discovered) self.assertNotIn(self.product_l4, discovered) def test_discovery_circular_category_reference(self): """Test handling of circular category references (edge case).""" # Create circular reference (if allowed): L1 -> L2 -> L1 # This should be prevented by Odoo constraints # or handled gracefully in discovery logic # Attempt to create circular ref may fail try: self.cat_l1.parent_id = self.cat_l5.id # Creates loop except Exception as exc: # Expected: Odoo should prevent circular refs. Log for visibility. import logging logging.getLogger(__name__).info( "Expected exception creating circular category: %s", str(exc) ) class TestEmptySourcesDiscovery(TransactionCase): """Test discovery behavior with empty/null sources.""" def setUp(self): super().setUp() self.group = self.env["res.partner"].create( { "name": "Test Group", "is_company": True, } ) self.category = self.env["product.category"].create( { "name": "Empty Category", } ) # No products in this category self.supplier = self.env["res.partner"].create( { "name": "Supplier No Products", "is_supplier": True, } ) # No products from this supplier start_date = datetime.now().date() self.group_order = self.env["group.order"].create( { "name": "Test Order", "group_ids": [(6, 0, [self.group.id])], "type": "regular", "start_date": start_date, "end_date": start_date + timedelta(days=7), "period": "weekly", "pickup_day": "3", "cutoff_day": "0", } ) def test_discovery_empty_category(self): """Test discovery from empty category.""" self.group_order.category_ids = [(4, self.category.id)] discovered = self.group_order.product_ids # Should return empty list self.assertEqual(len(discovered), 0) def test_discovery_empty_supplier(self): """Test discovery from supplier with no products.""" self.group_order.supplier_ids = [(4, self.supplier.id)] discovered = self.group_order.product_ids # Should return empty list self.assertEqual(len(discovered), 0) def test_discovery_all_sources_empty(self): """Test when all 3 sources are empty.""" # No direct products, empty category, empty supplier self.group_order.product_ids = [(6, 0, [])] self.group_order.category_ids = [(4, self.category.id)] self.group_order.supplier_ids = [(4, self.supplier.id)] discovered = self.group_order.product_ids # Should return empty self.assertEqual(len(discovered), 0) class TestProductDiscoveryOrdering(TransactionCase): """Test that discovered products are returned in consistent order.""" def setUp(self): super().setUp() self.group = self.env["res.partner"].create( { "name": "Test Group", "is_company": True, } ) self.category = self.env["product.category"].create( { "name": "Test Category", } ) # Create products with specific names self.products = [] for i in range(5): product = self.env["product.product"].create( { "name": f"Product {chr(65 + i)}", # A, B, C, D, E "type": "consu", "list_price": (i + 1) * 10.0, "categ_id": self.category.id, "is_published": True, "sale_ok": True, } ) self.products.append(product) start_date = datetime.now().date() self.group_order = self.env["group.order"].create( { "name": "Test Order", "group_ids": [(6, 0, [self.group.id])], "type": "regular", "start_date": start_date, "end_date": start_date + timedelta(days=7), "period": "weekly", "pickup_day": "3", "cutoff_day": "0", } ) def test_discovery_consistent_ordering(self): """Test that repeated calls return same order.""" self.group_order.category_ids = [(4, self.category.id)] discovered1 = list(self.group_order.product_ids) discovered2 = list(self.group_order.product_ids) # Order should be consistent self.assertEqual([p.id for p in discovered1], [p.id for p in discovered2]) def test_discovery_alphabetical_or_price_order(self): """Test that products are ordered predictably.""" self.group_order.category_ids = [(4, self.category.id)] discovered = list(self.group_order.product_ids) # Should be in some consistent order (name, price, ID, etc) # Verify they're the same products, regardless of order self.assertEqual(len(discovered), 5) discovered_ids = {p.id for p in discovered} expected_ids = {p.id for p in self.products} self.assertEqual(discovered_ids, expected_ids)