[ADD] website_sale_aplicoop: restore the test coverage lost with the dead code
The dead-code cleanup dropped 11 test files that were never wired into tests/__init__.py. Reviewing what they covered turned up real holes, so the worthwhile ones come back, rewritten against the current schema. Blacklists were the serious gap: product, supplier and category exclusions have absolute priority over product discovery and nothing exercised them. The old file had two separate defects. Its supplier fixtures wrote `main_seller_id` directly, but product_main_seller computes that field from `variant_seller_ids`, so the compute reset it to False and the blacklist had nothing to exclude; they now create real supplierinfo records. Worse, four whole classes asserted against `group_order.product_ids` -- the m2m *input* -- instead of the discovery result, so they set `category_ids` and then checked a field they never touched. Those go through `_get_products_for_group_order` now, and three tests that had no assertions at all got some. The remaining three failures were test bugs too, all Odoo 17->18 leftovers: * Date cases assumed `pickup_date` derives from `start_date`. The chain is cutoff -> pickup -> delivery, and a recurring order whose start date has passed rolls forward to the current cycle, so a 2024 order has no 2024 pickup. The new file anchors on future dates and finds the next 29 February dynamically, with a class documenting the roll-forward itself. * `/eskaera/labels` is `type="json"`; the old test hit it with a plain GET and read the resulting 400 as a bug. It is called over JSON-RPC now, and a test pins the 400 so nobody repeats it. Also `uom.uom.categ` -> `uom.category`. * `price_include` is computed in 18.0, so fixtures must set `price_include_override`. On top of that `_get_price` filters taxes by company and defaults to `env.company`, not the fixture's, which left the tax list empty -- `tax_included` was False for the wrong reason. Two of the portal tests were passing for the wrong reason as well: the access guard bounced them to /eskaera, which also answers 200. Membership has to be set from the member side with `is_group`, and a new test checks the final URL rather than the status alone. Each fixture that can silently build the wrong thing now carries a guard test. Left out on purpose: three files were unimplemented placeholders, and test_draft_persistence still deserves recovering (see the notes file). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
3eb79e2431
commit
3e4bd5e5db
6 changed files with 1817 additions and 0 deletions
869
website_sale_aplicoop/tests/test_product_discovery.py
Normal file
869
website_sale_aplicoop/tests/test_product_discovery.py
Normal file
|
|
@ -0,0 +1,869 @@
|
|||
# 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue