Odoo18-Base/addons/website_sale/controllers/product_configurator.py
2025-01-06 10:57:38 +07:00

311 lines
14 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.http import request, route
from odoo.tools import float_is_zero
from odoo.addons.sale.controllers.product_configurator import SaleProductConfiguratorController
from odoo.addons.website_sale.controllers.main import WebsiteSale
class WebsiteSaleProductConfiguratorController(SaleProductConfiguratorController, WebsiteSale):
@route(
route='/website_sale/should_show_product_configurator',
type='json',
auth='public',
website=True,
)
def website_sale_should_show_product_configurator(
self, product_template_id, ptav_ids, is_product_configured
):
""" Return whether the product configurator dialog should be shown.
:param int product_template_id: The product being checked, as a `product.template` id.
:param list(int) ptav_ids: The combination of the product, as a list of
`product.template.attribute.value` ids.
:param bool is_product_configured: Whether the product is already configured.
:rtype: bool
:return: Whether the product configurator dialog should be shown.
"""
product_template = request.env['product.template'].browse(product_template_id)
combination = request.env['product.template.attribute.value'].browse(ptav_ids)
single_product_variant = product_template.get_single_product_variant()
# We can't use `single_product_variant.get('has_optional_products')` as it doesn't take
# `combination` into account.
has_optional_products = bool(product_template.optional_product_ids.filtered(
lambda op: self._should_show_product(op, combination)
))
force_dialog = request.website.add_to_cart_action == 'force_dialog'
return (
force_dialog
or has_optional_products
or not (single_product_variant.get('product_id') or is_product_configured)
)
@route(
route='/website_sale/product_configurator/get_values',
type='json',
auth='public',
website=True,
)
def website_sale_product_configurator_get_values(self, *args, **kwargs):
self._populate_currency_and_pricelist(kwargs)
return super().sale_product_configurator_get_values(*args, **kwargs)
@route(
route='/website_sale/product_configurator/create_product',
type='json',
auth='public',
methods=['POST'],
website=True,
)
def website_sale_product_configurator_create_product(self, *args, **kwargs):
return super().sale_product_configurator_create_product(*args, **kwargs)
@route(
route='/website_sale/product_configurator/update_combination',
type='json',
auth='public',
methods=['POST'],
website=True,
)
def website_sale_product_configurator_update_combination(self, *args, **kwargs):
self._populate_currency_and_pricelist(kwargs)
return super().sale_product_configurator_update_combination(*args, **kwargs)
@route(
route='/website_sale/product_configurator/get_optional_products',
type='json',
auth='public',
website=True,
)
def website_sale_product_configurator_get_optional_products(self, *args, **kwargs):
self._populate_currency_and_pricelist(kwargs)
return super().sale_product_configurator_get_optional_products(*args, **kwargs)
@route(
route='/website_sale/product_configurator/update_cart',
type='json',
auth='public',
methods=['POST'],
website=True,
)
def website_sale_product_configurator_update_cart(
self, main_product, optional_products, **kwargs
):
""" Add the provided main and optional products to the cart.
Main and optional products have the following shape:
```
{
'product_id': int,
'product_template_id': int,
'parent_product_template_id': int,
'quantity': float,
'product_custom_attribute_values': list(dict),
'no_variant_attribute_value_ids': list(int),
}
```
Note: if product A is a parent of product B, then product A must come before product B in
the optional_products list. Otherwise, the corresponding order lines won't be linked.
:param dict main_product: The main product to add.
:param list(dict) optional_products: The optional products to add.
:param dict kwargs: Locally unused data passed to `_cart_update`.
:rtype: dict
:return: A dict containing information about the cart update.
"""
order_sudo = request.website.sale_get_order(force_create=True)
if order_sudo.state != 'draft':
request.session['sale_order_id'] = None
order_sudo = request.website.sale_get_order(force_create=True)
# The main product could theoretically have a parent, but we ignore it to avoid
# circularities in the linked line ids.
values = order_sudo._cart_update(
product_id=main_product['product_id'],
add_qty=main_product['quantity'],
product_custom_attribute_values=main_product['product_custom_attribute_values'],
no_variant_attribute_value_ids=[
int(value_id) for value_id in main_product['no_variant_attribute_value_ids']
],
**kwargs,
)
line_ids = {main_product['product_template_id']: values['line_id']}
if optional_products and values['line_id']:
for option in optional_products:
option_values = order_sudo._cart_update(
product_id=option['product_id'],
add_qty=option['quantity'],
product_custom_attribute_values=option['product_custom_attribute_values'],
no_variant_attribute_value_ids=[
int(value_id) for value_id in option['no_variant_attribute_value_ids']
],
# Using `line_ids[...]` instead of `line_ids.get(...)` ensures that this throws
# if an optional product contains bad data.
linked_line_id=line_ids[option['parent_product_template_id']],
**kwargs,
)
line_ids[option['product_template_id']] = option_values['line_id']
values['notification_info'] = self._get_cart_notification_information(
order_sudo, line_ids.values()
)
values['cart_quantity'] = order_sudo.cart_quantity
request.session['website_sale_cart_quantity'] = order_sudo.cart_quantity
return values
def _get_basic_product_information(
self, product_or_template, pricelist, combination, currency=None, date=None, **kwargs
):
""" Override of `sale` to append website data and apply taxes.
:param product.product|product.template product_or_template: The product for which to seek
information.
:param product.pricelist pricelist: The pricelist to use.
:param product.template.attribute.value combination: The combination of the product.
:param res.currency|None currency: The currency of the transaction.
:param datetime|None date: The date of the `sale.order`, to compute the price at the right
rate.
:param dict kwargs: Locally unused data passed to `super`.
:rtype: dict
:return: A dict with the following structure:
{
... # fields from `super`.
'price': float,
'can_be_sold': bool,
'category_name': str,
'currency_name': str,
'strikethrough_price': float, # if there's a strikethrough_price to display.
}
"""
basic_product_information = super()._get_basic_product_information(
product_or_template.with_context(display_default_code=not request.is_frontend),
pricelist,
combination,
currency=currency,
date=date,
**kwargs,
)
if request.is_frontend:
has_zero_price = float_is_zero(
basic_product_information['price'], precision_rounding=currency.rounding
)
basic_product_information['can_be_sold'] = not (
request.website.prevent_zero_price_sale and has_zero_price
)
# Don't compute the strikethrough price if there's a custom price (i.e. if `price_info`
# is populated).
strikethrough_price = self._get_strikethrough_price(
product_or_template.with_context(
**product_or_template._get_product_price_context(combination)
),
currency,
date,
basic_product_information['price'],
basic_product_information['pricelist_rule_id'],
) if 'price_info' not in basic_product_information else None
if strikethrough_price:
basic_product_information['strikethrough_price'] = strikethrough_price
return basic_product_information
def _get_ptav_price_extra(self, ptav, currency, date, product_or_template):
""" Override of `sale` to apply taxes.
:param product.template.attribute.value ptav: The product template attribute value for which
to compute the extra price.
:param res.currency currency: The currency to compute the extra price in.
:param datetime date: The date to compute the extra price at.
:param product.product|product.template product_or_template: The product on which the
product template attribute value applies.
:rtype: float
:return: The extra price for the product template attribute value.
"""
price_extra = super()._get_ptav_price_extra(ptav, currency, date, product_or_template)
if request.is_frontend:
return self._apply_taxes_to_price(price_extra, product_or_template, currency)
return price_extra
def _get_strikethrough_price(self, product_or_template, currency, date, price, pricelist_rule_id=None):
""" Return the strikethrough price of the product, if there is one.
:param product.product|product.template product_or_template: The product for which to
compute the strikethrough price.
:param res.currency currency: The currency to compute the strikethrough price in.
:param datetime date: The date to compute the strikethrough price at.
:param float price: The actual price of the product.
:rtype: float|None
:return: The strikethrough price of the product, if there is one.
"""
pricelist_rule = request.env['product.pricelist.item'].browse(pricelist_rule_id)
# First, try to use the base price as the strikethrough price.
# Apply taxes before comparing it to the actual price.
if pricelist_rule._show_discount_on_shop():
pricelist_base_price = self._apply_taxes_to_price(
pricelist_rule._compute_price_before_discount(
product=product_or_template,
quantity=1.0,
uom=product_or_template.uom_id,
date=date,
currency=currency,
),
product_or_template,
currency,
)
# Only show the base price if it's greater than the actual price.
if currency.compare_amounts(pricelist_base_price, price) == 1:
return pricelist_base_price
# Second, try to use `compare_list_price` as the strikethrough price.
# Don't apply taxes since this price should always be displayed as is.
if (
request.env.user.has_group('website_sale.group_product_price_comparison')
and product_or_template.compare_list_price
):
compare_list_price = product_or_template.currency_id._convert(
from_amount=product_or_template.compare_list_price,
to_currency=currency,
company=request.env.company,
date=date,
round=False,
)
# Only show `compare_list_price` if it's greater than the actual price.
if currency.compare_amounts(compare_list_price, price) == 1:
return compare_list_price
return None
def _should_show_product(self, product_template, parent_combination):
""" Override of `sale` to only show products that can be added to the cart.
:param product.template product_template: The product being checked.
:param product.template.attribute.value parent_combination: The combination of the parent
product.
:rtype: bool
:return: Whether the product should be shown in the configurator.
"""
should_show_product = super()._should_show_product(product_template, parent_combination)
if request.is_frontend:
return (
should_show_product
and product_template._is_add_to_cart_possible(parent_combination)
)
return should_show_product
@staticmethod
def _apply_taxes_to_price(price, product_or_template, currency):
product_taxes = product_or_template.sudo().taxes_id._filter_taxes_by_company(
request.env.company
)
if product_taxes:
fiscal_position = request.website.fiscal_position_id.sudo()
taxes = fiscal_position.map_tax(product_taxes)
return request.env['product.template']._apply_taxes_to_price(
price, currency, product_taxes, taxes, product_or_template, website=request.website
)
return price