addons-cm/website_sale_aplicoop/controllers/website_sale_pricing.py
GitHub Copilot 817ff31d39 [REF] website_sale_aplicoop: price through website_sale instead of privately
Eskaera carried its own pricing path: a `website_sale_aplicoop.pricelist_id`
setting resolved inside the Eskaera controllers, plus a local reimplementation
of the tax and discount maths. The pricelist part meant /shop and Eskaera could
quote different prices on the same website, and the maths part had drifted from
the original it was copied from.

Pricing is now the standard one. The setting is gone (a migration drops the
parameter, which would otherwise linger looking like live configuration) and
`_resolve_pricelist` is `request.website.pricelist_id`. Scoping a pricelist to
a website is already core's job through `product.pricelist.website_id`, so
running the plain shop on one website and the co-op on another needs no code
here.

Taxes are applied by `product.template._apply_taxes_to_price`, which fixes a
real defect: it calls `_get_tax_included_unit_price_from_price` first, and this
module did not. With a fiscal position remapping a tax-included tax, a product
at 121 (100 + 21%) was displayed at 121 instead of 110, because the price was
handed to the mapped tax as if it were already that tax's gross amount. The
helper is a no-op without a remapping, so ordinary pricing is untouched. It
also means the website's `show_line_subtotals_tax_selection` is respected
rather than overridden with a hardcoded tax-included display.

Listing a page now costs one `_compute_price_rule` call instead of one per
product, which matters on the lazy-loading path.

Two smaller things found on the way. `_get_product_price_rule` was being passed
`target_currency=`, which is not one of its arguments: it fell into **kwargs
and was ignored, so the conversion never happened. And `_compute_price_info`
resolved `product.product_variant_ids[0]`, but it receives variants, so a
multi-variant template was priced from its first variant rather than the one
asked for.

The delivery display price loses its hardcoded 5.74 fallback and reuses the
same helpers as everything else.

Kept local, because none of it is pricing: the /Kg and /L suffixes, the 0.1
quantity step for bulk goods, the base unit price and the supplier name.

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

310 lines
10 KiB
Python

