[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>
This commit is contained in:
parent
3e4bd5e5db
commit
817ff31d39
10 changed files with 496 additions and 202 deletions
|
|
@ -25,6 +25,7 @@ from .exceptions import GroupOrderUnavailable
|
|||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
class AplicoopWebsiteSale(WebsiteSale):
|
||||
"""Controlador personalizado para website_sale de Aplicoop.
|
||||
|
||||
|
|
@ -54,7 +55,12 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
# ========== PHASE 1: HELPER METHODS FOR VALIDATION AND CONFIGURATION ==========
|
||||
|
||||
def _resolve_pricelist(self):
|
||||
return _pricing._resolve_pricelist(self, request)
|
||||
"""The pricelist to quote in: whatever website_sale resolved.
|
||||
|
||||
Eskaera used to carry its own pricelist setting; it now prices with the
|
||||
standard one, so the shop, the cart and these pages cannot drift apart.
|
||||
"""
|
||||
return request.website.pricelist_id
|
||||
|
||||
def _prepare_product_display_info(self, product, product_price_info):
|
||||
return _pricing._prepare_product_display_info(
|
||||
|
|
@ -64,13 +70,12 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
request,
|
||||
)
|
||||
|
||||
def _get_pricing_info(self, product, pricelist, quantity=1.0, partner=None):
|
||||
def _get_pricing_info(self, product, pricelist, quantity=1.0):
|
||||
return _pricing._get_pricing_info(
|
||||
self,
|
||||
product,
|
||||
pricelist,
|
||||
quantity=quantity,
|
||||
partner=partner,
|
||||
request_obj=request,
|
||||
)
|
||||
|
||||
|
|
@ -176,7 +181,6 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
"""
|
||||
sale_order_lines = []
|
||||
pricelist = pricelist or self._resolve_pricelist()
|
||||
partner = request.env.user.partner_id
|
||||
|
||||
for item in items:
|
||||
try:
|
||||
|
|
@ -198,7 +202,6 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
product,
|
||||
pricelist,
|
||||
quantity=quantity,
|
||||
partner=partner,
|
||||
)
|
||||
|
||||
line_data = {
|
||||
|
|
@ -1657,13 +1660,11 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
# Extract items from the draft order
|
||||
items = []
|
||||
pricelist = self._resolve_pricelist()
|
||||
partner = current_user.partner_id
|
||||
for line in draft_order.order_line:
|
||||
pricing = self._get_pricing_info(
|
||||
line.product_id,
|
||||
pricelist,
|
||||
quantity=line.product_uom_qty,
|
||||
partner=partner,
|
||||
)
|
||||
items.append(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,42 +6,6 @@ from odoo.http import request
|
|||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _resolve_pricelist(self, request_obj=None):
|
||||
try:
|
||||
req = request_obj or request
|
||||
env = req.env
|
||||
website = req.website
|
||||
except RuntimeError:
|
||||
env = getattr(self, "env", None) or self.env
|
||||
website = env["website"].get_current_website()
|
||||
pricelist = None
|
||||
try:
|
||||
param_value = (
|
||||
env["ir.config_parameter"]
|
||||
.sudo()
|
||||
.get_param("website_sale_aplicoop.pricelist_id")
|
||||
)
|
||||
if param_value:
|
||||
pricelist = (
|
||||
env["product.pricelist"].browse(int(param_value)).exists() or None
|
||||
)
|
||||
except Exception as e:
|
||||
_logger.warning("_resolve_pricelist: error reading config param: %s", e)
|
||||
|
||||
if not pricelist:
|
||||
try:
|
||||
pricelist = website._get_current_pricelist()
|
||||
except Exception as e:
|
||||
_logger.warning(
|
||||
"_resolve_pricelist: fallback to website pricelist failed: %s", e
|
||||
)
|
||||
|
||||
if not pricelist:
|
||||
pricelist = env["product.pricelist"].sudo().search([], limit=1)
|
||||
|
||||
return pricelist
|
||||
|
||||
|
||||
def _prepare_product_display_info(self, product, product_price_info, request_obj=None):
|
||||
price_data = product_price_info.get(product.id, {})
|
||||
price = (
|
||||
|
|
@ -119,132 +83,182 @@ def _prepare_product_display_info(self, product, product_price_info, request_obj
|
|||
}
|
||||
|
||||
|
||||
def _get_pricing_info(
|
||||
self,
|
||||
product,
|
||||
pricelist,
|
||||
quantity=1.0,
|
||||
partner=None,
|
||||
request_obj=None,
|
||||
):
|
||||
req = request_obj or request
|
||||
def _pricing_context(record, request_obj=None):
|
||||
"""Return (env, website), both under an HTTP request and from the cron."""
|
||||
try:
|
||||
env = req.env
|
||||
website = req.website
|
||||
req = request_obj or request
|
||||
return req.env, req.website
|
||||
except RuntimeError:
|
||||
env = product.env
|
||||
website = env["website"].get_current_website()
|
||||
env = record.env
|
||||
return env, env["website"].get_current_website()
|
||||
|
||||
partner = partner or env.user.partner_id
|
||||
currency = pricelist.currency_id
|
||||
|
||||
def _pricing_company(product, website, env):
|
||||
website_company = (
|
||||
website.company_id
|
||||
if website and getattr(website, "company_id", False)
|
||||
else False
|
||||
)
|
||||
company = website_company or product.company_id or env.company
|
||||
return website_company or product.company_id or env.company
|
||||
|
||||
price, rule_id = pricelist._get_product_price_rule(
|
||||
product=product, quantity=quantity, target_currency=currency
|
||||
|
||||
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
|
||||
pricelist_item = env["product.pricelist.item"].sudo().browse(rule_id)
|
||||
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(pricelist),
|
||||
date=fields.Date.context_today(product),
|
||||
uom=product.uom_id,
|
||||
currency=currency,
|
||||
)
|
||||
|
||||
has_discounted_price = price_before_discount > price
|
||||
|
||||
fiscal_position = (
|
||||
website.fiscal_position_id.sudo()
|
||||
if website and getattr(website, "fiscal_position_id", False)
|
||||
else env["account.fiscal.position"].sudo()
|
||||
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
|
||||
)
|
||||
product_taxes = product.sudo().taxes_id._filter_taxes_by_company(company)
|
||||
taxes = fiscal_position.map_tax(product_taxes) if product_taxes else product_taxes
|
||||
tax_display = "total_included"
|
||||
|
||||
def compute_display(amount):
|
||||
if not taxes:
|
||||
return amount
|
||||
return taxes.compute_all(amount, currency, 1, product, partner)[tax_display]
|
||||
|
||||
display_price = compute_display(price)
|
||||
display_list_price = compute_display(price_before_discount)
|
||||
|
||||
return {
|
||||
"price_unit": price,
|
||||
"price": display_price,
|
||||
"list_price": display_list_price,
|
||||
"has_discounted_price": has_discounted_price,
|
||||
"has_discounted_price": price_before_discount > price,
|
||||
"discount": display_list_price - display_price,
|
||||
"tax_included": tax_display == "total_included",
|
||||
"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 = {}
|
||||
|
||||
def _tax_included_default(product_record):
|
||||
try:
|
||||
req = request_obj or request
|
||||
return req.website.show_line_subtotals_tax_selection != "tax_excluded"
|
||||
except RuntimeError:
|
||||
website = product_record.env["website"].get_current_website()
|
||||
if not website:
|
||||
return True
|
||||
return website.show_line_subtotals_tax_selection != "tax_excluded"
|
||||
|
||||
for product in products:
|
||||
product_variant = (
|
||||
product.product_variant_ids[0] if product.product_variant_ids else False
|
||||
)
|
||||
if product_variant and pricelist:
|
||||
try:
|
||||
try:
|
||||
req = request_obj or request
|
||||
partner = req.env.user.partner_id
|
||||
except RuntimeError:
|
||||
partner = product_variant.env.user.partner_id
|
||||
|
||||
pricing = _get_pricing_info(
|
||||
self,
|
||||
product_variant,
|
||||
pricelist,
|
||||
quantity=1.0,
|
||||
partner=partner,
|
||||
request_obj=request_obj,
|
||||
)
|
||||
product_price_info[product.id] = pricing
|
||||
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] = {
|
||||
"price_unit": product.list_price,
|
||||
"price": product.list_price,
|
||||
"list_price": product.list_price,
|
||||
"has_discounted_price": False,
|
||||
"discount": 0.0,
|
||||
"tax_included": _tax_included_default(product),
|
||||
}
|
||||
else:
|
||||
product_price_info[product.id] = {
|
||||
"price_unit": product.list_price,
|
||||
"price": product.list_price,
|
||||
"list_price": product.list_price,
|
||||
"has_discounted_price": False,
|
||||
"discount": 0.0,
|
||||
"tax_included": _tax_included_default(product),
|
||||
}
|
||||
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
|
||||
|
||||
|
||||
|
|
@ -265,46 +279,27 @@ def _get_delivery_product_display_price(
|
|||
self, delivery_product, pricelist=None, request_obj=None
|
||||
):
|
||||
if not delivery_product:
|
||||
return 5.74
|
||||
return 0.0
|
||||
try:
|
||||
base_price = float(delivery_product.list_price or 0.0)
|
||||
try:
|
||||
req = request_obj or request
|
||||
website = req.website
|
||||
partner = req.env.user.partner_id
|
||||
company = (
|
||||
website.company_id or delivery_product.company_id or req.env.company
|
||||
)
|
||||
except RuntimeError:
|
||||
env = delivery_product.env
|
||||
website = env["website"].get_current_website()
|
||||
partner = env.user.partner_id
|
||||
company = website.company_id or delivery_product.company_id or env.company
|
||||
|
||||
product_taxes = delivery_product.sudo().taxes_id._filter_taxes_by_company(
|
||||
company
|
||||
)
|
||||
fiscal_position = (
|
||||
website.fiscal_position_id.sudo()
|
||||
if website and getattr(website, "fiscal_position_id", False)
|
||||
else delivery_product.env["account.fiscal.position"]
|
||||
)
|
||||
taxes = (
|
||||
fiscal_position.map_tax(product_taxes) if product_taxes else product_taxes
|
||||
)
|
||||
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
|
||||
|
||||
currency = website.currency_id
|
||||
totals = taxes.compute_all(
|
||||
base_price,
|
||||
currency=currency,
|
||||
quantity=1.0,
|
||||
product=delivery_product,
|
||||
partner=partner,
|
||||
return float(
|
||||
_display_price(
|
||||
delivery_product,
|
||||
base_price,
|
||||
website.currency_id,
|
||||
product_taxes,
|
||||
taxes,
|
||||
website,
|
||||
)
|
||||
or 0.0
|
||||
)
|
||||
return float(totals.get("total_included", base_price) 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.",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue