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 @@