# 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. Discovery is owned by `group.order._get_products_for_group_order(order_id)` and returns the UNION of three inclusion sources: 1. `product_ids`: directly linked products 2. `category_ids`: products in those categories *and all their subcategories* 3. `supplier_ids`: products offered by those suppliers (via `seller_ids`) filtered by `active` / `is_published` / `sale_ok`, and then reduced by three blacklists that have absolute priority over every inclusion source: - `excluded_product_ids` - `excluded_supplier_ids` (matches `product_tmpl_id.main_seller_id`) - `excluded_category_ids` (recursive, includes subcategories) Note: `product_ids` is an inclusion *input*, never the discovery result. Every assertion here goes through `_get_products_for_group_order`. """ from datetime import datetime from datetime import timedelta from odoo.exceptions import UserError from odoo.tests.common import TransactionCase class ProductDiscoveryCommon: """Shared builders for the discovery test cases.""" def _create_group_order(self, name="Test Order", **overrides): start_date = datetime.now().date() vals = { "name": name, "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", } vals.update(overrides) return self.env["group.order"].create(vals) def _create_product(self, name, **overrides): """Create a sellable, published consu product. `consu` products are non-storable by default, so they are never dropped by the forecasted-stock filter in `_apply_stock_filter_and_sort`. """ vals = { "name": name, "type": "consu", "list_price": 10.0, "is_published": True, "sale_ok": True, } vals.update(overrides) return self.env["product.product"].create(vals) def _create_supplier(self, name): return self.env["res.partner"].create( { "name": name, "is_company": True, "supplier_rank": 1, } ) def _set_main_seller(self, product, supplier, price=1.0): """Attach a supplierinfo so `main_seller_id` computes to `supplier`. `product_main_seller.main_seller_id` is a stored *computed* field that reads the first entry of `variant_seller_ids`; writing it directly on create is silently discarded by the compute. """ self.env["product.supplierinfo"].create( { "partner_id": supplier.id, "product_tmpl_id": product.product_tmpl_id.id, "price": price, } ) return product def _discover(self, group_order=None): group_order = group_order or self.group_order return group_order._get_products_for_group_order(group_order.id) class TestProductDiscoveryUnion(ProductDiscoveryCommon, TransactionCase): """Discovery returns the union of the three inclusion sources.""" def setUp(self): super().setUp() self.group = self.env["res.partner"].create( { "name": "Test Group", "is_company": True, } ) self.supplier = self._create_supplier("Test Supplier") self.category1 = self.env["product.category"].create({"name": "Category 1"}) self.category2 = self.env["product.category"].create({"name": "Category 2"}) self.direct_product = self._create_product("Direct Product") self.cat1_product = self._create_product( "Category 1 Product", categ_id=self.category1.id, list_price=20.0 ) self.cat2_product = self._create_product( "Category 2 Product", categ_id=self.category2.id, list_price=30.0 ) # Reachable both through category1 and through the supplier. self.supplier_product = self._create_product( "Supplier Product", categ_id=self.category1.id, list_price=40.0 ) self._set_main_seller(self.supplier_product, self.supplier) self.group_order = self._create_group_order() def test_discovery_from_direct_products(self): """Directly linked products are discovered.""" self.group_order.product_ids = [(4, self.direct_product.id)] self.assertIn(self.direct_product, self._discover()) def test_discovery_from_categories(self): """Products of a linked category are discovered.""" self.group_order.category_ids = [(4, self.category1.id)] discovered = self._discover() self.assertIn(self.cat1_product, discovered) self.assertIn(self.supplier_product, discovered) # A product of an unrelated category stays out. self.assertNotIn(self.cat2_product, discovered) def test_discovery_from_suppliers(self): """Products offered by a linked supplier are discovered.""" self.group_order.supplier_ids = [(4, self.supplier.id)] discovered = self._discover() self.assertIn(self.supplier_product, discovered) self.assertNotIn(self.cat1_product, discovered) def test_discovery_union_of_all_sources(self): """The three sources add up instead of shadowing each other.""" self.group_order.product_ids = [(4, self.direct_product.id)] self.group_order.category_ids = [(4, self.category2.id)] self.group_order.supplier_ids = [(4, self.supplier.id)] discovered = self._discover() self.assertIn(self.direct_product, discovered) self.assertIn(self.cat2_product, discovered) self.assertIn(self.supplier_product, discovered) def test_discovery_union_no_duplicates(self): """A product reachable through all three sources appears once.""" 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._discover() count = sum(1 for product in discovered if product == self.supplier_product) self.assertEqual(count, 1) def test_discovery_filters_unpublished(self): """Unpublished products are excluded.""" unpublished = self._create_product( "Unpublished Product", categ_id=self.category1.id, is_published=False, ) self.group_order.category_ids = [(4, self.category1.id)] self.assertNotIn(unpublished, self._discover()) def test_discovery_filters_not_for_sale(self): """Products flagged as not sellable are excluded.""" not_for_sale = self._create_product( "Not For Sale", categ_id=self.category1.id, sale_ok=False, ) self.group_order.category_ids = [(4, self.category1.id)] self.assertNotIn(not_for_sale, self._discover()) def test_discovery_filters_archived(self): """Archived products are excluded even when linked directly.""" archived = self._create_product("Archived Product") self.group_order.product_ids = [(4, archived.id)] archived.active = False self.assertNotIn(archived, self._discover()) def test_discovery_without_any_source_is_empty(self): """An order with no inclusion source discovers nothing.""" self.assertEqual(len(self._discover()), 0) class TestDeepCategoryHierarchies(ProductDiscoveryCommon, TransactionCase): """Category inclusion walks the whole subtree, never upwards.""" def setUp(self): super().setUp() self.group = self.env["res.partner"].create( { "name": "Test Group", "is_company": True, } ) # Level 1 -> Level 2 -> Level 3 -> Level 4 -> Level 5 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} ) self.product_l2 = self._create_product("Product L2", categ_id=self.cat_l2.id) self.product_l4 = self._create_product("Product L4", categ_id=self.cat_l4.id) self.product_l5 = self._create_product("Product L5", categ_id=self.cat_l5.id) self.group_order = self._create_group_order() def test_discovery_root_category_includes_all_descendants(self): """Linking the root category discovers every nested product.""" self.group_order.category_ids = [(4, self.cat_l1.id)] discovered = self._discover() 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): """Linking a mid-level category discovers its subtree only.""" self.group_order.category_ids = [(4, self.cat_l3.id)] discovered = self._discover() self.assertIn(self.product_l4, discovered) self.assertIn(self.product_l5, discovered) # L2 is an ancestor of L3, not a descendant. self.assertNotIn(self.product_l2, discovered) def test_discovery_leaf_category_only_own_products(self): """Linking a leaf category discovers only its own products.""" self.group_order.category_ids = [(4, self.cat_l5.id)] discovered = self._discover() self.assertIn(self.product_l5, discovered) self.assertNotIn(self.product_l4, discovered) self.assertNotIn(self.product_l2, discovered) def test_circular_category_reference_is_rejected(self): """Odoo refuses a category loop, so discovery cannot recurse forever. `_get_products_for_group_order` walks `child_id` recursively with no depth guard, so it relies on the ORM rejecting cycles up front. """ with self.assertRaises(UserError): self.cat_l1.parent_id = self.cat_l5.id class TestEmptySourcesDiscovery(ProductDiscoveryCommon, TransactionCase): """Sources that resolve to nothing yield an empty recordset.""" def setUp(self): super().setUp() self.group = self.env["res.partner"].create( { "name": "Test Group", "is_company": True, } ) # A category and a supplier, both without any product attached. self.category = self.env["product.category"].create({"name": "Empty Category"}) self.supplier = self._create_supplier("Supplier No Products") self.group_order = self._create_group_order() def test_discovery_empty_category(self): """A category with no products discovers nothing.""" self.group_order.category_ids = [(4, self.category.id)] self.assertEqual(len(self._discover()), 0) def test_discovery_empty_supplier(self): """A supplier with no products discovers nothing.""" self.group_order.supplier_ids = [(4, self.supplier.id)] self.assertEqual(len(self._discover()), 0) def test_discovery_all_sources_empty(self): """All three sources empty discovers nothing.""" 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)] self.assertEqual(len(self._discover()), 0) def test_discovery_on_missing_order_is_empty(self): """An unknown order id resolves to an empty recordset, not a crash.""" missing_id = self.group_order.id self.group_order.unlink() products = self.env["group.order"]._get_products_for_group_order(missing_id) self.assertEqual(len(products), 0) class TestProductDiscoveryOrdering(ProductDiscoveryCommon, TransactionCase): """Discovery sorts by (out of stock, website_sequence, lowercased name).""" 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"}) # Same website_sequence everywhere so the name is the tie-breaker. # Created out of alphabetical order on purpose. self.products = self.env["product.product"] for name in ("Product D", "Product A", "Product E", "Product C", "Product B"): product = self._create_product(name, categ_id=self.category.id) product.product_tmpl_id.website_sequence = 10 self.products |= product self.group_order = self._create_group_order() self.group_order.category_ids = [(4, self.category.id)] def test_discovery_consistent_ordering(self): """Repeated calls return the same order.""" first = self._discover().ids second = self._discover().ids self.assertEqual(first, second) def test_discovery_sorted_by_name_within_same_sequence(self): """With an equal website_sequence, products come out name-sorted.""" discovered = self._discover() self.assertEqual( discovered.mapped("name"), ["Product A", "Product B", "Product C", "Product D", "Product E"], ) def test_discovery_respects_website_sequence(self): """website_sequence wins over the alphabetical tie-breaker.""" last_alphabetically = self.products.filtered(lambda p: p.name == "Product E") last_alphabetically.product_tmpl_id.website_sequence = 1 discovered = self._discover() self.assertEqual(discovered[0], last_alphabetically) def test_discovery_returns_every_category_product(self): """No product of the linked category is dropped along the way.""" discovered = self._discover() self.assertEqual(set(discovered.ids), set(self.products.ids)) class TestProductBlacklist(ProductDiscoveryCommon, 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, } ) self.supplier = self._create_supplier("Test Supplier") self.category = self.env["product.category"].create({"name": "Test Category"}) # 1. Direct product (will be added to product_ids) self.direct_product = self._create_product("Direct Product") # 2. Category product (will be included via category_ids) self.category_product = self._create_product( "Category Product", categ_id=self.category.id, list_price=20.0 ) # 3. Supplier product (will be included via supplier_ids) self.supplier_product = self._create_product( "Supplier Product", list_price=30.0 ) self._set_main_seller(self.supplier_product, self.supplier, price=25.0) # 4. Multi-source product (reachable from all three sources) self.multi_product = self._create_product( "Multi Source Product", categ_id=self.category.id, list_price=40.0 ) self._set_main_seller(self.multi_product, self.supplier, price=35.0) self.group_order = self._create_group_order("Test Blacklist Order") def test_blacklist_excludes_direct_product(self): """Test that excluded_product_ids filters out directly linked products.""" self.group_order.product_ids = [(4, self.direct_product.id)] self.assertIn(self.direct_product, self._discover()) self.group_order.excluded_product_ids = [(4, self.direct_product.id)] self.assertNotIn(self.direct_product, self._discover()) def test_blacklist_excludes_category_product(self): """Test that excluded_product_ids filters out products from categories.""" self.group_order.category_ids = [(4, self.category.id)] self.assertIn(self.category_product, self._discover()) self.group_order.excluded_product_ids = [(4, self.category_product.id)] self.assertNotIn(self.category_product, self._discover()) def test_blacklist_excludes_supplier_product(self): """Test that excluded_product_ids filters out products from suppliers.""" self.group_order.supplier_ids = [(4, self.supplier.id)] self.assertIn(self.supplier_product, self._discover()) self.group_order.excluded_product_ids = [(4, self.supplier_product.id)] self.assertNotIn(self.supplier_product, self._discover()) def test_blacklist_priority_over_all_sources(self): """Test that blacklist has absolute priority for multi-source products.""" 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)] self.assertIn(self.multi_product, self._discover()) self.group_order.excluded_product_ids = [(4, self.multi_product.id)] self.assertNotIn(self.multi_product, self._discover()) def test_empty_blacklist_no_effect(self): """Test that empty excluded_product_ids doesn't affect discovery.""" 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)] discovered = self._discover() self.assertEqual(len(self.group_order.excluded_product_ids), 0) self.assertIn(self.direct_product, discovered) self.assertIn(self.category_product, discovered) self.assertIn(self.supplier_product, discovered) def test_blacklist_multiple_products(self): """Test excluding multiple products at once.""" 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)] self.group_order.excluded_product_ids = [ (4, self.direct_product.id), (4, self.category_product.id), ] discovered = self._discover() self.assertNotIn(self.direct_product, discovered) self.assertNotIn(self.category_product, discovered) self.assertIn(self.supplier_product, discovered) def test_blacklist_available_products_count(self): """Test that available_products_count reflects blacklist.""" self.group_order.product_ids = [(4, self.direct_product.id)] self.group_order.category_ids = [(4, self.category.id)] initial_count = self.group_order.available_products_count self.assertGreater(initial_count, 0) self.group_order.excluded_product_ids = [(4, self.category_product.id)] self.assertEqual(self.group_order.available_products_count, initial_count - 1) class TestSupplierBlacklist(ProductDiscoveryCommon, 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, } ) self.supplier_A = self._create_supplier("Supplier A") self.supplier_B = self._create_supplier("Supplier B") self.supplier_C = self._create_supplier("Supplier C") self.category = self.env["product.category"].create({"name": "Test Category"}) # main_seller_id is computed from the supplierinfo entries, so each # product gets its main vendor through _set_main_seller. self.product_A = self._create_product( "Product from Supplier A", categ_id=self.category.id ) self._set_main_seller(self.product_A, self.supplier_A) self.product_B = self._create_product( "Product from Supplier B", categ_id=self.category.id, list_price=20.0 ) self._set_main_seller(self.product_B, self.supplier_B) self.product_C = self._create_product( "Product from Supplier C", categ_id=self.category.id, list_price=30.0 ) self._set_main_seller(self.product_C, self.supplier_C) self.product_no_seller = self._create_product( "Product without main seller", categ_id=self.category.id, list_price=40.0 ) self.group_order = self._create_group_order("Test Supplier Blacklist Order") def test_main_seller_is_computed_from_supplierinfo(self): """Guard: the fixtures really do carry a main vendor.""" self.assertEqual(self.product_A.product_tmpl_id.main_seller_id, self.supplier_A) self.assertEqual(self.product_B.product_tmpl_id.main_seller_id, self.supplier_B) self.assertFalse(self.product_no_seller.product_tmpl_id.main_seller_id) def test_supplier_blacklist_excludes_by_main_seller(self): """Test that supplier blacklist excludes products by main_seller_id.""" self.group_order.category_ids = [(4, self.category.id)] discovered = self._discover() self.assertIn(self.product_A, discovered) self.assertIn(self.product_B, discovered) self.assertIn(self.product_C, discovered) self.assertIn(self.product_no_seller, discovered) self.group_order.excluded_supplier_ids = [(4, self.supplier_A.id)] discovered = self._discover() self.assertNotIn(self.product_A, discovered) self.assertIn(self.product_B, discovered) self.assertIn(self.product_C, discovered) self.assertIn(self.product_no_seller, discovered) def test_supplier_blacklist_multiple_suppliers(self): """Test excluding multiple suppliers at once.""" self.group_order.category_ids = [(4, self.category.id)] self.group_order.excluded_supplier_ids = [ (4, self.supplier_A.id), (4, self.supplier_B.id), ] discovered = self._discover() self.assertNotIn(self.product_A, discovered) self.assertNotIn(self.product_B, discovered) self.assertIn(self.product_C, discovered) self.assertIn(self.product_no_seller, discovered) def test_supplier_blacklist_does_not_affect_no_main_seller(self): """Products without a main vendor survive a supplier blacklist.""" self.group_order.category_ids = [(4, self.category.id)] self.group_order.excluded_supplier_ids = [ (4, self.supplier_A.id), (4, self.supplier_B.id), (4, self.supplier_C.id), ] discovered = self._discover() self.assertNotIn(self.product_A, discovered) self.assertNotIn(self.product_B, discovered) self.assertNotIn(self.product_C, discovered) self.assertIn(self.product_no_seller, discovered) def test_supplier_blacklist_with_direct_product_inclusion(self): """Test that supplier blacklist affects even directly included products.""" self.group_order.product_ids = [(4, self.product_A.id)] self.assertIn(self.product_A, self._discover()) self.group_order.excluded_supplier_ids = [(4, self.supplier_A.id)] self.assertNotIn(self.product_A, self._discover()) def test_supplier_blacklist_with_supplier_inclusion(self): """Test that supplier blacklist has priority over supplier inclusion.""" self.group_order.supplier_ids = [(4, self.supplier_A.id)] self.assertIn(self.product_A, self._discover()) self.group_order.excluded_supplier_ids = [(4, self.supplier_A.id)] self.assertNotIn(self.product_A, self._discover()) def test_empty_supplier_blacklist_no_effect(self): """Test that empty excluded_supplier_ids doesn't affect discovery.""" self.group_order.category_ids = [(4, self.category.id)] discovered = self._discover() self.assertEqual(len(self.group_order.excluded_supplier_ids), 0) self.assertIn(self.product_A, discovered) self.assertIn(self.product_B, discovered) self.assertIn(self.product_C, discovered) def test_supplier_and_product_blacklist_combined(self): """Test that both product and supplier blacklists work together.""" self.group_order.category_ids = [(4, self.category.id)] self.group_order.excluded_supplier_ids = [(4, self.supplier_A.id)] self.group_order.excluded_product_ids = [(4, self.product_B.id)] discovered = self._discover() self.assertNotIn(self.product_A, discovered) self.assertNotIn(self.product_B, discovered) self.assertIn(self.product_C, discovered) self.assertIn(self.product_no_seller, discovered) def test_supplier_blacklist_available_products_count(self): """Test that available_products_count reflects supplier blacklist.""" self.group_order.category_ids = [(4, self.category.id)] self.assertEqual(self.group_order.available_products_count, 4) self.group_order.excluded_supplier_ids = [(4, self.supplier_A.id)] self.assertEqual(self.group_order.available_products_count, 3) class TestCategoryBlacklist(ProductDiscoveryCommon, 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, } ) # 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)"} ) self.product_parent = self._create_product( "Product in Parent Category", categ_id=self.parent_category.id ) self.product_child_A = self._create_product( "Product in Child A", categ_id=self.child_category_A.id, list_price=20.0 ) self.product_grandchild_A1 = self._create_product( "Product in Grandchild A1", categ_id=self.grandchild_category_A1.id, list_price=30.0, ) self.product_child_B = self._create_product( "Product in Child B", categ_id=self.child_category_B.id, list_price=40.0 ) self.product_other = self._create_product( "Product in Other Category", categ_id=self.other_category.id, list_price=50.0, ) self.group_order = self._create_group_order("Test Category Blacklist Order") def test_category_blacklist_excludes_single_category(self): """Test that category blacklist excludes products in that category.""" self.group_order.category_ids = [(4, self.parent_category.id)] discovered = self._discover() self.assertIn(self.product_parent, discovered) self.assertIn(self.product_child_A, discovered) self.group_order.excluded_category_ids = [(4, self.child_category_B.id)] discovered = self._discover() self.assertNotIn(self.product_child_B, discovered) self.assertIn(self.product_parent, discovered) self.assertIn(self.product_child_A, discovered) def test_category_blacklist_excludes_with_subcategories(self): """Excluding a category also excludes its subcategories.""" self.group_order.category_ids = [(4, self.parent_category.id)] self.group_order.excluded_category_ids = [(4, self.child_category_A.id)] discovered = self._discover() self.assertNotIn(self.product_child_A, discovered) self.assertNotIn(self.product_grandchild_A1, discovered) self.assertIn(self.product_parent, discovered) self.assertIn(self.product_child_B, discovered) def test_category_blacklist_excludes_parent_excludes_all_children(self): """Excluding the root of the hierarchy empties the whole subtree.""" self.group_order.category_ids = [(4, self.parent_category.id)] self.group_order.excluded_category_ids = [(4, self.parent_category.id)] discovered = self._discover() self.assertNotIn(self.product_parent, discovered) self.assertNotIn(self.product_child_A, discovered) self.assertNotIn(self.product_grandchild_A1, discovered) self.assertNotIn(self.product_child_B, discovered) def test_category_blacklist_with_direct_product_inclusion(self): """The category blacklist also beats a direct product inclusion.""" self.group_order.product_ids = [(4, self.product_child_A.id)] self.assertIn(self.product_child_A, self._discover()) self.group_order.excluded_category_ids = [(4, self.child_category_A.id)] self.assertNotIn(self.product_child_A, self._discover()) def test_category_blacklist_does_not_affect_other_categories(self): """Products outside the excluded subtree are untouched.""" self.group_order.category_ids = [ (4, self.parent_category.id), (4, self.other_category.id), ] self.group_order.excluded_category_ids = [(4, self.parent_category.id)] self.assertIn(self.product_other, self._discover()) def test_empty_category_blacklist_no_effect(self): """Test that empty excluded_category_ids doesn't affect discovery.""" self.group_order.category_ids = [(4, self.parent_category.id)] discovered = self._discover() self.assertEqual(len(self.group_order.excluded_category_ids), 0) self.assertIn(self.product_parent, discovered) self.assertIn(self.product_child_A, discovered) self.assertIn(self.product_grandchild_A1, discovered) self.assertIn(self.product_child_B, discovered) def test_multiple_category_exclusions(self): """Test excluding several categories at once.""" self.group_order.category_ids = [ (4, self.parent_category.id), (4, self.other_category.id), ] self.group_order.excluded_category_ids = [ (4, self.child_category_B.id), (4, self.other_category.id), ] discovered = self._discover() self.assertNotIn(self.product_child_B, discovered) self.assertNotIn(self.product_other, discovered) self.assertIn(self.product_parent, discovered) self.assertIn(self.product_child_A, discovered) def test_category_blacklist_combined_with_other_blacklists(self): """Category, product and supplier blacklists stack.""" supplier = self._create_supplier("Blacklisted Supplier") supplier_product = self._create_product( "Product from blacklisted supplier", categ_id=self.other_category.id ) self._set_main_seller(supplier_product, supplier) self.group_order.category_ids = [ (4, self.parent_category.id), (4, self.other_category.id), ] self.group_order.excluded_category_ids = [(4, self.child_category_A.id)] self.group_order.excluded_product_ids = [(4, self.product_child_B.id)] self.group_order.excluded_supplier_ids = [(4, supplier.id)] discovered = self._discover() self.assertNotIn(self.product_child_A, discovered) self.assertNotIn(self.product_grandchild_A1, discovered) self.assertNotIn(self.product_child_B, discovered) self.assertNotIn(supplier_product, discovered) self.assertIn(self.product_parent, discovered) self.assertIn(self.product_other, discovered) def test_category_blacklist_available_products_count(self): """Test that available_products_count reflects the category blacklist.""" self.group_order.category_ids = [(4, self.parent_category.id)] # parent + child A + grandchild A1 + child B self.assertEqual(self.group_order.available_products_count, 4) # Drops child A and its grandchild. self.group_order.excluded_category_ids = [(4, self.child_category_A.id)] self.assertEqual(self.group_order.available_products_count, 2)