From 74c5c00affd9785e7c9bfe011d4583849362f4dc Mon Sep 17 00:00:00 2001 From: Mitali Patel Date: Thu, 9 May 2019 09:37:26 +0000 Subject: [PATCH] [IMP] sale, website_sale : improve emails sent on confirmation and payment Purpose of this commit is to send the order and payment details email to user when they purchase any order from ecommerce or when do the payment from portal. Currently it was sending quotation mail when the payment is processed. Now it send an email for quotation, and one email for sale confirmation that may contain more detailed content if coming from website (eCommerce). In sale * create new confirmation template and set the default template in sales settings; * when payment enabled in sales settings then display template option to send default confirmation mail when payment is processed or order signed; * add tool method to find the right template to use when sending the quotation / SO by email; * ensure email is sent everytime SO is confirmed; In website_sale * add option in website setting in order, to set the confirmation mail template; * ensure email is sent everytime SO is confirmed; Related to task ID 1873634 Linked to PR #28781 --- addons/sale/__manifest__.py | 2 +- addons/sale/controllers/portal.py | 1 + addons/sale/data/mail_data.xml | 186 ++++++++++++++++-- addons/sale/data/sale_data.xml | 6 + addons/sale/models/payment.py | 16 +- addons/sale/models/res_config_settings.py | 4 + addons/sale/models/sale.py | 44 ++--- addons/sale/tests/test_sale_order.py | 4 +- addons/sale/tests/test_sale_signature.py | 4 +- .../sale/views/res_config_settings_views.xml | 12 ++ .../views/res_config_settings_views.xml | 2 +- .../website_sale/data/mail_template_data.xml | 2 +- .../src/js/website_sale_tour_shop_mail.js | 4 +- .../views/res_config_settings_views.xml | 12 ++ 14 files changed, 245 insertions(+), 54 deletions(-) diff --git a/addons/sale/__manifest__.py b/addons/sale/__manifest__.py index d16eb17bf8c..7de008c2fce 100644 --- a/addons/sale/__manifest__.py +++ b/addons/sale/__manifest__.py @@ -19,8 +19,8 @@ This module contains all the common features of Sales Management and eCommerce. 'report/invoice_report_templates.xml', 'report/report_all_channels_sales_views.xml', 'data/ir_sequence_data.xml', - 'data/sale_data.xml', 'data/mail_data.xml', + 'data/sale_data.xml', 'wizard/sale_make_invoice_advance_views.xml', 'views/sale_views.xml', 'views/sales_team_views.xml', diff --git a/addons/sale/controllers/portal.py b/addons/sale/controllers/portal.py index 5995879a2dc..03566515d24 100644 --- a/addons/sale/controllers/portal.py +++ b/addons/sale/controllers/portal.py @@ -220,6 +220,7 @@ class CustomerPortal(CustomerPortal): if not order_sudo.has_to_be_paid(): order_sudo.action_confirm() + order_sudo._send_order_confirmation_mail() pdf = request.env.ref('sale.action_report_saleorder').sudo().render_qweb_pdf([order_sudo.id])[0] diff --git a/addons/sale/data/mail_data.xml b/addons/sale/data/mail_data.xml index 40cabf7a910..34dd1065c21 100644 --- a/addons/sale/data/mail_data.xml +++ b/addons/sale/data/mail_data.xml @@ -60,11 +60,14 @@ - Sign and pay online - Accept and sign online + Sign & Pay Quotation + Accept & Sign Quotation - Pay online + Accept & Pay Quotation + + + View Quotation @@ -80,25 +83,170 @@

% set doc_name = 'quotation' if object.state in ('draft', 'sent') else 'order' - % if object.partner_id.parent_id: - Dear ${object.partner_id.name} (${object.partner_id.parent_id.name}), + Hello, +

+ Your + % if ctx.get('proforma'): + Pro forma invoice for ${doc_name} ${object.name} + % if object.origin: + (with reference: ${object.origin} ) + % endif + amounting in ${format_amount(object.amount_total, object.pricelist_id.currency_id)} is available. % else: - Dear ${object.partner_id.name}, + ${doc_name} ${object.name} + % if object.origin: + (with reference: ${object.origin} ) + % endif + amounting in ${format_amount(object.amount_total, object.pricelist_id.currency_id)} is ready for review. % endif -

- Here is - % if ctx.get('proforma') - in attachment your pro-forma invoice - % else - the ${doc_name} ${object.name} - % endif - % if object.origin: - (with reference: ${object.origin} ) - % endif - amounting in ${format_amount(object.amount_total, object.pricelist_id.currency_id)} - from ${object.company_id.name}. -

+

Do not hesitate to contact us if you have any question. +
+

