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 @@
+