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