diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index cffcc1acee9..b17a2cedb48 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -959,12 +959,11 @@ class WebsiteSale(payment_portal.PaymentPortal): # ------------------------------------------------------ def checkout_check_address(self, order): - billing_fields_required = self._get_mandatory_fields_billing(order.partner_id.country_id.id) - if not all(order.partner_id.read(billing_fields_required)[0].values()): - return request.redirect('/shop/address?partner_id=%d' % order.partner_id.id) + partner_invoice = order.partner_invoice_id + if not self._check_billing_partner_mandatory_fields(partner_invoice): + return request.redirect('/shop/address?partner_id=%d&is_invoice=1' % partner_invoice.id) - shipping_fields_required = self._get_mandatory_fields_shipping(order.partner_shipping_id.country_id.id) - if not all(order.partner_shipping_id.read(shipping_fields_required)[0].values()): + if not self._check_shipping_partner_mandatory_fields(order.partner_shipping_id): return request.redirect('/shop/address?partner_id=%d' % order.partner_shipping_id.id) def checkout_redirection(self, order): @@ -987,29 +986,49 @@ class WebsiteSale(payment_portal.PaymentPortal): def checkout_values(self, **kw): order = request.website.sale_get_order(force_create=True) - shippings = [] + bill_partners = [] + ship_partners = [] if order.partner_id != request.website.user_id.sudo().partner_id: Partner = order.partner_id.with_context(show_address=1).sudo() - shippings = Partner.search([ - ("id", "child_of", order.partner_id.commercial_partner_id.ids), - '|', ("type", "in", ["delivery", "other"]), ("id", "=", order.partner_id.commercial_partner_id.id) + commercial_partner = order.partner_id.commercial_partner_id + bill_partners = Partner.search([ + ("id", "child_of", commercial_partner.ids), + '|', ("type", "in", ["invoice", "other"]), ("id", "=", commercial_partner.id) ], order='id desc') - if shippings: - if kw.get('partner_id') or 'use_billing' in kw: - if 'use_billing' in kw: - partner_id = order.partner_id.id - else: - partner_id = int(kw.get('partner_id')) - if partner_id in shippings.mapped('id'): - order.partner_shipping_id = partner_id + ship_partners = Partner.search([ + ("id", "child_of", commercial_partner.ids), + '|', ("type", "in", ["delivery", "other"]), ("id", "=", commercial_partner.id) + ], order='id desc') + + # do not show commercial_partner_id if its mandatory fields are not complete to children + # as children can not edit (fill) the commercial_partner_id + if commercial_partner != order.partner_id: + if not self._check_billing_partner_mandatory_fields(commercial_partner): + bill_partners = bill_partners.filtered(lambda p: p.id != commercial_partner.id) + + if not self._check_shipping_partner_mandatory_fields(commercial_partner): + ship_partners = ship_partners.filtered(lambda p: p.id != commercial_partner.id) + + partner_id = int(kw.get('partner_id', 0)) + if partner_id: + if 'is_invoice' in kw and bill_partners and partner_id in bill_partners.mapped('id'): + order.partner_invoice_id = partner_id + elif ship_partners and partner_id in ship_partners.mapped('id'): + order.partner_shipping_id = partner_id values = { 'order': order, - 'shippings': shippings, + 'shippings': ship_partners, + 'billings': bill_partners, 'only_services': order and order.only_services or False } return values + def _check_billing_partner_mandatory_fields(self, partner_id): + ''' return True if all mandatory fields for billing address are complete ''' + billing_fields_required = self._get_mandatory_fields_billing(partner_id.country_id.id) + return all(partner_id.read(billing_fields_required)[0].values()) + def _get_mandatory_fields_billing(self, country_id=False): req = ["name", "email", "street", "city", "country_id"] if country_id: @@ -1020,6 +1039,11 @@ class WebsiteSale(payment_portal.PaymentPortal): req += ['zip'] return req + def _check_shipping_partner_mandatory_fields(self, partner_id): + ''' return True if all mandatory fields for shipping address are complete ''' + shipping_fields_required = self._get_mandatory_fields_shipping(partner_id.country_id.id) + return all(partner_id.read(shipping_fields_required)[0].values()) + def _get_mandatory_fields_shipping(self, country_id=False): req = ["name", "street", "city", "country_id"] if country_id: @@ -1037,13 +1061,16 @@ class WebsiteSale(payment_portal.PaymentPortal): error = dict() error_message = [] - if data.get('partner_id'): + if mode == ('edit', 'billing') and data.get('partner_id'): partner_su = request.env['res.partner'].sudo().browse(int(data['partner_id'])).exists() - name_change = partner_su and 'name' in data and data['name'] != partner_su.name - email_change = partner_su and 'email' in data and data['email'] != partner_su.email + name_change = partner_su and partner_su.name and 'name' in data and data['name'] != partner_su.name + email_change = partner_su and 'email' in data and data['email'] != partner_su.email and partner_su.email + invoices = request.env['account.move'].sudo().search( + [('partner_id', '=', partner_su.id)], limit=1 + ) if partner_su else False # Prevent changing the billing partner name if invoices have been issued. - if mode[1] == 'billing' and name_change and not partner_su.can_edit_vat(): + if name_change and not partner_su.can_edit_vat() and invoices: error['name'] = 'error' error_message.append(_( "Changing your name is not allowed once documents have been issued for your" @@ -1151,20 +1178,23 @@ class WebsiteSale(payment_portal.PaymentPortal): if request.website.specific_user_account: new_values['website_id'] = request.website.id + commercial_partner_id = order.partner_id.commercial_partner_id if mode[0] == 'new': + lang = request.lang.code if request.lang.code in request.website.mapped('language_ids.code') else None + if lang: + new_values['lang'] = lang new_values['company_id'] = request.website.company_id.id new_values['team_id'] = request.website.salesteam_id and request.website.salesteam_id.id new_values['user_id'] = request.website.salesperson_id.id - - lang = request.lang.code if request.lang.code in request.website.mapped('language_ids.code') else None - if lang: - new_values['lang'] = lang - if mode == ('edit', 'billing') and order.partner_id.type == 'contact': - new_values['type'] = 'other' - if mode[1] == 'shipping': - new_values['parent_id'] = order.partner_id.commercial_partner_id.id - new_values['type'] = 'delivery' - + # define type of the new partner + if mode[1] == 'billing': + new_values['type'] = 'other' if 'use_same' in values else 'invoice' + # for public user avoid linking to default archived 'Public user' partner + if commercial_partner_id.active: + new_values['parent_id'] = commercial_partner_id.id + elif mode[1] == 'shipping': + new_values['type'] = 'delivery' + new_values['parent_id'] = commercial_partner_id.id return new_values, errors, error_msg @http.route(['/shop/address'], type='http', methods=['GET', 'POST'], auth="public", website=True, sitemap=False) @@ -1176,35 +1206,36 @@ class WebsiteSale(payment_portal.PaymentPortal): if redirection: return redirection - mode = (False, False) can_edit_vat = False values, errors = {}, {} partner_id = int(kw.get('partner_id', -1)) - # IF PUBLIC ORDER - if order.partner_id.id == request.website.user_id.sudo().partner_id.id: + if order._is_public_order(): mode = ('new', 'billing') can_edit_vat = True - # IF ORDER LINKED TO A PARTNER - else: + else: # IF ORDER LINKED TO A PARTNER if partner_id > 0: - if partner_id == order.partner_id.id: - mode = ('edit', 'billing') - can_edit_vat = order.partner_id.can_edit_vat() + partners = Partner.search( + [('id', 'child_of', order.partner_id.commercial_partner_id.ids)] + ) + if 'is_invoice' in kw: + bill_partners_ids = partners.filtered(lambda p: p.type != 'delivery').ids + if partner_id in bill_partners_ids or order.partner_id.id == partner_id: + mode = ('edit', 'billing') + can_edit_vat = order.partner_id.can_edit_vat() + else: + return Forbidden() else: - shippings = Partner.search([('id', 'child_of', order.partner_id.commercial_partner_id.ids)]) - if order.partner_id.commercial_partner_id.id == partner_id: - mode = ('new', 'shipping') - partner_id = -1 - elif partner_id in shippings.mapped('id'): + ship_partners_ids = partners.filtered(lambda p: p.type != 'invoice').ids + if partner_id in ship_partners_ids: mode = ('edit', 'shipping') else: return Forbidden() if mode and partner_id != -1: values = Partner.browse(partner_id) elif partner_id == -1: - mode = ('new', 'shipping') + mode = ('new', 'billing') if 'is_invoice' in kw else ('new', 'shipping') else: # no mode - refresh without post? return request.redirect('/shop/checkout') @@ -1218,29 +1249,53 @@ class WebsiteSale(payment_portal.PaymentPortal): errors['error_message'] = error_msg values = kw else: + update_mode, address_mode = mode partner_id = self._checkout_form_save(mode, post, kw) # We need to validate _checkout_form_save return, because when partner_id not in shippings # it returns Forbidden() instead the partner_id if isinstance(partner_id, Forbidden): return partner_id + fpos_before = order.fiscal_position_id - if mode[1] == 'billing': - order.partner_id = partner_id - # This is the *only* thing that the front end user will see/edit anyway when choosing billing address - order.partner_invoice_id = partner_id - if not kw.get('use_same'): - kw['callback'] = kw.get('callback') or \ - (not order.only_services and (mode[0] == 'edit' and '/shop/checkout' or '/shop/address')) - # We need to update the pricelist(by the one selected by the customer), because onchange_partner reset it - # We only need to update the pricelist when it is not redirected to /confirm_order - if kw.get('callback', False) != '/shop/confirm_order': - request.website.sale_get_order(update_pricelist=True) - elif mode[1] == 'shipping': - order.partner_shipping_id = partner_id + update_values = {} + if update_mode == 'new': # New address + if order._is_public_order(): + update_values['partner_id'] = partner_id + + if address_mode == 'billing': + update_values['partner_invoice_id'] = partner_id + if kw.get('use_same'): + update_values['partner_shipping_id'] = partner_id + elif not kw.get('callback') and not order.only_services: + # Now that the billing is set, if shipping is necessary + # request the user to fill the shipping address + kw['callback'] = '/shop/address' + elif address_mode == 'shipping': + update_values['partner_shipping_id'] = partner_id + elif update_mode == 'edit': # Updating an existing address + if order.partner_id.id == partner_id: + # Editing the main partner of the SO --> also trigger a partner update to + # recompute fpos & any partner-related fields + update_values['partner_id'] = partner_id + + if address_mode == 'billing': + update_values['partner_invoice_id'] = partner_id + if not kw.get('callback') and not order.only_services: + kw['callback'] = '/shop/checkout' + elif address_mode == 'shipping': + update_values['partner_shipping_id'] = partner_id + + order.write(update_values) if order.fiscal_position_id != fpos_before: + # Recompute taxes on fpos change + # TODO recompute all prices too to correctly manage price_include taxes ? order._recompute_taxes() + if 'partner_id' in update_values: + # Force recomputation of pricelist on main customer address update + request.website.sale_get_order(update_pricelist=True) + # TDE FIXME: don't ever do this # -> TDE: you are the guy that did what we should never do in commit e6f038a order.message_partner_ids = [(4, partner_id), (3, request.website.partner_id.id)] @@ -1438,8 +1493,7 @@ class WebsiteSale(payment_portal.PaymentPortal): order = render_values['website_sale_order'] def_country_id = order.partner_id.country_id - # IF PUBLIC ORDER - if order.partner_id.id == request.website.user_id.sudo().partner_id.id: + if order._is_public_order(): if request.geoip.country_code: def_country_id = request.env['res.country'].search([('code', '=', request.geoip.country_code)], limit=1) else: @@ -1463,7 +1517,7 @@ class WebsiteSale(payment_portal.PaymentPortal): if redirection: return redirection - if order.partner_id.id == request.website.user_id.sudo().partner_id.id: + if order._is_public_order(): return request.redirect('/shop/address') redirection = self.checkout_check_address(order) diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index 471fa23f940..0292afdb2d0 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -622,3 +622,9 @@ class SaleOrder(models.Model): ]).available_carriers( self.partner_shipping_id ).filtered(_is_carrier_available) + + #=== TOOLING ===# + + def _is_public_order(self): + self.ensure_one() + return self.partner_id.id == request.website.user_id.sudo().partner_id.id diff --git a/addons/website_sale/models/website.py b/addons/website_sale/models/website.py index fb401b48712..61c524db31a 100644 --- a/addons/website_sale/models/website.py +++ b/addons/website_sale/models/website.py @@ -393,7 +393,6 @@ class Website(models.Model): # change the partner, and trigger the computes (fpos) sale_order_sudo.write({ 'partner_id': partner_sudo.id, - 'partner_invoice_id': partner_sudo.id, 'payment_term_id': self.sale_get_payment_term(partner_sudo), # Must be specified to ensure it is not recomputed when it shouldn't 'pricelist_id': pricelist_id, @@ -418,7 +417,7 @@ class Website(models.Model): def _prepare_sale_order_values(self, partner_sudo): self.ensure_one() - addr = partner_sudo.address_get(['delivery']) + addr = partner_sudo.address_get(['delivery', 'invoice']) if not request.website.is_public_user(): # FIXME VFE why not use last_website_so_id field ? last_sale_order = self.env['sale.order'].sudo().search( @@ -426,8 +425,11 @@ class Website(models.Model): limit=1, order="date_order desc, id desc", ) - if last_sale_order and last_sale_order.partner_shipping_id.active: # first = me - addr['delivery'] = last_sale_order.partner_shipping_id.id + if last_sale_order: + if last_sale_order.partner_shipping_id.active: # first = me + addr['delivery'] = last_sale_order.partner_shipping_id.id + if last_sale_order.partner_invoice_id.active: + addr['invoice'] = last_sale_order.partner_invoice_id.id affiliate_id = request.session.get('affiliate_id') salesperson_user_sudo = self.env['res.users'].sudo().browse(affiliate_id).exists() @@ -439,7 +441,7 @@ class Website(models.Model): 'fiscal_position_id': self.fiscal_position_id.id, 'partner_id': partner_sudo.id, - 'partner_invoice_id': partner_sudo.id, + 'partner_invoice_id': addr['invoice'], 'partner_shipping_id': addr['delivery'], 'pricelist_id': self.pricelist_id.id, diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js index 10ded89a840..84fb00cd83c 100644 --- a/addons/website_sale/static/src/js/website_sale.js +++ b/addons/website_sale/static/src/js/website_sale.js @@ -796,6 +796,7 @@ publicWidget.registry.WebsiteSaleLayout = publicWidget.Widget.extend({ publicWidget.registry.websiteSaleCart = publicWidget.Widget.extend({ selector: '.oe_website_sale .oe_cart', events: { + 'click .js_change_billing': '_onClickChangeBilling', 'click .js_change_shipping': '_onClickChangeShipping', 'click .js_edit_address': '_onClickEditAddress', 'click .js_delete_product': '_onClickDeleteProduct', @@ -805,19 +806,33 @@ publicWidget.registry.websiteSaleCart = publicWidget.Widget.extend({ // Handlers //-------------------------------------------------------------------------- + /** + * @private + * @param {Event} ev + */ + _onClickChangeBilling: function (ev) { + this._onClickChangeAddress(ev, 'all_billing', 'js_change_billing'); + }, /** * @private * @param {Event} ev */ _onClickChangeShipping: function (ev) { - var $old = $('.all_shipping').find('.card.border.border-primary'); - $old.find('.btn-ship').toggle(); - $old.addClass('js_change_shipping'); + this._onClickChangeAddress(ev, 'all_shipping', 'js_change_shipping'); + }, + /** + * @private + * @param {Event} ev + */ + _onClickChangeAddress: function (ev, rowAddrClass, cardClass) { + var $old = $(`.${rowAddrClass}`).find('.card.border.border-primary'); + $old.find('.btn-addr').toggle(); + $old.addClass(cardClass); $old.removeClass('border border-primary'); var $new = $(ev.currentTarget).parent('div.one_kanban').find('.card'); - $new.find('.btn-ship').toggle(); - $new.removeClass('js_change_shipping'); + $new.find('.btn-addr').toggle(); + $new.removeClass(cardClass); $new.addClass('border border-primary'); var $form = $(ev.currentTarget).parent('div.one_kanban').find('form.d-none'); diff --git a/addons/website_sale/static/src/scss/website_sale.scss b/addons/website_sale/static/src/scss/website_sale.scss index fadff7e17eb..a2a178f6a95 100644 --- a/addons/website_sale/static/src/scss/website_sale.scss +++ b/addons/website_sale/static/src/scss/website_sale.scss @@ -464,7 +464,7 @@ tr#empty { display: none; } -.js_change_shipping { +.js_change_shipping, .js_change_billing { cursor: pointer; } diff --git a/addons/website_sale/static/tests/tours/website_sale_complete_flow.js b/addons/website_sale/static/tests/tours/website_sale_complete_flow.js index a2f158f036a..42cd6347d20 100644 --- a/addons/website_sale/static/tests/tours/website_sale_complete_flow.js +++ b/addons/website_sale/static/tests/tours/website_sale_complete_flow.js @@ -239,7 +239,7 @@ }, { content: "Add new shipping address", - trigger: '.one_kanban form[action^="/shop/address"] .btn', + trigger: '.all_shipping .one_kanban form[action^="/shop/address"] .btn', }, { content: "Fulfill shipping address form", diff --git a/addons/website_sale/tests/test_sale_process.py b/addons/website_sale/tests/test_sale_process.py index a718e4b29cd..212dfd0dbda 100644 --- a/addons/website_sale/tests/test_sale_process.py +++ b/addons/website_sale/tests/test_sale_process.py @@ -146,6 +146,7 @@ class TestWebsiteSaleCheckoutAddress(TransactionCaseWithUserDemo, HttpCaseWithUs 'name': 'a res.partner address', 'email': 'email@email.email', 'street': 'ooo', 'city': 'ooo', 'zip': '1200', 'country_id': self.country_id, 'submitted': 1, } + self.default_billing_address_values = self.default_address_values | {'is_invoice': 1} def _create_so(self, partner_id=None, company_id=None): values = { @@ -165,7 +166,7 @@ class TestWebsiteSaleCheckoutAddress(TransactionCaseWithUserDemo, HttpCaseWithUs return self.env['sale.order'].create(values) def _get_last_address(self, partner): - ''' Useful to retrieve the last created shipping address ''' + """ Useful to retrieve the last created address (shipping or billing) """ return partner.child_ids.sorted('id', reverse=True)[0] # TEST WEBSITE @@ -233,7 +234,7 @@ class TestWebsiteSaleCheckoutAddress(TransactionCaseWithUserDemo, HttpCaseWithUs # 2. Logged in user/internal user, should not edit name or email address of billing self.default_address_values['partner_id'] = self.demo_partner.id - self.WebsiteSaleController.address(**self.default_address_values) + self.WebsiteSaleController.address(**self.default_billing_address_values) self.assertEqual(self.demo_partner.company_id, self.company_c, "Logged in user edited billing (the partner itself) should not get its company modified.") self.assertNotEqual(self.demo_partner.name, self.default_address_values['name'], "Employee cannot change their name during the checkout process.") self.assertNotEqual(self.demo_partner.email, self.default_address_values['email'], "Employee cannot change their email during the checkout process.") @@ -257,7 +258,7 @@ class TestWebsiteSaleCheckoutAddress(TransactionCaseWithUserDemo, HttpCaseWithUs # 2. Public user, edit billing self.default_address_values['partner_id'] = new_partner.id - self.WebsiteSaleController.address(**self.default_address_values) + self.WebsiteSaleController.address(**self.default_billing_address_values) self.assertEqual(new_partner.company_id, self.website.company_id, "Public user edited billing (the partner itself) should not get its company modified.") def test_04_apply_empty_pl(self): @@ -353,7 +354,7 @@ class TestWebsiteSaleCheckoutAddress(TransactionCaseWithUserDemo, HttpCaseWithUs # 2. Portal user, edit billing self.default_address_values['partner_id'] = self.portal_partner.id - self.WebsiteSaleController.address(**self.default_address_values) + self.WebsiteSaleController.address(**self.default_billing_address_values) # Name cannot be changed if there are issued invoices self.assertNotEqual(self.portal_partner.name, self.default_address_values['name'], "Portal User should not be able to change the name if they have invoices under their name.") @@ -383,7 +384,7 @@ class TestWebsiteSaleCheckoutAddress(TransactionCaseWithUserDemo, HttpCaseWithUs {'name': 'Tax 20% excl', 'amount': 20, 'price_include': False}, {'name': 'Tax 15% incl', 'amount': 15, 'price_include': True}, ]) - self.env['account.fiscal.position'].create([ + fpos_be, fpos_nl = self.env['account.fiscal.position'].create([ { 'sequence': 1, 'name': 'BE', @@ -426,13 +427,64 @@ class TestWebsiteSaleCheckoutAddress(TransactionCaseWithUserDemo, HttpCaseWithUs req.httprequest.method = "POST" self.WebsiteSaleController.address(**be_address_POST) + self.assertEqual( + so.fiscal_position_id, + fpos_be, + ) self.assertEqual( [so.amount_untaxed, so.amount_tax, so.amount_total], [90.91, 18.18, 109.09] # (100 : (1 + 10%)) * (1 + 20%) = 109.09 ) self.WebsiteSaleController.address(**nl_address_POST) + self.assertEqual( + so.fiscal_position_id, + fpos_nl, + ) self.assertEqual( [so.amount_untaxed, so.amount_tax, so.amount_total], [90.91, 13.64, 104.55] # (100 : (1 + 10%)) * (1 + 15%) = 104.55 ) + + def test_08_new_user_address_state(self): + ''' Test the billing and shipping addresses creation values. ''' + self._setUp_multicompany_env() + so = self._create_so(self.demo_partner.id) + + env = api.Environment(self.env.cr, self.demo_user.id, {}) + # change also website env for `sale_get_order` to not change order partner_id + with MockRequest(env, website=self.website.with_env(env), sale_order_id=so.id) as req: + req.httprequest.method = "POST" + + # check the default values + self.assertEqual(self.demo_partner, so.partner_invoice_id) + self.assertEqual(self.demo_partner, so.partner_shipping_id) + + # 1. Logged-in user, new shipping + self.WebsiteSaleController.address(**self.default_address_values) + new_shipping = self._get_last_address(self.demo_partner) + msg = "New shipping address should have its type set as 'delivery'" + self.assertTrue(new_shipping.type == 'delivery', msg) + + # 2. Logged-in user, new billing + self.WebsiteSaleController.address(**self.default_billing_address_values) + new_billing = self._get_last_address(self.demo_partner) + msg = "New billing should have its type set as 'invoice'" + self.assertTrue(new_billing.type == 'invoice', msg) + self.assertNotEqual(new_billing, self.demo_partner) + + # 3. Check that invoice/shipping address of so changed + self.assertEqual(new_billing, so.partner_invoice_id) + self.assertEqual(new_shipping, so.partner_shipping_id) + + # 4. Logged-in user, new billing use same + use_same = self.default_billing_address_values | {'use_same': 1} + self.WebsiteSaleController.address(**use_same) + new_billing_use_same = self._get_last_address(self.demo_partner) + msg = "New billing use same should have its type set as 'other'" + self.assertTrue(new_billing_use_same.type == 'other', msg) + self.assertNotEqual(new_billing, self.demo_partner) + + # 5. Check that invoice/shipping address of so changed + self.assertEqual(new_billing_use_same, so.partner_invoice_id) + self.assertEqual(new_billing_use_same, so.partner_shipping_id) diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml index 2b35acc5cec..44d6c1cfb3e 100644 --- a/addons/website_sale/views/templates.xml +++ b/addons/website_sale/views/templates.xml @@ -1735,7 +1735,7 @@
- +
@@ -1752,47 +1752,23 @@

Billing Address

-
- - - - - - -
+ + + + +

Shipping Address

-
-
-
- - -
- - - - - - - -
-
-
-
-
+ + + + + - + -