[ADD] website_sale_aplicoop: Supplier blacklist feature for group orders
- Add excluded_supplier_ids field for supplier exclusion - Filter products by main_seller_id (from product_main_seller addon) - Blacklist has absolute priority over all inclusion sources - Products with blacklisted main supplier never appear in orders - Update _get_products_for_group_order() with supplier blacklist logic - Add excluded_supplier_ids to 'Productos Excluidos' section in form view - Add comprehensive test suite (TestSupplierBlacklist class with 9 tests): * Test exclusion by main_seller_id * Test multiple supplier exclusion * Test products without main seller not affected * Test blacklist with direct product inclusion * Test blacklist priority over supplier inclusion * Test combined product and supplier blacklist * Test available_products_count with supplier blacklist - Add Spanish and Euskera translations - Update available_products_count computation to include excluded_supplier_ids - Version bump to 18.0.1.5.0 Use case: Exclude all products from specific supplier (e.g., temporary unavailability) Example: Category with 100 products, exclude supplier X → all products from X excluded Workflow: Bulk inclusion via categories + supplier-level exclusion + product-level exclusion
This commit is contained in:
parent
75ebb7b907
commit
d90f2cdc61
7 changed files with 371 additions and 4 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@
|
|||
<field name="product_ids" widget="many2many_tags" help="Specific products to include directly"/>
|
||||
</group>
|
||||
<group string="Productos Excluidos" col="2">
|
||||
<field name="excluded_supplier_ids" widget="many2many_tags" help="Suppliers excluded from this order. Products with these suppliers as main seller will not be available (blacklist has absolute priority)"/>
|
||||
<field name="excluded_product_ids" widget="many2many_tags" help="Products explicitly excluded from this order (blacklist has absolute priority over inclusions)"/>
|
||||
</group>
|
||||
</group>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue