From 165982dd2ffbee441cb3c27bf8ef259391f71773 Mon Sep 17 00:00:00 2001 From: docker-odoo Date: Mon, 25 Nov 2024 17:47:38 +0000 Subject: [PATCH] [MIG] portal_addresses: Migration to 18.0 --- portal_addresses/__manifest__.py | 9 +- portal_addresses/controllers/main.py | 233 +++++++++++++--------- portal_addresses/static/src/js/address.js | 65 ++++++ portal_addresses/views/templates.xml | 42 ++-- 4 files changed, 238 insertions(+), 111 deletions(-) create mode 100644 portal_addresses/static/src/js/address.js diff --git a/portal_addresses/__manifest__.py b/portal_addresses/__manifest__.py index 8b5a3bb04..244d2a39d 100644 --- a/portal_addresses/__manifest__.py +++ b/portal_addresses/__manifest__.py @@ -19,7 +19,7 @@ ############################################################################## { 'name': 'Portal Addresses', - 'version': "17.0.1.0.0", + 'version': "18.0.1.0.0", 'category': 'Tools', 'complexity': 'easy', 'author': 'ADHOC SA, Odoo Community Association (OCA)', @@ -35,6 +35,11 @@ 'views/portal_templates.xml', 'views/templates.xml', ], - 'installable': False, + 'installable': True, 'auto_install': True, + 'assets': { + 'web.assets_frontend': [ + '/portal_addresses/static/src/js/address.js' + ] + } } diff --git a/portal_addresses/controllers/main.py b/portal_addresses/controllers/main.py index a8f46b16d..c44d877b9 100644 --- a/portal_addresses/controllers/main.py +++ b/portal_addresses/controllers/main.py @@ -2,106 +2,58 @@ # For copyright and license notices, see __manifest__.py file in module root # directory ############################################################################## +import json from odoo import http from odoo.http import request -from werkzeug.exceptions import Forbidden from odoo.addons.website_sale.controllers.main import WebsiteSale +from odoo.tools import clean_context class WebsiteSalePortal(WebsiteSale): + @http.route( + '/portal/address', type='http', methods=['GET'], auth='public', website=True, sitemap=False + ) + def shop_address( + self, partner_id=None, address_type='billing', use_delivery_as_billing=None, **query_params + ): + """ Display the address form. + A partner and/or an address type can be given through the query string params to specify + which address to update or create, and its type. - @http.route(['/portal/address'], type='http', methods=['GET', 'POST'], - auth="public", website=True) - def portal_address(self, **kw): - Partner = request.env['res.partner'].with_context(show_address=1).sudo() - order = request.env['sale.order'].new({ + :param str partner_id: The partner whose address to update with the address form, if any. + :param str address_type: The type of the address: 'billing' or 'delivery'. + :param str use_delivery_as_billing: Whether the provided address should be used as both the + delivery and the billing address. 'true' or 'false'. + :param dict query_params: The additional query string parameters forwarded to + `_prepare_address_form_values`. + :return: The rendered address form. + :rtype: str + """ + partner_id = partner_id and int(partner_id) + order_sudo = request.env['sale.order'].new({ 'partner_id': request.env.user.partner_id.commercial_partner_id.id }) - - mode = (False, False) - values, errors = {}, {} - partner_id = int(kw.get('partner_id', -1)) - - if partner_id > 0: - partner_type = request.env['res.partner'].browse(partner_id).type - values = Partner.browse(partner_id) - if partner_type == 'invoice': - mode = ('edit', 'billing') - else: - mode = ('edit', 'shipping') - elif partner_id == -1: - mode = ('new', kw.get('mode') or 'shipping') - else: # no mode - refresh without post? - return request.redirect('/portal/addresses') - - # IF POSTED - if 'submitted' in kw: - pre_values = self.values_preprocess(kw) - errors, error_msg = self.checkout_form_validate(mode, kw, pre_values) - post, errors, error_msg = self.values_postprocess(order, mode, pre_values, errors, error_msg) - if errors: - errors['error_message'] = error_msg - values = kw - else: - partner_id = self._portal_address_form_save(mode, post, kw) - if isinstance(partner_id, Forbidden): - return partner_id - if mode[1] == 'billing': - order.partner_id = partner_id - elif mode[1] == 'shipping': - order.partner_shipping_id = partner_id - - order.message_partner_ids = [(4, partner_id), (3, request.website.partner_id.id)] - if not errors: - return request.redirect(kw.get('callback') or '/portal/addresses') - - render_values = { - 'website_sale_order': order, - 'partner_id': partner_id, - 'mode': mode, - 'checkout': values, - 'error': errors, - 'callback': kw.get('callback'), - 'only_services': order and order.only_services, - 'account_on_checkout': request.website.account_on_checkout, - 'is_public_user': request.website.is_public_user() - } - # para evitar modulo puente con l10n_ar_website_sale lo hacemos asi - if request.env['ir.module.module'].sudo().search([ - ('name', '=', 'l10n_ar_website_sale'), - ('state', '=', 'installed')], limit=1): - document_categories = request.env[ - 'l10n_latam.document.type'].sudo().search([]) - afip_responsabilities = request.env[ - 'l10n_ar.afip.responsibility.type'].sudo().search([]) - uid = request.session.uid or request.env.ref('base.public_user').id - Partner = request.env['res.users'].browse(uid).partner_id - Partner = Partner.with_context(show_address=1).sudo() - render_values.update({ - 'document_categories': document_categories, - 'afip_responsabilities': afip_responsabilities, - 'partner': Partner, - }) - render_values.update(self._get_country_related_render_values(kw, render_values)) - return request.render("portal_addresses.portal_address", render_values) - - def _portal_address_form_save(self, mode, checkout, all_values): - Partner = request.env['res.partner'] - if mode[0] == 'new': - partner_id = Partner.sudo().create(checkout).id - elif mode[0] == 'edit': - partner_id = int(all_values.get('partner_id', 0)) - if partner_id: - # double check - partner = request.env.user.partner_id - shippings = Partner.sudo().search( - [("id", "child_of", partner.commercial_partner_id.ids)]) - if partner_id not in shippings.mapped('id') and \ - partner_id != partner.id: - return Forbidden() - Partner.browse(partner_id).sudo().write(checkout) - return partner_id + # Retrieve the partner whose address to update, if any, and its address type. + partner_sudo, address_type = self._prepare_address_update( + order_sudo, partner_id=partner_id, address_type=address_type + ) + + if partner_sudo: # If editing an existing partner. + use_delivery_as_billing = ( + order_sudo.partner_shipping_id == order_sudo.partner_invoice_id + ) + + # Render the address form. + address_form_values = self._prepare_address_form_values( + order_sudo, + partner_sudo, + address_type=address_type, + use_delivery_as_billing=use_delivery_as_billing, + **query_params + ) + return request.render('portal_addresses.portal_address', address_form_values) + @http.route(['/portal/addresses'], type='http', auth="public", website=True) @@ -110,8 +62,6 @@ def portal_addresses(self, **post): order = request.env['sale.order'].new( {'partner_id': request.env.user.partner_id.commercial_partner_id.id}) - order.pricelist_id = order.partner_id.property_product_pricelist \ - and order.partner_id.property_product_pricelist.id or False Partner = order.partner_id.with_context(show_address=1).sudo() shippings = Partner.search( [("id", "child_of", order.partner_id.commercial_partner_id.ids), @@ -126,11 +76,106 @@ def portal_addresses(self, **post): values = { 'order': order, 'website_sale_order': order, - 'shippings': shippings, - 'billings': billings + 'delivery_addresses': shippings, + 'billing_addresses': billings } # Avoid useless rendering if called in ajax if post.get('xhr'): return 'ok' return request.render( "portal_addresses.addresses", values) + + @http.route( + '/portal/address/submit', type='http', methods=['POST'], auth='public', website=True, + sitemap=False + ) + def shop_address_submit( + self, partner_id=None, address_type='billing', use_delivery_as_billing=None, callback=None, + required_fields=None, **form_data + ): + """ Create or update an address. + + If it succeeds, it returns the URL to redirect (client-side) to. If it fails (missing or + invalid information), it highlights the problematic form input with the appropriate error + message. + + :param str partner_id: The partner whose address to update with the address form, if any. + :param str address_type: The type of the address: 'billing' or 'delivery'. + :param str use_delivery_as_billing: Whether the provided address should be used as both the + billing and the delivery address. 'true' or 'false'. + :param str callback: The URL to redirect to in case of successful address creation/update. + :param str required_fields: The additional required address values, as a comma-separated + list of `res.partner` fields. + :param dict form_data: The form data to process as address values. + :return: A JSON-encoded feedback, with either the success URL or an error message. + :rtype: str + """ + order_sudo = request.env['sale.order'].new({ + 'partner_id': request.env.user.partner_id.commercial_partner_id.id + }) + use_delivery_as_billing = False + partner_sudo, address_type = self._prepare_address_update( + order_sudo, partner_id=partner_id and int(partner_id), address_type=address_type + ) + # Parse form data into address values, and extract incompatible data as extra form data. + address_values, extra_form_data = self._parse_form_data(form_data) + + is_main_address = order_sudo.partner_id.id == partner_sudo.id + # Validate the address values and highlights the problems in the form, if any. + invalid_fields, missing_fields, error_messages = self._validate_address_values( + address_values, + partner_sudo, + address_type, + use_delivery_as_billing, + required_fields, + is_main_address=is_main_address, + **extra_form_data, + ) + if error_messages: + return json.dumps({ + 'invalid_fields': list(invalid_fields | missing_fields), + 'messages': error_messages, + }) + + is_new_address = False + if not partner_sudo: # Creation of a new address. + is_new_address = True + self._complete_address_values( + address_values, address_type, use_delivery_as_billing, order_sudo + ) + create_context = clean_context(request.env.context) + create_context.update({ + 'tracking_disable': True, + 'no_vat_validation': True, # Already verified in _validate_address_values + }) + partner_sudo = request.env['res.partner'].sudo().with_context( + create_context + ).create(address_values) + elif not self._are_same_addresses(address_values, partner_sudo): + partner_sudo.write(address_values) # Keep the same partner if nothing changed. + + partner_fnames = set() + if is_main_address: # Main address updated. + partner_fnames.add('partner_id') # Force the re-computation of partner-based fields. + + if address_type == 'billing': + partner_fnames.add('partner_invoice_id') + if is_new_address and order_sudo.only_services: + # The delivery address is required to make the order. + partner_fnames.add('partner_shipping_id') + callback = callback or self._get_extra_billing_info_route(order_sudo) + elif address_type == 'delivery': + partner_fnames.add('partner_shipping_id') + if use_delivery_as_billing: + partner_fnames.add('partner_invoice_id') + + if is_new_address or order_sudo.only_services: + callback = callback or '/shop/checkout?try_skip_step=true' + else: + callback = callback or '/shop/checkout' + + self._handle_extra_form_data(extra_form_data, address_values) + + return json.dumps({ + 'successUrl': callback, + }) diff --git a/portal_addresses/static/src/js/address.js b/portal_addresses/static/src/js/address.js new file mode 100644 index 000000000..ae4ca566c --- /dev/null +++ b/portal_addresses/static/src/js/address.js @@ -0,0 +1,65 @@ +/** @odoo-module **/ + +import publicWidget from "@web/legacy/js/public/public_widget"; + +const websiteSaleAddress = publicWidget.registry.websiteSaleAddress; + + +websiteSaleAddress.include({ + + // @override + _onSaveAddress: async function (ev) { + if (!this.addressForm.reportValidity()) { + return + } + + const submitButton = ev.currentTarget; + if (!ev.defaultPrevented && !submitButton.disabled) { + ev.preventDefault(); + if(ev.currentTarget.closest('form').action.includes('portal/address') ){ + submitButton.disabled = true; + const spinner = document.createElement('span'); + spinner.classList.add('fa', 'fa-cog', 'fa-spin'); + submitButton.appendChild(spinner); + + const result = await this.http.post( + '/portal/address/submit', + new FormData(this.addressForm), + ) + if (result.successUrl) { + window.location = '/portal/addresses'; + } else { + // Highlight missing/invalid form values + document.querySelectorAll('.is-invalid').forEach(element => { + if (!result.invalid_fields.includes(element.name)) { + element.classList.remove('is-invalid'); + } + }) + result.invalid_fields.forEach( + fieldName => this.addressForm[fieldName].classList.add('is-invalid') + ); + + // Display the error messages + // NOTE: setCustomValidity is not used as we would have to reset the error msg on + // input update, which is not worth catching for the rare cases where the + // server-side validation will catch validation issues (now that required inputs + // are also handled client-side) + const newErrors = result.messages.map(message => { + const errorHeader = document.createElement('h5'); + errorHeader.classList.add('text-danger'); + errorHeader.appendChild(document.createTextNode(message)); + return errorHeader; + }); + + this.errorsDiv.replaceChildren(...newErrors); + + // Re-enable button and remove spinner + submitButton.disabled = false; + spinner.remove(); + } + } else { + this._super(...arguments); + } + } + } +}) diff --git a/portal_addresses/views/templates.xml b/portal_addresses/views/templates.xml index a6228e71a..6066d4e65 100644 --- a/portal_addresses/views/templates.xml +++ b/portal_addresses/views/templates.xml @@ -1,13 +1,15 @@ @@ -18,16 +20,27 @@ -