338 lines
17 KiB
Python
338 lines
17 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from odoo import http, _
|
|
from odoo.http import request
|
|
from odoo.addons.payment import utils as payment_utils
|
|
from odoo.addons.website_sale.controllers.main import WebsiteSale, PaymentPortal
|
|
from odoo.exceptions import UserError, ValidationError
|
|
|
|
|
|
class WebsiteSaleDelivery(WebsiteSale):
|
|
_express_checkout_shipping_route = '/shop/express/shipping_address_change'
|
|
|
|
@http.route()
|
|
def shop_payment(self, **post):
|
|
order = request.website.sale_get_order()
|
|
if order and not order.only_services:
|
|
# Update order's carrier_id (will be the one of the partner if not defined)
|
|
# If a carrier_id is (re)defined, redirect to "/shop/payment" (GET method to avoid infinite loop)
|
|
carrier_id = post.get('carrier_id')
|
|
keep_carrier = False
|
|
if carrier_id:
|
|
carrier_id = int(carrier_id)
|
|
elif order.carrier_id: # If a carrier is selected.
|
|
keep_carrier = True # Check availability of selected carrier and recompute rate.
|
|
order._check_carrier_quotation(force_carrier_id=carrier_id, keep_carrier=keep_carrier)
|
|
if carrier_id:
|
|
return request.redirect("/shop/payment")
|
|
|
|
return super(WebsiteSaleDelivery, self).shop_payment(**post)
|
|
|
|
@http.route(['/shop/update_carrier'], type='json', auth='public', methods=['POST'], website=True, csrf=False)
|
|
def update_eshop_carrier(self, **post):
|
|
order = request.website.sale_get_order()
|
|
carrier_id = int(post['carrier_id'])
|
|
if order and carrier_id != order.carrier_id.id:
|
|
if any(tx.state not in ("cancel", "error", "draft") for tx in order.transaction_ids):
|
|
raise UserError(_('It seems that there is already a transaction for your order, you can not change the delivery method anymore'))
|
|
order._check_carrier_quotation(force_carrier_id=carrier_id)
|
|
return self._update_website_sale_delivery_return(order, **post)
|
|
|
|
@http.route(['/shop/carrier_rate_shipment'], type='json', auth='public', methods=['POST'], website=True)
|
|
def cart_carrier_rate_shipment(self, carrier_id, **kw):
|
|
order = request.website.sale_get_order(force_create=True)
|
|
|
|
if not int(carrier_id) 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']
|
|
|
|
res = {'carrier_id': carrier_id}
|
|
carrier = request.env['delivery.carrier'].sudo().browse(int(carrier_id))
|
|
rate = WebsiteSaleDelivery._get_rate(carrier, order)
|
|
if rate.get('success'):
|
|
res['status'] = True
|
|
res['new_amount_delivery'] = Monetary.value_to_html(rate['price'], {'display_currency': order.currency_id})
|
|
res['is_free_delivery'] = not bool(rate['price'])
|
|
res['error_message'] = rate['warning_message']
|
|
else:
|
|
res['status'] = False
|
|
res['new_amount_delivery'] = Monetary.value_to_html(0.0, {'display_currency': order.currency_id})
|
|
res['error_message'] = rate['error_message']
|
|
return res
|
|
|
|
@http.route()
|
|
def cart(self, **post):
|
|
order = request.website.sale_get_order()
|
|
if order and order.state != 'draft':
|
|
request.session['sale_order_id'] = None
|
|
order = request.website.sale_get_order()
|
|
if order and order.carrier_id:
|
|
# Express checkout is based on the amout of the sale order. If there is already a
|
|
# delivery line, Express Checkout form will display and compute the price of the
|
|
# delivery two times (One already computed in the total amount of the SO and one added
|
|
# in the form while selecting the delivery carrier)
|
|
order._remove_delivery_line()
|
|
|
|
return super().cart(**post)
|
|
|
|
@http.route()
|
|
def process_express_checkout(
|
|
self, billing_address, shipping_address=None, shipping_option=None, **kwargs
|
|
):
|
|
""" Override of `website_sale` to records the shipping information on the order when using
|
|
express checkout flow.
|
|
|
|
Depending on whether the partner is registered and logged in, either creates a new partner
|
|
or uses an existing one that matches all received data.
|
|
|
|
:param dict billing_address: Billing information sent by the express payment form.
|
|
:param dict shipping_address: Shipping information sent by the express payment form.
|
|
:param dict shipping_option: Carrier information sent by the express payment form.
|
|
:param dict kwargs: Optional data. This parameter is not used here.
|
|
:return int: The order's partner id.
|
|
"""
|
|
if not (shipping_address and shipping_option):
|
|
return super().process_express_checkout(billing_address, **kwargs)
|
|
|
|
order_sudo = request.website.sale_get_order()
|
|
|
|
# Update the partner with all the information
|
|
self._include_country_and_state_in_address(shipping_address)
|
|
|
|
# At this point, if the user is a public user, the order will have a partner created by
|
|
# `process_express_checkout_delivery_choice`. No need to check if he is connected or not.
|
|
|
|
if order_sudo.partner_shipping_id.name.endswith(order_sudo.name):
|
|
# The existing partner was created by `process_express_checkout_delivery_choice`, it
|
|
# means that the partner is missing information, so we update it.
|
|
order_sudo.partner_shipping_id = self._create_or_edit_partner(
|
|
shipping_address,
|
|
edit=True,
|
|
type='delivery',
|
|
partner_id=order_sudo.partner_shipping_id.id,
|
|
)
|
|
elif any(
|
|
shipping_address[k] != order_sudo.partner_shipping_id[k] for k in shipping_address
|
|
):
|
|
# The sale order's shipping partner's address is different from the one received. If all
|
|
# the sale order's child partners' address differs from the one received, we create a
|
|
# new partner. The phone isn't always checked because it isn't sent in shipping
|
|
# information with Google Pay.
|
|
child_partner_id = self._find_child_partner(
|
|
order_sudo.partner_id.commercial_partner_id.id, shipping_address
|
|
)
|
|
if child_partner_id:
|
|
order_sudo.partner_shipping_id = child_partner_id
|
|
else:
|
|
order_sudo.partner_shipping_id = self._create_or_edit_partner(
|
|
shipping_address,
|
|
type='delivery',
|
|
parent_id=order_sudo.partner_id.id,
|
|
)
|
|
|
|
# Process the delivery carrier
|
|
order_sudo._check_carrier_quotation(force_carrier_id=int(shipping_option['id']))
|
|
|
|
return super().process_express_checkout(billing_address, **kwargs)
|
|
|
|
@http.route(
|
|
_express_checkout_shipping_route, type='json', auth='public', methods=['POST'],
|
|
website=True, sitemap=False
|
|
)
|
|
def express_checkout_process_shipping_address(self, partial_shipping_address):
|
|
""" Processes shipping address and returns available carriers.
|
|
|
|
Depending on whether the partner is registered and logged in or not, creates a new partner
|
|
or uses an existing partner that matches the partial shipping address received.
|
|
|
|
:param dict shipping_address: a dictionary containing part of shipping information sent by
|
|
the express payment provider.
|
|
:return dict: all available carriers for `shipping_address` sorted by lowest price.
|
|
"""
|
|
order_sudo = request.website.sale_get_order()
|
|
public_partner = request.website.partner_id
|
|
|
|
self._include_country_and_state_in_address(partial_shipping_address)
|
|
if order_sudo.partner_id == public_partner:
|
|
# The partner_shipping_id and partner_invoice_id will be automatically computed when
|
|
# changing the partner_id of the SO. This allow website_sale to avoid create duplicates.
|
|
order_sudo.partner_id = self._create_or_edit_partner(
|
|
partial_shipping_address,
|
|
type='delivery',
|
|
name=_('Anonymous express checkout partner for order %s', order_sudo.name),
|
|
)
|
|
# Pricelist 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.
|
|
order_sudo.env.remove_to_compute(
|
|
order_sudo._fields['pricelist_id'], order_sudo
|
|
)
|
|
elif order_sudo.partner_shipping_id.name.endswith(order_sudo.name):
|
|
self._create_or_edit_partner(
|
|
partial_shipping_address,
|
|
edit=True,
|
|
type='delivery',
|
|
partner_id=order_sudo.partner_shipping_id.id,
|
|
)
|
|
elif any(
|
|
partial_shipping_address[k] != order_sudo.partner_shipping_id[k]
|
|
for k in partial_shipping_address
|
|
):
|
|
# Check if a child partner doesn't already exist with the same informations. The
|
|
# phone isn't always checked because it isn't sent in shipping information with
|
|
# Google Pay.
|
|
child_partner_id = self._find_child_partner(
|
|
order_sudo.partner_id.commercial_partner_id.id, partial_shipping_address
|
|
)
|
|
if child_partner_id:
|
|
order_sudo.partner_shipping_id = child_partner_id
|
|
else:
|
|
order_sudo.partner_shipping_id = self._create_or_edit_partner(
|
|
partial_shipping_address,
|
|
type='delivery',
|
|
parent_id=order_sudo.partner_id.id,
|
|
name=_('Anonymous express checkout partner for order %s', order_sudo.name),
|
|
)
|
|
|
|
# Returns the list of delivery carrier available for the sale order.
|
|
return sorted([{
|
|
'id': carrier.id,
|
|
'name': carrier.name,
|
|
'description': carrier.website_description,
|
|
'minorAmount': payment_utils.to_minor_currency_units(price, order_sudo.currency_id),
|
|
} for carrier, price in WebsiteSaleDelivery._get_carriers_express_checkout(order_sudo).items()
|
|
], key=lambda carrier: carrier['minorAmount'])
|
|
|
|
@staticmethod
|
|
def _get_carriers_express_checkout(order_sudo):
|
|
""" Return available carriers 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 carrier in order_sudo._get_delivery_methods():
|
|
rate = WebsiteSaleDelivery._get_rate(carrier, order_sudo, is_express_checkout_flow=True)
|
|
if rate['success']:
|
|
fname = f'{carrier.delivery_type}_use_locations'
|
|
if hasattr(carrier, fname) and getattr(carrier, fname):
|
|
continue # Express checkout doesn't allow selecting locations.
|
|
res[carrier] = rate['price']
|
|
return res
|
|
|
|
@staticmethod
|
|
def _get_rate(carrier, order, is_express_checkout_flow=False):
|
|
""" Compute the price of the order shipment and apply the taxes if relevant
|
|
|
|
:param recordset carrier: the carrier for which the rate is to be recovered
|
|
:param recordset order: the order for which the rate is to be recovered
|
|
:param boolean is_express_checkout_flow: Whether the flow is express checkout or not
|
|
:return dict: the rate, as returned in `rate_shipment()`
|
|
"""
|
|
# Some delivery carriers check if all the required fields are available before computing the
|
|
# rate, even if those fields aren't required for computing the rate (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 = carrier.rate_shipment(order.with_context(
|
|
express_checkout_partial_delivery_address=is_express_checkout_flow
|
|
))
|
|
if rate.get('success'):
|
|
tax_ids = carrier.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=carrier.product_id,
|
|
partner=order.partner_shipping_id,
|
|
)
|
|
if not is_express_checkout_flow and request.env.user.has_group(
|
|
'account.group_show_line_subtotals_tax_excluded'
|
|
):
|
|
rate['price'] = taxes['total_excluded']
|
|
else:
|
|
rate['price'] = taxes['total_included']
|
|
return rate
|
|
|
|
def order_lines_2_google_api(self, order_lines):
|
|
""" Transforms a list of order lines into a dict for google analytics """
|
|
order_lines_not_delivery = order_lines.filtered(lambda line: not line.is_delivery)
|
|
return super(WebsiteSaleDelivery, self).order_lines_2_google_api(order_lines_not_delivery)
|
|
|
|
def order_2_return_dict(self, order):
|
|
""" Returns the tracking_cart dict of the order for Google analytics """
|
|
ret = super(WebsiteSaleDelivery, self).order_2_return_dict(order)
|
|
delivery_line = order.order_line.filtered('is_delivery')
|
|
if delivery_line:
|
|
ret['shipping'] = delivery_line.price_unit
|
|
return ret
|
|
|
|
def _get_express_shop_payment_values(self, order, **kwargs):
|
|
values = super(WebsiteSaleDelivery, self)._get_express_shop_payment_values(order, **kwargs)
|
|
values['shipping_info_required'] = not order.only_services
|
|
values['shipping_address_update_route'] = self._express_checkout_shipping_route
|
|
return values
|
|
|
|
def _get_shop_payment_errors(self, order):
|
|
errors = super()._get_shop_payment_errors(order)
|
|
|
|
if not order.only_services and not order._get_delivery_methods():
|
|
errors.append((
|
|
_('Sorry, we are unable to ship your order'),
|
|
_('No shipping method is available for your current order and shipping address. '
|
|
'Please contact us for more information.'),
|
|
))
|
|
return errors
|
|
|
|
def _get_shop_payment_values(self, order, **kwargs):
|
|
values = super(WebsiteSaleDelivery, self)._get_shop_payment_values(order, **kwargs)
|
|
has_storable_products = any(line.product_id.type in ['consu', 'product'] for line in order.order_line)
|
|
|
|
if has_storable_products:
|
|
if order.carrier_id and not order.delivery_rating_success:
|
|
order._remove_delivery_line()
|
|
|
|
delivery_carriers = order._get_delivery_methods()
|
|
values['deliveries'] = delivery_carriers.sudo()
|
|
|
|
values['delivery_has_storable'] = has_storable_products
|
|
values['delivery_action_id'] = request.env.ref('delivery.action_delivery_carrier_form').id
|
|
return values
|
|
|
|
def _update_website_sale_delivery_return(self, order, **post):
|
|
Monetary = request.env['ir.qweb.field.monetary']
|
|
carrier_id = int(post['carrier_id'])
|
|
currency = order.currency_id
|
|
if order:
|
|
return {
|
|
'status': order.delivery_rating_success,
|
|
'error_message': order.delivery_message,
|
|
'carrier_id': carrier_id,
|
|
'is_free_delivery': not bool(order.amount_delivery),
|
|
'new_amount_delivery': Monetary.value_to_html(order.amount_delivery, {'display_currency': currency}),
|
|
'new_amount_untaxed': Monetary.value_to_html(order.amount_untaxed, {'display_currency': currency}),
|
|
'new_amount_tax': Monetary.value_to_html(order.amount_tax, {'display_currency': currency}),
|
|
'new_amount_total': Monetary.value_to_html(order.amount_total, {'display_currency': currency}),
|
|
'new_amount_total_raw': order.amount_total,
|
|
}
|
|
return {}
|
|
|
|
|
|
class PaymentPortalDelivery(PaymentPortal):
|
|
|
|
@http.route()
|
|
def shop_payment_transaction(self, *args, **kwargs):
|
|
order = request.website.sale_get_order()
|
|
if not order.only_services and not order.carrier_id:
|
|
raise ValidationError(_("No shipping method is selected."))
|
|
return super().shop_payment_transaction(*args, **kwargs)
|