import logging
from odoo import fields
from odoo.http import request
_logger = logging.getLogger(__name__)
def _prepare_product_display_info(self, product, product_price_info, request_obj=None):
price_data = product_price_info.get(product.id, {})
price = (
price_data.get("price", product.list_price)
if price_data
else product.list_price
)
price_safe = float(price) if price else 0.0
uom_category_name = ""
quantity_step = 1
price_unit_suffix = ""
bulk_unit_suffixes = {
"uom.product_uom_categ_kgm": "/Kg",
"uom.product_uom_categ_vol": "/L",
}
if product.uom_id:
uom = product.uom_id.sudo()
if uom.category_id:
uom_category_name = uom.category_id.sudo().name or ""
try:
try:
req = request_obj or request
ir_model_data = req.env["ir.model.data"].sudo()
except RuntimeError:
ir_model_data = product.env["ir.model.data"].sudo()
external_id = ir_model_data.search(
[
("model", "=", "uom.category"),
("res_id", "=", uom.category_id.id),
],
limit=1,
)
if external_id:
fractional_categories = [
"uom.product_uom_categ_kgm",
"uom.product_uom_categ_vol",
"uom.uom_categ_length",
"uom.uom_categ_surface",
]
full_xmlid = f"{external_id.module}.{external_id.name}"
if full_xmlid in fractional_categories:
quantity_step = 0.1
price_unit_suffix = bulk_unit_suffixes.get(full_xmlid, "")
except Exception as e:
_logger.warning(
"_prepare_product_display_info: Error detecting UoM category XML ID for product %s: %s",
product.id,
str(e),
)
base_unit_price = 0.0
if product.base_unit_count and price_safe:
base_unit_price = price_safe / product.base_unit_count
try:
req = request_obj or request
tr_env = req.env
except RuntimeError:
tr_env = product.env
out_of_stock_label = tr_env._("Out of stock")
add_to_cart_label = tr_env._(
"Add %(product_name)s to cart", product_name=product.name
)
return {
"display_price": price_safe,
"safe_uom_category": uom_category_name,
"quantity_step": quantity_step,
"price_unit_suffix": price_unit_suffix,
"base_unit_price": base_unit_price,
"out_of_stock_label": out_of_stock_label,
"add_to_cart_label": add_to_cart_label,
}
def _pricing_context(record, request_obj=None):
"""Return (env, website), both under an HTTP request and from the cron."""
try:
req = request_obj or request
return req.env, req.website
except RuntimeError:
env = record.env
return env, env["website"].get_current_website()
def _pricing_company(product, website, env):
website_company = (
website.company_id
if website and getattr(website, "company_id", False)
else False
)
return website_company or product.company_id or env.company
def _display_taxes(product, website, company):
"""Product taxes, and those same taxes after the website fiscal position."""
product_taxes = product.sudo().taxes_id._filter_taxes_by_company(company)
if not product_taxes:
return product_taxes, product_taxes
fiscal_position = (
website.fiscal_position_id.sudo()
if website and getattr(website, "fiscal_position_id", False)
else product.env["account.fiscal.position"].sudo()
)
return product_taxes, fiscal_position.map_tax(product_taxes)
def _display_price(product, price, currency, product_taxes, taxes, website):
"""Price as the shop displays it, computed by website_sale itself.
`product.template._apply_taxes_to_price` rebases the price onto the taxes
the fiscal position mapped to (which matters for tax-included taxes) and
then honours the website's `show_line_subtotals_tax_selection`.
"""
return product.product_tmpl_id._apply_taxes_to_price(
price, currency, product_taxes, taxes, product, website=website
)
def _build_pricing_info(
product, price, pricelist_item, currency, quantity, website, company
):
"""Assemble the pricing dict from an already resolved pricelist price."""
price_before_discount = price
if pricelist_item and pricelist_item._show_discount_on_shop():
price_before_discount = pricelist_item._compute_price_before_discount(
product=product,
quantity=quantity or 1.0,
date=fields.Date.context_today(product),
uom=product.uom_id,
currency=currency,
)
product_taxes, taxes = _display_taxes(product, website, company)
display_price = _display_price(
product, price, currency, product_taxes, taxes, website
)
display_list_price = _display_price(
product, price_before_discount, currency, product_taxes, taxes, website
)
return {
"price_unit": price,
"price": display_price,
"list_price": display_list_price,
"has_discounted_price": price_before_discount > price,
"discount": display_list_price - display_price,
"tax_included": _tax_included(website),
}
def _tax_included(website):
"""Whether displayed prices carry tax, per the website setting."""
if not website:
return True
return website.show_line_subtotals_tax_selection != "tax_excluded"
def _get_pricing_info(
self,
product,
pricelist,
quantity=1.0,
request_obj=None,
):
"""Price one product.
No partner argument: website_sale bills the tax display to the current
user's partner itself, which is the same partner every caller here passed.
"""
env, website = _pricing_context(product, request_obj)
currency = pricelist.currency_id
# No currency kwarg: _compute_price_rule already defaults to the
# pricelist's own currency, which is what we want here.
price, rule_id = pricelist._get_product_price_rule(product, quantity)
return _build_pricing_info(
product,
price,
env["product.pricelist.item"].sudo().browse(rule_id),
currency,
quantity,
website,
_pricing_company(product, website, env),
)
def _pricing_variant(product):
"""The variant to price: the record itself when it already is one."""
if product._name == "product.product":
return product
return product.product_variant_ids[:1]
def _fallback_pricing(product, website):
"""Plain list price, used when there is no pricelist or pricing failed."""
price = product.list_price
return {
"price_unit": price,
"price": price,
"list_price": price,
"has_discounted_price": False,
"discount": 0.0,
"tax_included": _tax_included(website),
}
def _compute_price_info(self, products, pricelist, request_obj=None):
"""Price a whole page of products with a single pricelist resolution."""
if not products:
return {}
env, website = _pricing_context(products, request_obj)
currency = pricelist.currency_id if pricelist else None
variants = {product.id: _pricing_variant(product) for product in products}
priceable = env["product.product"].browse()
for variant in variants.values():
priceable |= variant
# One pricelist resolution for the whole page rather than one per product.
rules = (
pricelist._compute_price_rule(priceable, 1.0) if pricelist and priceable else {}
)
product_price_info = {}
for product in products:
variant = variants[product.id]
if not variant or variant.id not in rules:
product_price_info[product.id] = _fallback_pricing(product, website)
continue
try:
price, rule_id = rules[variant.id]
product_price_info[product.id] = _build_pricing_info(
variant,
price,
env["product.pricelist.item"].sudo().browse(rule_id),
currency,
1.0,
website,
_pricing_company(variant, website, env),
)
except Exception as e:
_logger.warning(
"_compute_price_info: Error getting price for product %s (id=%s): %s. Using list_price fallback.",
product.name,
product.id,
str(e),
)
product_price_info[product.id] = _fallback_pricing(product, website)
return product_price_info
def _get_product_supplier_info(self, products):
product_supplier_info = {}
for product in products:
supplier_name = ""
if product.seller_ids:
partner = product.seller_ids[0].partner_id.sudo()
supplier_name = partner.comercial or partner.name or ""
if partner.city:
supplier_name += f" ({partner.city})"
product_supplier_info[product.id] = supplier_name
return product_supplier_info
def _get_delivery_product_display_price(
self, delivery_product, pricelist=None, request_obj=None
):
if not delivery_product:
return 0.0
try:
base_price = float(delivery_product.list_price or 0.0)
env, website = _pricing_context(delivery_product, request_obj)
company = _pricing_company(delivery_product, website, env)
product_taxes, taxes = _display_taxes(delivery_product, website, company)
if not taxes:
return base_price
return float(
_display_price(
delivery_product,
base_price,
website.currency_id,
product_taxes,
taxes,
website,
)
or 0.0
)
except Exception as e:
_logger.warning(
"_get_delivery_product_display_price: Error computing delivery display price for product %s (id=%s): %s. Using list_price fallback.",
delivery_product.name,
delivery_product.id,
str(e),
)
return float(delivery_product.list_price or 0.0)