[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
This commit is contained in:
Mitali Patel
2019-05-09 14:58:21 +00:00
committed by Thibault Delavallée
parent b55b4a42a6
commit 74c5c00aff
14 changed files with 245 additions and 54 deletions
+1 -1
View File
@@ -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',
+1
View File
@@ -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]
+167 -19
View File
@@ -60,11 +60,14 @@
<xpath expr="//t[@t-set='access_name']" position="after">
<t t-if="record._name == 'sale.order'">
<t t-if="record.has_to_be_signed(include_draft=True)">
<t t-if="record.has_to_be_paid()" t-set="access_name">Sign and pay online</t>
<t t-else="" t-set="access_name">Accept and sign online</t>
<t t-if="record.has_to_be_paid()" t-set="access_name">Sign &amp; Pay Quotation</t>
<t t-else="" t-set="access_name">Accept &amp; Sign Quotation</t>
</t>
<t t-elif="record.has_to_be_paid(include_draft=True)">
<t t-set="access_name">Pay online</t>
<t t-set="access_name">Accept &amp; Pay Quotation</t>
</t>
<t t-elif="record.state in ('draft', 'sent')">
<t t-set="access_name">View Quotation</t>
</t>
</t>
</xpath>
@@ -80,25 +83,170 @@
<div style="margin: 0px; padding: 0px;">
<p style="margin: 0px; padding: 0px; font-size: 13px;">
% 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,
<br/><br/>
Your
% if ctx.get('proforma'):
Pro forma invoice for ${doc_name} <strong>${object.name}</strong>
% if object.origin:
(with reference: ${object.origin} )
% endif
amounting in <strong>${format_amount(object.amount_total, object.pricelist_id.currency_id)}</strong> is available.
% else:
Dear ${object.partner_id.name},
${doc_name} <strong>${object.name}</strong>
% if object.origin:
(with reference: ${object.origin} )
% endif
amounting in <strong>${format_amount(object.amount_total, object.pricelist_id.currency_id)}</strong> is ready for review.
% endif
<br /><br />
Here is
% if ctx.get('proforma')
in attachment your pro-forma invoice
% else
the ${doc_name} <strong>${object.name}</strong>
% endif
% if object.origin:
(with reference: ${object.origin} )
% endif
amounting in <strong>${format_amount(object.amount_total, object.pricelist_id.currency_id)}</strong>
from ${object.company_id.name}.
<br /><br />
<br/><br/>
Do not hesitate to contact us if you have any question.
<br/>
</p>
</div>
</field>
<field name="report_template" ref="action_report_saleorder"/>
<field name="report_name">${(object.name or '').replace('/','_')}</field>
<field name="lang">${object.partner_id.lang}</field>
<field name="user_signature" eval="False"/>
<field name="auto_delete" eval="True"/>
</record>
<record id="mail_template_sale_confirmation" model="mail.template">
<field name="name">Sales Order: Confirmation Email</field>
<field name="model_id" ref="sale.model_sale_order"/>
<field name="subject">${object.company_id.name} ${(object.get_portal_last_transaction().state == 'pending') and 'Pending Order' or 'Order'} (Ref ${object.name or 'n/a' })</field>
<field name="email_from">${(object.user_id.email_formatted or user.email_formatted) | safe}</field>
<field name="partner_to">${object.partner_id.id}</field>
<field name="body_html" type="html">
<div style="margin: 0px; padding: 0px;">
<p style="margin: 0px; padding: 0px; font-size: 12px;">
Hello,
<br/><br/>
% set transaction = object.get_portal_last_transaction()
Your order <strong>${object.name}</strong> amounting in <strong>${format_amount(object.amount_total, object.currency_id)}</strong>
% if object.state == 'sale' or (transaction and transaction.state in ('done', 'authorized')) :
has been confirmed.<br/>
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 <strong>${object.reference}</strong>.
% endif
% endif
<br/><br/>
Do not hesitate to contact us if you have any question.
<br/><br/>
% if object.website_id:
<table width="100%" style="color: #454748; font-size: 12px; border-collapse: collapse;">
<tr style="border-bottom: 2px solid #dee2e6;">
<td width="18%"><strong>Products</strong></td>
<td></td>
<td><strong>Quantity</strong></td>
<td width="10%" align="center"><strong>Price</strong></td>
</tr>
% for line in object.order_line:
% if not line.is_delivery:
<tr style="${loop.cycle('background-color: #f2f2f2', 'background-color: #ffffff')}">
% if line.display_type == 'line_section':
<td colspan="4"><strong>${line.name}</strong></td>
% elif line.display_type == 'line_note':
<td colspan="4"><i>${line.name}</i></td>
% else:
<td style="width: 150px;">
<img src="/web/image/product.product/${line.product_id.id}/image_small" width="64" alt="Product image"></img>
</td>
<td align="left">${line.product_id.name}</td>
<td>${line.product_uom_qty}</td>
% if object.user_id.has_group('account.group_show_line_subtotals_tax_excluded'):
<td align="right">${format_amount(line.price_reduce_taxexcl, object.currency_id)}</td>
% endif
% if object.user_id.has_group('account.group_show_line_subtotals_tax_included'):
<td align="right">${format_amount(line.price_reduce_taxinc, object.currency_id)}</td>
% endif
% endif
</tr>
% endif
% endfor
</table>
<table width="40%" style="color: #454748; font-size: 12px; border-spacing: 0px 4px;" align="right">
% if object.carrier_id:
<tr>
<td style="border-top: 1px solid #dee2e6;" align="right"><strong>Delivery:</strong></td>
<td style="border-top: 1px solid #dee2e6;" align="right">${format_amount(object.amount_delivery, object.currency_id)}</td>
</tr>
<tr>
<td width="30%" align="right"><strong>SubTotal:</strong></td>
<td align="right">${format_amount(object.amount_untaxed, object.currency_id)}</td>
</tr>
% else:
<tr>
<td style="border-top: 1px solid #dee2e6;" width="30%" align="right"><strong>SubTotal:</strong></td>
<td style="border-top: 1px solid #dee2e6;" align="right">${format_amount(object.amount_untaxed, object.currency_id)}</td>
</tr>
% endif
<tr>
<td align="right"><strong>Taxes:</strong></td>
<td align="right">${format_amount(object.amount_tax, object.currency_id)}</td>
</tr>
<tr>
<td style="border-top: 1px solid #dee2e6;" align="right"><strong>Total:</strong></td>
<td style="border-top: 1px solid #dee2e6;" align="right">${format_amount(object.amount_total, object.currency_id)}</td>
</tr>
</table>
<br/>
<table width="100%" style="color: #454748; font-size: 12px;">
% if object.partner_invoice_id:
<tr>
<td style="padding-top: 10px;">
<strong>Bill to:</strong>
${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 ''}
</td>
</tr>
<tr>
<td>
<strong>Payment Method:</strong>
% if transaction.payment_token_id:
${transaction.payment_token_id.name}
% else:
${transaction.acquirer_id.name}
% endif
(${format_amount(transaction.amount, object.currency_id)})
</td>
</tr>
% endif
% if object.partner_shipping_id and not object.only_services:
<tr>
<td>
<br/>
<strong>Ship to:</strong>
${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 ''}
</td>
</tr>
% if object.carrier_id:
<tr>
<td>
<strong>Shipping Method:</strong>
${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
</td>
</tr>
% endif
% endif
</table>
% endif
</p>
</div>
</field>
+6
View File
@@ -9,5 +9,11 @@
<field name="state">code</field>
<field name="code">action = records.action_share()</field>
</record>
<!-- set default order confirmation template -->
<record id="default_confirmation_template" model="ir.config_parameter">
<field name="key">sale.default_confirmation_template</field>
<field name="value" ref="sale.mail_template_sale_confirmation"/>
</record>
</data>
</odoo>
+10 -6
View File
@@ -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()
@@ -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()
+22 -22
View File
@@ -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):
+3 -1
View File
@@ -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)
+3 -1
View File
@@ -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")
@@ -177,6 +177,18 @@
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box" id="confirmation_email_setting" attrs="{'invisible': [('portal_confirmation_pay', '=', False) , ('portal_confirmation_sign', '=', False)]}">
<div class="o_setting_right_pane">
<span class="o_form_label">Confirmation Email</span>
<div class="text-muted">
Automatic email sent after the customer has signed or paid online
</div>
<div class="row mt16">
<label for="confirmation_template_id" string="Email Template" class="col-lg-4 o_light_label"/>
<field name="confirmation_template_id" class="oe_inline"/>
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="group_sale_delivery_address"/>
@@ -6,7 +6,7 @@
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="sale.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@id='sale_config_online_confirmation_pay']" position="after">
<xpath expr="//div[@id='confirmation_email_setting']" position="after">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="group_sale_order_template"/>
@@ -2,7 +2,7 @@
<odoo>
<data noupdate="1">
<record id="mail_template_sale_cart_recovery" model="mail.template">
<field name="name">Sale Order: Cart Recovery Email</field>
<field name="name">Sales Order: Cart Recovery Email</field>
<field name="model_id" ref="sale.model_sale_order"/>
<field name="subject">You left items in your cart!</field>
<field name="email_from">${(object.user_id.email_formatted or user.email_formatted or '') | safe}</field>
@@ -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"]',
},
]);
@@ -308,6 +308,18 @@
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box" id="confirmation_email_setting">
<div class="o_setting_right_pane">
<span class="o_form_label">Confirmation Email</span>
<div class="text-muted">
Email sent to the customer after the checkout
</div>
<div class="row mt16">
<label for="confirmation_template_id" string="Email Template" class="col-lg-4 o_light_label"/>
<field name="confirmation_template_id" class="oe_inline"/>
</div>
</div>
</div>
<div class="col-xs-12 col-lg-6 o_setting_box" id="abandoned_carts_setting" title="Abandoned carts are all carts left unconfirmed by website visitors. You can find them in *Website > Orders > Abandoned Carts*. From there you can send recovery emails to visitors who entered their contact details.">
<div class="o_setting_left_pane"/>
<div class="o_setting_right_pane">