[IMP] website_sale: manage abandoned cart
Purpose: Add button to see the number of abandoned carts and the ability to send a recovery email to those customers. On the abandoned carts menu, you can see the SO from the website where no reminder email has been sent. By selecting them, you can send an email to the customer to tell him there is still a cart waiting. Clicking on the link, he can reuse his old cart or merge it with his current one. An abandoned cart is a SO from a website sales team, and created after a treshold parametered by a 'delay' settings (number of hours to be considered as abandoned).
This commit is contained in:
committed by
Jérome Maes
parent
35e80c1ab5
commit
bfa85c84a6
@@ -21,7 +21,7 @@
|
||||
h4 { margin: 0 0 0 8px; }
|
||||
|
||||
h2, h4 { color: @odoo-brand-primary; }
|
||||
|
||||
|
||||
.col-md-5, .col-md-7 {
|
||||
padding: 15px;
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
}
|
||||
|
||||
.nv-groups .nv-point, g circle {
|
||||
fill: @odoo-brand-optional;
|
||||
fill: @odoo-brand-optional;
|
||||
}
|
||||
|
||||
.nv-groups .nv-point {
|
||||
@@ -137,6 +137,7 @@
|
||||
height: 80px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
&.o_main {
|
||||
background-color: @odoo-brand-optional;
|
||||
&:hover {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
'security/website_sale.xml',
|
||||
'data/data.xml',
|
||||
'data/web_planner_data.xml',
|
||||
'data/mail_template_data.xml',
|
||||
'views/product_views.xml',
|
||||
'views/account_views.xml',
|
||||
'views/sale_report_views.xml',
|
||||
|
||||
@@ -86,10 +86,8 @@ class WebsiteSaleBackend(WebsiteBackend):
|
||||
('invoice_status', '=', 'to invoice'),
|
||||
]),
|
||||
order_carts_abandoned_count=request.env['sale.order'].search_count(sale_order_domain + [
|
||||
('state', '=', 'draft'),
|
||||
('order_line', '!=', 'False'),
|
||||
('partner_id', '!=', request.env.ref('base.public_partner').id),
|
||||
('date_order', '<=', fields.Datetime.to_string(datetime.now() - timedelta(hours=1))),
|
||||
('is_abandoned_cart', '=', True),
|
||||
('cart_recovery_email_sent', '=', False)
|
||||
]),
|
||||
payment_to_capture_count=request.env['payment.transaction'].search_count([
|
||||
('state', '=', 'authorized'),
|
||||
|
||||
@@ -332,8 +332,29 @@ class WebsiteSale(http.Controller):
|
||||
return request.redirect(redirect)
|
||||
|
||||
@http.route(['/shop/cart'], type='http', auth="public", website=True)
|
||||
def cart(self, **post):
|
||||
def cart(self, access_token=None, revive='', **post):
|
||||
"""
|
||||
Main cart management + abandoned cart revival
|
||||
access_token: Abandoned cart SO access token
|
||||
revive: Revival method when abandoned cart. Can be 'merge' or 'squash'
|
||||
"""
|
||||
order = request.website.sale_get_order()
|
||||
values = {}
|
||||
if access_token:
|
||||
abandoned_order = request.env['sale.order'].sudo().search([('access_token', '=', access_token)], limit=1)
|
||||
if not abandoned_order: # wrong token (or SO has been deleted)
|
||||
return request.render('website.404')
|
||||
if abandoned_order.state != 'draft': # abandoned cart already finished
|
||||
values.update({'abandoned_proceed': True})
|
||||
elif revive == 'squash' or (revive == 'merge' and not request.session['sale_order_id']): # restore old cart or merge with unexistant
|
||||
request.session['sale_order_id'] = abandoned_order.id
|
||||
return request.redirect('/shop/cart')
|
||||
elif revive == 'merge':
|
||||
abandoned_order.order_line.write({'order_id': request.session['sale_order_id']})
|
||||
abandoned_order.action_cancel()
|
||||
elif abandoned_order.id != request.session['sale_order_id']: # abandoned cart found, user have to choose what to do
|
||||
values.update({'access_token': abandoned_order.access_token})
|
||||
|
||||
if order:
|
||||
from_currency = order.company_id.currency_id
|
||||
to_currency = order.pricelist_id.currency_id
|
||||
@@ -341,11 +362,11 @@ class WebsiteSale(http.Controller):
|
||||
else:
|
||||
compute_currency = lambda price: price
|
||||
|
||||
values = {
|
||||
values.update({
|
||||
'website_sale_order': order,
|
||||
'compute_currency': compute_currency,
|
||||
'suggested_products': [],
|
||||
}
|
||||
})
|
||||
if order:
|
||||
_order = order
|
||||
if not request.env.context.get('pricelist'):
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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="email_from">${(object.user_id.email or '')|safe}</field>
|
||||
<field name="subject">You left items in your cart.</field>
|
||||
<field name="partner_to">${object.partner_id.id}</field>
|
||||
<field name="model_id" ref="sale.model_sale_order"/>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
<field name="lang">${object.partner_id.lang}</field>
|
||||
<field name="body_html" type="xml">
|
||||
<div style="margin:auto;width:100%;">
|
||||
<img src="/web/image/res.company/${user.company_id.id}/logo" style="height: auto; width: 80px; margin-top:30px;" alt="${user.company_id.name}"/>
|
||||
<h1 style="color:#A9A9A9;">THERE'S SOMETHING IN YOUR CART.</h1>
|
||||
<p>Would you like to complete your purchase?</p><br/><br/>
|
||||
% if object.order_line:
|
||||
% for line in object.order_line:
|
||||
<hr/>
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td style="padding: 10px; width:150px;">
|
||||
<img src="/web/image/product.product/${line.product_id.id}/image" height="100px" width="100px"></img>
|
||||
</td>
|
||||
<td>
|
||||
<strong>${line.product_id.display_name}</strong><br/>${line.name}
|
||||
</td>
|
||||
<td width="100px" align="right">
|
||||
${(line.product_uom_qty) | int} ${(line.product_uom.name)}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
% endfor
|
||||
<hr/>
|
||||
% endif
|
||||
<br/><br/>
|
||||
<center>
|
||||
<a href="/shop/cart?access_token=${object.access_token}" target="_blank" style="background-color: #1abc9c; padding: 20px; text-decoration: none; color: #fff; border-radius: 5px; font-size: 16px;" class="o_default_snippet_text">Resume order</a><br/><br/><br/>
|
||||
<p><b>Thank you for shopping with ${user.company_id.name}!</b></p>
|
||||
</center>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
from . import crm_team
|
||||
from . import ir_http
|
||||
from . import mail_compose_message
|
||||
from . import product
|
||||
from . import sale_order
|
||||
from . import website
|
||||
|
||||
@@ -19,16 +19,14 @@ class CrmTeam(models.Model):
|
||||
string='Amount of Abandoned Carts', readonly=True)
|
||||
|
||||
def _compute_abandoned_carts(self):
|
||||
# abandoned carts are draft sales orders that have no order lines,
|
||||
# abandoned carts to recover are draft sales orders that have no order lines,
|
||||
# a partner other than the public user, and created over an hour ago
|
||||
# and the recovery mail was not yet sent
|
||||
website_teams = self.filtered(lambda team: team.team_type == 'website')
|
||||
if website_teams:
|
||||
abandoned_carts_data = self.env['sale.order'].read_group([
|
||||
('team_id', 'in', website_teams.ids),
|
||||
('date_order', '<', fields.Datetime.to_string(datetime.now() - relativedelta(hours=1))),
|
||||
('state', '=', 'draft'),
|
||||
('partner_id', '!=', self.env.ref('base.public_partner').id),
|
||||
('order_line', '!=', False),
|
||||
('is_abandoned_cart', '=', True),
|
||||
('cart_recovery_email_sent', '=', False)
|
||||
], ['amount_total', 'team_id'], ['team_id'])
|
||||
counts = {data['team_id'][0]: data['team_id_count'] for data in abandoned_carts_data}
|
||||
amounts = {data['team_id'][0]: data['amount_total'] for data in abandoned_carts_data}
|
||||
@@ -43,15 +41,19 @@ class CrmTeam(models.Model):
|
||||
|
||||
def get_abandoned_carts(self):
|
||||
self.ensure_one()
|
||||
time_constraint = fields.Datetime.to_string(fields.datetime.now() - relativedelta(hours=1))
|
||||
return {
|
||||
'name': _('Abandoned Carts'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_type': 'form',
|
||||
'view_mode': 'tree,form',
|
||||
'domain': [('state', '=', 'draft'), ('partner_id', '!=', self.env.ref('base.public_partner').id), ('date_order', '<', time_constraint)],
|
||||
'domain': [('is_abandoned_cart', '=', True)],
|
||||
'search_view_id': self.env.ref('sale.sale_order_view_search_inherit_sale').id,
|
||||
'context': {'search_default_team_id': self.id, 'default_team_id': self.id},
|
||||
'context': {
|
||||
'search_default_team_id': self.id,
|
||||
'default_team_id': self.id,
|
||||
'search_default_recovery_email': 1,
|
||||
'create': False
|
||||
},
|
||||
'res_model': 'sale.order',
|
||||
'help': _('''<p class="oe_view_nocontent_create">
|
||||
You can find all abandoned carts here, i.e. the carts generated by your website's visitors from over an hour ago that haven't been confirmed yet.</p>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import datetime
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
class MailComposeMessage(models.TransientModel):
|
||||
_inherit = 'mail.compose.message'
|
||||
|
||||
@api.multi
|
||||
def send_mail(self, auto_commit=False):
|
||||
context = self._context
|
||||
# TODO TDE: clean that brole one day
|
||||
if self.model == 'sale.order':
|
||||
self.env['sale.order'].search([
|
||||
('id', 'in', context.get('active_ids')),
|
||||
('cart_recovery_email_sent', '!=', True),
|
||||
('is_abandoned_cart', '=', True)
|
||||
]).write({'cart_recovery_email_sent': True})
|
||||
self = self.with_context(mail_post_autofollow=True)
|
||||
return super(MailComposeMessage, self).send_mail(auto_commit=auto_commit)
|
||||
@@ -12,6 +12,12 @@ class ResConfigSettings(models.TransientModel):
|
||||
else:
|
||||
return self.env.ref('sale.email_template_edi_sale').id
|
||||
|
||||
def _default_recovery_mail_template(self):
|
||||
try:
|
||||
return self.env.ref('website_sale.mail_template_sale_cart_recovery').id
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
salesperson_id = fields.Many2one('res.users', related='website_id.salesperson_id', string='Salesperson')
|
||||
salesteam_id = fields.Many2one('crm.team', related='website_id.salesteam_id', string='Sales Channel', domain=[('team_type', '!=', 'pos')])
|
||||
module_website_sale_delivery = fields.Boolean("Shipping Costs")
|
||||
@@ -41,6 +47,10 @@ class ResConfigSettings(models.TransientModel):
|
||||
|
||||
module_l10n_eu_service = fields.Boolean(string="EU Digital Goods VAT")
|
||||
|
||||
cart_recovery_mail_template = fields.Many2one('mail.template', string='Cart Recovery Email',
|
||||
default=_default_recovery_mail_template, domain="[('model', '=', 'sale.order')]")
|
||||
cart_abandoned_delay = fields.Float("Abandoned Delay", default=1.0, help="number of hours after which the cart is considered abandoned")
|
||||
|
||||
@api.model
|
||||
def get_values(self):
|
||||
res = super(ResConfigSettings, self).get_values()
|
||||
@@ -52,9 +62,15 @@ class ResConfigSettings(models.TransientModel):
|
||||
if self.env['ir.module.module'].search([('name', '=', 'website_sale_delivery')], limit=1).state in ('installed', 'to install', 'to upgrade'):
|
||||
sale_delivery_settings = 'website'
|
||||
|
||||
cart_recovery_mail_template = int(params.get_param('website_sale.cart_recovery_mail_template_id'))
|
||||
if not cart_recovery_mail_template:
|
||||
cart_recovery_mail_template = self._default_recovery_mail_template()
|
||||
|
||||
res.update(
|
||||
automatic_invoice=params.get_param('website_sale.automatic_invoice', default=False),
|
||||
sale_delivery_settings=sale_delivery_settings,
|
||||
cart_recovery_mail_template=cart_recovery_mail_template,
|
||||
cart_abandoned_delay=float(params.get_param('website_sale.cart_abandoned_delay', '1.0'))
|
||||
)
|
||||
return res
|
||||
|
||||
@@ -62,6 +78,8 @@ class ResConfigSettings(models.TransientModel):
|
||||
super(ResConfigSettings, self).set_values()
|
||||
value = self.module_account_invoicing and self.default_invoice_policy == 'order' and self.automatic_invoice
|
||||
self.env['ir.config_parameter'].sudo().set_param('website_sale.automatic_invoice', value)
|
||||
self.env['ir.config_parameter'].sudo().set_param('website_sale.cart_recovery_mail_template_id', self.cart_recovery_mail_template.id)
|
||||
self.env['ir.config_parameter'].sudo().set_param('website_sale.cart_abandoned_delay', self.cart_abandoned_delay)
|
||||
|
||||
@api.onchange('sale_delivery_settings')
|
||||
def _onchange_sale_delivery_settings(self):
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import logging
|
||||
import random
|
||||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import api, models, fields, _
|
||||
from odoo.http import request
|
||||
from odoo.osv import expression
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
@@ -24,6 +27,8 @@ class SaleOrder(models.Model):
|
||||
string="Can be directly marked as paid", store=True,
|
||||
help="""Checked if the sales order can directly be marked as paid, i.e. if the quotation
|
||||
is sent or confirmed and if the payment acquire is of the type transfer or manual""")
|
||||
is_abandoned_cart = fields.Boolean('Abandoned Cart', compute='_compute_abandoned_cart', search='_search_abandoned_cart')
|
||||
cart_recovery_email_sent = fields.Boolean('Cart recovery email already sent')
|
||||
|
||||
@api.depends('state', 'payment_tx_id', 'payment_tx_id.state',
|
||||
'payment_acquirer_id', 'payment_acquirer_id.provider')
|
||||
@@ -38,6 +43,30 @@ class SaleOrder(models.Model):
|
||||
order.cart_quantity = int(sum(order.mapped('website_order_line.product_uom_qty')))
|
||||
order.only_services = all(l.product_id.type in ('service', 'digital') for l in order.website_order_line)
|
||||
|
||||
@api.multi
|
||||
@api.depends('team_id.team_type', 'date_order', 'order_line', 'state', 'partner_id')
|
||||
def _compute_abandoned_cart(self):
|
||||
abandoned_delay = float(self.env['ir.config_parameter'].sudo().get_param('website_sale.cart_abandoned_delay', '1.0'))
|
||||
abandoned_datetime = fields.Datetime.to_string(datetime.utcnow() - relativedelta(hours=abandoned_delay))
|
||||
for order in self:
|
||||
domain = order.date_order <= abandoned_datetime and order.team_id.team_type == 'website' and order.state == 'draft' and order.partner_id.id != self.env.ref('base.public_partner').id and order.order_line
|
||||
order.is_abandoned_cart = bool(domain)
|
||||
|
||||
def _search_abandoned_cart(self, operator, value):
|
||||
abandoned_delay = float(self.env['ir.config_parameter'].sudo().get_param('website_sale.cart_abandoned_delay', '1.0'))
|
||||
abandoned_datetime = fields.Datetime.to_string(datetime.utcnow() - relativedelta(hours=abandoned_delay))
|
||||
abandoned_domain = expression.normalize_domain([
|
||||
('date_order', '<=', abandoned_datetime),
|
||||
('team_id.team_type', '=', 'website'),
|
||||
('state', '=', 'draft'),
|
||||
('partner_id.id', '!=', self.env.ref('base.public_partner').id),
|
||||
('order_line', '!=', False)
|
||||
])
|
||||
# is_abandoned domain possibilities
|
||||
if (operator not in expression.NEGATIVE_TERM_OPERATORS and value) or (operator in expression.NEGATIVE_TERM_OPERATORS and not value):
|
||||
return abandoned_domain
|
||||
return expression.distribute_not(abandoned_domain) # negative domain
|
||||
|
||||
@api.multi
|
||||
def _cart_find_product_line(self, product_id=None, line_id=None, **kwargs):
|
||||
self.ensure_one()
|
||||
@@ -173,6 +202,32 @@ class SaleOrder(models.Model):
|
||||
accessory_products -= order.website_order_line.mapped('product_id')
|
||||
return random.sample(accessory_products, len(accessory_products))
|
||||
|
||||
@api.multi
|
||||
def action_recovery_email_send(self):
|
||||
composer_form_view_id = self.env.ref('mail.email_compose_message_wizard_form').id
|
||||
try:
|
||||
default_template = self.env.ref('website_sale.mail_template_sale_cart_recovery', raise_if_not_found=False)
|
||||
default_template_id = default_template.id if default_template else False
|
||||
template_id = int(self.env['ir.config_parameter'].sudo().get_param('website_sale.cart_recovery_mail_template_id', default_template_id))
|
||||
except:
|
||||
template_id = False
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_type': 'form',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'mail.compose.message',
|
||||
'view_id': composer_form_view_id,
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'default_composition_mode': 'mass_mail' if len(self) > 1 else 'comment',
|
||||
'default_res_id': self.ids[0],
|
||||
'default_model': 'sale.order',
|
||||
'default_use_template': bool(template_id),
|
||||
'default_template_id': template_id,
|
||||
'active_ids': self.ids,
|
||||
},
|
||||
}
|
||||
|
||||
def action_mark_as_paid(self):
|
||||
""" Mark directly a sales order as paid if:
|
||||
- State: Quotation Sent, or sales order
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<div t-if="widget.dashboards_data.sales.summary.order_carts_abandoned_count" class="o_inner_box o_main o_box_item" title="Send a recovery email to visitors who haven't completed their order.">
|
||||
<a href="#" class="o_dashboard_action" name="website_sale.action_abandoned_orders_ecommerce">
|
||||
<div class="o_highlight"><t t-esc="widget.dashboards_data.sales.summary.order_carts_abandoned_count"/></div>
|
||||
Abandoned Carts
|
||||
Abandoned Carts to Recover
|
||||
</a>
|
||||
<t t-set="vignette_nbr" t-value="vignette_nbr + 1"/>
|
||||
</div>
|
||||
|
||||
@@ -16,8 +16,7 @@
|
||||
<div>
|
||||
<a name="get_abandoned_carts" type="object">
|
||||
<field name="abandoned_carts_count"/>
|
||||
<t t-if="record.abandoned_carts_count.raw_value != 1">Abandoned Carts</t>
|
||||
<t t-else="">Abandoned Cart</t>
|
||||
Abandoned Carts to Recover
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -344,6 +344,27 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-6 o_setting_box" id="abandoned_carts_setting" title="Send a recovery email to authentified shoppers">
|
||||
<div class="o_setting_left_pane"/>
|
||||
<div class="o_setting_right_pane">
|
||||
<label string="Email for Abandonned carts"/>
|
||||
<div class="text-muted">
|
||||
Default recovery email to send when a cart is abandoned
|
||||
</div>
|
||||
<div class="content-group">
|
||||
<div class="row mt16">
|
||||
<label for="cart_recovery_mail_template" string="Email Template" class="col-md-3 o_light_label"/>
|
||||
<field name="cart_recovery_mail_template" class="oe_inline"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-group">
|
||||
<div class="row mt16">
|
||||
<label for="cart_abandoned_delay" string="Cart are considered abandoned after" class="col-md-6 o_light_label"/>
|
||||
<span class="col-md-3"><field name="cart_abandoned_delay"/>hours</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-6 o_setting_box" id="checkout_assignation_setting" groups="base.group_no_one">
|
||||
<div class="o_setting_right_pane">
|
||||
<label string="Assignation"/>
|
||||
|
||||
@@ -7,10 +7,37 @@
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<filter string="Confirmed Orders" name="order_confirmed" domain="[('state', 'in', ('sale', 'done'))]"/>
|
||||
<separator/>
|
||||
<filter string="From Website" name="from_website" domain="[('team_id.team_type', '=', 'website')]"/>
|
||||
<separator/>
|
||||
<filter string="Recovery Email to Send" name="recovery_email" domain="[('cart_recovery_email_sent', '=', False)]" />
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_view_form_cart_recovery" model="ir.ui.view">
|
||||
<field name="name">sale.order.form.abandoned.cart</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="inherit_id" ref="sale.view_order_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='team_id']" position="after">
|
||||
<field name="is_abandoned_cart" invisible="1"/>
|
||||
<field name="cart_recovery_email_sent" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='action_quotation_send' and @states='draft']" position="attributes">
|
||||
<!-- The '| and the '&' opertors are necessary because draft state of the parent concatenate the domain -->
|
||||
<attribute name="attrs">{'invisible': ['|','&',('is_abandoned_cart', '=', True), ('cart_recovery_email_sent', '=', False)]}</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='action_quotation_send']" position="after">
|
||||
<button name="action_recovery_email_send"
|
||||
string="Send a Recovery Email"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
attrs="{'invisible': ['|', ('is_abandoned_cart', '=', False), ('cart_recovery_email_sent', '=', True)]}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_orders_ecommerce" model="ir.actions.act_window">
|
||||
<field name="name">Orders</field>
|
||||
<field name="res_model">sale.order</field>
|
||||
@@ -35,8 +62,8 @@
|
||||
<field name="name">Abandoned Carts</field>
|
||||
<field name="res_model">sale.order</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="domain">[('state', '=', 'draft'), ('order_line', '!=', False), ('partner_id','!=', context.get('public_partner_id')), ('team_id.team_type', '=', 'website'), ('date_order', '<=', (datetime.datetime.utcnow() - datetime.timedelta(hours=1)).strftime('%Y-%m-%d %H:%M:%S'))]</field>
|
||||
<field name="context" eval="{'show_sale': True, 'create': False, 'public_partner_id': ref('base.public_partner')}"/>
|
||||
<field name="domain">[('is_abandoned_cart', '=', True)]</field>
|
||||
<field name="context" eval="{'show_sale': True, 'create': False, 'search_default_recovery_email' : 1}"/>
|
||||
<field name="search_view_id" ref="view_sales_order_filter_ecommerce"/>
|
||||
<field name="help" type="xml">
|
||||
<p>Find here all the abandoned carts, i.e. the carts generated by your website's visitors more than one hour ago and not confirmed.</p>
|
||||
@@ -53,6 +80,19 @@
|
||||
<field name="help">You don't have any order from the website.</field>
|
||||
</record>
|
||||
|
||||
<!-- Server action to send multiple recovery email-->
|
||||
<record id="ir_actions_server_sale_cart_recovery_email" model="ir.actions.server">
|
||||
<field name="name">Sale: Send a Cart Recovery Email</field>
|
||||
<field name="type">ir.actions.server</field>
|
||||
<field name="model_id" ref="model_sale_order"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
if records:
|
||||
action = records.action_recovery_email_send()
|
||||
</field>
|
||||
<field name="binding_model_id" ref="sale.model_sale_order"/>
|
||||
</record>
|
||||
|
||||
<!-- Main website sale menu items -->
|
||||
<menuitem id="menu_orders" name="Orders"
|
||||
parent="website.menu_website_configuration" sequence="2"
|
||||
@@ -66,7 +106,7 @@
|
||||
<menuitem id="website.menu_website_global_configuration" name="Configuration"
|
||||
parent="website.menu_website_configuration" sequence="100"
|
||||
groups="base.group_system,sales_team.group_sale_manager"/>
|
||||
|
||||
|
||||
<menuitem id="menu_ecommerce_settings" name="eCommerce"
|
||||
parent="website.menu_website_global_configuration"/>
|
||||
<menuitem id="menu_product_settings" name="Products"
|
||||
|
||||
@@ -879,6 +879,20 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-sm-9 oe_cart">
|
||||
<div t-if="abandoned_proceed or access_token" class="mt8 mb8 alert alert-info"> <!-- abandoned cart choices -->
|
||||
<t t-if="abandoned_proceed">
|
||||
<p>Your previous cart has already been completed.</p>
|
||||
<p t-if="website_sale_order">Please proceed your current cart.</p>
|
||||
</t>
|
||||
<t t-if="access_token">
|
||||
<p>This is your current cart.</p>
|
||||
<p>
|
||||
<strong><a t-attf-href="/shop/cart/?access_token=#{access_token}&revive=squash">Click here</a></strong> if you want to restore your previous cart. Your current cart will be replaced with your previous cart.</p>
|
||||
<p>
|
||||
<strong><a t-attf-href="/shop/cart/?access_token=#{access_token}&revive=merge">Click here</a></strong> if you want to merge your previous cart into current cart.
|
||||
</p>
|
||||
</t>
|
||||
</div>
|
||||
<t t-call="website_sale.cart_lines" />
|
||||
<div class="clearfix" />
|
||||
<a t-if="not optional_products and website_sale_order and website_sale_order.website_order_line" class="btn btn-primary pull-right mb32" href="/shop/checkout">
|
||||
@@ -1648,4 +1662,5 @@
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
|
||||
Reference in New Issue
Block a user