[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:
committed by
Thibault Delavallée
parent
b55b4a42a6
commit
74c5c00aff
@@ -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',
|
||||
|
||||
@@ -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
@@ -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 & Pay Quotation</t>
|
||||
<t t-else="" t-set="access_name">Accept & 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 & 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user