addons-cm/website_sale_aplicoop/tests/test_pricing_delegation.py
GitHub Copilot f81c1ca8e7 [IMP] website_sale_aplicoop: surface the tax display setting on the Eskaera page
Pricing now follows website_sale, which includes the website's `Display
Product Prices`. That setting defaults to tax-excluded in 18.0, so a website
nobody configured shows Eskaera prices without VAT -- correct behaviour, but
easy to read as a bug when the members are consumers and expect the price they
will pay.

The setting is a related field website_sale already exposes, so it now appears
in the Eskaera block too, right where the co-op switches the feature on, rather
than only under eCommerce.

Tests cover both values on the shop and the listing paths, so the tax-excluded
case is a decision on record instead of a surprise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 19:13:57 +02:00

341 lines
13 KiB
Python

# Copyright 2025 Criptomart
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
"""Pricing that is delegated to website_sale instead of reimplemented.
Three things this pins down:
- Eskaera prices with the standard website pricelist. The addon no longer
carries a pricelist setting of its own, nor overrides how the website
resolves one, so `/shop` and Eskaera cannot quote different prices.
- taxes are applied by `product.template._apply_taxes_to_price`, so a fiscal
position that remaps a tax-included tax rebases the price -- the step this
addon used to skip.
- a page of products costs one pricelist resolution, not one per product.
"""
from types import SimpleNamespace
from unittest.mock import patch
from odoo.tests import tagged
from odoo.tests.common import TransactionCase
from ..controllers.website_sale import AplicoopWebsiteSale
REQUEST_PATCH_TARGET = (
"odoo.addons.website_sale_aplicoop.controllers.website_sale.request"
)
class PricingDelegationCommon:
"""A company, a website and a tax-included product to price."""
def setUp(self):
super().setUp()
self.controller = AplicoopWebsiteSale()
self.company = self.env["res.company"].create({"name": "Pricing Deleg Co"})
self.website = self.env.ref("website.default_website").sudo()
self.website.write(
{
"company_id": self.company.id,
"show_line_subtotals_tax_selection": "tax_included",
}
)
self.tax_group = self.env["account.tax.group"].create(
{"name": "IVA Deleg", "company_id": self.company.id}
)
self.country_es = self.env.ref("base.es")
def _create_pricelist(self, name, percent=None):
vals = {"name": name, "company_id": self.company.id}
if percent is not None:
vals["item_ids"] = [
(
0,
0,
{
"compute_price": "percentage",
"percent_price": percent,
"applied_on": "3_global",
},
)
]
return self.env["product.pricelist"].create(vals)
def _create_tax(self, name, amount, included=False):
return self.env["account.tax"].create(
{
"name": name,
"amount": amount,
"amount_type": "percent",
"type_tax_use": "sale",
"price_include_override": (
"tax_included" if included else "tax_excluded"
),
"company_id": self.company.id,
"country_id": self.country_es.id,
"tax_group_id": self.tax_group.id,
}
)
def _create_product(self, name, list_price, tax=None):
return self.env["product.product"].create(
{
"name": name,
"type": "consu",
"list_price": list_price,
"is_published": True,
"sale_ok": True,
"company_id": self.company.id,
"taxes_id": [(6, 0, [tax.id])] if tax else False,
}
)
@tagged("post_install", "-at_install")
class TestStandardPricelistResolution(PricingDelegationCommon, TransactionCase):
"""Eskaera prices with the website pricelist, like the rest of the shop."""
def test_no_aplicoop_pricelist_setting_is_left(self):
"""The old per-addon pricelist setting is gone for good.
It used to let Eskaera quote a different pricelist from `/shop`, which
is exactly the drift this addon should not introduce.
"""
self.assertNotIn(
"aplicoop_pricelist_id", self.env["res.config.settings"]._fields
)
def test_website_resolution_is_left_untouched(self):
"""`_get_current_pricelist` is core's; the addon does not override it."""
website_cls = type(self.env["website"])
self.assertNotIn("_get_current_pricelist", website_cls.__dict__)
self.assertEqual(
self.website.pricelist_id, self.website._get_current_pricelist()
)
def test_the_controller_quotes_the_website_pricelist(self):
"""The Eskaera helper hands back exactly what the website resolved."""
expected = self._create_pricelist("Website PL", percent=15.0)
request_mock = SimpleNamespace(
env=self.env,
website=SimpleNamespace(pricelist_id=expected),
)
with patch(REQUEST_PATCH_TARGET, request_mock):
resolved = self.controller._resolve_pricelist()
self.assertEqual(resolved, expected)
@tagged("post_install", "-at_install")
class TestTaxDisplayFollowsTheWebsite(PricingDelegationCommon, TransactionCase):
"""Displayed prices honour the website's `Display Product Prices`.
Eskaera used to hardcode the tax-included display and ignore the setting.
Now it follows website_sale, which means a website left on the Odoo default
(`tax_excluded`) shows prices without VAT -- correct, but surprising enough
to be worth pinning down here.
"""
def setUp(self):
super().setUp()
self.pricelist = self._create_pricelist("Display PL")
self.tax_21 = self._create_tax("IVA 21%", 21.0)
self.product = self._create_product("Display Product", 100.0, self.tax_21)
def _price(self):
return self.controller._get_pricing_info(
self.product, self.pricelist, quantity=1.0
)
def test_gross_price_on_a_tax_included_website(self):
"""A consumer-facing website quotes what the member pays."""
self.website.show_line_subtotals_tax_selection = "tax_included"
pricing = self._price()
self.assertAlmostEqual(pricing["price"], 121.0, places=2)
self.assertAlmostEqual(pricing["price_unit"], 100.0, places=2)
self.assertTrue(pricing["tax_included"])
def test_net_price_on_a_tax_excluded_website(self):
"""A website set to tax-excluded shows the net price."""
self.website.show_line_subtotals_tax_selection = "tax_excluded"
pricing = self._price()
self.assertAlmostEqual(pricing["price"], 100.0, places=2)
self.assertAlmostEqual(pricing["price_unit"], 100.0, places=2)
self.assertFalse(pricing["tax_included"])
def test_the_setting_is_what_moves_the_price(self):
"""Nothing but the website setting changes between the two."""
self.website.show_line_subtotals_tax_selection = "tax_included"
gross = self._price()["price"]
self.website.show_line_subtotals_tax_selection = "tax_excluded"
net = self._price()["price"]
self.assertAlmostEqual(gross - net, 21.0, places=2)
def test_batched_listing_follows_the_same_setting(self):
"""The listing path must not disagree with the single-product one."""
self.website.show_line_subtotals_tax_selection = "tax_excluded"
batched = self.controller._compute_price_info(self.product, self.pricelist)
self.assertAlmostEqual(batched[self.product.id]["price"], 100.0, places=2)
self.assertFalse(batched[self.product.id]["tax_included"])
@tagged("post_install", "-at_install")
class TestTaxIncludedFiscalPosition(PricingDelegationCommon, TransactionCase):
"""A remapped tax-included tax must rebase the displayed price."""
def setUp(self):
super().setUp()
self.pricelist = self._create_pricelist("Plain PL")
self.tax_21_incl = self._create_tax("IVA 21% incl", 21.0, included=True)
self.tax_10_incl = self._create_tax("IVA 10% incl", 10.0, included=True)
# 100 net + 21% already inside the price.
self.product = self._create_product("Tax Incl Product", 121.0, self.tax_21_incl)
def _fiscal_position(self):
position = self.env["account.fiscal.position"].create(
{"name": "Remap 21->10", "company_id": self.company.id}
)
self.env["account.fiscal.position.tax"].create(
{
"position_id": position.id,
"tax_src_id": self.tax_21_incl.id,
"tax_dest_id": self.tax_10_incl.id,
}
)
return position
def test_price_is_rebased_onto_the_mapped_tax(self):
"""121 gross at 21% becomes 110 gross at 10%, not 121.
Without `_get_tax_included_unit_price_from_price` the 121 was handed
straight to the mapped tax, which read it as already being the 10%
gross price and displayed 121.
"""
self.website.fiscal_position_id = self._fiscal_position()
pricing = self.controller._get_pricing_info(
self.product, self.pricelist, quantity=1.0
)
self.assertAlmostEqual(pricing["price"], 110.0, places=2)
def test_price_is_untouched_without_a_fiscal_position(self):
"""The rebasing is a no-op in the ordinary case."""
self.website.fiscal_position_id = False
pricing = self.controller._get_pricing_info(
self.product, self.pricelist, quantity=1.0
)
self.assertAlmostEqual(pricing["price"], 121.0, places=2)
@tagged("post_install", "-at_install")
class TestPricingBatching(PricingDelegationCommon, TransactionCase):
"""A product listing resolves the pricelist once for the whole page."""
def setUp(self):
super().setUp()
self.pricelist = self._create_pricelist("Batch PL", percent=10.0)
self.tax = self._create_tax("IVA 21%", 21.0)
self.products = self.env["product.product"]
for index in range(5):
self.products |= self._create_product(
f"Batch Product {index}", 100.0 + index, self.tax
)
def test_pricelist_is_resolved_once_for_the_page(self):
"""One `_compute_price_rule` call, not one per product."""
calls = []
original = type(self.pricelist)._compute_price_rule
def counting_compute_price_rule(pricelist_self, products, *args, **kwargs):
calls.append(len(products))
return original(pricelist_self, products, *args, **kwargs)
self.patch(
type(self.pricelist), "_compute_price_rule", counting_compute_price_rule
)
self.controller._compute_price_info(self.products, self.pricelist)
self.assertEqual(
calls,
[len(self.products)],
msg=f"Expected a single batched call, got {calls}",
)
def test_batched_prices_match_the_single_product_helper(self):
"""Batching must not change any number it produces."""
batched = self.controller._compute_price_info(self.products, self.pricelist)
for product in self.products:
single = self.controller._get_pricing_info(
product, self.pricelist, quantity=1.0
)
self.assertAlmostEqual(
batched[product.id]["price"], single["price"], places=2
)
self.assertAlmostEqual(
batched[product.id]["price_unit"], single["price_unit"], places=2
)
def test_prices_the_given_variant(self):
"""A variant is priced as itself, not as its template's first one."""
attribute = self.env["product.attribute"].create(
{
"name": "Size Deleg",
"value_ids": [
(0, 0, {"name": "Small"}),
(0, 0, {"name": "Large"}),
],
}
)
template = self.env["product.template"].create(
{
"name": "Multi Variant Product",
"type": "consu",
"list_price": 100.0,
"is_published": True,
"sale_ok": True,
"company_id": self.company.id,
"attribute_line_ids": [
(
0,
0,
{
"attribute_id": attribute.id,
"value_ids": [(6, 0, attribute.value_ids.ids)],
},
)
],
}
)
small, large = template.product_variant_ids[0], template.product_variant_ids[1]
# Make the two variants cost visibly different amounts.
large.write({"list_price": 100.0})
self.env["product.pricelist.item"].create(
{
"pricelist_id": self.pricelist.id,
"applied_on": "0_product_variant",
"product_id": large.id,
"compute_price": "fixed",
"fixed_price": 500.0,
}
)
priced = self.controller._compute_price_info(large, self.pricelist)
self.assertAlmostEqual(priced[large.id]["price_unit"], 500.0, places=2)
self.assertNotEqual(large.id, small.id)