mirror of
https://github.com/OCA/bank-statement-import.git
synced 2025-01-20 12:37:43 +02:00
@@ -166,6 +166,7 @@ EVENT_DESCRIPTIONS = {
|
|||||||
'T9800': _('Display only transaction'),
|
'T9800': _('Display only transaction'),
|
||||||
'T9900': _('Other'),
|
'T9900': _('Other'),
|
||||||
}
|
}
|
||||||
|
NO_DATA_FOR_DATE_AVAIL_MSG = 'Data for the given start date is not available.'
|
||||||
|
|
||||||
|
|
||||||
class OnlineBankStatementProviderPayPal(models.Model):
|
class OnlineBankStatementProviderPayPal(models.Model):
|
||||||
@@ -416,7 +417,18 @@ class OnlineBankStatementProviderPayPal(models.Model):
|
|||||||
interval_end.isoformat() + 'Z',
|
interval_end.isoformat() + 'Z',
|
||||||
page,
|
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(
|
interval_transactions = map(
|
||||||
lambda transaction: self._paypal_preparse_transaction(
|
lambda transaction: self._paypal_preparse_transaction(
|
||||||
transaction
|
transaction
|
||||||
@@ -465,15 +477,22 @@ class OnlineBankStatementProviderPayPal(models.Model):
|
|||||||
return Decimal(transaction_amount['value'])
|
return Decimal(transaction_amount['value'])
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _paypal_validate(self, content):
|
def _paypal_decode_error(self, content):
|
||||||
content = json.loads(content)
|
generic_error = content.get('name')
|
||||||
if 'error' in content and content['error']:
|
if generic_error:
|
||||||
raise UserError(
|
return UserError('%s: %s' % (
|
||||||
content['error_description']
|
generic_error,
|
||||||
if 'error_description' in content
|
content.get('message') or _('Unknown error'),
|
||||||
else 'Unknown error'
|
))
|
||||||
)
|
|
||||||
return content
|
identity_error = content.get('error')
|
||||||
|
if identity_error:
|
||||||
|
UserError('%s: %s' % (
|
||||||
|
generic_error,
|
||||||
|
content.get('error_description') or _('Unknown error'),
|
||||||
|
))
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _paypal_retrieve(self, url, auth, data=None):
|
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:
|
with self._paypal_urlopen(url, auth, data) as response:
|
||||||
content = response.read().decode('utf-8')
|
content = response.read().decode('utf-8')
|
||||||
except HTTPError as e:
|
except HTTPError as e:
|
||||||
content = self._paypal_validate(
|
content = json.loads(e.read().decode('utf-8'))
|
||||||
e.read().decode('utf-8')
|
|
||||||
)
|
# NOTE: Workaround for INVALID_REQUEST (see ROADMAP.rst)
|
||||||
if 'name' in content and content['name']:
|
if self.env.context.get('invalid_data_workaround') \
|
||||||
raise UserError('%s: %s' % (
|
and content.get('name') == 'INVALID_REQUEST' \
|
||||||
content['name'],
|
and content.get('message') == NO_DATA_FOR_DATE_AVAIL_MSG:
|
||||||
content['error_description']
|
return {
|
||||||
if 'error_description' in content
|
'transaction_details': [],
|
||||||
else 'Unknown error',
|
'page': 1,
|
||||||
))
|
'total_items': 0,
|
||||||
raise e
|
'total_pages': 0,
|
||||||
return self._paypal_validate(content)
|
}
|
||||||
|
|
||||||
|
raise self._paypal_decode_error(content) or e
|
||||||
|
return json.loads(content)
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _paypal_urlopen(self, url, auth, data=None):
|
def _paypal_urlopen(self, url, auth, data=None):
|
||||||
|
|||||||
@@ -5,3 +5,7 @@
|
|||||||
* `PayPal Transaction Info <https://developer.paypal.com/docs/api/sync/v1/#definition-transaction_info>`_
|
* `PayPal Transaction Info <https://developer.paypal.com/docs/api/sync/v1/#definition-transaction_info>`_
|
||||||
defines extra fields like ``tip_amount``, ``shipping_amount``, etc. that
|
defines extra fields like ``tip_amount``, ``shipping_amount``, etc. that
|
||||||
could be useful to be decomposed from a single transaction.
|
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).
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ from dateutil.relativedelta import relativedelta
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
import json
|
import json
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
from urllib.error import HTTPError
|
||||||
|
|
||||||
from odoo.tests import common
|
|
||||||
from odoo import fields
|
from odoo import fields
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
from odoo.tests import common
|
||||||
|
|
||||||
_module_ns = 'odoo.addons.account_bank_statement_import_online_paypal'
|
_module_ns = 'odoo.addons.account_bank_statement_import_online_paypal'
|
||||||
_provider_class = (
|
_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(
|
class TestAccountBankAccountStatementImportOnlinePayPal(
|
||||||
common.TransactionCase
|
common.TransactionCase
|
||||||
):
|
):
|
||||||
@@ -156,6 +183,87 @@ class TestAccountBankAccountStatementImportOnlinePayPal(
|
|||||||
with self.assertRaises(Exception):
|
with self.assertRaises(Exception):
|
||||||
provider._paypal_get_token()
|
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):
|
def test_empty_pull(self):
|
||||||
journal = self.AccountJournal.create({
|
journal = self.AccountJournal.create({
|
||||||
'name': 'Bank',
|
'name': 'Bank',
|
||||||
|
|||||||
Reference in New Issue
Block a user