diff --git a/account_bank_statement_import_online_paypal/README.rst b/account_bank_statement_import_online_paypal/README.rst index 53666cd0..f3808f57 100644 --- a/account_bank_statement_import_online_paypal/README.rst +++ b/account_bank_statement_import_online_paypal/README.rst @@ -86,6 +86,10 @@ Known issues / Roadmap * `PayPal Transaction Info `_ defines extra fields like ``tip_amount``, ``shipping_amount``, etc. that could be useful to be decomposed from a single transaction. +* There's a known issue with PayPal API that on every Monday for couple of + hours after UTC midnight it returns ``INVALID_REQUEST`` incorrectly: their + servers have not inflated the data yet. PayPal tech support confirmed this + behaviour in case #06650320 (private). Bug Tracker =========== diff --git a/account_bank_statement_import_online_paypal/__manifest__.py b/account_bank_statement_import_online_paypal/__manifest__.py index 0e9d0506..30040ea3 100644 --- a/account_bank_statement_import_online_paypal/__manifest__.py +++ b/account_bank_statement_import_online_paypal/__manifest__.py @@ -4,7 +4,7 @@ { 'name': 'Online Bank Statements: PayPal.com', - 'version': '12.0.1.0.0', + 'version': '12.0.1.0.1', 'author': 'Brainbean Apps, ' 'Dataplug, ' diff --git a/account_bank_statement_import_online_paypal/i18n/account_bank_statement_import_online_paypal.pot b/account_bank_statement_import_online_paypal/i18n/account_bank_statement_import_online_paypal.pot index 736ec753..fe712b34 100644 --- a/account_bank_statement_import_online_paypal/i18n/account_bank_statement_import_online_paypal.pot +++ b/account_bank_statement_import_online_paypal/i18n/account_bank_statement_import_online_paypal.pot @@ -234,20 +234,20 @@ msgid "Electronic funds transfer (EFT)" msgstr "" #. module: account_bank_statement_import_online_paypal -#: code:addons/account_bank_statement_import_online_paypal/models/online_bank_statement_provider_paypal.py:362 +#: code:addons/account_bank_statement_import_online_paypal/models/online_bank_statement_provider_paypal.py:363 #, python-format msgid "Failed to acquire token using Client ID and Secret!" msgstr "" #. module: account_bank_statement_import_online_paypal -#: code:addons/account_bank_statement_import_online_paypal/models/online_bank_statement_provider_paypal.py:240 -#: code:addons/account_bank_statement_import_online_paypal/models/online_bank_statement_provider_paypal.py:266 +#: code:addons/account_bank_statement_import_online_paypal/models/online_bank_statement_provider_paypal.py:241 +#: code:addons/account_bank_statement_import_online_paypal/models/online_bank_statement_provider_paypal.py:267 #, python-format msgid "Failed to resolve transaction %s (%s)" msgstr "" #. module: account_bank_statement_import_online_paypal -#: code:addons/account_bank_statement_import_online_paypal/models/online_bank_statement_provider_paypal.py:336 +#: code:addons/account_bank_statement_import_online_paypal/models/online_bank_statement_provider_paypal.py:337 #, python-format msgid "Fee for %s" msgstr "" @@ -500,13 +500,13 @@ msgid "International credit card withdrawal" msgstr "" #. module: account_bank_statement_import_online_paypal -#: code:addons/account_bank_statement_import_online_paypal/models/online_bank_statement_provider_paypal.py:360 +#: code:addons/account_bank_statement_import_online_paypal/models/online_bank_statement_provider_paypal.py:361 #, python-format msgid "Invalid token type!" msgstr "" #. module: account_bank_statement_import_online_paypal -#: code:addons/account_bank_statement_import_online_paypal/models/online_bank_statement_provider_paypal.py:302 +#: code:addons/account_bank_statement_import_online_paypal/models/online_bank_statement_provider_paypal.py:303 #, python-format msgid "Invoice %s" msgstr "" @@ -554,7 +554,7 @@ msgid "Mobile payment, made through a mobile phone" msgstr "" #. module: account_bank_statement_import_online_paypal -#: code:addons/account_bank_statement_import_online_paypal/models/online_bank_statement_provider_paypal.py:500 +#: code:addons/account_bank_statement_import_online_paypal/models/online_bank_statement_provider_paypal.py:522 #, python-format msgid "No authentication specified!" msgstr "" @@ -589,7 +589,7 @@ msgid "Partner fee" msgstr "" #. module: account_bank_statement_import_online_paypal -#: code:addons/account_bank_statement_import_online_paypal/models/online_bank_statement_provider_paypal.py:356 +#: code:addons/account_bank_statement_import_online_paypal/models/online_bank_statement_provider_paypal.py:357 #, python-format msgid "PayPal App features are configured incorrectly!" msgstr "" @@ -607,7 +607,7 @@ msgid "PayPal Here payment" msgstr "" #. module: account_bank_statement_import_online_paypal -#: code:addons/account_bank_statement_import_online_paypal/models/online_bank_statement_provider_paypal.py:199 +#: code:addons/account_bank_statement_import_online_paypal/models/online_bank_statement_provider_paypal.py:200 #, python-format msgid "PayPal allows retrieving transactions only up to 3 years in the past. Please import older transactions manually. See https://www.paypal.com/us/smarthelp/article/why-can't-i-access-transaction-history-greater-than-3-years-ts2241" msgstr "" @@ -828,7 +828,7 @@ msgid "Third-party recoupment" msgstr "" #. module: account_bank_statement_import_online_paypal -#: code:addons/account_bank_statement_import_online_paypal/models/online_bank_statement_provider_paypal.py:341 +#: code:addons/account_bank_statement_import_online_paypal/models/online_bank_statement_provider_paypal.py:342 #, python-format msgid "Transaction fee for %s" msgstr "" @@ -852,11 +852,18 @@ msgid "Transfer to external GL entity" msgstr "" #. module: account_bank_statement_import_online_paypal -#: code:addons/account_bank_statement_import_online_paypal/models/online_bank_statement_provider_paypal.py:516 +#: code:addons/account_bank_statement_import_online_paypal/models/online_bank_statement_provider_paypal.py:538 #, python-format msgid "Unknown authentication specified!" msgstr "" +#. module: account_bank_statement_import_online_paypal +#: code:addons/account_bank_statement_import_online_paypal/models/online_bank_statement_provider_paypal.py:485 +#: code:addons/account_bank_statement_import_online_paypal/models/online_bank_statement_provider_paypal.py:492 +#, python-format +msgid "Unknown error" +msgstr "" + #. module: account_bank_statement_import_online_paypal #: code:addons/account_bank_statement_import_online_paypal/models/online_bank_statement_provider_paypal.py:59 #, python-format diff --git a/account_bank_statement_import_online_paypal/models/online_bank_statement_provider_paypal.py b/account_bank_statement_import_online_paypal/models/online_bank_statement_provider_paypal.py index fed83e05..b0e27b9c 100644 --- a/account_bank_statement_import_online_paypal/models/online_bank_statement_provider_paypal.py +++ b/account_bank_statement_import_online_paypal/models/online_bank_statement_provider_paypal.py @@ -166,6 +166,7 @@ EVENT_DESCRIPTIONS = { 'T9800': _('Display only transaction'), 'T9900': _('Other'), } +NO_DATA_FOR_DATE_AVAIL_MSG = 'Data for the given start date is not available.' class OnlineBankStatementProviderPayPal(models.Model): @@ -416,7 +417,18 @@ class OnlineBankStatementProviderPayPal(models.Model): interval_end.isoformat() + 'Z', page, )) - data = self._paypal_retrieve(url, token) + + # NOTE: Workaround for INVALID_REQUEST (see ROADMAP.rst) + invalid_data_workaround = self.env.context.get( + 'test_account_bank_statement_import_online_paypal_monday', + interval_start.weekday() == 0 and ( + datetime.utcnow() - interval_start + ).total_seconds() < 28800 + ) + + data = self.with_context( + invalid_data_workaround=invalid_data_workaround, + )._paypal_retrieve(url, token) interval_transactions = map( lambda transaction: self._paypal_preparse_transaction( transaction @@ -465,15 +477,22 @@ class OnlineBankStatementProviderPayPal(models.Model): return Decimal(transaction_amount['value']) @api.model - def _paypal_validate(self, content): - content = json.loads(content) - if 'error' in content and content['error']: - raise UserError( - content['error_description'] - if 'error_description' in content - else 'Unknown error' - ) - return content + def _paypal_decode_error(self, content): + generic_error = content.get('name') + if generic_error: + return UserError('%s: %s' % ( + generic_error, + content.get('message') or _('Unknown error'), + )) + + identity_error = content.get('error') + if identity_error: + UserError('%s: %s' % ( + generic_error, + content.get('error_description') or _('Unknown error'), + )) + + return None @api.model def _paypal_retrieve(self, url, auth, data=None): @@ -481,18 +500,21 @@ class OnlineBankStatementProviderPayPal(models.Model): with self._paypal_urlopen(url, auth, data) as response: content = response.read().decode('utf-8') except HTTPError as e: - content = self._paypal_validate( - e.read().decode('utf-8') - ) - if 'name' in content and content['name']: - raise UserError('%s: %s' % ( - content['name'], - content['error_description'] - if 'error_description' in content - else 'Unknown error', - )) - raise e - return self._paypal_validate(content) + content = json.loads(e.read().decode('utf-8')) + + # NOTE: Workaround for INVALID_REQUEST (see ROADMAP.rst) + if self.env.context.get('invalid_data_workaround') \ + and content.get('name') == 'INVALID_REQUEST' \ + and content.get('message') == NO_DATA_FOR_DATE_AVAIL_MSG: + return { + 'transaction_details': [], + 'page': 1, + 'total_items': 0, + 'total_pages': 0, + } + + raise self._paypal_decode_error(content) or e + return json.loads(content) @api.model def _paypal_urlopen(self, url, auth, data=None): diff --git a/account_bank_statement_import_online_paypal/readme/ROADMAP.rst b/account_bank_statement_import_online_paypal/readme/ROADMAP.rst index d8114209..f6968426 100644 --- a/account_bank_statement_import_online_paypal/readme/ROADMAP.rst +++ b/account_bank_statement_import_online_paypal/readme/ROADMAP.rst @@ -5,3 +5,7 @@ * `PayPal Transaction Info `_ defines extra fields like ``tip_amount``, ``shipping_amount``, etc. that could be useful to be decomposed from a single transaction. +* There's a known issue with PayPal API that on every Monday for couple of + hours after UTC midnight it returns ``INVALID_REQUEST`` incorrectly: their + servers have not inflated the data yet. PayPal tech support confirmed this + behaviour in case #06650320 (private). diff --git a/account_bank_statement_import_online_paypal/static/description/index.html b/account_bank_statement_import_online_paypal/static/description/index.html index 15eafb05..91282689 100644 --- a/account_bank_statement_import_online_paypal/static/description/index.html +++ b/account_bank_statement_import_online_paypal/static/description/index.html @@ -438,6 +438,10 @@ for details.
  • PayPal Transaction Info defines extra fields like tip_amount, shipping_amount, etc. that could be useful to be decomposed from a single transaction.
  • +
  • There’s a known issue with PayPal API that on every Monday for couple of +hours after UTC midnight it returns INVALID_REQUEST incorrectly: their +servers have not inflated the data yet. PayPal tech support confirmed this +behaviour in case #06650320 (private).
  • diff --git a/account_bank_statement_import_online_paypal/tests/test_account_bank_statement_import_online_paypal.py b/account_bank_statement_import_online_paypal/tests/test_account_bank_statement_import_online_paypal.py index fc7e814c..c600184a 100644 --- a/account_bank_statement_import_online_paypal/tests/test_account_bank_statement_import_online_paypal.py +++ b/account_bank_statement_import_online_paypal/tests/test_account_bank_statement_import_online_paypal.py @@ -7,9 +7,11 @@ from dateutil.relativedelta import relativedelta from decimal import Decimal import json from unittest import mock +from urllib.error import HTTPError -from odoo.tests import common from odoo import fields +from odoo.exceptions import UserError +from odoo.tests import common _module_ns = 'odoo.addons.account_bank_statement_import_online_paypal' _provider_class = ( @@ -19,6 +21,31 @@ _provider_class = ( ) +class FakeHTTPError(HTTPError): + def __init__(self, content): + self.content = content + + def read(self): + return self.content.encode('utf-8') + + +class UrlopenRetValMock: + def __init__(self, content, throw=False): + self.content = content + self.throw = throw + + def __enter__(self): + return self + + def __exit__(self, type, value, tb): + pass + + def read(self): + if self.throw: + raise FakeHTTPError(self.content) + return self.content.encode('utf-8') + + class TestAccountBankAccountStatementImportOnlinePayPal( common.TransactionCase ): @@ -156,6 +183,87 @@ class TestAccountBankAccountStatementImportOnlinePayPal( with self.assertRaises(Exception): provider._paypal_get_token() + def test_no_data_on_monday(self): + journal = self.AccountJournal.create({ + 'name': 'Bank', + 'type': 'bank', + 'code': 'BANK', + 'currency_id': self.currency_eur.id, + 'bank_statements_source': 'online', + 'online_bank_statement_provider': 'paypal', + }) + + provider = journal.online_bank_statement_provider_id + mocked_response = UrlopenRetValMock("""{ + "debug_id": "eec890ebd5798", + "details": "xxxxxx", + "links": "xxxxxx", + "message": "Data for the given start date is not available.", + "name": "INVALID_REQUEST" +}""", throw=True) + with mock.patch( + _provider_class + '._paypal_urlopen', + return_value=mocked_response, + ), self.mock_token(): + data = provider.with_context( + test_account_bank_statement_import_online_paypal_monday=True, + )._obtain_statement_data( + self.now - relativedelta(hours=1), + self.now, + ) + + self.assertIsNone(data) + + def test_error_handling_1(self): + journal = self.AccountJournal.create({ + 'name': 'Bank', + 'type': 'bank', + 'code': 'BANK', + 'currency_id': self.currency_eur.id, + 'bank_statements_source': 'online', + 'online_bank_statement_provider': 'paypal', + }) + + provider = journal.online_bank_statement_provider_id + mocked_response = UrlopenRetValMock("""{ + "message": "MSG", + "name": "ERROR" +}""", throw=True) + with mock.patch( + _provider_class + '._paypal_urlopen', + return_value=mocked_response, + ), self.mock_token(): + with self.assertRaises(UserError): + provider._obtain_statement_data( + self.now - relativedelta(years=5), + self.now, + ) + + def test_error_handling_2(self): + journal = self.AccountJournal.create({ + 'name': 'Bank', + 'type': 'bank', + 'code': 'BANK', + 'currency_id': self.currency_eur.id, + 'bank_statements_source': 'online', + 'online_bank_statement_provider': 'paypal', + }) + + provider = journal.online_bank_statement_provider_id + mocked_response = UrlopenRetValMock("""{ + "error_description": "DESC", + "error": "ERROR" +}""", throw=True) + with mock.patch( + _provider_class + '._paypal_urlopen', + return_value=mocked_response, + ), self.mock_token(): + with self.assertRaises(UserError): + provider._obtain_statement_data( + self.now - relativedelta(years=5), + self.now, + ) + def test_empty_pull(self): journal = self.AccountJournal.create({ 'name': 'Bank',