+
+ + + ${(object.name or '').replace('/','_')} + ${object.partner_id.lang} + + + + + + Sales Order: Confirmation Email + + ${object.company_id.name} ${(object.get_portal_last_transaction().state == 'pending') and 'Pending Order' or 'Order'} (Ref ${object.name or 'n/a' }) + ${(object.user_id.email_formatted or user.email_formatted) | safe} + ${object.partner_id.id} + +
+

+ Hello, +

+ % set transaction = object.get_portal_last_transaction() + Your order ${object.name} amounting in ${format_amount(object.amount_total, object.currency_id)} + % if object.state == 'sale' or (transaction and transaction.state in ('done', 'authorized')) : + has been confirmed.
+ Thank you for your trust! + % elif transaction and transaction.state == 'pending' : + is pending. It will be confirmed when the payment is received. + % if object.reference: + Your payment reference is ${object.reference}. + % endif + % endif +

+ Do not hesitate to contact us if you have any question. +

+ % if object.website_id: + + + + + + + + % for line in object.order_line: + % if not line.is_delivery: + + % if line.display_type == 'line_section': + + % elif line.display_type == 'line_note': + + % else: + + + + % if object.user_id.has_group('account.group_show_line_subtotals_tax_excluded'): + + % endif + % if object.user_id.has_group('account.group_show_line_subtotals_tax_included'): + + % endif + % endif + + % endif + % endfor +
ProductsQuantityPrice
${line.name}${line.name} + Product image + ${line.product_id.name}${line.product_uom_qty}${format_amount(line.price_reduce_taxexcl, object.currency_id)}${format_amount(line.price_reduce_taxinc, object.currency_id)}
+ + % if object.carrier_id: + + + + + + + + + % else: + + + + + % endif + + + + + + + + +
Delivery:${format_amount(object.amount_delivery, object.currency_id)}
SubTotal:${format_amount(object.amount_untaxed, object.currency_id)}
SubTotal:${format_amount(object.amount_untaxed, object.currency_id)}
Taxes:${format_amount(object.amount_tax, object.currency_id)}
Total:${format_amount(object.amount_total, object.currency_id)}
+
+ + % if object.partner_invoice_id: + + + + + + + % endif + % if object.partner_shipping_id and not object.only_services: + + + + % if object.carrier_id: + + + + % endif + % endif +
+ Bill to: + ${object.partner_invoice_id.street or ''} + ${object.partner_invoice_id.city or ''} + ${object.partner_invoice_id.state_id.name or ''} + ${object.partner_invoice_id.zip or ''} + ${object.partner_invoice_id.country_id.name or ''} +
+ Payment Method: + % if transaction.payment_token_id: + ${transaction.payment_token_id.name} + % else: + ${transaction.acquirer_id.name} + % endif + (${format_amount(transaction.amount, object.currency_id)}) +
+
+ Ship to: + ${object.partner_shipping_id.street or ''} + ${object.partner_shipping_id.city or ''} + ${object.partner_shipping_id.state_id.name or ''} + ${object.partner_shipping_id.zip or ''} + ${object.partner_shipping_id.country_id.name or ''} +
+ Shipping Method: + ${object.carrier_id.name} + % if object.carrier_id.fixed_price == 0.0: + (Free) + % else: + (${format_amount(object.carrier_id.fixed_price, object.currency_id)}) + % endif +
+ % endif

