[IMP] website_sale: improved checkout process

- confirmation is now a page displaying a sale order, which polls on the
related transaction to have its status
- when receiving an incoming notification thorugh the acquirer controller
the sale order is updated, then the confirmation page is displayed
- this allows to have a confirmation page that is independant from the 
rest of the checkout process
- cleaned confirmation page, removed order details

bzr revid: tde@openerp.com-20131121120902-fm8b7u94gmepohgi
This commit is contained in:
Thibault Delavallée 2013-11-21 13:09:02 +01:00
parent 3ee84c32d2
commit 2693636989
5 changed files with 146 additions and 151 deletions

View File

@ -448,9 +448,15 @@ class Ecommerce(http.Controller):
@website.route(['/shop/mycart/'], type='http', auth="public", multilang=True)
def mycart(self, **post):
order = request.registry['website'].get_current_order(request.cr, request.uid, context=request.context)
cr, uid, context = request.cr, request.uid, request.context
prod_obj = request.registry.get('product.product')
# must have a draft sale order with lines at this point, otherwise reset
order = context.get('website_sale_order')
if order and order.state != 'draft':
request.registry['website'].sale_reset_order(cr, uid, context=context)
return request.redirect('/shop/')
if 'promo' in post:
self.change_pricelist(post.get('promo'))
@ -462,7 +468,7 @@ class Ecommerce(http.Controller):
product_ids.append(line.product_id.id)
suggested_ids = list(set(suggested_ids) - set(product_ids))
if suggested_ids:
suggested_ids = prod_obj.search(request.cr, request.uid, [('id', 'in', suggested_ids)], context=request.context)
suggested_ids = prod_obj.search(cr, uid, [('id', 'in', suggested_ids)], context=context)
# select 3 random products
suggested_products = []
@ -473,7 +479,7 @@ class Ecommerce(http.Controller):
values = {
'int': int,
'get_categories': self.get_categories,
'suggested_products': prod_obj.browse(request.cr, request.uid, suggested_products, request.context),
'suggested_products': prod_obj.browse(cr, uid, suggested_products, context),
}
return request.website.render("website_sale.mycart", values)
@ -498,10 +504,15 @@ class Ecommerce(http.Controller):
def checkout(self, **post):
cr, uid, context, registry = request.cr, request.uid, request.context, request.registry
order = request.registry['website'].get_current_order(request.cr, request.uid, context=request.context)
# must have a draft sale order with lines at this point, otherwise reset
order = context.get('website_sale_order')
if not order or order.state != 'draft' or not order.order_line:
return self.mycart(**post)
request.registry['website'].sale_reset_order(cr, uid, context=context)
return request.redirect('/shop/')
# if transaction pending / done: redirect to confirmation
tx = context.get('website_sale_transaction')
if tx and tx.state != 'draft':
return request.redirect('/shop/payment/confirmation/%s' % order.id)
orm_partner = registry.get('res.partner')
orm_user = registry.get('res.users')
@ -549,10 +560,15 @@ class Ecommerce(http.Controller):
def confirm_order(self, **post):
cr, uid, context, registry = request.cr, request.uid, request.context, request.registry
# must have a draft sale order with lines at this point, otherwise redirect to shop
order = request.registry['website'].get_current_order(request.cr, request.uid, context=request.context)
if not order or order.state != 'draft' or not order.order_line:
return self.mycart(**post)
request.registry['website'].sale_reset_order(cr, uid, context=context)
return request.redirect('/shop/')
# if transaction pending / done: redirect to confirmation
tx = context.get('website_sale_transaction')
if tx and tx.state != 'draft':
return request.redirect('/shop/payment/confirmation/%s' % order.id)
orm_parter = registry.get('res.partner')
orm_user = registry.get('res.users')
@ -636,19 +652,29 @@ class Ecommerce(http.Controller):
return request.redirect("/shop/payment/")
@website.route(['/shop/payment/'], type='http', auth="public", multilang=True)
def payment(self, payment_acquirer_id=None, **post):
def payment(self, **post):
""" Payment step. This page proposes several payment means based on available
payment.acquirer. State at this point :
- a draft sale order with lines; otherwise, clean context / session and
back to the shop
- no transaction in context / session, or only a draft one, if the customer
did go to a payment.acquirer website but closed the tab without
paying / canceling
"""
cr, uid, context = request.cr, request.uid, request.context
payment_obj = request.registry.get('payment.acquirer')
# if no sale order at this stage: back to checkout beginning
order = context.get('website_sale_order')
if not order or not order.order_line:
return request.redirect("/shop/checkout/")
if not order or not order.state == 'draft' or not order.order_line:
request.registry['website'].sale_reset_order(cr, uid, context=context)
return request.redirect("/shop/")
# alread a transaction: forward to confirmation
tx = context.get('website_sale_transaction')
if tx and not tx.state == 'draft':
return request.redirect('/shop/confirmation')
print 'embetatn'
# return request.redirect('/shop/confirmation/%s' % order.id)
partner_id = False
shipping_partner_id = False
@ -661,7 +687,6 @@ class Ecommerce(http.Controller):
values = {
'partner': partner_id,
'payment_acquirer_id': payment_acquirer_id,
'order': order
}
values.update(request.registry.get('sale.order')._get_website_data(cr, uid, order, context))
@ -680,7 +705,7 @@ class Ecommerce(http.Controller):
order.pricelist_id.currency_id,
partner_id=shipping_partner_id,
tx_custom_values={
'return_url': '/shop/confirmation',
'return_url': '/shop/payment/validate',
},
context=context)
@ -688,7 +713,7 @@ class Ecommerce(http.Controller):
@website.route(['/shop/payment/transaction/<int:acquirer_id>'],
type='http', methods=['POST'], auth="public")
def payment_transaction(self, acquirer_id=None, **post):
def payment_transaction(self, acquirer_id, **post):
""" Hook method that creates a payment.transaction and redirect to the
acquirer, using post values to re-create the post action.
@ -717,6 +742,7 @@ class Ecommerce(http.Controller):
'currency_id': order.pricelist_id.currency_id.id,
'partner_id': order.partner_id.id,
'reference': order.name,
'sale_order_id': order.id,
}, context=context)
request.httprequest.session['website_sale_transaction_id'] = tx_id
elif tx and tx.state == 'draft': # button cliked but no more info -> rewrite on tx or create a new one ?
@ -728,30 +754,47 @@ class Ecommerce(http.Controller):
acquirer_total_url = '%s?%s' % (acquirer_form_post_url, urllib.urlencode(post))
return request.redirect(acquirer_total_url)
@website.route('/shop/payment/confirm', type='json', auth="public", multilang=True)
def payment_confirm(self, transaction_id=None, **post):
@website.route('/shop/payment/get_status/<model("sale.order"):order>', type='json', auth="public", multilang=True)
def payment_get_status(self, order, **post):
cr, uid, context = request.cr, request.uid, request.context
payment_obj = request.registry.get('payment.transaction')
if transaction_id:
tx = payment_obj.browse(cr, uid, transaction_id, context=context)
else:
tx = context.get('website_sale_transaction')
if not order:
return {
'state': 'error',
}
tx_ids = request.registry['payment.transaction'].search(
cr, uid, [
'|', ('sale_order_id', '=', order.id), ('reference', '=', order.name)
], context=context)
if not tx_ids:
return {
'state': 'error'
}
tx = request.registry['payment.transaction'].browse(cr, uid, tx_ids[0], context=context)
return {
'state': tx.state,
}
@website.route('/shop/payment/validate', type='json', auth="public", multilang=True)
def payment_validate(self):
@website.route('/shop/payment/validate/', type='http', auth="public", multilang=True)
def payment_validate(self, transaction_id=None, sale_order_id=None, **post):
""" Method that should be called by the server when receiving an update
for a transaction. State at this point :
- UDPATE ME
"""
cr, uid, context = request.cr, request.uid, request.context
email_act = None
sale_order_obj = request.registry['sale.order']
order = context.get('website_sale_order')
tx = context.get('website_sale_transaction')
if transaction_id is None:
tx = context.get('website_sale_transaction')
else:
tx = request.registry['payment.transaction'].browse(cr, uid, transaction_id, context=context)
if not order or not tx:
return request.redirect("/shop/checkout/")
if sale_order_id is None:
order = context.get('website_sale_order')
else:
order = request.registry['sale.order'].browse(cr, uid, sale_order_id, context=context)
if tx.state == 'done':
# confirm the quotation
@ -770,26 +813,23 @@ class Ecommerce(http.Controller):
compose_id = request.registry['mail.compose.message'].create(cr, uid, {}, context=create_ctx)
request.registry['mail.compose.message'].send_mail(cr, uid, [compose_id], context=create_ctx)
return True
# clean context and session, then redirect to the confirmation page
request.registry['website'].sale_reset_order(cr, uid, context=context)
@website.route(['/shop/confirmation/'], type='http', auth="public", multilang=True)
def payment_confirmation(self, **post):
context = request.context
return request.redirect('/shop/confirmation/%s' % order.id)
# if no sale order at this stage: back to shop
order = context.get('website_sale_order')
if not order or not order.order_line:
return request.redirect("/shop/")
@website.route(['/shop/confirmation/<model("sale.order"):order>'], type='http', auth="public", multilang=True)
def payment_confirmation(self, order, **post):
""" End of checkout process controller. Confirmation is basically seing
the status of a sale.order. State at this point :
# no transaction: back to payment
tx = context.get('website_sale_transaction')
if not tx:
return request.redirect('/shop/payment')
- should not have any context / session info: clean them
- take a sale.order id, because we request a sale.order and are not
session dependant anymore
"""
cr, uid, context = request.cr, request.uid, request.context
res = request.website.render("website_sale.confirmation", {'order': order})
# request.httprequest.session['ecommerce_order_id'] = False
# request.httprequest.session['ecommerce_pricelist'] = False
return res
return request.website.render("website_sale.confirmation", {'order': order})
@website.route(['/shop/change_sequence/'], type='json', auth="public")
def change_sequence(self, id, top):

View File

@ -65,6 +65,17 @@ class Website(osv.Model):
return tx
return False
def sale_reset_order(self, cr, uid, context=None):
request.httprequest.session.update({
'ecommerce_order_id': False,
'ecommerce_pricelist': False,
'website_sale_transaction_id': False,
})
request.context.update({
'website_sale_order': False,
'website_sale_transaction': False,
})
def preprocess_request(self, cr, uid, ids, request, context=None):
request.context.update({
'website_sale_order': self.get_current_order(cr, uid, context=context),

View File

@ -2,6 +2,7 @@
from openerp.osv import orm, fields
from openerp.addons.web import http
class Website(orm.Model):
_inherit = 'website'
@ -16,3 +17,12 @@ class Website(orm.Model):
'pricelist_id': fields.function(
_get_pricelist, type='many2one', obj='product.pricelist')
}
class PaymentTransaction(orm.Model):
_inherit = 'payment.transaction'
_columns = {
# link with the sale order
'sale_order_id': fields.many2one('sale.order', 'Sale Order'),
}

View File

@ -2,46 +2,35 @@ $(document).ready(function () {
var _poll_nbr = 0;
function payment_transaction_validate() {
return openerp.jsonRpc('/shop/payment/validate', 'call', {});
}
function payment_transaction_poll_status() {
return openerp.jsonRpc('/shop/payment/confirm', 'call', {
}).done(function (result) {
// console.log('--done', result);
var order_node = $('div.oe_website_sale_tx_status');
if (! order_node || order_node.data('orderId') === undefined) {
return;
}
var order_id = order_node.data('orderId');
return openerp.jsonRpc('/shop/payment/get_status/' + order_id, 'call', {
}).then(function (result) {
var tx_node = $('div.oe_website_sale_tx_status');
var txt = '<h3>Your transaction is waiting confirmation.</h3>';
_poll_nbr += 1;
if (result.state == 'done') {
$('div.oe_website_sale_confirmation').html(function() {
return "<h2>Thanks for your order</h2>";
});
if (result.state == 'pending' && _poll_nbr <= 5) {
txt = "<h3>Your transaction is waiting confirmation.</h3>";
setTimeout(function () {
payment_transaction_poll_status();
}, 1000);
}
else if (result.state == 'done') {
txt = "<h3>Your payment has been received.</h3>";
}
else if (result.state == 'pending') {
if (_poll_nbr <= 2) {
$('div.oe_website_sale_confirmation').html(function() {
return "<h2>Waiting validation ...</h2>";
});
setTimeout(function () {
return inner_done = payment_transaction_poll_status();
}, 1000);
// return inner_done;
}
else {
$('div.oe_website_sale_confirmation').html(function() {
return "<h2>You payment is currently under review. Please come back later.</h2>";
});
}
txt = "<h3>Your transaction is waiting confirmation. You may try to refresh this page.</h3>";
}
else if (result.state == 'cancel') {
$('div.oe_website_sale_confirmation').html(function() {
return "<h2>The payment seems to have been canceled.</h2>";
});
txt = "<h3>The payment seems to have been canceled.</h3>";
}
tx_node.html(txt);
});
}
payment_transaction_poll_status().done(function (result) {
// console.log('finished', result);
payment_transaction_validate();
});
payment_transaction_poll_status();
});

View File

@ -925,83 +925,28 @@
<li class="text-muted">Payment<span class="chevron"></span></li>
<li class="text-primary">Confirmation<span class="chevron"></span></li>
</ul>
<h1 class="mb32">Validate Order</h1>
<h1 class="mb32">Order Confirmed</h1>
<div class="row">
<div class="col-md-8 oe_mycart">
<table class='table table-striped table-condensed' id="mycart_products" t-if="website_sale_order and website_sale_order.order_line">
<colgroup>
<col width="80"/>
<col/>
<col width="100"/>
<col width="120"/>
</colgroup>
<thead>
<tr>
<th colspan="2">Product</th>
<th>Price</th>
<th>Quantity</th>
</tr>
</thead>
<tbody>
<tr t-foreach="website_sale_order.order_line" t-as="line">
<td colspan="2" t-if="not line.product_id.product_tmpl_id"></td>
<td t-if="line.product_id.product_tmpl_id">
<a t-href="/shop/product/#{ line.product_id.product_tmpl_id.id }/">
<span t-field="line.product_id.image_small"
t-field-options='{"widget": "image", "class": "img-rounded"}'/>
</a>
</td>
<td t-if="line.product_id.product_tmpl_id">
<strong t-field="line.product_id.name"/>
</td>
<td class="text-center">
<span t-field="line.price_unit" t-field-options='{
"widget": "monetary",
"display_currency": "website.pricelist_id.currency_id"
}'/>
</td>
<td>
<div t-esc="line.product_uom_qty"/>
</td>
</tr>
</tbody>
</table>
<table class='pull-right mb16' id="mycart_total">
<colgroup>
<col width="100"/>
<col width="120"/>
</colgroup>
<thead>
<tr style="border-top: 1px solid #000">
<th><h3>Total:</h3></th>
<th class="text-right"><h3><span t-field="website_sale_order.amount_total" t-field-options='{
"widget": "monetary",
"display_currency": "website.pricelist_id.currency_id"
}'/></h3></th>
</tr>
<tr class="text-muted">
<td><abbr title="Taxes may be updated after providing shipping address">Incl. Taxes:</abbr></td>
<td class="text-right"><span t-field="website_sale_order.amount_tax" t-field-options='{
"widget": "monetary",
"display_currency": "website.pricelist_id.currency_id"
}'/></td>
</tr>
</thead>
</table>
<div class="clearfix"/>
<div class="oe_structure"/>
</div>
<div class="col-md-3 col-md-offset-1 text-muted" id="right_column">
<h4>Bill To:</h4>
<div t-field="website_sale_order.partner_invoice_id"/>
<div class="col-md-8 oe_mycart">
<h2>Thank you for your order.</h2>
<div class="oe_website_sale_tx_status" t-att-data-order-id="order.id">
</div>
<div class="clearfix"/>
<div class="oe_structure"/>
</div>
<div class="col-md-3 col-md-offset-1 text-muted" id="right_column">
<h4>Bill To:</h4>
<div t-field="order.partner_invoice_id"/>
<h4 class="mt32">Ship To:</h4>
<div t-field="website_sale_order.partner_shipping_id"/>
</div>
</div>
<h4 class="mt32">Ship To:</h4>
<div t-field="order.partner_shipping_id"/>
<div class="oe_website_sale_confirmation">
<h2>Tanks you for your order.</h2>
<h4 class="mt32">Amount:</h4>
<!-- <div t-field="order.amount_total" t-field-options='{
"widget": "monetary",
"display_currency": "website.pricelist_id.currency_id"
}'/> -->
</div>
</div>
</div>