From cb1b5970a5e1abd67da7997531640c9a20ea9104 Mon Sep 17 00:00:00 2001 From: Martin Maes Date: Thu, 27 Oct 2022 14:17:43 +0000 Subject: [PATCH] [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 --- addons/website_sale_delivery/__manifest__.py | 1 + .../website_sale_delivery/controllers/main.py | 41 +++- .../models/sale_order.py | 1 + .../static/src/js/website_sale_delivery.js | 217 +++++++++++++++++- .../views/locations_carrier_templates.xml | 20 ++ .../views/website_sale_delivery_templates.xml | 18 +- 6 files changed, 274 insertions(+), 24 deletions(-) create mode 100644 addons/website_sale_delivery/views/locations_carrier_templates.xml diff --git a/addons/website_sale_delivery/__manifest__.py b/addons/website_sale_delivery/__manifest__.py index 13c8309b6d4..3be003a3954 100644 --- a/addons/website_sale_delivery/__manifest__.py +++ b/addons/website_sale_delivery/__manifest__.py @@ -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': [ diff --git a/addons/website_sale_delivery/controllers/main.py b/addons/website_sale_delivery/controllers/main.py index 6d41e7537d2..91d841a488a 100644 --- a/addons/website_sale_delivery/controllers/main.py +++ b/addons/website_sale_delivery/controllers/main.py @@ -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): diff --git a/addons/website_sale_delivery/models/sale_order.py b/addons/website_sale_delivery/models/sale_order.py index b6c10287f5b..9b0a3ff01b8 100644 --- a/addons/website_sale_delivery/models/sale_order.py +++ b/addons/website_sale_delivery/models/sale_order.py @@ -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): diff --git a/addons/website_sale_delivery/static/src/js/website_sale_delivery.js b/addons/website_sale_delivery/static/src/js/website_sale_delivery.js index 61a4b9561c4..ba5c2fc2a91 100644 --- a/addons/website_sale_delivery/static/src/js/website_sale_delivery.js +++ b/addons/website_sale_delivery/static/src/js/website_sale_delivery.js @@ -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 diff --git a/addons/website_sale_delivery/views/locations_carrier_templates.xml b/addons/website_sale_delivery/views/locations_carrier_templates.xml new file mode 100644 index 00000000000..8b2d0991f9d --- /dev/null +++ b/addons/website_sale_delivery/views/locations_carrier_templates.xml @@ -0,0 +1,20 @@ + + + + diff --git a/addons/website_sale_delivery/views/website_sale_delivery_templates.xml b/addons/website_sale_delivery/views/website_sale_delivery_templates.xml index 792bd3beda9..7031f67886c 100644 --- a/addons/website_sale_delivery/views/website_sale_delivery_templates.xml +++ b/addons/website_sale_delivery/views/website_sale_delivery_templates.xml @@ -16,10 +16,10 @@