diff --git a/website_sale_aplicoop/CHANGELOG.md b/website_sale_aplicoop/CHANGELOG.md index b7a35f8..1e59f70 100644 --- a/website_sale_aplicoop/CHANGELOG.md +++ b/website_sale_aplicoop/CHANGELOG.md @@ -1,5 +1,42 @@ # Changelog - Website Sale Aplicoop +## [18.0.1.5.0] - 2026-02-22 + +### Added + +- **Supplier Blacklist Feature**: New exclusion system by supplier for group orders + - New field: `excluded_supplier_ids` (Many2many to res.partner) + - Filters products by `main_seller_id` (from product_main_seller addon) + - Blacklist has absolute priority over all inclusion sources + - Products whose main supplier is blacklisted never appear + - Comprehensive test suite in `test_product_discovery.py` (TestSupplierBlacklist class with 9 tests) + +### Changed + +- **Product Discovery Logic**: Extended to filter by supplier blacklist + - `_get_products_for_group_order()` now applies `excluded_supplier_ids` filter + - Products with `main_seller_id` in excluded_supplier_ids are filtered out + - `_compute_available_products_count()` now depends on `excluded_supplier_ids` + - Detailed logging for excluded suppliers and affected products + +- **UI Updates**: "Productos Excluidos" section now includes both: + - `excluded_supplier_ids`: Blacklist suppliers + - `excluded_product_ids`: Blacklist specific products + +### Technical Details + +- New M2M relation: `group_order_excluded_supplier_rel` +- Filter logic: `products.filtered(lambda p: p.product_tmpl_id.main_seller_id not in excluded_supplier_ids)` +- Works in combination with product blacklist (both filters apply) +- Uses `main_seller_id` from product_main_seller addon (NOT default_supplier_id) + +### Use Case + +- Admin wants to exclude all products from a specific supplier (e.g., temporary unavailability) +- Add category with 100 products → add problematic supplier to excluded_supplier_ids +- Result: All products from that supplier are excluded, even if directly included +- Combined workflow: Category inclusion + supplier exclusion + individual product exclusion + ## [18.0.1.4.0] - 2026-02-22 ### Added diff --git a/website_sale_aplicoop/__manifest__.py b/website_sale_aplicoop/__manifest__.py index 67101ed..e217951 100644 --- a/website_sale_aplicoop/__manifest__.py +++ b/website_sale_aplicoop/__manifest__.py @@ -3,7 +3,7 @@ { # noqa: B018 "name": "Website Sale - Aplicoop", - "version": "18.0.1.4.0", + "version": "18.0.1.5.0", "category": "Website/Sale", "summary": "Modern replacement of legacy Aplicoop - Collaborative consumption group orders", "author": "Odoo Community Association (OCA), Criptomart", diff --git a/website_sale_aplicoop/i18n/es.po b/website_sale_aplicoop/i18n/es.po index 61a95f7..300a044 100644 --- a/website_sale_aplicoop/i18n/es.po +++ b/website_sale_aplicoop/i18n/es.po @@ -146772,3 +146772,18 @@ msgstr "" #. model_terms:ir.ui.view,arch_db:website_sale_aplicoop.view_group_order_form msgid "Specific products to include directly" msgstr "Productos específicos a incluir directamente" + +#. module: website_sale_aplicoop +#. model_terms:ir.ui.view,arch_db:website_sale_aplicoop.view_group_order_form +msgid "" +"Suppliers excluded from this order. Products with these suppliers as main " +"seller will not be available (blacklist has absolute priority)" +msgstr "" +"Proveedores excluidos de este pedido. Los productos cuyo proveedor principal" +" sea uno de estos no estarán disponibles (la lista negra tiene prioridad " +"absoluta)" + +#. module: website_sale_aplicoop +#. model:ir.model.fields,field_description:website_sale_aplicoop.field_group_order__excluded_supplier_ids +msgid "Proveedores Excluidos" +msgstr "Proveedores Excluidos" diff --git a/website_sale_aplicoop/i18n/eu.po b/website_sale_aplicoop/i18n/eu.po index 510f99e..b16faa2 100644 --- a/website_sale_aplicoop/i18n/eu.po +++ b/website_sale_aplicoop/i18n/eu.po @@ -146743,3 +146743,18 @@ msgstr "" #. model_terms:ir.ui.view,arch_db:website_sale_aplicoop.view_group_order_form msgid "Specific products to include directly" msgstr "Zuzenean sartzeko produktu zehatzak" + +#. module: website_sale_aplicoop +#. model_terms:ir.ui.view,arch_db:website_sale_aplicoop.view_group_order_form +msgid "" +"Suppliers excluded from this order. Products with these suppliers as main " +"seller will not be available (blacklist has absolute priority)" +msgstr "" +"Eskaera honetatik kanporatutako hornitzaileak. Hornitzaile hauetako bat " +"hornitzaile nagusi duten produktuak ez dira eskuragarri egongo (zerrenda " +"beltzak lehentasun osoa du)" + +#. module: website_sale_aplicoop +#. model:ir.model.fields,field_description:website_sale_aplicoop.field_group_order__excluded_supplier_ids +msgid "Proveedores Excluidos" +msgstr "Kanporatutako Hornitzaileak" diff --git a/website_sale_aplicoop/models/group_order.py b/website_sale_aplicoop/models/group_order.py index 5f0e8fb..6cb9920 100644 --- a/website_sale_aplicoop/models/group_order.py +++ b/website_sale_aplicoop/models/group_order.py @@ -196,6 +196,15 @@ class GroupOrder(models.Model): tracking=True, help="Products explicitly excluded from this order (blacklist has absolute priority)", ) + excluded_supplier_ids = fields.Many2many( + "res.partner", + "group_order_excluded_supplier_rel", + "order_id", + "supplier_id", + domain=[("supplier_rank", ">", 0)], + tracking=True, + help="Suppliers excluded from this order. Products with these suppliers as main seller will not be available (blacklist has absolute priority)", + ) # === Estado === state = fields.Selection( @@ -241,7 +250,13 @@ class GroupOrder(models.Model): help="Total count of available products from all sources", ) - @api.depends("product_ids", "category_ids", "supplier_ids", "excluded_product_ids") + @api.depends( + "product_ids", + "category_ids", + "supplier_ids", + "excluded_product_ids", + "excluded_supplier_ids", + ) def _compute_available_products_count(self): """Count all available products from all sources.""" for record in self: @@ -395,17 +410,35 @@ class GroupOrder(models.Model): ).filtered("active") products |= supplier_products - # 4) Apply blacklist filter (absolute priority) + # 4) Apply product blacklist filter (absolute priority) if order.excluded_product_ids: excluded_count = len(products & order.excluded_product_ids) products = products - order.excluded_product_ids _logger.info( - "Group order %d: Excluded %d products from blacklist (total available: %d)", + "Group order %d: Excluded %d products from product blacklist (total: %d)", order.id, excluded_count, len(products), ) + # 5) Apply supplier blacklist filter (absolute priority) + # Exclude products whose main seller is in the excluded suppliers list + if order.excluded_supplier_ids: + # Filter products where main_seller_id is in excluded_supplier_ids + excluded_by_supplier = products.filtered( + lambda p: p.product_tmpl_id.main_seller_id + and p.product_tmpl_id.main_seller_id in order.excluded_supplier_ids + ) + if excluded_by_supplier: + products = products - excluded_by_supplier + _logger.info( + "Group order %d: Excluded %d products from supplier blacklist (main sellers: %s) (total: %d)", + order.id, + len(excluded_by_supplier), + ", ".join(order.excluded_supplier_ids.mapped("name")), + len(products), + ) + return products def _get_products_paginated(self, order_id, page=1, per_page=20): diff --git a/website_sale_aplicoop/tests/test_product_discovery.py b/website_sale_aplicoop/tests/test_product_discovery.py index 3aa1eb3..e54b40e 100644 --- a/website_sale_aplicoop/tests/test_product_discovery.py +++ b/website_sale_aplicoop/tests/test_product_discovery.py @@ -733,3 +733,269 @@ class TestProductBlacklist(TransactionCase): # 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) diff --git a/website_sale_aplicoop/views/group_order_views.xml b/website_sale_aplicoop/views/group_order_views.xml index 0c3796c..cb55416 100644 --- a/website_sale_aplicoop/views/group_order_views.xml +++ b/website_sale_aplicoop/views/group_order_views.xml @@ -87,6 +87,7 @@ +