From de8d90e12edfb9f965e7c145c48a6f0c86bb7d32 Mon Sep 17 00:00:00 2001 From: Damien Bouvy Date: Thu, 16 Apr 2015 12:11:02 +0200 Subject: [PATCH] [IMP] website_quote: add payment management Until now, you could only sign and confirm a sale order from the frontend. From now on, you can also pay sale orders if the 'Immediate Payment' option is checked on the sale order. --- addons/website_quote/__openerp__.py | 2 +- addons/website_quote/controllers/main.py | 111 +++++++++++++++++- addons/website_quote/models/__init__.py | 1 + addons/website_quote/models/order.py | 15 ++- addons/website_quote/models/payment.py | 28 +++++ .../static/src/js/website_quotation.js | 73 +++++++++--- .../website_quote/views/website_quotation.xml | 111 ++++++++++++++++-- .../views/website_quotation_backend.xml | 2 + 8 files changed, 306 insertions(+), 37 deletions(-) create mode 100644 addons/website_quote/models/payment.py diff --git a/addons/website_quote/__openerp__.py b/addons/website_quote/__openerp__.py index 35c35ff5fb6..9830a3ee15b 100644 --- a/addons/website_quote/__openerp__.py +++ b/addons/website_quote/__openerp__.py @@ -10,7 +10,7 @@ OpenERP Sale Quote Roller """, 'author': 'OpenERP SA', - 'depends': ['website', 'sale', 'mail', 'web_tip'], + 'depends': ['website', 'sale', 'mail', 'web_tip', 'payment', 'website_portal'], 'data': [ 'quotation_report.xml', 'views/website_quotation.xml', diff --git a/addons/website_quote/controllers/main.py b/addons/website_quote/controllers/main.py index eeb085c67a7..c414f363a96 100644 --- a/addons/website_quote/controllers/main.py +++ b/addons/website_quote/controllers/main.py @@ -55,14 +55,41 @@ class sale_quote(http.Controller): pdf = report_obj.get_pdf(request.cr, SUPERUSER_ID, [order_id], 'website_quote.report_quote', data=None, context=request.context) pdfhttpheaders = [('Content-Type', 'application/pdf'), ('Content-Length', len(pdf))] return request.make_response(pdf, headers=pdfhttpheaders) + user = request.registry['res.users'].browse(request.cr, SUPERUSER_ID, request.uid, context=request.context) + tx_id = request.registry['payment.transaction'].search(request.cr, SUPERUSER_ID, [('reference', '=', order.name)], context=request.context) + tx = request.registry['payment.transaction'].browse(request.cr, SUPERUSER_ID, tx_id, context=request.context) if tx_id else False values = { 'quotation': order, 'message': message and int(message) or False, 'option': bool(filter(lambda x: not x.line_id, order.options)), 'order_valid': (not order.validity_date) or (now <= order.validity_date), 'days_valid': days, - 'action': action + 'action': action, + 'breadcrumb': user.partner_id == order.partner_id, + 'tx_id': tx_id, + 'tx_state': tx.state if tx else False, + 'tx_post_msg': tx.acquirer_id.post_msg if tx else False, + 'need_payment': not tx_id and order.state == 'manual' } + + if order.require_payment or (not tx_id and order.state == 'manual'): + payment_obj = request.registry.get('payment.acquirer') + acquirer_ids = payment_obj.search(request.cr, SUPERUSER_ID, [('website_published', '=', True), ('company_id', '=', order.company_id.id)], context=request.context) + values['acquirers'] = list(payment_obj.browse(request.cr, token and SUPERUSER_ID or request.uid, acquirer_ids, context=request.context)) + render_ctx = dict(request.context, submit_class='btn btn-primary', submit_txt=_('Pay & Confirm')) + for acquirer in values['acquirers']: + acquirer.button = payment_obj.render( + request.cr, SUPERUSER_ID, acquirer.id, + order.name, + order.amount_total, + order.pricelist_id.currency_id.id, + partner_id=order.partner_id.id, + tx_values={ + 'return_url': '/quote/' + str(order_id) + '/confirm', + 'type': 'form', + 'alias_usage': _('If we store your payment information on our server, subscription payments will be made automatically.') + }, + context=render_ctx) return request.website.render('website_quote.so_quotation', values) @http.route(['/quote/accept'], type='json', auth="public", website=True) @@ -71,6 +98,8 @@ class sale_quote(http.Controller): order = order_obj.browse(request.cr, SUPERUSER_ID, order_id) if token != order.access_token: return request.website.render('website.404') + if order.require_payment: + return request.website.render('website.404') attachments=sign and [('signature.png', sign.decode('base64'))] or [] order_obj.action_button_confirm(request.cr, SUPERUSER_ID, [order_id], context=request.context) message = _('Order signed by %s') % (signer,) @@ -175,3 +204,83 @@ class sale_quote(http.Controller): line = request.registry.get('sale.order.line').create(request.cr, SUPERUSER_ID, vals, context=request.context) option_obj.write(request.cr, SUPERUSER_ID, [option.id], {'line_id': line}, context=request.context) return werkzeug.utils.redirect("/quote/%s/%s#pricing" % (order.id, token)) + + # note dbo: website_sale code + @http.route(['/quote//transaction/'], type='json', auth="public", website=True) + def payment_transaction(self, acquirer_id, order_id): + """ Json method that creates a payment.transaction, used to create a + transaction when the user clicks on 'pay now' button. After having + created the transaction, the event continues and the user is redirected + to the acquirer website. + + :param int acquirer_id: id of a payment.acquirer record. If not set the + user is redirected to the checkout page + """ + cr, uid, context = request.cr, request.uid, request.context + transaction_obj = request.registry.get('payment.transaction') + order = request.registry.get('sale.order').browse(cr, SUPERUSER_ID, order_id, context=context) + + if not order or not order.order_line or acquirer_id is None: + return request.redirect("/quote/" + str(order_id)) + + # find an already existing transaction + tx_id = transaction_obj.search(cr, SUPERUSER_ID, [('reference', '=', order.name)], context=context) + tx = transaction_obj.browse(cr, SUPERUSER_ID, tx_id, context=context) + if tx: + if tx.state == 'draft': # button cliked but no more info -> rewrite on tx or create a new one ? + tx.write({ + 'acquirer_id': acquirer_id, + }) + tx_id = tx.id + else: + tx_id = transaction_obj.create(cr, SUPERUSER_ID, { + 'acquirer_id': acquirer_id, + 'type': 'form', + 'amount': order.amount_total, + 'currency_id': order.pricelist_id.currency_id.id, + 'partner_id': order.partner_id.id, + 'partner_country_id': order.partner_id.country_id.id, + 'reference': order.name, + 'sale_order_id': order.id, + }, context=context) + request.session['sale_transaction_id'] = tx_id + tx = transaction_obj.browse(cr, SUPERUSER_ID, tx_id, context=context) + + # confirm the quotation + if tx.acquirer_id.auto_confirm == 'at_pay_now': + request.registry['sale.order'].action_button_confirm(cr, SUPERUSER_ID, [order.id], context=request.context) + + return tx_id + + @http.route(['/quote//confirm'], type='http', auth="public", website=True) + def confirm(self, order_id): + cr, uid, context = request.cr, request.uid, request.context + transaction_obj = request.registry.get('payment.transaction') + sale_order_obj = request.registry.get('sale.order') + order = sale_order_obj.browse(cr, SUPERUSER_ID, order_id, context=context) + + if not order or not order.order_line: + return request.redirect("/quote/%s" % (order_id)) + + # find transaction + tx_id = transaction_obj.search(cr, SUPERUSER_ID, [('reference', '=', order.name)], context=context) + tx = transaction_obj.browse(cr, SUPERUSER_ID, tx_id, context=context) + # create draft invoice if transaction is ok + if tx and tx.state == 'pending': + sale_order_obj.action_button_confirm(cr, SUPERUSER_ID, [order_id], context=context) + sale_order_obj.signal_workflow(cr, SUPERUSER_ID, [order.id], 'manual_invoice', context=context) + message = _('Order payed by %s, waiting for payment confirmation') % (tx.partner_id.name,) + self.__message_post(message, order_id, type='comment', subtype='mt_comment') + elif tx and tx.state == 'done': + sale_order_obj.signal_workflow(cr, SUPERUSER_ID, [order.id], 'manual_invoice', context=context) + invoice = order.invoice_ids + request.registry['account.invoice'].signal_workflow(cr, SUPERUSER_ID, [invoice.id], 'invoice_open', context=context) + period_id = request.registry['account.period'].find(cr, SUPERUSER_ID, time.strftime('%Y-%m-%d'), context=context)[0] + message = _('Order payed by %s') % (tx.partner_id.name,) + self.__message_post(message, order_id, type='comment', subtype='mt_comment') + # if website_sale is installed, we need to clean the sale_order linked to the website + # otherwise the system will mix transactions on the next SO + if hasattr(request.website, 'sale_reset'): + request.website.sale_reset() + + return request.redirect("/quote/%s/%s" % (order.id, order.access_token)) diff --git a/addons/website_quote/models/__init__.py b/addons/website_quote/models/__init__.py index 49178edb61d..5649c941501 100644 --- a/addons/website_quote/models/__init__.py +++ b/addons/website_quote/models/__init__.py @@ -1 +1,2 @@ import order +import payment diff --git a/addons/website_quote/models/order.py b/addons/website_quote/models/order.py index 380e83c76ed..a25030ad2ba 100644 --- a/addons/website_quote/models/order.py +++ b/addons/website_quote/models/order.py @@ -36,6 +36,7 @@ class sale_quote_template(osv.osv): 'note': fields.text('Terms and conditions'), 'options': fields.one2many('sale.quote.option', 'template_id', 'Optional Products Lines', copy=True), 'number_of_days': fields.integer('Quotation Duration', help='Number of days for the validity date computation of the quotation'), + 'require_payment': fields.boolean('Immediate Payment', help="Require immediate payment by the customer when validating the order from the website quote"), } def open_template(self, cr, uid, quote_id, context=None): return { @@ -159,7 +160,8 @@ class sale_order(osv.osv): 'options' : fields.one2many('sale.order.option', 'order_id', 'Optional Products Lines', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, copy=True), 'amount_undiscounted': fields.function(_get_total, string='Amount Before Discount', type="float", digits_compute=dp.get_precision('Account')), - 'quote_viewed': fields.boolean('Quotation Viewed') + 'quote_viewed': fields.boolean('Quotation Viewed'), + 'require_payment': fields.boolean('Immediate Payment', help="Require immediate payment by the customer when validating the order from the website quote"), } def _get_template_id(self, cr, uid, context=None): @@ -180,7 +182,7 @@ class sale_order(osv.osv): return { 'type': 'ir.actions.act_url', 'target': 'self', - 'url': '/quote/%s' % (quote.id) + 'url': '/quote/%s/%s' % (quote.id, quote.access_token) } def onchange_template_id(self, cr, uid, ids, template_id, partner=False, fiscal_position=False, pricelist_id=False, context=None): @@ -233,7 +235,14 @@ class sale_order(osv.osv): date = False if quote_template.number_of_days > 0: date = (datetime.datetime.now() + datetime.timedelta(quote_template.number_of_days)).strftime("%Y-%m-%d") - data = {'order_line': lines, 'website_description': quote_template.website_description, 'note': quote_template.note, 'options': options, 'validity_date': date} + data = { + 'order_line': lines, + 'website_description': quote_template.website_description, + 'note': quote_template.note, + 'options': options, + 'validity_date': date, + 'require_payment': quote_template.require_payment + } return {'value': data} def recommended_products(self, cr, uid, ids, context=None): diff --git a/addons/website_quote/models/payment.py b/addons/website_quote/models/payment.py new file mode 100644 index 00000000000..991decaa3cb --- /dev/null +++ b/addons/website_quote/models/payment.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- + +from openerp import SUPERUSER_ID +from openerp.osv import orm, fields + + +class PaymentTransaction(orm.Model): + _inherit = 'payment.transaction' + + _columns = { + # link with the sale order + 'sale_order_id': fields.many2one('sale.order', 'Sale Order'), + } + + def form_feedback(self, cr, uid, data, acquirer_name, context=None): + """ Override to confirm the sale order, if defined, and if the transaction + is done. """ + tx = None + res = super(PaymentTransaction, self).form_feedback(cr, uid, data, acquirer_name, context=context) + + # fetch the tx, check its state, confirm the potential SO + tx_find_method_name = '_%s_form_get_tx_from_data' % acquirer_name + if hasattr(self, tx_find_method_name): + tx = getattr(self, tx_find_method_name)(cr, uid, data, context=context) + if tx and tx.state == 'done' and tx.acquirer_id.auto_confirm == 'at_pay_confirm' and tx.sale_order_id and tx.sale_order_id.state in ['draft', 'sent']: + self.pool['sale.order'].action_button_confirm(cr, SUPERUSER_ID, [tx.sale_order_id.id], context=context) + + return res diff --git a/addons/website_quote/static/src/js/website_quotation.js b/addons/website_quote/static/src/js/website_quotation.js index feb1901677a..61d54e8a29a 100644 --- a/addons/website_quote/static/src/js/website_quotation.js +++ b/addons/website_quote/static/src/js/website_quotation.js @@ -57,34 +57,38 @@ this.$("#signature").jSignature('reset'); }, submitForm: function(ev){ - ev.preventDefault(); // extract data var self = this; var href = self.$el.find('form').attr("action"); - var order_id = href.match(/accept\/([0-9]+)/); + var action = href.match(/quote\/([a-z]+)/); + var order_id = href.match(/quote\/[a-z]+\/([0-9]+)/); var token = href.match(/token=(.*)/); if (token){ token = token[1]; } - // process : display errors, or submit - var signer_name = self.$("#name").val(); - var signature = self.$("#signature").jSignature("getData",'image'); - var is_empty = signature ? this.empty_sign[1] == signature[1] : false; - self.$('#signer').toggleClass('has-error', !signer_name); - self.$('#drawsign').toggleClass('panel-danger', is_empty).toggleClass('panel-default', !is_empty); - if (is_empty || ! signer_name){ + + if (action[1]=='accept') { + ev.preventDefault(); + // process : display errors, or submit + var signer_name = self.$("#name").val(); + var signature = self.$("#signature").jSignature("getData",'image'); + var is_empty = signature ? this.empty_sign[1] == signature[1] : false; + self.$('#signer').toggleClass('has-error', !signer_name); + self.$('#drawsign').toggleClass('panel-danger', is_empty).toggleClass('panel-default', !is_empty); + if (is_empty || ! signer_name){ + return false; + } + openerp.jsonRpc("/quote/"+action[1], 'call', { + 'order_id': parseInt(order_id[1]), + 'token': token, + 'signer': signer_name, + 'sign': signature?JSON.stringify(signature[1]):false, + }).then(function (data) { + self.$el.modal('hide'); + window.location.href = '/quote/'+order_id[1]+'/'+token+'?message=3'; + }); return false; } - openerp.jsonRpc("/quote/accept", 'call', { - 'order_id': parseInt(order_id[1]), - 'token': token, - 'signer': signer_name, - 'sign': signature?JSON.stringify(signature[1]):false, - }).then(function (data) { - self.$el.modal('hide'); - window.location.href = '/quote/'+order_id[1]+'/'+token+'?message=3'; - }); - return false; }, }); @@ -155,3 +159,34 @@ }); }()); + +// dbo note: website_sale code for payment +// if we standardize payment somehow, this should disappear +$(document).ready(function () { + + // When choosing an acquirer, display its Pay Now button + var $payment = $("#payment_method"); + $payment.on("click", "input[name='acquirer']", function (ev) { + var payment_id = $(ev.currentTarget).val(); + $("div.oe_quote_acquirer_button[data-id]", $payment).addClass("hidden"); + $("div.oe_quote_acquirer_button[data-id='"+payment_id+"']", $payment).removeClass("hidden"); + }) + .find("input[name='acquirer']:checked").click(); + + // When clicking on payment button: create the tx using json then continue to the acquirer + $payment.on("click", 'button[type="submit"],button[name="submit"]', function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + var $form = $(ev.currentTarget).parents('form'); + var acquirer_id = $(ev.currentTarget).parents('div.oe_quote_acquirer_button').first().data('id'); + if (! acquirer_id) { + return false; + } + var href = $(location).attr("href"); + var order_id = href.match(/quote\/([0-9]+)/)[1]; + openerp.jsonRpc('/quote/' + order_id +'/transaction/' + acquirer_id, 'call', {}).then(function (data) { + $form.submit(); + }); + }); + +}); diff --git a/addons/website_quote/views/website_quotation.xml b/addons/website_quote/views/website_quotation.xml index 95f1c9ceedb..85ba2a73034 100644 --- a/addons/website_quote/views/website_quotation.xml +++ b/addons/website_quote/views/website_quotation.xml @@ -19,7 +19,7 @@ Products Quantity Taxes - + Discount Unit Price Price @@ -155,15 +155,15 @@ @@ -178,8 +178,8 @@
+

@@ -201,7 +206,8 @@
This offer expires in -
+
+ days @@ -255,10 +261,20 @@ and do not hesitate to contact us for any question.
+
+ + Your payment has been received, thank you for your trust. + +
+
+ + Your transaction is waiting confirmation. + +