diff --git a/addons/sale/data/sale_data.xml b/addons/sale/data/sale_data.xml index 8abe2f432d8..c326950d0e5 100644 --- a/addons/sale/data/sale_data.xml +++ b/addons/sale/data/sale_data.xml @@ -9,5 +9,11 @@ code action = records.action_share()
+ + + + sale.default_confirmation_template + + diff --git a/addons/sale/models/payment.py b/addons/sale/models/payment.py index 717e6087620..ae73e9ba0e7 100644 --- a/addons/sale/models/payment.py +++ b/addons/sale/models/payment.py @@ -61,25 +61,28 @@ class PaymentTransaction(models.Model): super(PaymentTransaction, self)._set_transaction_pending() for record in self: - sales_orders = record.sale_order_ids.filtered(lambda so: so.state == 'draft') - sales_orders.force_quotation_send() + sales_orders = record.sale_order_ids.filtered(lambda so: so.state in ['draft', 'sent']) + sales_orders.filtered(lambda so: so.state == 'draft').with_context(tracking_disable=True).write({'state': 'sent'}) if record.acquirer_id.provider == 'transfer': for so in record.sale_order_ids: so.reference = record._compute_sale_order_reference(so) + # send order confirmation mail + sales_orders._send_order_confirmation_mail() @api.multi def _set_transaction_authorized(self): # Override of '_set_transaction_authorized' in the 'payment' module # to confirm the quotations automatically. super(PaymentTransaction, self)._set_transaction_authorized() - sales_orders = self.mapped('sale_order_ids').filtered(lambda so: so.state == 'draft') - sales_orders.force_quotation_send() - sales_orders = self.mapped('sale_order_ids').filtered(lambda so: so.state == 'sent') + sales_orders = self.mapped('sale_order_ids').filtered(lambda so: so.state in ['draft', 'sent']) for so in sales_orders: # For loop because some override of action_confirm are ensure_one. so.action_confirm() + # send order confirmation mail + sales_orders._send_order_confirmation_mail() + @api.multi def _reconcile_after_transaction_done(self): # Override of '_set_transaction_done' in the 'payment' module @@ -88,7 +91,8 @@ class PaymentTransaction(models.Model): for so in sales_orders: # For loop because some override of action_confirm are ensure_one. so.action_confirm() - so.force_quotation_send() + # send order confirmation mail + sales_orders._send_order_confirmation_mail() # invoice the sale orders if needed self._invoice_sale_orders() res = super(PaymentTransaction, self)._reconcile_after_transaction_done() diff --git a/addons/sale/models/res_config_settings.py b/addons/sale/models/res_config_settings.py index 43cdf1d8c15..7cebb89f4b6 100644 --- a/addons/sale/models/res_config_settings.py +++ b/addons/sale/models/res_config_settings.py @@ -71,6 +71,10 @@ class ResConfigSettings(models.TransientModel): domain="[('model', '=', 'account.invoice')]", config_parameter='sale.default_email_template', default=lambda self: self.env.ref('account.email_template_edi_invoice', False)) + confirmation_template_id = fields.Many2one('mail.template', string='Confirmation Email', + domain="[('model', '=', 'sale.order')]", + config_parameter='sale.default_confirmation_template', + help="Email sent to the customer once the order is paid.") def set_values(self): super(ResConfigSettings, self).set_values() diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index 1cb36b31da0..219463e3189 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -600,21 +600,24 @@ class SaleOrder(models.Model): def action_cancel(self): return self.write({'state': 'cancel'}) + def _find_mail_template(self, force_confirmation_template=False): + template_id = False + + if force_confirmation_template or (self.state == 'sale' and not self.env.context.get('proforma', False)): + template_id = int(self.env['ir.config_parameter'].sudo().get_param('sale.default_confirmation_template')) + template_id = self.env['mail.template'].search([('id', '=', template_id)]).id + if not template_id: + template_id = self.env['ir.model.data'].xmlid_to_res_id('sale.mail_template_sale_confirmation', raise_if_not_found=False) + if not template_id: + template_id = self.env['ir.model.data'].xmlid_to_res_id('sale.email_template_edi_sale', raise_if_not_found=False) + + return template_id + @api.multi def action_quotation_send(self): - ''' - This function opens a window to compose an email, with the edi sale template message loaded by default - ''' + ''' Opens a wizard to compose an email, with relevant mail template loaded by default ''' self.ensure_one() - ir_model_data = self.env['ir.model.data'] - try: - template_id = ir_model_data.get_object_reference('sale', 'email_template_edi_sale')[1] - except ValueError: - template_id = False - try: - compose_form_id = ir_model_data.get_object_reference('mail', 'email_compose_message_wizard_form')[1] - except ValueError: - compose_form_id = False + template_id = self._find_mail_template() ctx = { 'default_model': 'sale.order', 'default_res_id': self.ids[0], @@ -632,8 +635,8 @@ class SaleOrder(models.Model): 'view_type': 'form', 'view_mode': 'form', 'res_model': 'mail.compose.message', - 'views': [(compose_form_id, 'form')], - 'view_id': compose_form_id, + 'views': [(False, 'form')], + 'view_id': False, 'target': 'new', 'context': ctx, } @@ -647,14 +650,11 @@ class SaleOrder(models.Model): return super(SaleOrder, self.with_context(mail_post_autofollow=True)).message_post(**kwargs) @api.multi - def force_quotation_send(self): - for order in self: - email_act = order.action_quotation_send() - if email_act and email_act.get('context'): - email_ctx = email_act['context'] - email_ctx.update(default_email_from=order.company_id.email) - order.with_context(**email_ctx).message_post_with_template(email_ctx.get('default_template_id')) - return True + def _send_order_confirmation_mail(self): + template_id = self._find_mail_template(force_confirmation_template=True) + if template_id: + for order in self: + order.with_context(force_send=True).message_post_with_template(template_id, composition_mode='comment', notif_layout="mail.mail_notification_paynow") @api.multi def action_done(self): diff --git a/addons/sale/tests/test_sale_order.py b/addons/sale/tests/test_sale_order.py index cd115e5bbab..9968ffb6bf4 100644 --- a/addons/sale/tests/test_sale_order.py +++ b/addons/sale/tests/test_sale_order.py @@ -86,7 +86,9 @@ class TestSaleOrder(TestCommonSaleNoChart): self.sale_order.order_line._compute_product_updatable() self.assertTrue(self.sale_order.order_line[0].product_updatable) # send quotation - self.sale_order.force_quotation_send() + email_act = self.sale_order.action_quotation_send() + email_ctx = email_act.get('context', {}) + self.sale_order.with_context(**email_ctx).message_post_with_template(email_ctx.get('default_template_id')) self.assertTrue(self.sale_order.state == 'sent', 'Sale: state after sending is wrong') self.sale_order.order_line._compute_product_updatable() self.assertTrue(self.sale_order.order_line[0].product_updatable) diff --git a/addons/sale/tests/test_sale_signature.py b/addons/sale/tests/test_sale_signature.py index 3daa3e5a6ad..076104cbf53 100644 --- a/addons/sale/tests/test_sale_signature.py +++ b/addons/sale/tests/test_sale_signature.py @@ -22,6 +22,8 @@ class TestSaleSignature(odoo.tests.HttpCase): }) # must be sent to the user so he can see it - sales_order.force_quotation_send() + email_act = sales_order.action_quotation_send() + email_ctx = email_act.get('context', {}) + sales_order.with_context(**email_ctx).message_post_with_template(email_ctx.get('default_template_id')) self.start_tour("/", 'sale_signature', login="portal") diff --git a/addons/sale/views/res_config_settings_views.xml b/addons/sale/views/res_config_settings_views.xml index 86abec7541c..6ae4798b98f 100644 --- a/addons/sale/views/res_config_settings_views.xml +++ b/addons/sale/views/res_config_settings_views.xml @@ -177,6 +177,18 @@ +
+
+ Confirmation Email +
+ Automatic email sent after the customer has signed or paid online +
+
+
+
+
diff --git a/addons/sale_management/views/res_config_settings_views.xml b/addons/sale_management/views/res_config_settings_views.xml index 07cbfbf1f07..1b9cc3f8b00 100644 --- a/addons/sale_management/views/res_config_settings_views.xml +++ b/addons/sale_management/views/res_config_settings_views.xml @@ -6,7 +6,7 @@ res.config.settings - +
diff --git a/addons/website_sale/data/mail_template_data.xml b/addons/website_sale/data/mail_template_data.xml index e342812712f..18a1bb70a51 100644 --- a/addons/website_sale/data/mail_template_data.xml +++ b/addons/website_sale/data/mail_template_data.xml @@ -2,7 +2,7 @@ - Sale Order: Cart Recovery Email + Sales Order: Cart Recovery Email You left items in your cart! ${(object.user_id.email_formatted or user.email_formatted or '') | safe} diff --git a/addons/website_sale/static/src/js/website_sale_tour_shop_mail.js b/addons/website_sale/static/src/js/website_sale_tour_shop_mail.js index 445513924bb..d5a998500ed 100644 --- a/addons/website_sale/static/src/js/website_sale_tour_shop_mail.js +++ b/addons/website_sale/static/src/js/website_sale_tour_shop_mail.js @@ -87,7 +87,7 @@ tour.register('shop_mail', { }, { content: "wait mail to be sent, and go see it", - trigger: '.o_thread_message_content:contains("Here is"):contains("the order")', + trigger: '.o_thread_message_content:contains("Your"):contains("order")', run: function () { window.location.href = "/web#action=mail.action_view_mail_mail&view_type=list"; }, @@ -98,7 +98,7 @@ tour.register('shop_mail', { }, { content: "check it's the correct email, and the URL is correct too", - trigger: 'div.oe_form_field_html[name="body_html"] p:contains("Here is"):contains("the order")', + trigger: 'div.oe_form_field_html[name="body_html"] p:contains("Your"):contains("order")', extra_trigger: 'div.oe_form_field_html[name="body_html"] a[href^="http://my-test-domain.com"]', }, ]); diff --git a/addons/website_sale/views/res_config_settings_views.xml b/addons/website_sale/views/res_config_settings_views.xml index db46110900e..51da1a1df51 100644 --- a/addons/website_sale/views/res_config_settings_views.xml +++ b/addons/website_sale/views/res_config_settings_views.xml @@ -308,6 +308,18 @@
+
+
+ Confirmation Email +
+ Email sent to the customer after the checkout +
+
+
+
+