File: product_configurator.py

package info (click to toggle)
odoo 18.0.0%2Bdfsg-2
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 878,716 kB
  • sloc: javascript: 927,937; python: 685,670; xml: 388,524; sh: 1,033; sql: 415; makefile: 26
file content (302 lines) | stat: -rw-r--r-- 13,598 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# 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'],
            ) 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):
        """ 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.
        """
        # First, try to use the base price as the strikethrough price.
        # Apply taxes before comparing it to the actual price.
        base_price = self._apply_taxes_to_price(
            request.env['product.pricelist.item']._compute_base_price(
                product_or_template, 1.0, product_or_template.uom_id, date, currency
            ),
            product_or_template,
            currency,
        )
        # Only show the base price if it's greater than the actual price.
        if currency.compare_amounts(base_price, price) == 1:
            return 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