# 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) class TestProductBlacklist(TransactionCase): """Test blacklist (excluded_product_ids) functionality. The blacklist must have absolute priority over all inclusion sources: - Direct product_ids - Products from category_ids - Products from supplier_ids If a product is in excluded_product_ids, it should NEVER appear in the discovered products, regardless of how it was included. """ 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_company": True, "supplier_rank": 1, } ) # Create category self.category = self.env["product.category"].create( { "name": "Test Category", } ) # Create products # 1. Direct product (will be added to product_ids) self.direct_product = self.env["product.product"].create( { "name": "Direct Product", "type": "consu", "list_price": 10.0, "is_published": True, "sale_ok": True, } ) # 2. Category product (will be included via category_ids) self.category_product = self.env["product.product"].create( { "name": "Category Product", "type": "consu", "list_price": 20.0, "categ_id": self.category.id, "is_published": True, "sale_ok": True, } ) # 3. Supplier product (will be included via supplier_ids) product_tmpl = self.env["product.template"].create( { "name": "Supplier Product", "type": "consu", "list_price": 30.0, "is_published": True, "sale_ok": True, } ) self.supplier_product = product_tmpl.product_variant_ids[0] self.env["product.supplierinfo"].create( { "partner_id": self.supplier.id, "product_tmpl_id": product_tmpl.id, "price": 25.0, } ) # 4. Multi-source product (in all three sources) multi_tmpl = self.env["product.template"].create( { "name": "Multi Source Product", "type": "consu", "list_price": 40.0, "categ_id": self.category.id, "is_published": True, "sale_ok": True, } ) self.multi_product = multi_tmpl.product_variant_ids[0] self.env["product.supplierinfo"].create( { "partner_id": self.supplier.id, "product_tmpl_id": multi_tmpl.id, "price": 35.0, } ) start_date = datetime.now().date() self.group_order = self.env["group.order"].create( { "name": "Test Blacklist 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_blacklist_excludes_direct_product(self): """Test that excluded_product_ids filters out directly linked products.""" # Add product directly self.group_order.product_ids = [(4, self.direct_product.id)] # Product should be discoverable products = self.group_order._get_products_for_group_order(self.group_order.id) self.assertIn(self.direct_product, products) # Now add to blacklist self.group_order.excluded_product_ids = [(4, self.direct_product.id)] # Product should NOT be discoverable anymore products = self.group_order._get_products_for_group_order(self.group_order.id) self.assertNotIn(self.direct_product, products) def test_blacklist_excludes_category_product(self): """Test that excluded_product_ids filters out products from categories.""" # Add category (includes category_product) self.group_order.category_ids = [(4, self.category.id)] # Product should be discoverable products = self.group_order._get_products_for_group_order(self.group_order.id) self.assertIn(self.category_product, products) # Now add to blacklist self.group_order.excluded_product_ids = [(4, self.category_product.id)] # Product should NOT be discoverable anymore products = self.group_order._get_products_for_group_order(self.group_order.id) self.assertNotIn(self.category_product, products) def test_blacklist_excludes_supplier_product(self): """Test that excluded_product_ids filters out products from suppliers.""" # Add supplier (includes supplier_product) self.group_order.supplier_ids = [(4, self.supplier.id)] # Product should be discoverable products = self.group_order._get_products_for_group_order(self.group_order.id) self.assertIn(self.supplier_product, products) # Now add to blacklist self.group_order.excluded_product_ids = [(4, self.supplier_product.id)] # Product should NOT be discoverable anymore products = self.group_order._get_products_for_group_order(self.group_order.id) self.assertNotIn(self.supplier_product, products) def test_blacklist_priority_over_all_sources(self): """Test that blacklist has absolute priority even for multi-source products.""" # Add multi_product via all three sources self.group_order.product_ids = [(4, self.multi_product.id)] self.group_order.category_ids = [(4, self.category.id)] self.group_order.supplier_ids = [(4, self.supplier.id)] # Product should be discoverable products = self.group_order._get_products_for_group_order(self.group_order.id) self.assertIn(self.multi_product, products) # Now add to blacklist self.group_order.excluded_product_ids = [(4, self.multi_product.id)] # Product should NOT be discoverable anymore, despite being in all sources products = self.group_order._get_products_for_group_order(self.group_order.id) self.assertNotIn(self.multi_product, products) def test_empty_blacklist_no_effect(self): """Test that empty excluded_product_ids doesn't affect discovery.""" # Add products via various sources self.group_order.product_ids = [(4, self.direct_product.id)] self.group_order.category_ids = [(4, self.category.id)] self.group_order.supplier_ids = [(4, self.supplier.id)] # All products should be discoverable products = self.group_order._get_products_for_group_order(self.group_order.id) self.assertIn(self.direct_product, products) self.assertIn(self.category_product, products) self.assertIn(self.supplier_product, products) # Excluded list is empty - should have no effect self.assertEqual(len(self.group_order.excluded_product_ids), 0) def test_blacklist_multiple_products(self): """Test excluding multiple products at once.""" # Add all products self.group_order.product_ids = [(4, self.direct_product.id)] self.group_order.category_ids = [(4, self.category.id)] self.group_order.supplier_ids = [(4, self.supplier.id)] # Exclude two products self.group_order.excluded_product_ids = [ (4, self.direct_product.id), (4, self.category_product.id), ] products = self.group_order._get_products_for_group_order(self.group_order.id) # These two should NOT be in results self.assertNotIn(self.direct_product, products) self.assertNotIn(self.category_product, products) # But supplier_product should still be there self.assertIn(self.supplier_product, products) def test_blacklist_available_products_count(self): """Test that available_products_count reflects blacklist.""" # Add products self.group_order.product_ids = [(4, self.direct_product.id)] self.group_order.category_ids = [(4, self.category.id)] # Count should include direct + category products initial_count = self.group_order.available_products_count self.assertGreater(initial_count, 0) # Exclude one product self.group_order.excluded_product_ids = [(4, self.category_product.id)] # Count should decrease by 1 new_count = self.group_order.available_products_count self.assertEqual(new_count, initial_count - 1) class TestSupplierBlacklist(TransactionCase): """Test supplier blacklist (excluded_supplier_ids) functionality. The supplier blacklist filters out products whose main_seller_id (from product_main_seller addon) is in the excluded suppliers list. Blacklist has absolute priority over inclusion sources. """ def setUp(self): super().setUp() self.group = self.env["res.partner"].create( { "name": "Test Group", "is_company": True, } ) # Create suppliers self.supplier_A = self.env["res.partner"].create( { "name": "Supplier A", "is_company": True, "supplier_rank": 1, } ) self.supplier_B = self.env["res.partner"].create( { "name": "Supplier B", "is_company": True, "supplier_rank": 1, } ) self.supplier_C = self.env["res.partner"].create( { "name": "Supplier C", "is_company": True, "supplier_rank": 1, } ) # Create category self.category = self.env["product.category"].create( { "name": "Test Category", } ) # Create products with different main sellers # Product 1: main seller = Supplier A tmpl_1 = self.env["product.template"].create( { "name": "Product from Supplier A", "type": "consu", "list_price": 10.0, "categ_id": self.category.id, "is_published": True, "sale_ok": True, "main_seller_id": self.supplier_A.id, } ) self.product_A = tmpl_1.product_variant_ids[0] # Product 2: main seller = Supplier B tmpl_2 = self.env["product.template"].create( { "name": "Product from Supplier B", "type": "consu", "list_price": 20.0, "categ_id": self.category.id, "is_published": True, "sale_ok": True, "main_seller_id": self.supplier_B.id, } ) self.product_B = tmpl_2.product_variant_ids[0] # Product 3: main seller = Supplier C tmpl_3 = self.env["product.template"].create( { "name": "Product from Supplier C", "type": "consu", "list_price": 30.0, "categ_id": self.category.id, "is_published": True, "sale_ok": True, "main_seller_id": self.supplier_C.id, } ) self.product_C = tmpl_3.product_variant_ids[0] # Product 4: no main seller tmpl_4 = self.env["product.template"].create( { "name": "Product without main seller", "type": "consu", "list_price": 40.0, "categ_id": self.category.id, "is_published": True, "sale_ok": True, } ) self.product_no_seller = tmpl_4.product_variant_ids[0] start_date = datetime.now().date() self.group_order = self.env["group.order"].create( { "name": "Test Supplier Blacklist 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_supplier_blacklist_excludes_by_main_seller(self): """Test that supplier blacklist excludes products by main_seller_id.""" # Add all products via category self.group_order.category_ids = [(4, self.category.id)] # All products should be discoverable initially products = self.group_order._get_products_for_group_order(self.group_order.id) self.assertIn(self.product_A, products) self.assertIn(self.product_B, products) self.assertIn(self.product_C, products) self.assertIn(self.product_no_seller, products) # Exclude Supplier A self.group_order.excluded_supplier_ids = [(4, self.supplier_A.id)] # Product A should NOT be discoverable anymore products = self.group_order._get_products_for_group_order(self.group_order.id) self.assertNotIn(self.product_A, products) self.assertIn(self.product_B, products) self.assertIn(self.product_C, products) self.assertIn(self.product_no_seller, products) def test_supplier_blacklist_multiple_suppliers(self): """Test excluding multiple suppliers at once.""" # Add all products via category self.group_order.category_ids = [(4, self.category.id)] # Exclude Suppliers A and B self.group_order.excluded_supplier_ids = [ (4, self.supplier_A.id), (4, self.supplier_B.id), ] products = self.group_order._get_products_for_group_order(self.group_order.id) # Products A and B should NOT be in results self.assertNotIn(self.product_A, products) self.assertNotIn(self.product_B, products) # But product C and no-seller product should be there self.assertIn(self.product_C, products) self.assertIn(self.product_no_seller, products) def test_supplier_blacklist_does_not_affect_no_main_seller(self): """Test that products without main_seller_id are not affected by supplier blacklist.""" # Add all products via category self.group_order.category_ids = [(4, self.category.id)] # Exclude all suppliers self.group_order.excluded_supplier_ids = [ (4, self.supplier_A.id), (4, self.supplier_B.id), (4, self.supplier_C.id), ] products = self.group_order._get_products_for_group_order(self.group_order.id) # Products with main sellers should NOT be in results self.assertNotIn(self.product_A, products) self.assertNotIn(self.product_B, products) self.assertNotIn(self.product_C, products) # But product without main seller should still be there self.assertIn(self.product_no_seller, products) def test_supplier_blacklist_with_direct_product_inclusion(self): """Test that supplier blacklist affects even directly included products.""" # Add product A directly self.group_order.product_ids = [(4, self.product_A.id)] # Product should be discoverable products = self.group_order._get_products_for_group_order(self.group_order.id) self.assertIn(self.product_A, products) # Exclude Supplier A self.group_order.excluded_supplier_ids = [(4, self.supplier_A.id)] # Product A should NOT be discoverable anymore, even though directly included products = self.group_order._get_products_for_group_order(self.group_order.id) self.assertNotIn(self.product_A, products) def test_supplier_blacklist_with_supplier_inclusion(self): """Test that supplier blacklist has priority over supplier inclusion.""" # Add Supplier A to included suppliers self.group_order.supplier_ids = [(4, self.supplier_A.id)] # Also add Supplier A to excluded suppliers (blacklist has priority) self.group_order.excluded_supplier_ids = [(4, self.supplier_A.id)] products = self.group_order._get_products_for_group_order(self.group_order.id) # Product A should NOT be discoverable (blacklist wins) self.assertNotIn(self.product_A, products) def test_empty_supplier_blacklist_no_effect(self): """Test that empty excluded_supplier_ids doesn't affect discovery.""" # Add products via category self.group_order.category_ids = [(4, self.category.id)] # All products should be discoverable products = self.group_order._get_products_for_group_order(self.group_order.id) self.assertIn(self.product_A, products) self.assertIn(self.product_B, products) self.assertIn(self.product_C, products) # Excluded supplier list is empty - should have no effect self.assertEqual(len(self.group_order.excluded_supplier_ids), 0) def test_supplier_and_product_blacklist_combined(self): """Test that both product and supplier blacklists work together.""" # Add all products via category self.group_order.category_ids = [(4, self.category.id)] # Exclude Supplier A (affects product_A) self.group_order.excluded_supplier_ids = [(4, self.supplier_A.id)] # Exclude product B directly self.group_order.excluded_product_ids = [(4, self.product_B.id)] products = self.group_order._get_products_for_group_order(self.group_order.id) # Products A and B should NOT be in results self.assertNotIn(self.product_A, products) self.assertNotIn(self.product_B, products) # But products C and no-seller should be there self.assertIn(self.product_C, products) self.assertIn(self.product_no_seller, products) def test_supplier_blacklist_available_products_count(self): """Test that available_products_count reflects supplier blacklist.""" # Add products self.group_order.category_ids = [(4, self.category.id)] # Count should include all 4 products initial_count = self.group_order.available_products_count self.assertEqual(initial_count, 4) # Exclude Supplier A self.group_order.excluded_supplier_ids = [(4, self.supplier_A.id)] # Count should decrease by 1 (product_A excluded) new_count = self.group_order.available_products_count self.assertEqual(new_count, 3) class TestCategoryBlacklist(TransactionCase): """Test category blacklist (excluded_category_ids) functionality. The category blacklist filters out products in the excluded categories AND all their subcategories (recursive). Blacklist has absolute priority over inclusion sources. """ def setUp(self): super().setUp() self.group = self.env["res.partner"].create( { "name": "Test Group", "is_company": True, } ) # Create category hierarchy: # Parent Category # ├── Child Category A # │ └── Grandchild Category A1 # └── Child Category B self.parent_category = self.env["product.category"].create( { "name": "Parent Category", } ) self.child_category_A = self.env["product.category"].create( { "name": "Child Category A", "parent_id": self.parent_category.id, } ) self.grandchild_category_A1 = self.env["product.category"].create( { "name": "Grandchild Category A1", "parent_id": self.child_category_A.id, } ) self.child_category_B = self.env["product.category"].create( { "name": "Child Category B", "parent_id": self.parent_category.id, } ) self.other_category = self.env["product.category"].create( { "name": "Other Category (not in hierarchy)", } ) # Create products in different categories # Product in parent category self.product_parent = self.env["product.product"].create( { "name": "Product in Parent Category", "type": "consu", "list_price": 10.0, "categ_id": self.parent_category.id, "is_published": True, "sale_ok": True, } ) # Product in Child A self.product_child_A = self.env["product.product"].create( { "name": "Product in Child A", "type": "consu", "list_price": 20.0, "categ_id": self.child_category_A.id, "is_published": True, "sale_ok": True, } ) # Product in Grandchild A1 self.product_grandchild_A1 = self.env["product.product"].create( { "name": "Product in Grandchild A1", "type": "consu", "list_price": 30.0, "categ_id": self.grandchild_category_A1.id, "is_published": True, "sale_ok": True, } ) # Product in Child B self.product_child_B = self.env["product.product"].create( { "name": "Product in Child B", "type": "consu", "list_price": 40.0, "categ_id": self.child_category_B.id, "is_published": True, "sale_ok": True, } ) # Product in Other Category self.product_other = self.env["product.product"].create( { "name": "Product in Other Category", "type": "consu", "list_price": 50.0, "categ_id": self.other_category.id, "is_published": True, "sale_ok": True, } ) start_date = datetime.now().date() self.group_order = self.env["group.order"].create( { "name": "Test Category Blacklist 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_category_blacklist_excludes_single_category(self): """Test that category blacklist excludes products in that category.""" # Add parent category to inclusion (includes all products in hierarchy) self.group_order.category_ids = [(4, self.parent_category.id)] # All products should be discoverable initially products = self.group_order._get_products_for_group_order(self.group_order.id) self.assertIn(self.product_parent, products) self.assertIn(self.product_child_A, products) self.assertIn(self.product_grandchild_A1, products) self.assertIn(self.product_child_B, products) # Exclude Child Category B self.group_order.excluded_category_ids = [(4, self.child_category_B.id)] # Product in Child B should NOT be discoverable products = self.group_order._get_products_for_group_order(self.group_order.id) self.assertNotIn(self.product_child_B, products) # But others should still be there self.assertIn(self.product_parent, products) self.assertIn(self.product_child_A, products) self.assertIn(self.product_grandchild_A1, products) def test_category_blacklist_excludes_with_subcategories(self): """Test that excluding a category also excludes all its subcategories (recursive).""" # Add parent category to inclusion self.group_order.category_ids = [(4, self.parent_category.id)] # Exclude Child Category A (should also exclude Grandchild A1) self.group_order.excluded_category_ids = [(4, self.child_category_A.id)] products = self.group_order._get_products_for_group_order(self.group_order.id) # Products in Child A and Grandchild A1 should NOT be discoverable self.assertNotIn(self.product_child_A, products) self.assertNotIn(self.product_grandchild_A1, products) # Recursive exclusion # But products in parent and Child B should still be there self.assertIn(self.product_parent, products) self.assertIn(self.product_child_B, products) def test_category_blacklist_excludes_parent_excludes_all_children(self): """Test that excluding parent category excludes ALL descendants.""" # Add parent category to inclusion (includes all) self.group_order.category_ids = [(4, self.parent_category.id)] # Exclude the parent category (should exclude ALL products in hierarchy) self.group_order.excluded_category_ids = [(4, self.parent_category.id)] products = self.group_order._get_products_for_group_order(self.group_order.id) # ALL products in the hierarchy should be excluded self.assertNotIn(self.product_parent, products) self.assertNotIn(self.product_child_A, products) self.assertNotIn(self.product_grandchild_A1, products) self.assertNotIn(self.product_child_B, products) # Result should be empty (no products available) self.assertEqual(len(products), 0) def test_category_blacklist_with_direct_product_inclusion(self): """Test that category blacklist affects even directly included products.""" # Add product directly self.group_order.product_ids = [(4, self.product_child_A.id)] # Product should be discoverable products = self.group_order._get_products_for_group_order(self.group_order.id) self.assertIn(self.product_child_A, products) # Exclude the category self.group_order.excluded_category_ids = [(4, self.child_category_A.id)] # Product should NOT be discoverable anymore, even though directly included products = self.group_order._get_products_for_group_order(self.group_order.id) self.assertNotIn(self.product_child_A, products) def test_category_blacklist_does_not_affect_other_categories(self): """Test that excluding a category doesn't affect products in unrelated categories.""" # Add all categories to inclusion self.group_order.category_ids = [ (4, self.parent_category.id), (4, self.other_category.id), ] # Exclude parent category self.group_order.excluded_category_ids = [(4, self.parent_category.id)] products = self.group_order._get_products_for_group_order(self.group_order.id) # Products in parent hierarchy should NOT be there self.assertNotIn(self.product_parent, products) self.assertNotIn(self.product_child_A, products) self.assertNotIn(self.product_child_B, products) # But product in other category should still be there self.assertIn(self.product_other, products) def test_empty_category_blacklist_no_effect(self): """Test that empty excluded_category_ids doesn't affect discovery.""" # Add parent category self.group_order.category_ids = [(4, self.parent_category.id)] # All products should be discoverable products = self.group_order._get_products_for_group_order(self.group_order.id) self.assertIn(self.product_parent, products) self.assertIn(self.product_child_A, products) self.assertIn(self.product_grandchild_A1, products) self.assertIn(self.product_child_B, products) # Excluded category list is empty - should have no effect self.assertEqual(len(self.group_order.excluded_category_ids), 0) def test_multiple_category_exclusions(self): """Test excluding multiple categories at once.""" # Add parent category self.group_order.category_ids = [(4, self.parent_category.id)] # Exclude both child categories self.group_order.excluded_category_ids = [ (4, self.child_category_A.id), (4, self.child_category_B.id), ] products = self.group_order._get_products_for_group_order(self.group_order.id) # Products in both child categories (+ grandchild) should NOT be there self.assertNotIn(self.product_child_A, products) self.assertNotIn(self.product_grandchild_A1, products) self.assertNotIn(self.product_child_B, products) # But product in parent category should still be there self.assertIn(self.product_parent, products) def test_category_blacklist_combined_with_other_blacklists(self): """Test that category blacklist works together with product and supplier blacklists.""" # Add parent category self.group_order.category_ids = [(4, self.parent_category.id)] # Exclude Child A category (affects product_child_A and product_grandchild_A1) self.group_order.excluded_category_ids = [(4, self.child_category_A.id)] # Also exclude product_child_B directly self.group_order.excluded_product_ids = [(4, self.product_child_B.id)] products = self.group_order._get_products_for_group_order(self.group_order.id) # Products excluded by category blacklist self.assertNotIn(self.product_child_A, products) self.assertNotIn(self.product_grandchild_A1, products) # Product excluded by product blacklist self.assertNotIn(self.product_child_B, products) # Only product_parent should be available self.assertIn(self.product_parent, products) self.assertEqual(len(products), 1) def test_category_blacklist_available_products_count(self): """Test that available_products_count reflects category blacklist.""" # Add parent category (4 products in hierarchy) self.group_order.category_ids = [(4, self.parent_category.id)] # Count should include all 4 products initial_count = self.group_order.available_products_count self.assertEqual(initial_count, 4) # Exclude Child Category A (should exclude 2 products: child_A + grandchild_A1) self.group_order.excluded_category_ids = [(4, self.child_category_A.id)] # Count should decrease by 2 new_count = self.group_order.available_products_count self.assertEqual(new_count, 2)