diff --git a/addons/account_payment/__init__.py b/addons/account_payment/__init__.py index e9917144f69..3ea0f0cac45 100644 --- a/addons/account_payment/__init__.py +++ b/addons/account_payment/__init__.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import controllers from . import models \ No newline at end of file diff --git a/addons/account_payment/controllers/__init__.py b/addons/account_payment/controllers/__init__.py new file mode 100644 index 00000000000..f453df57491 --- /dev/null +++ b/addons/account_payment/controllers/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import portal +from . import payment \ No newline at end of file diff --git a/addons/account_payment/controllers/payment.py b/addons/account_payment/controllers/payment.py new file mode 100644 index 00000000000..ecd331d2d9c --- /dev/null +++ b/addons/account_payment/controllers/payment.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from werkzeug import url_encode + +from odoo import http, _ +from odoo.http import request, route + + +class PaymentPortal(http.Controller): + + @route('/invoice/pay//form_tx', type='json', auth="public", website=True) + def invoice_pay_form(self, acquirer_id, invoice_id, save_token=False, access_token=None, **kwargs): + """ Json method that creates a payment.transaction, used to create a + transaction when the user clicks on 'pay now' button on the payment + form. + + :return html: form containing all values related to the acquirer to + redirect customers to the acquirer website """ + success_url = kwargs.get('success_url', '/my') + callback_method = kwargs.get('callback_method', '') + + invoice_sudo = request.env['account.invoice'].sudo().browse(invoice_id) + if not invoice_sudo: + return False + + try: + acquirer = request.env['payment.acquirer'].browse(int(acquirer_id)) + except: + return False + + token = request.env['payment.token'].sudo() # currently no support of payment tokens + tx = request.env['payment.transaction'].sudo()._check_or_create_invoice_tx( + invoice_sudo, + acquirer, + payment_token=token, + tx_type='form_save' if save_token else 'form', + add_tx_values={ + 'callback_model_id': request.env['ir.model'].sudo().search([('model', '=', invoice_sudo._name)], limit=1).id, + 'callback_res_id': invoice_sudo.id, + 'callback_method': callback_method, + }) + + # set the transaction id into the session + request.session['portal_invoice_%s_transaction_id' % invoice_sudo.id] = tx.id + + return tx.render_invoice_button( + invoice_sudo, + success_url, + submit_txt=_('Pay & Confirm'), + render_values={ + 'type': 'form_save' if save_token else 'form', + 'alias_usage': _('If we store your payment information on our server, subscription payments will be made automatically.'), + } + ) + + @http.route('/invoice/pay//s2s_token_tx', type='http', auth='public', website=True) + def invoice_pay_token(self, invoice_id, pm_id=None, **kwargs): + """ Use a token to perform a s2s transaction """ + error_url = kwargs.get('error_url', '/my') + success_url = kwargs.get('success_url', '/my') + callback_method = kwargs.get('callback_method', '') + access_token = kwargs.get('access_token') + params = {} + if access_token: + params['access_token'] = access_token + + invoice_sudo = request.env['account.invoice'].sudo().browse(invoice_id) + if not invoice_sudo: + params['error'] = 'pay_invoice_invalid_doc' + return request.redirect('%s?%s' % (error_url, url_encode(params))) + + try: + token = request.env['payment.token'].sudo().browse(int(pm_id)) + except (ValueError, TypeError): + token = False + if not token: + params['error'] = 'pay_invoice_invalid_token' + return request.redirect('%s?%s' % (error_url, url_encode(params))) + + # find an existing tx or create a new one + tx = request.env['payment.transaction'].sudo()._check_or_create_invoice_tx( + invoice_sudo, + token.acquirer_id, + payment_token=token, + tx_type='server2server', + add_tx_values={ + 'callback_model_id': request.env['ir.model'].sudo().search([('model', '=', invoice_sudo._name)], limit=1).id, + 'callback_res_id': invoice_sudo.id, + 'callback_method': callback_method, + }) + + # set the transaction id into the session + request.session['portal_invoice_%s_transaction_id' % invoice_sudo.id] = tx.id + + # proceed to the payment + res = tx.confirm_invoice_token() + if res is not True: + params['error'] = res + return request.redirect('%s?%s' % (error_url, url_encode(params))) + + params['success'] = 'pay_invoice' + return request.redirect('%s?%s' % (success_url, url_encode(params))) diff --git a/addons/account_payment/controllers/portal.py b/addons/account_payment/controllers/portal.py new file mode 100644 index 00000000000..3f3c804e607 --- /dev/null +++ b/addons/account_payment/controllers/portal.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.account.controllers.portal import CustomerPortal +from odoo.http import request + + +class CustomerPortal(CustomerPortal): + + def _invoice_get_page_view_values(self, invoice, access_token, **kwargs): + values = super(CustomerPortal, self)._invoice_get_page_view_values(invoice, access_token, **kwargs) + values.update(request.env['payment.acquirer']._get_available_payment_input(invoice.partner_id, invoice.company_id)) + return values diff --git a/addons/account_payment/models/payment.py b/addons/account_payment/models/payment.py index 7fa7899be93..b0dfeb691bb 100644 --- a/addons/account_payment/models/payment.py +++ b/addons/account_payment/models/payment.py @@ -1,10 +1,165 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import fields, models +import logging + +from odoo import fields, models, _ +from odoo.tools import float_compare + +_logger = logging.getLogger(__name__) class PaymentTransaction(models.Model): _inherit = 'payment.transaction' account_invoice_id = fields.Many2one('account.invoice', string='Invoice') + + def form_feedback(self, data, acquirer_name): + """ Override to confirm the invoice, if defined, and if the transaction is done. """ + tx = None + res = super(PaymentTransaction, self).form_feedback(data, acquirer_name) + + # fetch the tx + 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)(data) + + if tx and tx.account_invoice_id: + _logger.info( + '<%s> transaction <%s> processing form feedback for invoice <%s>: tx ref:%s, tx amount: %s', + acquirer_name, tx.id, tx.account_invoice_id.id, tx.reference, tx.amount) + tx._confirm_invoice() + + return res + + def confirm_invoice_token(self): + """ Confirm a transaction token and call SO confirmation if it is a success. + + :return: True if success; error string otherwise """ + self.ensure_one() + if self.payment_token_id and self.partner_id == self.account_invoice_id.partner_id: + try: + s2s_result = self.s2s_do_transaction() + except Exception as e: + _logger.warning( + _("<%s> transaction (%s) failed : <%s>") % + (self.acquirer_id.provider, self.id, str(e))) + return 'pay_invoice_tx_fail' + + valid_state = 'authorized' if self.acquirer_id.capture_manually else 'done' + if not s2s_result or self.state != valid_state: + _logger.warning( + _("<%s> transaction (%s) invalid state : %s") % + (self.acquirer_id.provider, self.id, self.state_mesage)) + return 'pay_invoice_tx_state' + + try: + # Auto-confirm SO if necessary + return self._confirm_invoice() + except Exception as e: + _logger.warning( + _("<%s> transaction (%s) invoice confirmation failed : <%s>") % + (self.acquirer_id.provider, self.id, str(e))) + return 'pay_invoice_tx_confirm' + return 'pay_invoice_tx_token' + + def _confirm_invoice(self): + """ Check tx state, confirm and pay potential invoice """ + self.ensure_one() + # check tx state, confirm the potential SO + if self.account_invoice_id.state != 'open': + _logger.warning('<%s> transaction STATE INCORRECT for invoice %s (ID %s, state %s)', self.acquirer_id.provider, self.account_invoice_id.number, self.account_invoice_id.id, self.account_invoice_id.state) + return 'pay_invoice_invalid_doc_state' + if not float_compare(self.amount, self.account_invoice_id.amount_total, 2) == 0: + _logger.warning('<%s> transaction AMOUNT MISMATCH for invoice %s (ID %s)', self.acquirer_id.provider, self.account_invoice_id.number, self.account_invoice_id.id) + return 'pay_invoice_tx_amount' + + if self.state == 'authorized' and self.acquirer_id.capture_manually: + _logger.info('<%s> transaction authorized, nothing to do with invoice %s (ID %s)', self.acquirer_id.provider, self.account_invoice_id.number, self.account_invoice_id.id) + elif self.state == 'done': + _logger.info('<%s> transaction completed, paying invoice %s (ID %s)', self.acquirer_id.provider, self.account_invoice_id.number, self.account_invoice_id.id) + self._pay_invoice() + else: + _logger.warning('<%s> transaction MISMATCH for invoice %s (ID %s)', self.acquirer_id.provider, self.account_invoice_id.number, self.account_invoice_id.id) + return 'pay_invoice_tx_state' + return True + + def _pay_invoice(self): + self.ensure_one() + + # force company to ensure journals/accounts etc. are correct + # company_id needed for default_get on account.journal + # force_company needed for company_dependent fields + ctx_company = {'company_id': self.account_invoice_id.company_id.id, + 'force_company': self.account_invoice_id.company_id.id} + invoice = self.account_invoice_id.with_context(**ctx_company) + + if not self.acquirer_id.journal_id: + default_journal = self.env['account.journal'].search([('type', '=', 'bank')], limit=1) + if not default_journal: + _logger.warning('<%s> transaction completed, could not auto-generate payment for invoice %s (ID %s) (no journal set on acquirer)', + self.acquirer_id.provider, self.account_invoice_id.number, self.account_invoice_id.id) + return False + self.acquirer_id.journal_id = default_journal + + invoice.pay_and_reconcile(self.acquirer_id.journal_id, pay_amount=invoice.amount_total) + invoice.payment_ids.write({'payment_transaction_id': self.ids[0]}) + _logger.info('<%s> transaction <%s> completed, reconciled invoice %s (ID %s))', + self.acquirer_id.provider, self.ids[0], invoice.number, invoice.id) + + return True + + def render_invoice_button(self, invoice, return_url, submit_txt=None, render_values=None): + values = { + 'return_url': return_url, + 'partner_id': invoice.partner_id.id, + } + if render_values: + values.update(render_values) + return self.acquirer_id.with_context(submit_class='btn btn-primary', submit_txt=submit_txt or _('Pay Now')).sudo().render( + self.reference, + invoice.amount_total, + invoice.currency_id.id, + values=values, + ) + + def _check_or_create_invoice_tx(self, invoice, acquirer, payment_token=None, tx_type='form', add_tx_values=None): + tx = self + if not tx: + tx = self.search([('reference', '=', invoice.number)], limit=1) + + if tx and tx.state in ['error', 'cancel']: # filter incorrect states + tx = False + if (tx and acquirer and tx.acquirer_id != acquirer) or (tx and tx.account_invoice_id != invoice): # filter unmatching + tx = False + if tx and tx.payment_token_id and payment_token and payment_token != tx.payment_token_id: # new or distinct token + tx = False + + # still draft tx, no more info -> create a new one + if tx and tx.state == 'draft': + tx = False + + if not tx: + tx_values = { + 'acquirer_id': acquirer.id, + 'type': tx_type, + 'amount': invoice.amount_total, + 'currency_id': invoice.currency_id.id, + 'partner_id': invoice.partner_id.id, + 'partner_country_id': invoice.partner_id.country_id.id, + 'reference': self.get_next_reference(invoice.number), + 'account_invoice_id': invoice.id, + } + if add_tx_values: + tx_values.update(add_tx_values) + if payment_token and payment_token.sudo().partner_id == invoice.partner_id: + tx_values['payment_token_id'] = payment_token.id + + tx = self.create(tx_values) + + # update invoice + invoice.write({ + 'payment_tx_id': tx.id, + }) + + return tx diff --git a/addons/account_payment/views/account_portal_templates.xml b/addons/account_payment/views/account_portal_templates.xml index 12dc68013af..86541440506 100644 --- a/addons/account_payment/views/account_portal_templates.xml +++ b/addons/account_payment/views/account_portal_templates.xml @@ -5,10 +5,122 @@ - + - + + + + + + + + + \ No newline at end of file