# 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