[IMP] delivery: Sendcloud and Fedex delivery points
The main goal of this commit is to add the possibility to use delivery/pick up points with the carrier Sendcloud. The user can check the "Use Sendcloud Location" on the shipping method and choose the radius and radius unit to limit the distance between the address of the recipient and the pick up point. As Fedex already had a similar feature and to prepare the pick up points implementation for future carriers, we factorized the existing Fedex code into the website_sale_delivery module. The issue where it is impossible to select a pick up location when there is only carrier has also been fixed. The factorization applies to : - /controllers/main: - set_access_point sets the address of the access point directly in the order - get_access_point retrieves the data set in set_access_point - get_close_locations calls the API of the carrier and returns the pick up locations obtained - /website_sale_delivery.js: - getCurrentLocation retrieves the location already set in the order and displays it - _onClickShowLocations displays a dropdown with all the pick up points available in the area - _displayCarrierDropper displays the button showing the dropdown - _onClickLocation sets the location in the order Part-of: odoo/odoo#105848
This commit is contained in:
@@ -12,6 +12,7 @@ Configure your own methods with a pricing grid or integrate with carriers for a
|
||||
'views/website_sale_delivery_templates.xml',
|
||||
'views/website_sale_delivery_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/locations_carrier_templates.xml',
|
||||
'data/website_sale_delivery_data.xml'
|
||||
],
|
||||
'demo': [
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
from odoo import http, _
|
||||
from odoo.http import request
|
||||
from odoo.addons.payment import utils as payment_utils
|
||||
@@ -30,6 +31,7 @@ class WebsiteSaleDelivery(WebsiteSale):
|
||||
@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()
|
||||
order.access_point_address = {}
|
||||
carrier_id = int(post['carrier_id'])
|
||||
if order and carrier_id != order.carrier_id.id:
|
||||
if any(tx.state not in ("canceled", "error", "draft") for tx in order.transaction_ids):
|
||||
@@ -201,7 +203,44 @@ class WebsiteSaleDelivery(WebsiteSale):
|
||||
order_sudo.currency_id,
|
||||
),
|
||||
} for carrier in order_sudo._get_delivery_methods()],
|
||||
key=lambda carrier: carrier['minorAmount'])
|
||||
key=lambda carrier: carrier['minorAmount'])
|
||||
|
||||
@http.route('/shop/access_point/set', type='json', auth='public', methods=['POST'], csrf=False, website=True, sitemap=False)
|
||||
def set_access_point(self, access_point_encoded):
|
||||
order = request.website.sale_get_order()
|
||||
if hasattr(order.carrier_id, order.carrier_id.delivery_type + '_use_locations'):
|
||||
use_location = getattr(order.carrier_id, order.carrier_id.delivery_type + '_use_locations')
|
||||
access_point = use_location and access_point_encoded or None
|
||||
order.write({'access_point_address': access_point})
|
||||
|
||||
@http.route('/shop/access_point/get', type='json', auth='public', csrf=False, website=True, sitemap=False)
|
||||
def get_access_point(self):
|
||||
order = request.website.sale_get_order()
|
||||
if not order.carrier_id.delivery_type:
|
||||
return {}
|
||||
order_location = json.loads(order.access_point_address) if order.access_point_address else False
|
||||
if not order_location:
|
||||
return {}
|
||||
address = order_location['address']
|
||||
return {order.carrier_id.delivery_type + '_access_point': address}
|
||||
|
||||
@http.route('/shop/access_point/close_locations', type='json', auth='public', csrf=False, website=True, sitemap=False)
|
||||
def get_close_locations(self):
|
||||
order = request.website.sale_get_order()
|
||||
try:
|
||||
error = {'error': 'No pick-up point available for that shipping address'}
|
||||
if not hasattr(order.carrier_id, '_' + order.carrier_id.delivery_type + '_get_close_locations'):
|
||||
return error
|
||||
close_locations = getattr(order.carrier_id, '_' + order.carrier_id.delivery_type + '_get_close_locations')(order.partner_shipping_id)
|
||||
partner_address = order.partner_shipping_id
|
||||
inline_partner_address = ' '.join((part or '') for part in [partner_address.street, partner_address.street2, partner_address.zip, partner_address.country_id.code])
|
||||
if len(close_locations) < 0:
|
||||
return {'error': 'No pick-up point available for that shipping address'}
|
||||
for location in close_locations:
|
||||
location['address_stringified'] = json.dumps(location)
|
||||
return {'close_locations': close_locations, 'partner_address': inline_partner_address}
|
||||
except UserError as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
@staticmethod
|
||||
def _get_rate(carrier, order, is_express_checkout_flow=False):
|
||||
|
||||
@@ -11,6 +11,7 @@ class SaleOrder(models.Model):
|
||||
compute='_compute_amount_delivery',
|
||||
string='Delivery Amount',
|
||||
help="The amount without tax.", store=True, tracking=True)
|
||||
access_point_address = fields.Text('Delivery Point Address')
|
||||
|
||||
@api.depends('order_line.price_unit', 'order_line.tax_id', 'order_line.discount', 'order_line.product_uom_qty')
|
||||
def _compute_amount_delivery(self):
|
||||
|
||||
@@ -5,6 +5,7 @@ var core = require('web.core');
|
||||
var publicWidget = require('web.public.widget');
|
||||
|
||||
var _t = core._t;
|
||||
var qweb = core.qweb;
|
||||
var concurrency = require('web.concurrency');
|
||||
var dp = new concurrency.DropPrevious();
|
||||
|
||||
@@ -12,7 +13,10 @@ publicWidget.registry.websiteSaleDelivery = publicWidget.Widget.extend({
|
||||
selector: '.oe_website_sale',
|
||||
events: {
|
||||
'change select[name="shipping_id"]': '_onSetAddress',
|
||||
'click #delivery_carrier .o_delivery_carrier_select': '_onCarrierClick',
|
||||
'click .o_delivery_carrier_select': '_onCarrierClick',
|
||||
"click .o_address_select": "_onClickLocation",
|
||||
"click .o_remove_order_location": "_onClickRemoveLocation",
|
||||
"click .o_show_pickup_locations": "_onClickShowLocations",
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -35,24 +39,196 @@ publicWidget.registry.websiteSaleDelivery = publicWidget.Widget.extend({
|
||||
$carriers.filter(':checked').click();
|
||||
}
|
||||
|
||||
// Asynchronously retrieve every carrier price
|
||||
_.each($carriers, function (carrierInput, k) {
|
||||
self._getCurrentLocation();
|
||||
|
||||
_.each($carriers, async function (carrierInput, k) {
|
||||
self._showLoading($(carrierInput));
|
||||
self._rpc({
|
||||
const result = await self._rpc({
|
||||
route: '/shop/carrier_rate_shipment',
|
||||
params: {
|
||||
'carrier_id': carrierInput.value,
|
||||
},
|
||||
}).then(self._handleCarrierUpdateResultBadge.bind(self));
|
||||
})
|
||||
self._handleCarrierUpdateResult(result)
|
||||
});
|
||||
|
||||
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_getCurrentLocation: async function () {
|
||||
const self=this;
|
||||
const data = await self._rpc({
|
||||
route: "/shop/access_point/get",
|
||||
})
|
||||
const order_locations = document.getElementsByClassName("o_order_location");
|
||||
for (const order_loc of order_locations) {
|
||||
const show_loc = order_loc.parentElement.nextElementSibling;
|
||||
// We could end not having the property
|
||||
let delivery_type=""
|
||||
if(order_loc.parentElement.parentElement.parentElement.children[0].attributes[8]){
|
||||
delivery_type = order_loc.parentElement.parentElement.parentElement.children[0].attributes[8].nodeValue;
|
||||
}
|
||||
if (!show_loc)
|
||||
continue;
|
||||
if (data[delivery_type+'_access_point']) {
|
||||
order_loc.innerText = data[delivery_type+'_access_point']
|
||||
order_loc.parentElement.classList.remove("d-none");
|
||||
show_loc.classList.add("d-none");
|
||||
break;
|
||||
} else {
|
||||
order_loc.parentElement.classList.add("d-none");
|
||||
show_loc.classList.remove("d-none");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onClickRemoveLocation: async function (ev) {
|
||||
ev.stopPropagation();
|
||||
const self = this;
|
||||
await this._rpc({
|
||||
route: "/shop/access_point/set",
|
||||
params: {
|
||||
access_point_encoded: null,
|
||||
},
|
||||
})
|
||||
self._getCurrentLocation();
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onClickShowLocations: async function (ev) {
|
||||
const self = this;
|
||||
const show_pickup_locations = ev.currentTarget;
|
||||
let modal = show_pickup_locations.nextElementSibling;
|
||||
if (!show_pickup_locations.getElementsByClassName("link-primary").length)
|
||||
return;
|
||||
const should_load_content = modal.firstChild ? false : true;
|
||||
while (modal.firstChild) {
|
||||
modal.lastChild.remove();
|
||||
}
|
||||
if (!should_load_content)
|
||||
return;
|
||||
|
||||
var delivery_type = ev.currentTarget.closest(".o_delivery_carrier_select").childNodes[1].attributes[8].nodeValue;
|
||||
|
||||
const delivery_type_id = ev.currentTarget.closest(".o_delivery_carrier_select").childNodes[1].value;
|
||||
await this._checkCarrier(ev,delivery_type_id)
|
||||
|
||||
$(qweb.render(delivery_type + "_pickup_location_loading")).appendTo($(modal));
|
||||
const data = await self._rpc({
|
||||
route: "/shop/access_point/close_locations",
|
||||
})
|
||||
if (modal.firstChild)
|
||||
modal.firstChild.remove();
|
||||
if(!data || data && data.error || (data && data.close_locations && data.close_locations.length==0)){
|
||||
const error_message = document.createElement("em");
|
||||
error_message.classList.add("text-error");
|
||||
error_message.innerText = data.error ? data.error : "No available Pick-Up Locations";
|
||||
modal.appendChild(error_message);
|
||||
return;
|
||||
}
|
||||
|
||||
var list_to_render = delivery_type + "_pickup_location_list";
|
||||
var data_to_render = {partner_address: data.partner_address};
|
||||
data_to_render[delivery_type+"_pickup_locations"] = data.close_locations;
|
||||
$(qweb.render(list_to_render, data_to_render)).appendTo($(modal));
|
||||
this._displayCarrierDropper(ev);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} obj // can be either an event or an objet returned by the controller
|
||||
*/
|
||||
_displayCarrierDropper: async function (obj) {
|
||||
let current_id = obj.carrier_id
|
||||
const show_locations = document
|
||||
.getElementById("delivery_carrier")
|
||||
.getElementsByClassName("o_show_pickup_locations");
|
||||
|
||||
// We check if it's an event
|
||||
if('currentTarget' in obj){
|
||||
const is_full_page_event = !obj.currentTarget.closest(".o_delivery_carrier_select")
|
||||
if(is_full_page_event)return;
|
||||
for (const show_loc of show_locations){
|
||||
this._specificDropperDisplay(show_loc);
|
||||
}
|
||||
}
|
||||
else{
|
||||
for (const show_loc of show_locations){
|
||||
const current_carrier_id = show_loc.closest("li").getElementsByTagName("input")[0].value;
|
||||
if (current_carrier_id == current_id) {
|
||||
this._specificDropperDisplay(show_loc);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} doc_carrier //carrier element from document
|
||||
*/
|
||||
_specificDropperDisplay: function (doc_carrier) {
|
||||
if(!doc_carrier)
|
||||
return;
|
||||
if(!doc_carrier.closest("li").getElementsByTagName("input")[0].attributes[8])
|
||||
return;
|
||||
const current_carrier_type = doc_carrier.closest("li").getElementsByTagName("input")[0].attributes[8].nodeValue;
|
||||
const location_still_loading = document.getElementById("#"+current_carrier_type+"_addresses")
|
||||
if(location_still_loading)return
|
||||
while (doc_carrier.firstChild) {
|
||||
doc_carrier.lastChild.remove();
|
||||
}
|
||||
const current_carrier_checked = doc_carrier.closest("li").getElementsByTagName("input")[0].checked;
|
||||
const span = document.createElement("em");
|
||||
const is_pick_up_location_list = doc_carrier.nextElementSibling.childNodes.length > 0
|
||||
if(current_carrier_checked){
|
||||
const chevron_down = document.createElement("i");
|
||||
if(is_pick_up_location_list)
|
||||
chevron_down.classList.add("fa", "fa-angle-up");
|
||||
else
|
||||
chevron_down.classList.add("fa", "fa-angle-down");
|
||||
span.textContent = "Pick-Up Locations ";
|
||||
span.classList.add("link-primary");
|
||||
span.appendChild(chevron_down);
|
||||
}
|
||||
else {
|
||||
span.textContent = "select to see available Pick-Up Locations";
|
||||
span.classList.add("text-muted");
|
||||
}
|
||||
doc_carrier.appendChild(span);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onClickLocation: async function (ev) {
|
||||
ev.preventDefault();
|
||||
const carrierId = ev.currentTarget.closest(".o_delivery_carrier_select").childNodes[1].value;
|
||||
await this._checkCarrier(ev,carrierId)
|
||||
const self = this;
|
||||
const modal = ev.target.closest(".o_list_pickup_locations");
|
||||
const encoded_location = ev.target.previousElementSibling.innerText;
|
||||
await self._rpc({
|
||||
route: "/shop/access_point/set",
|
||||
params: {
|
||||
access_point_encoded: encoded_location,
|
||||
},
|
||||
})
|
||||
while (modal.firstChild) {
|
||||
modal.lastChild.remove();
|
||||
}
|
||||
self._getCurrentLocation();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {jQuery} $carrierInput
|
||||
@@ -100,6 +276,13 @@ publicWidget.registry.websiteSaleDelivery = publicWidget.Widget.extend({
|
||||
if (result.new_amount_total_raw !== undefined) {
|
||||
this._updateShippingCost(result.new_amount_total_raw);
|
||||
}
|
||||
|
||||
var $carriers_inputs = $('#delivery_carrier input[name="delivery_type"]');
|
||||
var carriers = [];
|
||||
_.each($carriers_inputs, function (carrierInput, k) {
|
||||
carriers.push(carrierInput.dataset.deliveryType);
|
||||
})
|
||||
this._displayCarrierDropper(result);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
@@ -108,7 +291,7 @@ publicWidget.registry.websiteSaleDelivery = publicWidget.Widget.extend({
|
||||
_handleCarrierUpdateResultBadge: function (result) {
|
||||
var $carrierBadge = $('#delivery_carrier input[name="delivery_type"][value=' + result.carrier_id + '] ~ .o_wsale_delivery_badge_price');
|
||||
|
||||
if (result.status === true) {
|
||||
if (result.status === true) {
|
||||
// if free delivery (`free_over` field), show 'Free', not '$0'
|
||||
if (result.is_free_delivery) {
|
||||
$carrierBadge.text(_t('Free'));
|
||||
@@ -130,7 +313,7 @@ publicWidget.registry.websiteSaleDelivery = publicWidget.Widget.extend({
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onCarrierClick: function (ev) {
|
||||
_onCarrierClick: async function (ev) {
|
||||
var $radio = $(ev.currentTarget).find('input[type="radio"]');
|
||||
this._showLoading($radio);
|
||||
$radio.prop("checked", true);
|
||||
@@ -139,12 +322,26 @@ publicWidget.registry.websiteSaleDelivery = publicWidget.Widget.extend({
|
||||
var disabledReasons = $payButton.data('disabled_reasons') || {};
|
||||
disabledReasons.carrier_selection = true;
|
||||
$payButton.data('disabled_reasons', disabledReasons);
|
||||
dp.add(this._rpc({
|
||||
const data = await dp.add(this._rpc({
|
||||
route: '/shop/update_carrier',
|
||||
params: {
|
||||
carrier_id: $radio.val(),
|
||||
},
|
||||
})).then(this._handleCarrierUpdateResult.bind(this));
|
||||
}))
|
||||
this._handleCarrierUpdateResult(data);
|
||||
},
|
||||
|
||||
_checkCarrier: async function (ev, carrier_id) {
|
||||
ev.stopPropagation();
|
||||
await dp.add(this._rpc({
|
||||
route: '/shop/update_carrier',
|
||||
params: {
|
||||
carrier_id: carrier_id,
|
||||
},
|
||||
}))
|
||||
var closestDocElement = ev.currentTarget.closest('.o_delivery_carrier_select');
|
||||
var $radio = $(closestDocElement).find('input[type="radio"]');
|
||||
$radio.prop("checked", true);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="payment_delivery_methods_inherit_website_sale_delivery" inherit_id="website_sale_delivery.payment_delivery_methods">
|
||||
<xpath expr="//span[@name='price']" position="after">
|
||||
<t t-set='delivery_method' t-value="delivery.delivery_type+'_use_locations'" />
|
||||
<div class="small">
|
||||
<div class="d-none">
|
||||
<b class="o_order_location"/>
|
||||
<span class="fa fa-trash-o ms-2 o_remove_order_location" aria-label="Remove this location" title="Remove this location"/>
|
||||
</div>
|
||||
<t t-if="delivery_method in delivery.fields_get()">
|
||||
<t t-if="delivery[delivery_method]">
|
||||
<div class="o_show_pickup_locations"/>
|
||||
<div class="o_list_pickup_locations"/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -16,10 +16,10 @@
|
||||
</template>
|
||||
|
||||
<template id="payment_delivery_methods">
|
||||
<input t-att-value="delivery.id" t-att-id="'delivery_%i' % delivery.id" type="radio" name="delivery_type" t-att-checked="order.carrier_id and order.carrier_id.id == delivery.id and 'checked' or False" t-att-class="'d-none' if delivery_nb == 1 else ''"/>
|
||||
<input t-att-value="delivery.id" t-att-id="'delivery_%i' % delivery.id" t-att-delivery_type="delivery.delivery_type" type="radio" name="delivery_type" t-att-checked="order.carrier_id and order.carrier_id.id == delivery.id and 'checked' or False"/>
|
||||
<label class="label-optional" t-field="delivery.name"/>
|
||||
<t t-set='badge_class' t-value="(delivery_nb != 1 and 'float-end ' or '') + 'badge text-bg-secondary'" />
|
||||
<span t-attf-class="#{badge_class} o_wsale_delivery_badge_price">Select to compute delivery rate</span>
|
||||
<t t-set='badge_class' t-value="'float-end ' + 'badge text-bg-secondary'" />
|
||||
<span t-attf-class="#{badge_class} o_wsale_delivery_badge_price" name="price">Select to compute delivery rate</span>
|
||||
<t t-if="delivery.website_description">
|
||||
<div t-field="delivery.website_description" class="text-muted mt8"/>
|
||||
</t>
|
||||
@@ -28,14 +28,6 @@
|
||||
<template id="payment_delivery_shipping_method" name="Delivery Shipping method" inherit_id="website_sale.address_on_payment">
|
||||
<xpath expr="//div[@id='shipping_and_billing']" position="inside">
|
||||
<t t-set="delivery_nb" t-value="deliveries and len(deliveries) or 0"/>
|
||||
<div t-if="delivery_nb == 1" id="delivery_carrier" class="mt4">
|
||||
<b>Shipping Method: </b>
|
||||
<t t-foreach="deliveries" t-as="delivery">
|
||||
<div class="o_delivery_carrier_select d-inline">
|
||||
<t t-call="website_sale_delivery.payment_delivery_methods"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
@@ -43,8 +35,8 @@
|
||||
<xpath expr="//t[@t-if='website_sale_order.amount_total']" position="before">
|
||||
<div t-if="deliveries" id="delivery_carrier">
|
||||
<t t-set="delivery_nb" t-value="len(deliveries)"/>
|
||||
<h3 t-if="delivery_nb > 1" class="mb24">Choose a delivery method</h3>
|
||||
<div t-if="delivery_nb > 1" class="card border-0" id="delivery_method">
|
||||
<h3 class="mb24">Choose a delivery method</h3>
|
||||
<div class="card border-0" id="delivery_method">
|
||||
<ul class="list-group">
|
||||
<t t-foreach="deliveries" t-as="delivery">
|
||||
<li class="list-group-item o_delivery_carrier_select">
|
||||
|
||||
Reference in New Issue
Block a user