File: delivery.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 (276 lines) | stat: -rw-r--r-- 13,085 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
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from odoo import _
from odoo.exceptions import UserError, ValidationError
from odoo.http import request, route

from odoo.addons.payment import utils as payment_utils
from odoo.addons.website_sale.controllers.main import WebsiteSale


class Delivery(WebsiteSale):
    _express_checkout_delivery_route = '/shop/express/shipping_address_change'

    @route('/shop/delivery_methods', type='json', auth='public', website=True)
    def shop_delivery_methods(self):
        """ Fetch available delivery methods and render them in the delivery form.

        :return: The rendered delivery form.
        :rtype: str
        """
        order_sudo = request.website.sale_get_order()
        values = {
            'delivery_methods': order_sudo._get_delivery_methods(),
            'selected_dm_id': order_sudo.carrier_id.id,
            'order': order_sudo,  # Needed for accessing default values for pickup points.
        }
        values |= self._get_additional_delivery_context()
        return request.env['ir.ui.view']._render_template('website_sale.delivery_form', values)

    def _get_additional_delivery_context(self):
        """ Hook to update values used for rendering the website_sale.delivery_form template. """
        return {}

    @route('/shop/set_delivery_method', type='json', auth='public', website=True)
    def shop_set_delivery_method(self, dm_id=None, **kwargs):
        """ Set the delivery method on the current order and return the order summary values.

        If the delivery method is already set, the order summary values are returned immediately.

        :param str dm_id: The delivery method to set, as a `delivery.carrier` id.
        :param dict kwargs: The keyword arguments forwarded to `_order_summary_values`.
        :return: The order summary values, if any.
        :rtype: dict
        """
        order_sudo = request.website.sale_get_order()
        if not order_sudo:
            return {}

        dm_id = int(dm_id)
        if dm_id != order_sudo.carrier_id.id:
            for tx_sudo in order_sudo.transaction_ids:
                if tx_sudo.state not in ('draft', 'cancel', 'error'):
                    raise UserError(_(
                        "It seems that there is already a transaction for your order; you can't"
                        " change the delivery method anymore."
                    ))

            delivery_method_sudo = request.env['delivery.carrier'].sudo().browse(dm_id).exists()
            order_sudo._set_delivery_method(delivery_method_sudo)
        return self._order_summary_values(order_sudo, **kwargs)

    def _order_summary_values(self, order, **kwargs):
        """ Return the summary values of the order.

        :param sale.order order: The sales order whose summary values to return.
        :param dict kwargs: The keyword arguments. This parameter is not used here.
        :return: The order summary values.
        :rtype: dict
        """
        Monetary = request.env['ir.qweb.field.monetary']
        currency = order.currency_id
        return {
            'success': True,
            'is_free_delivery': not bool(order.amount_delivery),
            'amount_delivery': Monetary.value_to_html(
                order.amount_delivery, {'display_currency': currency}
            ),
            'amount_untaxed': Monetary.value_to_html(
                order.amount_untaxed, {'display_currency': currency}
            ),
            'amount_tax': Monetary.value_to_html(
                order.amount_tax, {'display_currency': currency}
            ),
            'amount_total': Monetary.value_to_html(
                order.amount_total, {'display_currency': currency}
            ),
        }

    @route('/shop/get_delivery_rate', type='json', auth='public', methods=['POST'], website=True)
    def shop_get_delivery_rate(self, dm_id):
        """ Return the delivery rate data for the given delivery method.

        :param str dm_id: The delivery method whose rate to get, as a `delivery.carrier` id.
        :return: The delivery rate data.
        :rtype: dict
        """
        order = request.website.sale_get_order()
        if not order:
            raise ValidationError(_("Your cart is empty."))

        if int(dm_id) not in order._get_delivery_methods().ids:
            raise UserError(_(
                "It seems that a delivery method is not compatible with your address. Please"
                " refresh the page and try again."
            ))

        Monetary = request.env['ir.qweb.field.monetary']
        delivery_method = request.env['delivery.carrier'].sudo().browse(int(dm_id)).exists()
        rate = Delivery._get_rate(delivery_method, order)
        if rate['success']:
            rate['amount_delivery'] = Monetary.value_to_html(
                rate['price'], {'display_currency': order.currency_id}
            )
            rate['is_free_delivery'] = not bool(rate['price'])
        else:
            rate['amount_delivery'] = Monetary.value_to_html(
                0.0, {'display_currency': order.currency_id}
            )
        return rate

    @route('/website_sale/set_pickup_location', type='json', auth='public', website=True)
    def website_sale_set_pickup_location(self, pickup_location_data):
        """ Fetch the order from the request and set the pickup location on the current order.

        :param str pickup_location_data: The JSON-formatted pickup location address.
        :return: None
        """
        order_sudo = request.website.sale_get_order()
        order_sudo._set_pickup_location(pickup_location_data)

    @route('/website_sale/get_pickup_locations', type='json', auth='public', website=True)
    def website_sale_get_pickup_locations(self, zip_code=None, **kwargs):
        """ Fetch the order from the request and return the pickup locations close to the zip code.

        Determine the country based on GeoIP or fallback on the order's delivery address' country.

        :param int zip_code: The zip code to look up to.
        :return: The close pickup locations data.
        :rtype: dict
        """
        order_sudo = request.website.sale_get_order()
        country = order_sudo.partner_shipping_id.country_id
        return order_sudo._get_pickup_locations(zip_code, country, **kwargs)

    @route(_express_checkout_delivery_route, type='json', auth='public', website=True)
    def express_checkout_process_delivery_address(self, partial_delivery_address):
        """ Process the shipping address and return the available delivery methods.

        Depending on whether the partner is registered and logged in, a new partner is created or we
        use an existing partner that matches the partial delivery address received.

        :param dict partial_delivery_address: The delivery information sent by the express payment
                                              provider.
        :return: The available delivery methods, sorted by lowest price.
        :rtype: dict
        """
        order_sudo = request.website.sale_get_order()
        if not order_sudo:
            return []

        self._include_country_and_state_in_address(partial_delivery_address)
        partial_delivery_address, _side_values = self._parse_form_data(partial_delivery_address)
        if order_sudo._is_anonymous_cart():
            # The partner_shipping_id and partner_invoice_id will be automatically computed when
            # changing the partner_id of the SO. This allows website_sale to avoid creating
            # duplicates.
            partial_delivery_address['name'] = _(
                'Anonymous express checkout partner for order %s',
                order_sudo.name,
            )
            new_partner_sudo = self._create_new_address(
                address_values=partial_delivery_address,
                address_type='delivery',
                use_delivery_as_billing=False,
                order_sudo=order_sudo,
            )
            # Pricelists are recomputed every time the partner is changed. We don't want to
            # recompute the price with another pricelist at this state since the customer has
            # already accepted the amount and validated the payment.
            with request.env.protecting(['pricelist_id'], order_sudo):
                order_sudo.partner_id = new_partner_sudo
        elif order_sudo.partner_shipping_id.name.endswith(order_sudo.name):
            order_sudo.partner_shipping_id.write(partial_delivery_address)
            # TODO VFE TODO VCR do we want to trigger cart recomputation here ?
            # order_sudo._update_address(
            #     order_sudo.partner_shipping_id.id, ['partner_shipping_id']
            # )
        elif not self._are_same_addresses(
            partial_delivery_address,
            order_sudo.partner_shipping_id,
        ):
            # Check if a child partner doesn't already exist with the same information. The phone
            # isn't always checked because it isn't sent in delivery information with Google Pay.
            child_partner_id = self._find_child_partner(
                order_sudo.partner_id.commercial_partner_id.id, partial_delivery_address
            )
            partial_delivery_address['name'] = _(
                'Anonymous express checkout partner for order %s',
                order_sudo.name,
            )
            order_sudo.partner_shipping_id = child_partner_id or self._create_new_address(
                address_values=partial_delivery_address,
                address_type='delivery',
                use_delivery_as_billing=False,
                order_sudo=order_sudo,
            )

        # Return the list of delivery methods available for the sales order.
        return sorted([{
            'id': dm.id,
            'name': dm.name,
            'description': dm.website_description,
            'minorAmount': payment_utils.to_minor_currency_units(price, order_sudo.currency_id),
        } for dm, price in Delivery._get_delivery_methods_express_checkout(order_sudo).items()
        ], key=lambda dm: dm['minorAmount'])

    @staticmethod
    def _get_delivery_methods_express_checkout(order_sudo):
        """ Return available delivery methods and their prices for the given order.

        :param sale.order order_sudo: The sudoed sales order.
        :rtype: dict
        :return: A dict with a `delivery.carrier` recordset as key, and a rate shipment price as
                 value.
        """
        res = {}
        for dm in order_sudo._get_delivery_methods():
            rate = Delivery._get_rate(dm, order_sudo, is_express_checkout_flow=True)
            if rate['success']:
                fname = f'{dm.delivery_type}_use_locations'
                if hasattr(dm, fname) and getattr(dm, fname):
                    continue  # Express checkout doesn't allow selecting locations.
                res[dm] = rate['price']
        return res

    @staticmethod
    def _get_rate(delivery_method, order, is_express_checkout_flow=False):
        """ Compute the delivery rate and apply the taxes if relevant.

        :param delivery.carrier delivery_method: The delivery method for which the rate must be
                                                 computed.
        :param sale.order order: The current sales order.
        :param boolean is_express_checkout_flow: Whether the flow is express checkout.
        :return: The delivery rate data.
        :rtype: dict
        """
        # Some delivery methods check if all the required fields are available before computing the
        # rate, even if those fields aren't required for the computation (although they are for
        # delivering the goods). If we only have partial information about the delivery address, but
        # still want to compute the rate, this context key will ensure that we only check the
        # required fields for a partial delivery address (city, zip, country_code, state_code).
        rate = delivery_method.rate_shipment(order.with_context(
            express_checkout_partial_delivery_address=is_express_checkout_flow
        ))
        if rate.get('success'):
            tax_ids = delivery_method.product_id.taxes_id.filtered(
                lambda t: t.company_id == order.company_id
            )
            if tax_ids:
                fpos = order.fiscal_position_id
                tax_ids = fpos.map_tax(tax_ids)
                taxes = tax_ids.compute_all(
                    rate['price'],
                    currency=order.currency_id,
                    quantity=1.0,
                    product=delivery_method.product_id,
                    partner=order.partner_shipping_id,
                )
                if (
                    not is_express_checkout_flow
                    and request.website.show_line_subtotals_tax_selection == 'tax_excluded'
                ):
                    rate['price'] = taxes['total_excluded']
                else:
                    rate['price'] = taxes['total_included']
        return rate