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
|