[MERGE][IMP] (website_)sale: send and improve emails when payment is processed from portal or ecommerce

Purpose of this merge is to improve emails sent when confirming
sale orders. A new template is added that contains lines with image
for bought product to be used in eCommerce.

See sub commits and task for more details.

Related to task ID 1873634

closes odoo/odoo#33283

Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
Robot Odoo
2019-05-09 18:11:52 +02:00
committed by GitHub
16 changed files with 256 additions and 65 deletions
+9 -7
View File
@@ -262,13 +262,15 @@
<t t-esc="access_name"/>
</a>
</div>
<div style="margin: 0px; padding: 0px; font-size:13px;">
Best regards,
</div>
<div>&amp;nbsp;</div>
<div t-if="record.user_id and record.user_id.sudo().signature" style="font-size: 13px;">
<div t-raw="record.user_id.sudo().signature"/>
</div>
<t t-if="record.user_id and not record.env.user._is_superuser()">
<div style="margin: 0px; padding: 0px; font-size:13px;">
Best regards,
</div>
<div>&amp;nbsp;</div>
<div t-if="record.user_id.sudo().signature" style="font-size: 13px;">
<div t-raw="record.user_id.sudo().signature"/>
</div>
</t>
</td>
</tr>
<!-- FOOTER -->
+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>
+11 -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
@@ -87,7 +90,9 @@ class PaymentTransaction(models.Model):
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.with_context(send_email=True).action_confirm()
so.action_confirm()
# 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 -25
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):
@@ -674,9 +674,6 @@ class SaleOrder(models.Model):
This method should be extended when the confirmation should generated
other documents. In this method, the SO are in 'sale' state (not yet 'done').
"""
if self.env.context.get('send_email'):
self.force_quotation_send()
# create an analytic account if at least an expense product
for order in self:
if any([expense_policy not in [False, 'no'] for expense_policy in order.order_line.mapped('product_id.expense_policy')]):
+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">
+1 -1
View File
@@ -50,7 +50,7 @@ class _Cleaner(clean.Cleaner):
'margin', 'margin-top', 'margin-left', 'margin-bottom', 'margin-right',
'white-space',
# box model
'border', 'border-color', 'border-radius', 'border-style', 'border-width', 'border-top',
'border', 'border-color', 'border-radius', 'border-style', 'border-width', 'border-top', 'border-bottom',
'height', 'width', 'max-width', 'min-width', 'min-height',
# tables
'border-collapse', 'border-spacing', 'caption-side', 'empty-cells', 'table-layout']