diff --git a/addons/payment_sips/__init__.py b/addons/payment_sips/__init__.py
index 0d6cb337a6e..ba88cb2944c 100644
--- a/addons/payment_sips/__init__.py
+++ b/addons/payment_sips/__init__.py
@@ -1,7 +1,11 @@
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
from . import models
from . import controllers
-from odoo.addons.payment.models.payment_acquirer import create_missing_journal_for_acquirers
-from odoo.addons.payment import reset_payment_provider
+
+from odoo.addons.payment import reset_payment_acquirer
+from odoo.addons.payment.models.payment_acquirer import create_missing_journals # post-init hook
+
def uninstall_hook(cr, registry):
- reset_payment_provider(cr, registry, 'sips')
+ reset_payment_acquirer(cr, registry, 'sips')
diff --git a/addons/payment_sips/__manifest__.py b/addons/payment_sips/__manifest__.py
index c3afefb2714..c6628c76a1a 100644
--- a/addons/payment_sips/__manifest__.py
+++ b/addons/payment_sips/__manifest__.py
@@ -1,12 +1,9 @@
-# -*- encoding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-# Copyright 2015 Eezee-It
+# Original Copyright 2015 Eezee-It, modified and maintained by Odoo.
{
'name': 'Worldline SIPS',
- 'version': '1.1',
- 'author': 'Eezee-It',
+ 'version': '2.0',
'category': 'Accounting/Payment Acquirers',
'sequence': 385,
'description': """
@@ -21,8 +18,7 @@ not guaranteed.""",
'views/payment_sips_templates.xml',
'data/payment_acquirer_data.xml',
],
- 'installable': True,
'application': True,
- 'post_init_hook': 'create_missing_journal_for_acquirers',
+ 'post_init_hook': 'create_missing_journals',
'uninstall_hook': 'uninstall_hook',
}
diff --git a/addons/payment_sips/controllers/__init__.py b/addons/payment_sips/controllers/__init__.py
index 65a8c12013d..80ee4da1c5e 100644
--- a/addons/payment_sips/controllers/__init__.py
+++ b/addons/payment_sips/controllers/__init__.py
@@ -1,3 +1,3 @@
-# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import main
diff --git a/addons/payment_sips/controllers/main.py b/addons/payment_sips/controllers/main.py
index 1a9f99fa701..c9d53415f1a 100644
--- a/addons/payment_sips/controllers/main.py
+++ b/addons/payment_sips/controllers/main.py
@@ -1,50 +1,57 @@
-# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+# Original Copyright 2015 Eezee-It, modified and maintained by Odoo.
-# Copyright 2015 Eezee-It
-
-import json
import logging
import pprint
+
import werkzeug
from odoo import http
+from odoo.exceptions import ValidationError
from odoo.http import request
_logger = logging.getLogger(__name__)
class SipsController(http.Controller):
- _notify_url = '/payment/sips/ipn/'
_return_url = '/payment/sips/dpn/'
+ _notify_url = '/payment/sips/ipn/'
- def sips_validate_data(self, **post):
- sips = request.env['payment.acquirer'].search([('provider', '=', 'sips')], limit=1)
- security = sips.sudo()._sips_generate_shasign(post)
- if security == post['Seal']:
- _logger.debug('Sips: validated data')
- return request.env['payment.transaction'].sudo().form_feedback(post, 'sips')
- _logger.warning('Sips: data are corrupted')
- return False
-
- @http.route('/payment/sips/ipn/', type='http', auth='public', methods=['POST'], csrf=False)
- def sips_ipn(self, **post):
- """ Sips IPN. """
- _logger.info('Beginning Sips IPN form_feedback with post data %s', pprint.pformat(post)) # debug
- if not post:
- # SIPS sometimes sends empty notifications, the reason why is
- # unclear but they tend to pollute logs and do not provide any
- # meaningful information; log as a warning instead of a traceback
- _logger.warning('Sips: received empty notification; skip.')
- else:
- self.sips_validate_data(**post)
- return ''
-
- @http.route('/payment/sips/dpn', type='http', auth="public", methods=['POST'], csrf=False)
+ @http.route(_return_url, type='http', auth='public', methods=['POST'], csrf=False)
def sips_dpn(self, **post):
""" Sips DPN """
+ _logger.info("beginning Sips DPN _handle_feedback_data with data %s", pprint.pformat(post))
try:
- _logger.info('Beginning Sips DPN form_feedback with post data %s', pprint.pformat(post)) # debug
- self.sips_validate_data(**post)
- except:
+ if self._sips_validate_data(post):
+ request.env['payment.transaction'].sudo()._handle_feedback_data('sips', post)
+ except ValidationError:
pass
- return werkzeug.utils.redirect('/payment/process')
+ return werkzeug.utils.redirect('/payment/status')
+
+ @http.route(_notify_url, type='http', auth='public', methods=['POST'], csrf=False)
+ def sips_ipn(self, **post):
+ """ Sips IPN. """
+ _logger.info("beginning Sips IPN _handle_feedback_data with data %s", pprint.pformat(post))
+ if not post:
+ # SIPS sometimes sends empty notifications, the reason why is unclear but they tend to
+ # pollute logs and do not provide any meaningful information; log as a warning instead
+ # of a traceback.
+ _logger.warning("received empty notification; skip.")
+ else:
+ try:
+ if self._sips_validate_data(post):
+ request.env['payment.transaction'].sudo()._handle_feedback_data('sips', post)
+ except ValidationError:
+ pass # Acknowledge the notification to avoid getting spammed
+ return ''
+
+ def _sips_validate_data(self, post):
+ tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_feedback_data('sips', post)
+ acquirer_sudo = tx_sudo.acquirer_id
+ security = acquirer_sudo._sips_generate_shasign(post['Data'])
+ if security == post['Seal']:
+ _logger.debug('validated data')
+ return True
+ else:
+ _logger.warning('data are tampered')
+ return False
diff --git a/addons/payment_sips/data/payment_acquirer_data.xml b/addons/payment_sips/data/payment_acquirer_data.xml
index f6a489c72cf..043cbece8a3 100644
--- a/addons/payment_sips/data/payment_acquirer_data.xml
+++ b/addons/payment_sips/data/payment_acquirer_data.xml
@@ -1,14 +1,12 @@
-
-
+
-
- Sips
-
- sips
-
-
-
+
+ sips
+
+ False
+ False
+ False
+
-
diff --git a/addons/payment_sips/models/__init__.py b/addons/payment_sips/models/__init__.py
index ef12533682f..8e04237423a 100644
--- a/addons/payment_sips/models/__init__.py
+++ b/addons/payment_sips/models/__init__.py
@@ -1,3 +1,4 @@
-# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from . import payment
+from . import payment_acquirer
+from . import payment_transaction
diff --git a/addons/payment_sips/models/const.py b/addons/payment_sips/models/const.py
index 7c6dd9374ee..4b6bd9094fb 100644
--- a/addons/payment_sips/models/const.py
+++ b/addons/payment_sips/models/const.py
@@ -1,7 +1,4 @@
-
-from collections import namedtuple
-
-Currency = namedtuple('Currency', ['iso_id', 'decimal'])
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
# ISO 4217 Data for currencies supported by sips
# NOTE: these are listed on the Atos Wordline SIPS POST documentation page
@@ -12,52 +9,63 @@ Currency = namedtuple('Currency', ['iso_id', 'decimal'])
# restrictive, I'm gonna assume they are supported when using the right flow
# and payment methods, which may not work in SIMU...
# Since SIPS advises to use 'in production', well...
-SIPS_SUPPORTED_CURRENCIES = {
- 'ARS': Currency('032', 2),
- 'AUD': Currency('036', 2),
- 'BHD': Currency('048', 3),
- 'KHR': Currency('116', 2),
- 'CAD': Currency('124', 2),
- 'LKR': Currency('144', 2),
- 'CNY': Currency('156', 2),
- 'HRK': Currency('191', 2),
- 'CZK': Currency('203', 2),
- 'DKK': Currency('208', 2),
- 'HKD': Currency('344', 2),
- 'HUF': Currency('348', 2),
- 'ISK': Currency('352', 0),
- 'INR': Currency('356', 2),
- 'ILS': Currency('376', 2),
- 'JPY': Currency('392', 0),
- 'KRW': Currency('410', 0),
- 'KWD': Currency('414', 3),
- 'MYR': Currency('458', 2),
- 'MUR': Currency('480', 2),
- 'MXN': Currency('484', 2),
- 'NPR': Currency('524', 2),
- 'NZD': Currency('554', 2),
- 'NOK': Currency('578', 2),
- 'QAR': Currency('634', 2),
- 'RUB': Currency('643', 2),
- 'SAR': Currency('682', 2),
- 'SGD': Currency('702', 2),
- 'ZAR': Currency('710', 2),
- 'SEK': Currency('752', 2),
- 'CHF': Currency('756', 2),
- 'THB': Currency('764', 2),
- 'AED': Currency('784', 2),
- 'TND': Currency('788', 3),
- 'GBP': Currency('826', 2),
- 'USD': Currency('840', 2),
- 'TWD': Currency('901', 2),
- 'RSD': Currency('941', 2),
- 'RON': Currency('946', 2),
- 'TRY': Currency('949', 2),
- 'XOF': Currency('952', 0),
- 'XPF': Currency('953', 0),
- 'BGN': Currency('975', 2),
- 'EUR': Currency('978', 2),
- 'UAH': Currency('980', 2),
- 'PLN': Currency('996', 2),
- 'BRL': Currency('986', 2),
+SUPPORTED_CURRENCIES = {
+ 'ARS': '032',
+ 'AUD': '036',
+ 'BHD': '048',
+ 'KHR': '116',
+ 'CAD': '124',
+ 'LKR': '144',
+ 'CNY': '156',
+ 'HRK': '191',
+ 'CZK': '203',
+ 'DKK': '208',
+ 'HKD': '344',
+ 'HUF': '348',
+ 'ISK': '352',
+ 'INR': '356',
+ 'ILS': '376',
+ 'JPY': '392',
+ 'KRW': '410',
+ 'KWD': '414',
+ 'MYR': '458',
+ 'MUR': '480',
+ 'MXN': '484',
+ 'NPR': '524',
+ 'NZD': '554',
+ 'NOK': '578',
+ 'QAR': '634',
+ 'RUB': '643',
+ 'SAR': '682',
+ 'SGD': '702',
+ 'ZAR': '710',
+ 'SEK': '752',
+ 'CHF': '756',
+ 'THB': '764',
+ 'AED': '784',
+ 'TND': '788',
+ 'GBP': '826',
+ 'USD': '840',
+ 'TWD': '901',
+ 'RSD': '941',
+ 'RON': '946',
+ 'TRY': '949',
+ 'XOF': '952',
+ 'XPF': '953',
+ 'BGN': '975',
+ 'EUR': '978',
+ 'UAH': '980',
+ 'PLN': '996',
+ 'BRL': '986',
+}
+
+# Mapping of transaction states to Sips response codes.
+# See https://documentation.sips.worldline.com/en/WLSIPS.001-GD-Data-dictionary.html#Sips.001_DD_en-Value-currencyCode
+RESPONSE_CODES_MAPPING = {
+ 'pending': ('60',),
+ 'done': ('00',),
+ 'cancel': (
+ '03', '05', '12', '14', '17', '24', '25', '30', '34', '40', '51', '54', '63', '75', '90',
+ '94', '97', '99'
+ ),
}
diff --git a/addons/payment_sips/models/payment.py b/addons/payment_sips/models/payment.py
deleted file mode 100644
index 979e002b181..00000000000
--- a/addons/payment_sips/models/payment.py
+++ /dev/null
@@ -1,215 +0,0 @@
-# coding: utf-8
-
-# Copyright 2015 Eezee-It
-
-import datetime
-from dateutil import parser
-import json
-import logging
-import pytz
-import re
-import time
-from hashlib import sha256
-
-from werkzeug import urls
-
-from odoo import models, fields, api
-from odoo.tools.float_utils import float_compare
-from odoo.tools.translate import _
-from odoo.addons.payment.models.payment_acquirer import ValidationError
-from odoo.addons.payment_sips.controllers.main import SipsController
-
-from .const import SIPS_SUPPORTED_CURRENCIES
-
-_logger = logging.getLogger(__name__)
-
-
-class AcquirerSips(models.Model):
- _inherit = 'payment.acquirer'
-
- provider = fields.Selection(selection_add=[('sips', 'Sips')], ondelete={'sips': 'set default'})
- sips_merchant_id = fields.Char('Merchant ID', required_if_provider='sips', groups='base.group_user')
- sips_secret = fields.Char('Secret Key', size=64, required_if_provider='sips', groups='base.group_user')
- sips_test_url = fields.Char("Test url", required_if_provider='sips', default='https://payment-webinit.simu.sips-atos.com/paymentInit')
- sips_prod_url = fields.Char("Production url", required_if_provider='sips', default='https://payment-webinit.sips-atos.com/paymentInit')
- sips_version = fields.Char("Interface Version", required_if_provider='sips', default='HP_2.31')
- sips_key_version = fields.Integer("Secret Key Version", required_if_provider='sips', default=2)
-
- def _sips_generate_shasign(self, values):
- """ Generate the shasign for incoming or outgoing communications.
- :param dict values: transaction values
- :return string: shasign
- """
- if self.provider != 'sips':
- raise ValidationError(_('Incorrect payment acquirer provider'))
- data = values['Data']
- key = self.sips_secret
-
- shasign = sha256((data + key).encode('utf-8'))
- return shasign.hexdigest()
-
- def sips_form_generate_values(self, values):
- self.ensure_one()
- base_url = self.get_base_url()
- currency = self.env['res.currency'].sudo().browse(values['currency_id'])
- sips_currency = SIPS_SUPPORTED_CURRENCIES.get(currency.name)
- if not sips_currency:
- raise ValidationError(_('Currency not supported by Wordline: %s') % currency.name)
- # rounded to its smallest unit, depends on the currency
- amount = round(values['amount'] * (10 ** sips_currency.decimal))
-
- sips_tx_values = dict(values)
- data = {
- 'amount': amount,
- 'currencyCode': sips_currency.iso_id,
- 'merchantId': self.sips_merchant_id,
- 'normalReturnUrl': urls.url_join(base_url, SipsController._return_url),
- 'automaticResponseUrl': urls.url_join(base_url, SipsController._notify_url),
- 'transactionReference': values['reference'],
- 'statementReference': values['reference'],
- 'keyVersion': self.sips_key_version,
- }
- sips_tx_values.update({
- 'Data': '|'.join([f'{k}={v}' for k,v in data.items()]),
- 'InterfaceVersion': self.sips_version,
- })
-
- return_context = {}
- if sips_tx_values.get('return_url'):
- return_context['return_url'] = urls.url_quote(sips_tx_values.get('return_url'))
- return_context['reference'] = sips_tx_values['reference']
- sips_tx_values['Data'] += '|returnContext=%s' % (json.dumps(return_context))
-
- shasign = self._sips_generate_shasign(sips_tx_values)
- sips_tx_values['Seal'] = shasign
- return sips_tx_values
-
- def sips_get_form_action_url(self):
- self.ensure_one()
- return self.sips_prod_url if self.state == 'enabled' else self.sips_test_url
-
-
-class TxSips(models.Model):
- _inherit = 'payment.transaction'
-
- _sips_valid_tx_status = ['00']
- _sips_wait_tx_status = ['90', '99']
- _sips_refused_tx_status = ['05', '14', '34', '54', '75', '97']
- _sips_error_tx_status = ['03', '12', '24', '25', '30', '40', '51', '63', '94']
- _sips_pending_tx_status = ['60']
- _sips_cancel_tx_status = ['17']
-
- @api.model
- def _compute_reference(self, values=None, prefix=None):
- res = super()._compute_reference(values=values, prefix=prefix)
- acquirer = self.env['payment.acquirer'].browse(values.get('acquirer_id'))
- if acquirer and acquirer.provider == 'sips':
- return re.sub(r'[^0-9a-zA-Z]+', 'x', res) + 'x' + str(int(time.time()))
- return res
-
- # --------------------------------------------------
- # FORM RELATED METHODS
- # --------------------------------------------------
-
- def _sips_data_to_object(self, data):
- res = {}
- for element in data.split('|'):
- (key, value) = element.split('=')
- res[key] = value
- return res
-
- @api.model
- def _sips_form_get_tx_from_data(self, data):
- """ Given a data dict coming from sips, verify it and find the related
- transaction record. """
-
- data = self._sips_data_to_object(data.get('Data'))
- reference = data.get('transactionReference')
-
- if not reference:
- return_context = json.loads(data.get('returnContext', '{}'))
- reference = return_context.get('reference')
-
- payment_tx = self.search([('reference', '=', reference)])
- if not payment_tx:
- error_msg = _('Sips: received data for reference %s; no order found') % reference
- _logger.error(error_msg)
- raise ValidationError(error_msg)
- return payment_tx
-
- def _sips_form_get_invalid_parameters(self, data):
- invalid_parameters = []
-
- data = self._sips_data_to_object(data.get('Data'))
-
- # amounts should match
- # get currency decimals from const
- sips_currency = SIPS_SUPPORTED_CURRENCIES.get(self.currency_id.name)
- # convert from int to float using decimals from currency
- amount_converted = float(data.get('amount', '0.0')) / (10 ** sips_currency.decimal)
- if float_compare(amount_converted, self.amount, sips_currency.decimal) != 0:
- invalid_parameters.append(('amount', data.get('amount'), '%.2f' % self.amount))
-
- return invalid_parameters
-
- def _sips_form_validate(self, data):
- data = self._sips_data_to_object(data.get('Data'))
- status = data.get('responseCode')
- date = data.get('transactionDateTime')
- if date:
- try:
- # dateutil.parser 2.5.3 and up should handle dates formatted as
- # '2020-04-08T05:54:18+02:00', which strptime does not
- # (+02:00 does not work as %z expects +0200 before Python 3.7)
- # See odoo/odoo#49160
- date = parser.parse(date).astimezone(pytz.utc).replace(tzinfo=None)
- except:
- # fallback on now to avoid failing to register the payment
- # because a provider formats their dates badly or because
- # some library is not behaving
- date = fields.Datetime.now()
- data = {
- 'acquirer_reference': data.get('transactionReference'),
- 'date': date,
- }
- res = False
- if status in self._sips_valid_tx_status:
- msg = f'ref: {self.reference}, got valid response [{status}], set as done.'
- _logger.info(msg)
- data.update(state_message=msg)
- self.write(data)
- self._set_transaction_done()
- res = True
- elif status in self._sips_error_tx_status:
- msg = f'ref: {self.reference}, got response [{status}], set as cancel.'
- data.update(state_message=msg)
- self.write(data)
- self._set_transaction_cancel()
- elif status in self._sips_wait_tx_status:
- msg = f'ref: {self.reference}, got wait response [{status}], set as cancel.'
- data.update(state_message=msg)
- self.write(data)
- self._set_transaction_cancel()
- elif status in self._sips_refused_tx_status:
- msg = f'ref: {self.reference}, got refused response [{status}], set as cancel.'
- data.update(state_message=msg)
- self.write(data)
- self._set_transaction_cancel()
- elif status in self._sips_pending_tx_status:
- msg = f'ref: {self.reference}, got pending response [{status}], set as pending.'
- data.update(state_message=msg)
- self.write(data)
- self._set_transaction_pending()
- elif status in self._sips_cancel_tx_status:
- msg = f'ref: {self.reference}, got cancel response [{status}], set as cancel.'
- data.update(state_message=msg)
- self.write(data)
- self._set_transaction_cancel()
- else:
- msg = f'ref: {self.reference}, got unrecognized response [{status}], set as cancel.'
- data.update(state_message=msg)
- self.write(data)
- self._set_transaction_cancel()
-
- _logger.info(msg)
- return res
diff --git a/addons/payment_sips/models/payment_acquirer.py b/addons/payment_sips/models/payment_acquirer.py
new file mode 100644
index 00000000000..b96a2c17446
--- /dev/null
+++ b/addons/payment_sips/models/payment_acquirer.py
@@ -0,0 +1,56 @@
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+# Original Copyright 2015 Eezee-It, modified and maintained by Odoo.
+
+from hashlib import sha256
+
+from odoo import api, fields, models
+
+from .const import SUPPORTED_CURRENCIES
+
+
+class PaymentAcquirer(models.Model):
+ _inherit = 'payment.acquirer'
+
+ provider = fields.Selection(
+ selection_add=[('sips', "Sips")], ondelete={'sips': 'set default'})
+ sips_merchant_id = fields.Char(
+ string="Merchant ID", help="The ID solely used to identify the merchant account with Sips",
+ required_if_provider='sips')
+ sips_secret = fields.Char(
+ string="SIPS Secret Key", size=64, required_if_provider='sips', groups='base.group_system')
+ sips_key_version = fields.Integer(
+ string="Secret Key Version", required_if_provider='sips', default=2)
+ sips_test_url = fields.Char(
+ string="Test URL", required_if_provider='sips',
+ default="https://payment-webinit.simu.sips-atos.com/paymentInit")
+ sips_prod_url = fields.Char(
+ string="Production URL", required_if_provider='sips',
+ default="https://payment-webinit.sips-atos.com/paymentInit")
+ sips_version = fields.Char(
+ string="Interface Version", required_if_provider='sips', default="HP_2.31")
+
+ @api.model
+ def _get_compatible_acquirers(self, *args, currency_id=None, **kwargs):
+ """ Override of payment to unlist Sips acquirers when the currency is not supported. """
+ acquirers = super()._get_compatible_acquirers(*args, currency_id=currency_id, **kwargs)
+
+ currency = self.env['res.currency'].browse(currency_id).exists()
+ if currency and currency.name not in SUPPORTED_CURRENCIES:
+ acquirers = acquirers.filtered(lambda a: a.provider != 'sips')
+
+ return acquirers
+
+ def _sips_generate_shasign(self, data):
+ """ Generate the shasign for incoming or outgoing communications.
+
+ Note: self.ensure_one()
+
+ :param str data: The data to use to generate the shasign
+ :return: shasign
+ :rtype: str
+ """
+ self.ensure_one()
+
+ key = self.sips_secret
+ shasign = sha256((data + key).encode('utf-8'))
+ return shasign.hexdigest()
diff --git a/addons/payment_sips/models/payment_transaction.py b/addons/payment_sips/models/payment_transaction.py
new file mode 100644
index 00000000000..139c2e7e1bc
--- /dev/null
+++ b/addons/payment_sips/models/payment_transaction.py
@@ -0,0 +1,168 @@
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+# Original Copyright 2015 Eezee-It, modified and maintained by Odoo.
+
+import json
+import logging
+
+from werkzeug import urls
+
+from odoo import _, api, models
+from odoo.exceptions import ValidationError
+
+from odoo.addons.payment import utils as payment_utils
+from odoo.addons.payment_sips.controllers.main import SipsController
+from .const import RESPONSE_CODES_MAPPING, SUPPORTED_CURRENCIES
+
+_logger = logging.getLogger(__name__)
+
+
+class PaymentTransaction(models.Model):
+ _inherit = 'payment.transaction'
+
+ @api.model
+ def _compute_reference(self, provider, prefix=None, separator='-', **kwargs):
+ """ Override of payment to ensure that Sips requirements for references are satisfied.
+
+ Sips requirements for transaction are as follows:
+ - References can only be made of alphanumeric characters.
+ This is satisfied by forcing the custom separator to 'x' to ensure that no '-' character
+ will be used to append a suffix. Additionally, the prefix is sanitized if it was provided,
+ and generated with 'tx' as default otherwise. This prevents the prefix to be generated
+ based on document names that may contain non-alphanum characters (eg: INV/2020/...).
+ - References must be unique at provider level for a given merchant account.
+ This is satisfied by singularizing the prefix with the current datetime. If two
+ transactions are created simultaneously, `_compute_reference` ensures the uniqueness of
+ references by suffixing a sequence number.
+
+ :param str provider: The provider of the acquirer handling the transaction
+ :param str prefix: The custom prefix used to compute the full reference
+ :param str separator: The custom separator used to separate the prefix from the suffix
+ :return: The unique reference for the transaction
+ :rtype: str
+ """
+ if provider == 'sips':
+ # We use an empty separator for cosmetic reasons: As the default prefix is 'tx', we want
+ # the singularized prefix to look like 'tx2020...' and not 'txx2020...'.
+ prefix = payment_utils.singularize_reference_prefix(separator='')
+ separator = 'x' # Still, we need a dedicated separator between the prefix and the seq.
+ return super()._compute_reference(provider, prefix=prefix, separator=separator, **kwargs)
+
+ def _get_specific_rendering_values(self, processing_values):
+ """ Override of payment to return Sips-specific rendering values.
+
+ Note: self.ensure_one() from `_get_processing_values`
+
+ :param dict processing_values: The generic and specific processing values of the transaction
+ :return: The dict of acquirer-specific processing values
+ :rtype: dict
+ """
+ res = super()._get_specific_rendering_values(processing_values)
+ if self.provider != 'sips':
+ return res
+
+ base_url = self.get_base_url()
+ data = {
+ 'amount': payment_utils.to_minor_currency_units(self.amount, self.currency_id),
+ 'currencyCode': SUPPORTED_CURRENCIES[self.currency_id.name], # The ISO 4217 code
+ 'merchantId': self.acquirer_id.sips_merchant_id,
+ 'normalReturnUrl': urls.url_join(base_url, SipsController._return_url),
+ 'automaticResponseUrl': urls.url_join(base_url, SipsController._notify_url),
+ 'transactionReference': self.reference,
+ 'statementReference': self.reference,
+ 'keyVersion': self.acquirer_id.sips_key_version,
+ 'returnContext': json.dumps(dict(reference=self.reference)),
+ }
+ api_url = self.acquirer_id.sips_prod_url if self.acquirer_id.state == 'enabled' \
+ else self.acquirer_id.sips_test_url
+ data = '|'.join([f'{k}={v}' for k, v in data.items()])
+ return {
+ 'api_url': api_url,
+ 'Data': data,
+ 'InterfaceVersion': self.acquirer_id.sips_version,
+ 'Seal': self.acquirer_id._sips_generate_shasign(data),
+ }
+
+ @api.model
+ def _get_tx_from_feedback_data(self, provider, data):
+ """ Override of payment to find the transaction based on Sips data.
+
+ :param str provider: The provider of the acquirer that handled the transaction
+ :param dict data: The feedback data sent by the provider
+ :return: The transaction if found
+ :rtype: recordset of `payment.transaction`
+ :raise: ValidationError if the data match no transaction
+ :raise: ValidationError if the currency is not supported
+ :raise: ValidationError if the amount mismatch
+ """
+ tx = super()._get_tx_from_feedback_data(provider, data)
+ if provider != 'sips':
+ return tx
+
+ data = self._sips_data_to_object(data['Data'])
+ reference = data.get('transactionReference')
+
+ if not reference:
+ return_context = json.loads(data.get('returnContext', '{}'))
+ reference = return_context.get('reference')
+
+ tx = self.search([('reference', '=', reference), ('provider', '=', 'sips')])
+ if not tx:
+ raise ValidationError(
+ "Sips: " + _("No transaction found matching reference %s.", reference)
+ )
+
+ sips_currency = SUPPORTED_CURRENCIES.get(tx.currency_id.name)
+ if not sips_currency:
+ raise ValidationError(
+ "Sips: " + _("This currency is not supported: %s.", tx.currency_id.name)
+ )
+
+ amount_converted = payment_utils.to_major_currency_units(
+ float(data.get('amount', '0.0')), tx.currency_id
+ )
+ if tx.currency_id.compare_amounts(amount_converted, tx.amount) != 0:
+ raise ValidationError(
+ "Sips: " + _(
+ "Incorrect amount: received %(received).2f, expected %(expected).2f",
+ received=amount_converted, expected=tx.amount
+ )
+ )
+ return tx
+
+ def _process_feedback_data(self, data):
+ """ Override of payment to process the transaction based on Sips data.
+
+ Note: self.ensure_one()
+
+ :param dict data: The feedback data sent by the provider
+ :return: None
+ """
+ super()._process_feedback_data(data)
+ if self.provider != 'sips':
+ return
+
+ data = self._sips_data_to_object(data.get('Data'))
+ self.acquirer_reference = data.get('transactionReference')
+ response_code = data.get('responseCode')
+ if response_code in RESPONSE_CODES_MAPPING['pending']:
+ status = "pending"
+ self._set_pending()
+ elif response_code in RESPONSE_CODES_MAPPING['done']:
+ status = "done"
+ self._set_done()
+ elif response_code in RESPONSE_CODES_MAPPING['cancel']:
+ status = "cancel"
+ self._set_canceled()
+ else:
+ status = "error"
+ self._set_error(_("Unrecognized response received from the payment provider."))
+ _logger.info(
+ "ref: %s, got response [%s], set as '%s'.", self.reference, response_code, status
+ )
+
+ def _sips_data_to_object(self, data):
+ res = {}
+ for element in data.split('|'):
+ key, value = element.split('=')
+ res[key] = value
+ return res
diff --git a/addons/payment_sips/tests/__init__.py b/addons/payment_sips/tests/__init__.py
index 70427147097..d502f348aa2 100644
--- a/addons/payment_sips/tests/__init__.py
+++ b/addons/payment_sips/tests/__init__.py
@@ -1,3 +1,4 @@
-# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from . import common
from . import test_sips
diff --git a/addons/payment_sips/tests/common.py b/addons/payment_sips/tests/common.py
new file mode 100644
index 00000000000..e113c2a32aa
--- /dev/null
+++ b/addons/payment_sips/tests/common.py
@@ -0,0 +1,18 @@
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from odoo.addons.payment.tests.common import PaymentCommon
+
+
+class SipsCommon(PaymentCommon):
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+
+ cls.sips = cls._prepare_acquirer('sips', update_values={
+ 'sips_merchant_id': 'dummy_mid',
+ 'sips_secret': 'dummy_secret',
+ })
+
+ # Override default values
+ cls.acquirer = cls.sips
+ cls.currency = cls.currency_euro
diff --git a/addons/payment_sips/tests/test_sips.py b/addons/payment_sips/tests/test_sips.py
index 3b0bd2279e4..8cb413aa144 100644
--- a/addons/payment_sips/tests/test_sips.py
+++ b/addons/payment_sips/tests/test_sips.py
@@ -1,40 +1,67 @@
-# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import json
+
+from freezegun import freeze_time
+
+from odoo.exceptions import ValidationError
from odoo.tests import tagged
-from odoo.addons.payment.tests.common import PaymentAcquirerCommon
+from odoo.tools import mute_logger
+from .common import SipsCommon
+from ..controllers.main import SipsController
+from ..models.payment_acquirer import SUPPORTED_CURRENCIES
-@tagged('post_install', '-at_install', '-standard', 'external')
-class SipsTest(PaymentAcquirerCommon):
+@tagged('post_install', '-at_install')
+class SipsTest(SipsCommon):
- @classmethod
- def setUpClass(cls, chart_template_ref=None):
- super().setUpClass(chart_template_ref=chart_template_ref)
- cls.sips = cls.env.ref('payment.payment_acquirer_sips')
- cls.sips.write({
- 'state': 'test',
- 'sips_merchant_id': 'dummy_mid',
- 'sips_secret': 'dummy_secret',
- })
+ def test_compatible_acquirers(self):
+ for curr in SUPPORTED_CURRENCIES:
+ currency = self._prepare_currency(curr)
+ acquirers = self.env['payment.acquirer']._get_compatible_acquirers(
+ partner_id=self.partner.id,
+ company_id=self.company.id,
+ currency_id=currency.id,
+ )
+ self.assertIn(self.sips, acquirers)
- def test_10_sips_form_render(self):
- self.assertEqual(self.sips.state, 'test', 'test without test environment')
+ unsupported_currency = self._prepare_currency('VEF')
+ acquirers = self.env['payment.acquirer']._get_compatible_acquirers(
+ partner_id=self.partner.id,
+ company_id=self.company.id,
+ currency_id=unsupported_currency.id,
+ )
+ self.assertNotIn(self.sips, acquirers)
- # ----------------------------------------
- # Test: button direct rendering
- # ----------------------------------------
+ # freeze time for consistent singularize_prefix behavior during the test
+ @freeze_time("2011-11-02 12:00:21")
+ def test_reference(self):
+ tx = self.create_transaction(flow="redirect", reference="")
+ self.assertEqual(tx.reference, "tx20111102120021",
+ "Payulatam: transaction reference wasn't correctly singularized.")
- # render the button
- tx = self.env['payment.transaction'].create({
- 'acquirer_id': self.sips.id,
- 'amount': 100.0,
- 'reference': 'SO404',
- 'currency_id': self.currency_euro.id,
- })
- self.sips.render('SO404', 100.0, self.currency_euro.id, values=self.buyer_values).decode('utf-8')
+ def test_redirect_form_values(self):
+ tx = self.create_transaction(flow="redirect")
- def test_20_sips_form_management(self):
- self.assertEqual(self.sips.state, 'test', 'test without test environment')
+ with mute_logger('odoo.addons.payment.models.payment_transaction'):
+ processing_values = tx._get_processing_values()
+ form_info = self._extract_values_from_html_form(processing_values['redirect_form_html'])
+ form_inputs = form_info['inputs']
+ self.assertEqual(form_info['action'], self.sips.sips_test_url)
+ self.assertEqual(form_inputs['InterfaceVersion'], self.sips.sips_version)
+ return_url = self._build_url(SipsController._return_url)
+ notify_url = self._build_url(SipsController._notify_url)
+ self.assertEqual(form_inputs['Data'],
+ f'amount=111111|currencyCode=978|merchantId=dummy_mid|normalReturnUrl={return_url}|' \
+ f'automaticResponseUrl={notify_url}|transactionReference={self.reference}|' \
+ f'statementReference={self.reference}|keyVersion={self.sips.sips_key_version}|' \
+ f'returnContext={json.dumps(dict(reference=self.reference))}'
+ )
+ self.assertEqual(form_inputs['Seal'],
+ '4d7cc67c0168e8ce11c25fbe1937231c644861e320702ab544022b032b9eb4a2')
+
+ def test_feedback_processing(self):
# typical data posted by Sips after client has successfully paid
sips_post_data = {
'Data': 'captureDay=0|captureMode=AUTHOR_CAPTURE|currencyCode=840|'
@@ -56,22 +83,22 @@ class SipsTest(PaymentAcquirerCommon):
'Encode': '',
'InterfaceVersion': 'HP_2.4',
'Seal': 'f03f64da6f57c171904d12bf709b1d6d3385131ac914e97a7e1db075ed438f3e',
- 'locale': 'en'
+ 'locale': 'en',
}
- tx = self.env['payment.transaction'].create({
- 'amount': 314.0,
- 'acquirer_id': self.sips.id,
- 'currency_id': self.currency_euro.id,
- 'reference': 'SO100x1',
- 'partner_name': 'Norbert Buyer',
- 'partner_country_id': self.country_france.id})
+ with self.assertRaises(ValidationError): # unknown transaction
+ self.env['payment.transaction']._handle_feedback_data('sips', sips_post_data)
- # validate it
- tx.form_feedback(sips_post_data, 'sips')
+ self.amount = 314.0
+ self.reference = 'SO100x1'
+
+ tx = self.create_transaction(flow="redirect")
+
+ # Validate the transaction
+ self.env['payment.transaction']._handle_feedback_data('sips', sips_post_data)
self.assertEqual(tx.state, 'done', 'Sips: validation did not put tx into done state')
- self.assertEqual(tx.acquirer_reference, 'SO100x1', 'Sips: validation did not update tx id')
-
+ self.assertEqual(tx.acquirer_reference, self.reference, 'Sips: validation did not update tx id')
+
# same process for an payment in error on sips's end
sips_post_data = {
'Data': 'captureDay=0|captureMode=AUTHOR_CAPTURE|currencyCode=840|'
@@ -87,53 +114,8 @@ class SipsTest(PaymentAcquirerCommon):
'InterfaceVersion': 'HP_2.4',
'Seal': '6e1995ea5432580860a04d8515b6eb1507996f97b3c5fa04fb6d9568121a16a2'
}
- tx = self.env['payment.transaction'].create({
- 'amount': 314.0,
- 'acquirer_id': self.sips.id,
- 'currency_id': self.currency_euro.id,
- 'reference': 'SO100x2',
- 'partner_name': 'Norbert Buyer',
- 'partner_country_id': self.country_france.id})
- tx.form_feedback(sips_post_data, 'sips')
- # check state
- self.assertEqual(tx.state, 'cancel', 'Sips: erroneous validation did not put tx into error state')
+ self.reference = 'SO100x2'
+ tx2 = self.create_transaction(flow="redirect")
- def test_30_sips_badly_formatted_date(self):
- self.assertEqual(self.sips.state, 'test', 'test without test environment')
-
- # typical data posted by Sips after client has successfully paid
- bad_date = '2020-04-08T06:15:59+56:00'
- sips_post_data = {
- 'Data': 'captureDay=0|captureMode=AUTHOR_CAPTURE|currencyCode=840|'
- 'merchantId=002001000000001|orderChannel=INTERNET|'
- 'responseCode=00|transactionDateTime=%s|'
- 'transactionReference=SO100x1|keyVersion=1|'
- 'acquirerResponseCode=00|amount=31400|authorisationId=0020000006791167|'
- 'paymentMeanBrand=IDEAL|paymentMeanType=CREDIT_TRANSFER|'
- 'customerIpAddress=127.0.0.1|returnContext={"return_url": '
- '"/payment/process", "reference": '
- '"SO100x1"}|holderAuthentRelegation=N|holderAuthentStatus=|'
- 'transactionOrigin=INTERNET|paymentPattern=ONE_SHOT|customerMobilePhone=null|'
- 'mandateAuthentMethod=null|mandateUsage=null|transactionActors=null|'
- 'mandateId=null|captureLimitDate=20200408|dccStatus=null|dccResponseCode=null|'
- 'dccAmount=null|dccCurrencyCode=null|dccExchangeRate=null|'
- 'dccExchangeRateValidity=null|dccProvider=null|'
- 'statementReference=SO100x1|panEntryMode=MANUAL|walletType=null|'
- 'holderAuthentMethod=NO_AUTHENT_METHOD' % (bad_date,),
- 'Encode': '',
- 'InterfaceVersion': 'HP_2.4',
- 'Seal': 'f03f64da6f57c171904d12bf709b1d6d3385131ac914e97a7e1db075ed438f3e',
- 'locale': 'en'
- }
-
- tx = self.env['payment.transaction'].create({
- 'amount': 314.0,
- 'acquirer_id': self.sips.id,
- 'currency_id': self.currency_euro.id,
- 'reference': 'SO100x1',
- 'partner_name': 'Norbert Buyer',
- 'partner_country_id': self.country_france.id})
-
- # validate it
- tx.form_feedback(sips_post_data, 'sips')
- self.assertEqual(tx.state, 'done', 'Sips: validation did not put tx into done state when date format was weird')
+ self.env['payment.transaction']._handle_feedback_data('sips', sips_post_data)
+ self.assertEqual(tx2.state, 'cancel', 'Sips: erroneous validation did not put tx into error state')
diff --git a/addons/payment_sips/views/payment_sips_templates.xml b/addons/payment_sips/views/payment_sips_templates.xml
index 477bb964140..7c68506c488 100644
--- a/addons/payment_sips/views/payment_sips_templates.xml
+++ b/addons/payment_sips/views/payment_sips_templates.xml
@@ -1,11 +1,12 @@
-
-
-
+
+
+
-
+
+
+
diff --git a/addons/payment_sips/views/payment_views.xml b/addons/payment_sips/views/payment_views.xml
index 633da7e527a..3b20af3b7f7 100644
--- a/addons/payment_sips/views/payment_views.xml
+++ b/addons/payment_sips/views/payment_views.xml
@@ -1,24 +1,22 @@
-
-
- acquirer.form.sips
- payment.acquirer
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ Sips Acquirer Form
+ payment.acquirer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-