[IMP] account_bank_statement_import_online_transferwise: black, isort, prettier

This commit is contained in:
Alexey Pelykh
2021-05-11 21:48:18 +02:00
parent 0f031e1748
commit a5b2db7aad
6 changed files with 526 additions and 508 deletions

View File

@@ -3,25 +3,16 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
{
'name': 'Online Bank Statements: Wise.com (TransferWise.com)',
'version': '12.0.1.0.3',
'author':
'CorporateHub, '
'Odoo Community Association (OCA)',
'maintainers': ['alexey-pelykh'],
'website': 'https://github.com/OCA/bank-statement-import/',
'license': 'AGPL-3',
'category': 'Accounting',
'summary': 'Online bank statements for Wise.com (TransferWise.com)',
'depends': [
'account_bank_statement_import_online',
'web_widget_dropdown_dynamic',
],
'external_dependencies': {
'python': ['cryptography'],
},
'data': [
'views/online_bank_statement_provider.xml',
],
'installable': True,
"name": "Online Bank Statements: Wise.com (TransferWise.com)",
"version": "12.0.1.0.3",
"author": "CorporateHub, " "Odoo Community Association (OCA)",
"maintainers": ["alexey-pelykh"],
"website": "https://github.com/OCA/bank-statement-import/",
"license": "AGPL-3",
"category": "Accounting",
"summary": "Online bank statements for Wise.com (TransferWise.com)",
"depends": ["account_bank_statement_import_online", "web_widget_dropdown_dynamic"],
"external_dependencies": {"python": ["cryptography"]},
"data": ["views/online_bank_statement_provider.xml"],
"installable": True,
}

View File

@@ -2,79 +2,76 @@
# Copyright 2020-2021 CorporateHub (https://corporatehub.eu)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from base64 import b64encode
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from dateutil.relativedelta import relativedelta
import dateutil.parser
from decimal import Decimal
import itertools
import json
import pytz
import logging
import urllib.parse
import urllib.request
from base64 import b64encode
from decimal import Decimal
from urllib.error import HTTPError
from odoo import api, fields, models, _
import dateutil.parser
import pytz
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from dateutil.relativedelta import relativedelta
from odoo import _, api, fields, models
from odoo.exceptions import UserError
import logging
_logger = logging.getLogger(__name__)
TRANSFERWISE_API_BASE = 'https://api.transferwise.com'
TRANSFERWISE_API_BASE = "https://api.transferwise.com"
class OnlineBankStatementProviderTransferwise(models.Model):
_inherit = 'online.bank.statement.provider'
_inherit = "online.bank.statement.provider"
# NOTE: This is needed to workaround possible multiple 'origin' fields
# present in the same view, resulting in wrong field view configuraion
# if more than one is widget="dynamic_dropdown"
transferwise_profile = fields.Char(
related='origin',
readonly=False,
)
transferwise_profile = fields.Char(related="origin", readonly=False,)
@api.model
def values_transferwise_profile(self):
api_base = self.env.context.get('api_base') or TRANSFERWISE_API_BASE
api_key = self.env.context.get('api_key')
api_base = self.env.context.get("api_base") or TRANSFERWISE_API_BASE
api_key = self.env.context.get("api_key")
if not api_key:
return []
try:
url = api_base + '/v1/profiles'
url = api_base + "/v1/profiles"
data = self._transferwise_retrieve(url, api_key)
except:
_logger.warning('Unable to get profiles', exc_info=True)
except BaseException:
_logger.warning("Unable to get profiles", exc_info=True)
return []
return list(map(
lambda entry: (
str(entry['id']),
'%s %s (personal)' % (
entry['details']['firstName'],
entry['details']['lastName'],
)
if entry['type'] == 'personal'
else entry['details']['name']
),
data
))
return list(
map(
lambda entry: (
str(entry["id"]),
"%s %s (personal)"
% (entry["details"]["firstName"], entry["details"]["lastName"],)
if entry["type"] == "personal"
else entry["details"]["name"],
),
data,
)
)
@api.model
def _get_available_services(self):
return super()._get_available_services() + [
('transferwise', 'Wise.com (TransferWise.com)'),
("transferwise", "Wise.com (TransferWise.com)"),
]
@api.multi
def _obtain_statement_data(self, date_since, date_until):
self.ensure_one()
if self.service != 'transferwise':
if self.service != "transferwise":
return super()._obtain_statement_data(
date_since,
date_until,
date_since, date_until,
) # pragma: no cover
api_base = self.api_base or TRANSFERWISE_API_BASE
@@ -82,13 +79,9 @@ class OnlineBankStatementProviderTransferwise(models.Model):
private_key = self.certificate_private_key
if private_key:
private_key = serialization.load_pem_private_key(
private_key.encode(),
password=None,
backend=default_backend(),
private_key.encode(), password=None, backend=default_backend(),
)
currency = (
self.currency_id or self.company_id.currency_id
).name
currency = (self.currency_id or self.company_id.currency_id).name
if date_since.tzinfo:
date_since = date_since.astimezone(pytz.utc).replace(tzinfo=None)
@@ -96,17 +89,14 @@ class OnlineBankStatementProviderTransferwise(models.Model):
date_until = date_until.astimezone(pytz.utc).replace(tzinfo=None)
# Get corresponding balance by currency
url = api_base + '/v1/borderless-accounts?profileId=%s' % (
self.origin,
)
url = api_base + "/v1/borderless-accounts?profileId={}".format(self.origin)
data = self._transferwise_retrieve(url, api_key, private_key)
if not data:
return None
borderless_account = data[0]['id']
balance = list(filter(
lambda balance: balance['currency'] == currency,
data[0]['balances']
))
borderless_account = data[0]["id"]
balance = list(
filter(lambda balance: balance["currency"] == currency, data[0]["balances"])
)
if not balance:
return None
@@ -114,10 +104,10 @@ class OnlineBankStatementProviderTransferwise(models.Model):
# - intervalStart <= date < intervalEnd
# Get starting balance
starting_balance_timestamp = date_since.isoformat() + 'Z'
starting_balance_timestamp = date_since.isoformat() + "Z"
url = api_base + (
'/v3/profiles/%s/borderless-accounts/%s/statement.json' +
'?currency=%s&intervalStart=%s&intervalEnd=%s&type=COMPACT'
"/v3/profiles/%s/borderless-accounts/%s/statement.json"
+ "?currency=%s&intervalStart=%s&intervalEnd=%s&type=COMPACT"
) % (
self.origin,
borderless_account,
@@ -126,7 +116,7 @@ class OnlineBankStatementProviderTransferwise(models.Model):
starting_balance_timestamp,
)
data = self._transferwise_retrieve(url, api_key, private_key)
balance_start = data['endOfStatementBalance']['value']
balance_start = data["endOfStatementBalance"]["value"]
# Get statements, using 469 days (around 1 year 3 month) as step.
interval_step = relativedelta(days=469)
@@ -136,158 +126,139 @@ class OnlineBankStatementProviderTransferwise(models.Model):
balance_end = None
while interval_start < interval_end:
url = api_base + (
'/v3/profiles/%s/borderless-accounts/%s/statement.json' +
'?currency=%s&intervalStart=%s&intervalEnd=%s&type=COMPACT'
"/v3/profiles/%s/borderless-accounts/%s/statement.json"
+ "?currency=%s&intervalStart=%s&intervalEnd=%s&type=COMPACT"
) % (
self.origin,
borderless_account,
currency,
interval_start.isoformat() + 'Z',
min(
interval_start + interval_step, interval_end
).isoformat() + 'Z',
interval_start.isoformat() + "Z",
min(interval_start + interval_step, interval_end).isoformat() + "Z",
)
data = self._transferwise_retrieve(url, api_key, private_key)
transactions += data['transactions']
balance_end = data['endOfStatementBalance']['value']
transactions += data["transactions"]
balance_end = data["endOfStatementBalance"]["value"]
interval_start += interval_step
if balance_end is None:
raise UserError(_('Ending balance unavailable'))
raise UserError(_("Ending balance unavailable"))
# Normalize transactions' date, sort by it, and get lines
transactions = map(
lambda transaction: self._transferwise_preparse_transaction(
transaction
),
transactions
lambda transaction: self._transferwise_preparse_transaction(transaction),
transactions,
)
lines = list(itertools.chain.from_iterable(map(
lambda x: self._transferwise_transaction_to_lines(x),
sorted(
transactions,
key=lambda transaction: transaction['date']
lines = list(
itertools.chain.from_iterable(
map(
lambda x: self._transferwise_transaction_to_lines(x),
sorted(transactions, key=lambda transaction: transaction["date"]),
)
)
)))
)
return lines, {
'balance_start': balance_start,
'balance_end_real': balance_end,
}
return lines, {"balance_start": balance_start, "balance_end_real": balance_end}
@api.model
def _transferwise_preparse_transaction(self, transaction):
transaction['date'] = dateutil.parser.parse(
transaction['date']
).replace(tzinfo=None)
transaction["date"] = dateutil.parser.parse(transaction["date"]).replace(
tzinfo=None
)
return transaction
@api.model
def _transferwise_transaction_to_lines(self, transaction):
transaction_type = transaction['type']
reference_number = transaction['referenceNumber']
details = transaction.get('details', {})
exchange_details = transaction.get('exchangeDetails')
recipient = details.get('recipient')
total_fees = transaction.get('totalFees')
date = transaction['date']
payment_reference = details.get('paymentReference')
description = details.get('description')
transaction_type = transaction["type"]
reference_number = transaction["referenceNumber"]
details = transaction.get("details", {})
exchange_details = transaction.get("exchangeDetails")
recipient = details.get("recipient")
total_fees = transaction.get("totalFees")
date = transaction["date"]
payment_reference = details.get("paymentReference")
description = details.get("description")
note = reference_number
if description:
note = '%s: %s' % (
note,
description
)
amount = transaction['amount']
amount_value = amount.get('value', 0)
fees_value = total_fees.get('value', Decimal())
if transaction_type == 'CREDIT' \
and details.get('type') == 'MONEY_ADDED':
note = "{}: {}".format(note, description)
amount = transaction["amount"]
amount_value = amount.get("value", 0)
fees_value = total_fees.get("value", Decimal())
if transaction_type == "CREDIT" and details.get("type") == "MONEY_ADDED":
fees_value = fees_value.copy_negate()
else:
fees_value = fees_value.copy_sign(amount_value)
amount_value -= fees_value
unique_import_id = '%s-%s-%s' % (
transaction_type,
reference_number,
int(date.timestamp()),
unique_import_id = "{}-{}-{}".format(
transaction_type, reference_number, int(date.timestamp()),
)
line = {
'name': payment_reference or description or '',
'amount': str(amount_value),
'date': date,
'note': note,
'unique_import_id': unique_import_id,
"name": payment_reference or description or "",
"amount": str(amount_value),
"date": date,
"note": note,
"unique_import_id": unique_import_id,
}
if recipient:
if 'name' in recipient:
line.update({
'partner_name': recipient['name'],
})
if 'bankAccount' in recipient:
line.update({
'account_number': recipient['bankAccount'],
})
elif 'merchant' in details:
merchant = details['merchant']
if 'name' in merchant:
line.update({
'partner_name': merchant['name'],
})
if "name" in recipient:
line.update({"partner_name": recipient["name"]})
if "bankAccount" in recipient:
line.update({"account_number": recipient["bankAccount"]})
elif "merchant" in details:
merchant = details["merchant"]
if "name" in merchant:
line.update({"partner_name": merchant["name"]})
else:
if 'senderName' in details:
line.update({
'partner_name': details['senderName'],
})
if 'senderAccount' in details:
line.update({
'account_number': details['senderAccount'],
})
if "senderName" in details:
line.update({"partner_name": details["senderName"]})
if "senderAccount" in details:
line.update({"account_number": details["senderAccount"]})
if exchange_details:
to_amount = exchange_details['toAmount']
from_amount = exchange_details['fromAmount']
to_amount = exchange_details["toAmount"]
from_amount = exchange_details["fromAmount"]
other_amount_value = (
to_amount['value']
if to_amount['currency'] != amount['currency']
else from_amount['value']
to_amount["value"]
if to_amount["currency"] != amount["currency"]
else from_amount["value"]
)
other_currency_name = (
to_amount['currency']
if to_amount['currency'] != amount['currency']
else from_amount['currency']
to_amount["currency"]
if to_amount["currency"] != amount["currency"]
else from_amount["currency"]
)
other_amount_value = other_amount_value.copy_abs()
if amount_value.is_signed():
other_amount_value = other_amount_value.copy_negate()
other_currency = self.env['res.currency'].search(
[('name', '=', other_currency_name)],
limit=1
other_currency = self.env["res.currency"].search(
[("name", "=", other_currency_name)], limit=1
)
if other_amount_value and other_currency:
line.update({
'amount_currency': str(other_amount_value),
'currency_id': other_currency.id,
})
line.update(
{
"amount_currency": str(other_amount_value),
"currency_id": other_currency.id,
}
)
lines = [line]
if fees_value:
lines += [{
'name': _('Fee for %s') % reference_number,
'amount': str(fees_value),
'date': date,
'partner_name': 'Wise (former TransferWise)',
'unique_import_id': '%s-FEE' % unique_import_id,
'note': _('Transaction fee for %s') % reference_number,
}]
lines += [
{
"name": _("Fee for %s") % reference_number,
"amount": str(fees_value),
"date": date,
"partner_name": "Wise (former TransferWise)",
"unique_import_id": "%s-FEE" % unique_import_id,
"note": _("Transaction fee for %s") % reference_number,
}
]
return lines
@api.model
def _transferwise_validate(self, content):
content = json.loads(content, parse_float=Decimal)
if 'error' in content and content['error']:
if "error" in content and content["error"]:
raise UserError(
content['error_description']
if 'error_description' in content
else 'Unknown error'
content["error_description"]
if "error_description" in content
else "Unknown error"
)
return content
@@ -296,31 +267,23 @@ class OnlineBankStatementProviderTransferwise(models.Model):
try:
with self._transferwise_urlopen(url, api_key) as response:
content = response.read().decode(
response.headers.get_content_charset() or 'utf-8'
response.headers.get_content_charset() or "utf-8"
)
except HTTPError as e:
if e.code != 403 or \
e.headers.get('X-2FA-Approval-Result') != 'REJECTED':
if e.code != 403 or e.headers.get("X-2FA-Approval-Result") != "REJECTED":
raise e
if not private_key:
raise UserError(_(
'Strong Customer Authentication is not configured'
))
one_time_token = e.headers['X-2FA-Approval']
raise UserError(_("Strong Customer Authentication is not configured"))
one_time_token = e.headers["X-2FA-Approval"]
signature = private_key.sign(
one_time_token.encode(),
padding.PKCS1v15(),
hashes.SHA256(),
one_time_token.encode(), padding.PKCS1v15(), hashes.SHA256(),
)
with self._transferwise_urlopen(
url,
api_key,
one_time_token,
b64encode(signature).decode(),
url, api_key, one_time_token, b64encode(signature).decode(),
) as response:
content = response.read().decode(
response.headers.get_content_charset() or 'utf-8'
response.headers.get_content_charset() or "utf-8"
)
return self._transferwise_validate(content)
@@ -328,17 +291,17 @@ class OnlineBankStatementProviderTransferwise(models.Model):
@api.model
def _transferwise_urlopen(self, url, api_key, ott=None, signature=None):
if not api_key:
raise UserError(_('No API key specified!'))
raise UserError(_("No API key specified!"))
request = urllib.request.Request(url)
request.add_header('Authorization', 'Bearer %s' % api_key)
request.add_header("Authorization", "Bearer %s" % api_key)
if ott and signature:
request.add_header('X-2FA-Approval', ott)
request.add_header('X-Signature', signature)
request.add_header("X-2FA-Approval", ott)
request.add_header("X-Signature", signature)
return urllib.request.urlopen(request)
@api.onchange('certificate_private_key', 'service')
@api.onchange("certificate_private_key", "service")
def _onchange_transferwise_certificate_private_key(self):
if self.service != 'transferwise':
if self.service != "transferwise":
return
self.certificate_public_key = False
@@ -351,22 +314,23 @@ class OnlineBankStatementProviderTransferwise(models.Model):
password=None,
backend=default_backend(),
)
self.certificate_public_key = private_key.public_key().public_bytes(
serialization.Encoding.PEM,
serialization.PublicFormat.PKCS1,
).decode()
except:
_logger.warning('Unable to parse key', exc_info=True)
raise UserError(_('Unable to parse key'))
self.certificate_public_key = (
private_key.public_key()
.public_bytes(
serialization.Encoding.PEM, serialization.PublicFormat.PKCS1,
)
.decode()
)
except BaseException:
_logger.warning("Unable to parse key", exc_info=True)
raise UserError(_("Unable to parse key"))
@api.multi
def _transferwise_generate_key(self):
self.ensure_one()
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend(),
public_exponent=65537, key_size=2048, backend=default_backend(),
)
self.certificate_private_key = private_key.private_bytes(
serialization.Encoding.PEM,
@@ -374,10 +338,11 @@ class OnlineBankStatementProviderTransferwise(models.Model):
serialization.NoEncryption(),
).decode()
self.certificate_public_key = private_key.public_key().public_bytes(
serialization.Encoding.PEM,
serialization.PublicFormat.PKCS1,
).decode()
self.certificate_public_key = (
private_key.public_key()
.public_bytes(serialization.Encoding.PEM, serialization.PublicFormat.PKCS1,)
.decode()
)
@api.multi
def button_transferwise_generate_key(self):

View File

@@ -2,26 +2,26 @@
# Copyright 2020-2021 CorporateHub (https://corporatehub.eu)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from datetime import datetime
from dateutil.relativedelta import relativedelta
from decimal import Decimal
import json
from datetime import datetime
from decimal import Decimal
from unittest import mock
from urllib.error import HTTPError
from odoo.tests import common
from odoo import fields
from dateutil.relativedelta import relativedelta
_module_ns = 'odoo.addons.account_bank_statement_import_online_transferwise'
from odoo import fields
from odoo.tests import common
_module_ns = "odoo.addons.account_bank_statement_import_online_transferwise"
_provider_class = (
_module_ns
+ '.models.online_bank_statement_provider_transferwise'
+ '.OnlineBankStatementProviderTransferwise'
+ ".models.online_bank_statement_provider_transferwise"
+ ".OnlineBankStatementProviderTransferwise"
)
class MockedResponse:
class Headers(dict):
def get_content_charset(self):
return None
@@ -37,41 +37,35 @@ class MockedResponse:
def __exit__(self, exc_type, exc_value, traceback):
pass
# pylint: disable=method-required-super
def read(self):
if self.exception is not None:
raise self.exception()
return self.data
class TestAccountBankAccountStatementImportOnlineTransferwise(
common.TransactionCase
):
class TestAccountBankAccountStatementImportOnlineTransferwise(common.TransactionCase):
def setUp(self):
super().setUp()
self.now = fields.Datetime.now()
self.currency_eur = self.env.ref('base.EUR')
self.currency_usd = self.env.ref('base.USD')
self.AccountJournal = self.env['account.journal']
self.OnlineBankStatementProvider = self.env[
'online.bank.statement.provider'
]
self.AccountBankStatement = self.env['account.bank.statement']
self.AccountBankStatementLine = self.env['account.bank.statement.line']
self.currency_eur = self.env.ref("base.EUR")
self.currency_usd = self.env.ref("base.USD")
self.AccountJournal = self.env["account.journal"]
self.OnlineBankStatementProvider = self.env["online.bank.statement.provider"]
self.AccountBankStatement = self.env["account.bank.statement"]
self.AccountBankStatementLine = self.env["account.bank.statement.line"]
Provider = self.OnlineBankStatementProvider
self.transferwise_parse_transaction = lambda payload: (
Provider._transferwise_transaction_to_lines(
Provider._transferwise_preparse_transaction(
json.loads(
payload,
parse_float=Decimal,
)
json.loads(payload, parse_float=Decimal,)
)
)
)
self.response_balance = MockedResponse(data="""[
self.response_balance = MockedResponse(
data=b"""[
{
"id": 42,
"balances": [
@@ -80,24 +74,26 @@ class TestAccountBankAccountStatementImportOnlineTransferwise(
}
]
}
]""".encode())
self.response_ott = MockedResponse(exception=lambda: HTTPError(
'https://wise.com/',
403,
'403',
{
'X-2FA-Approval-Result': 'REJECTED',
'X-2FA-Approval': '0123456789',
},
None,
))
self.response_transactions = MockedResponse(data="""{
]"""
)
self.response_ott = MockedResponse(
exception=lambda: HTTPError(
"https://wise.com/",
403,
"403",
{"X-2FA-Approval-Result": "REJECTED", "X-2FA-Approval": "0123456789"},
None,
)
)
self.response_transactions = MockedResponse(
data=b"""{
"transactions": [],
"endOfStatementBalance": {
"value": 42.00,
"currency": "EUR"
}
}""".encode())
}"""
)
def test_values_transferwise_profile(self):
mocked_response = json.loads(
@@ -117,63 +113,58 @@ class TestAccountBankAccountStatementImportOnlineTransferwise(
"name": "Brainbean Apps OÜ"
}
}
]""", parse_float=Decimal)
]""",
parse_float=Decimal,
)
values_transferwise_profile = []
with mock.patch(
_provider_class + '._transferwise_retrieve',
return_value=mocked_response,
_provider_class + "._transferwise_retrieve", return_value=mocked_response,
):
values_transferwise_profile = (
self.OnlineBankStatementProvider.with_context({
'api_base': 'https://example.com',
'api_key': 'dummy',
}).values_transferwise_profile()
)
values_transferwise_profile = self.OnlineBankStatementProvider.with_context(
{"api_base": "https://example.com", "api_key": "dummy"}
).values_transferwise_profile()
self.assertEqual(
values_transferwise_profile,
[
('1234567890', 'Alexey Pelykh (personal)'),
('1234567891', 'Brainbean Apps OÜ'),
]
("1234567890", "Alexey Pelykh (personal)"),
("1234567891", "Brainbean Apps OÜ"),
],
)
def test_values_transferwise_profile_no_key(self):
values_transferwise_profile = (
self.OnlineBankStatementProvider.with_context({
'api_base': 'https://example.com',
}).values_transferwise_profile()
)
values_transferwise_profile = self.OnlineBankStatementProvider.with_context(
{"api_base": "https://example.com"}
).values_transferwise_profile()
self.assertEqual(values_transferwise_profile, [])
def test_values_transferwise_profile_error(self):
values_transferwise_profile = []
with mock.patch(
_provider_class + '._transferwise_retrieve',
_provider_class + "._transferwise_retrieve",
side_effect=lambda: Exception(),
):
values_transferwise_profile = (
self.OnlineBankStatementProvider.with_context({
'api_base': 'https://example.com',
'api_key': 'dummy',
}).values_transferwise_profile()
)
values_transferwise_profile = self.OnlineBankStatementProvider.with_context(
{"api_base": "https://example.com", "api_key": "dummy"}
).values_transferwise_profile()
self.assertEqual(values_transferwise_profile, [])
def test_pull(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': 'transferwise',
})
journal = self.AccountJournal.create(
{
"name": "Bank",
"type": "bank",
"code": "BANK",
"currency_id": self.currency_eur.id,
"bank_statements_source": "online",
"online_bank_statement_provider": "transferwise",
}
)
provider = journal.online_bank_statement_provider_id
provider.origin = '1234567891'
provider.origin = "1234567891"
def mock_response(url, api_key, private_key=None):
if '/borderless-accounts?profileId=1234567891' in url:
if "/borderless-accounts?profileId=1234567891" in url:
payload = """[
{
"id": 42,
@@ -184,7 +175,7 @@ class TestAccountBankAccountStatementImportOnlineTransferwise(
]
}
]"""
elif '/borderless-accounts/42/statement.json' in url:
elif "/borderless-accounts/42/statement.json" in url:
payload = """{
"transactions": [],
"endOfStatementBalance": {
@@ -193,57 +184,58 @@ class TestAccountBankAccountStatementImportOnlineTransferwise(
}
}"""
return json.loads(payload, parse_float=Decimal)
with mock.patch(
_provider_class + '._transferwise_retrieve',
side_effect=mock_response,
_provider_class + "._transferwise_retrieve", side_effect=mock_response,
):
data = provider._obtain_statement_data(
self.now - relativedelta(hours=1),
self.now,
self.now - relativedelta(hours=1), self.now,
)
self.assertEqual(len(data[0]), 0)
self.assertEqual(data[1]['balance_start'], 42.0)
self.assertEqual(data[1]['balance_end_real'], 42.0)
self.assertEqual(data[1]["balance_start"], 42.0)
self.assertEqual(data[1]["balance_end_real"], 42.0)
def test_pull_no_data(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': 'transferwise',
})
journal = self.AccountJournal.create(
{
"name": "Bank",
"type": "bank",
"code": "BANK",
"currency_id": self.currency_eur.id,
"bank_statements_source": "online",
"online_bank_statement_provider": "transferwise",
}
)
provider = journal.online_bank_statement_provider_id
provider.origin = '1234567891'
provider.password = 'API_KEY'
provider.origin = "1234567891"
provider.password = "API_KEY"
with mock.patch(
_provider_class + '._transferwise_retrieve',
return_value=[],
_provider_class + "._transferwise_retrieve", return_value=[],
):
data = provider._obtain_statement_data(
self.now - relativedelta(hours=1),
self.now,
self.now - relativedelta(hours=1), self.now,
)
self.assertFalse(data)
def test_update_public_key(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': 'transferwise',
})
journal = self.AccountJournal.create(
{
"name": "Bank",
"type": "bank",
"code": "BANK",
"currency_id": self.currency_eur.id,
"bank_statements_source": "online",
"online_bank_statement_provider": "transferwise",
}
)
provider = journal.online_bank_statement_provider_id
provider.origin = '1234567891'
provider.password = 'API_KEY'
provider.origin = "1234567891"
provider.password = "API_KEY"
with common.Form(provider) as provider_form:
provider_form.certificate_private_key = """
@@ -279,22 +271,24 @@ edF6byMgXSzgOWYuRPXwmHpBQV0GiexQUAxVyUzaVWfil69LaFfXaw==
self.assertTrue(provider.certificate_public_key)
def test_sca(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': 'transferwise',
})
journal = self.AccountJournal.create(
{
"name": "Bank",
"type": "bank",
"code": "BANK",
"currency_id": self.currency_eur.id,
"bank_statements_source": "online",
"online_bank_statement_provider": "transferwise",
}
)
provider = journal.online_bank_statement_provider_id
provider.origin = '1234567891'
provider.password = 'API_KEY'
provider.origin = "1234567891"
provider.password = "API_KEY"
provider.button_transferwise_generate_key()
with mock.patch(
'urllib.request.urlopen',
"urllib.request.urlopen",
side_effect=[
self.response_balance,
self.response_ott,
@@ -304,16 +298,16 @@ edF6byMgXSzgOWYuRPXwmHpBQV0GiexQUAxVyUzaVWfil69LaFfXaw==
],
):
data = provider._obtain_statement_data(
self.now - relativedelta(hours=1),
self.now,
self.now - relativedelta(hours=1), self.now,
)
self.assertEqual(len(data[0]), 0)
self.assertEqual(data[1]['balance_start'], 42.0)
self.assertEqual(data[1]['balance_end_real'], 42.0)
self.assertEqual(data[1]["balance_start"], 42.0)
self.assertEqual(data[1]["balance_end_real"], 42.0)
def test_transaction_parse_1(self):
lines = self.transferwise_parse_transaction("""{
lines = self.transferwise_parse_transaction(
"""{
"type": "CREDIT",
"date": "2000-01-01T00:00:00.000Z",
"amount": {
@@ -337,23 +331,28 @@ edF6byMgXSzgOWYuRPXwmHpBQV0GiexQUAxVyUzaVWfil69LaFfXaw==
"currency": "EUR"
},
"referenceNumber": "TRANSFER-123456789"
}""")
}"""
)
self.assertEqual(len(lines), 1)
self.assertEqual(lines[0], {
'date': datetime(2000, 1, 1),
'amount': '0.42',
'name': 'REF-XYZ',
'note': (
'TRANSFER-123456789: Received money from SENDER with reference'
' REF-XYZ'
),
'partner_name': 'SENDER',
'account_number': 'XX00 0000 0000 0000',
'unique_import_id': 'CREDIT-TRANSFER-123456789-946684800',
})
self.assertEqual(
lines[0],
{
"date": datetime(2000, 1, 1),
"amount": "0.42",
"name": "REF-XYZ",
"note": (
"TRANSFER-123456789: Received money from SENDER with reference"
" REF-XYZ"
),
"partner_name": "SENDER",
"account_number": "XX00 0000 0000 0000",
"unique_import_id": "CREDIT-TRANSFER-123456789-946684800",
},
)
def test_transaction_parse_2(self):
lines = self.transferwise_parse_transaction("""{
lines = self.transferwise_parse_transaction(
"""{
"type": "DEBIT",
"date": "2000-01-01T00:00:00.000Z",
"amount": {
@@ -379,28 +378,36 @@ edF6byMgXSzgOWYuRPXwmHpBQV0GiexQUAxVyUzaVWfil69LaFfXaw==
"currency": "EUR"
},
"referenceNumber": "TRANSFER-123456789"
}""")
}"""
)
self.assertEqual(len(lines), 2)
self.assertEqual(lines[0], {
'date': datetime(2000, 1, 1),
'amount': '-200.00',
'name': 'INVOICE 42-01',
'note': 'TRANSFER-123456789: Sent money to John Doe',
'partner_name': 'John Doe',
'account_number': 'XX00 0000 0000 0000',
'unique_import_id': 'DEBIT-TRANSFER-123456789-946684800',
})
self.assertEqual(lines[1], {
'date': datetime(2000, 1, 1),
'amount': '-0.60',
'name': 'Fee for TRANSFER-123456789',
'note': 'Transaction fee for TRANSFER-123456789',
'partner_name': 'Wise (former TransferWise)',
'unique_import_id': 'DEBIT-TRANSFER-123456789-946684800-FEE',
})
self.assertEqual(
lines[0],
{
"date": datetime(2000, 1, 1),
"amount": "-200.00",
"name": "INVOICE 42-01",
"note": "TRANSFER-123456789: Sent money to John Doe",
"partner_name": "John Doe",
"account_number": "XX00 0000 0000 0000",
"unique_import_id": "DEBIT-TRANSFER-123456789-946684800",
},
)
self.assertEqual(
lines[1],
{
"date": datetime(2000, 1, 1),
"amount": "-0.60",
"name": "Fee for TRANSFER-123456789",
"note": "Transaction fee for TRANSFER-123456789",
"partner_name": "Wise (former TransferWise)",
"unique_import_id": "DEBIT-TRANSFER-123456789-946684800-FEE",
},
)
def test_transaction_parse_3(self):
lines = self.transferwise_parse_transaction("""{
lines = self.transferwise_parse_transaction(
"""{
"type": "DEBIT",
"date": "2000-01-01T00:00:00.000Z",
"amount": {
@@ -436,24 +443,27 @@ edF6byMgXSzgOWYuRPXwmHpBQV0GiexQUAxVyUzaVWfil69LaFfXaw==
"currency": "USD"
},
"referenceNumber": "CARD-123456789"
}""")
}"""
)
self.assertEqual(len(lines), 1)
self.assertEqual(lines[0], {
'date': datetime(2000, 1, 1),
'amount': '-123.45',
'name': (
'Card transaction of 1234.56 USD issued by Paypal *XX CITY'
),
'note': (
'CARD-123456789: Card transaction of 1234.56 USD issued by '
'Paypal *XX CITY'
),
'partner_name': 'Paypal *XX',
'unique_import_id': 'DEBIT-CARD-123456789-946684800',
})
self.assertEqual(
lines[0],
{
"date": datetime(2000, 1, 1),
"amount": "-123.45",
"name": ("Card transaction of 1234.56 USD issued by Paypal *XX CITY"),
"note": (
"CARD-123456789: Card transaction of 1234.56 USD issued by "
"Paypal *XX CITY"
),
"partner_name": "Paypal *XX",
"unique_import_id": "DEBIT-CARD-123456789-946684800",
},
)
def test_transaction_parse_4(self):
lines = self.transferwise_parse_transaction("""{
lines = self.transferwise_parse_transaction(
"""{
"type": "DEBIT",
"date": "2000-01-01T00:00:00.000Z",
"amount": {
@@ -499,34 +509,40 @@ edF6byMgXSzgOWYuRPXwmHpBQV0GiexQUAxVyUzaVWfil69LaFfXaw==
"currency": "EUR"
},
"referenceNumber": "CARD-123456789"
}""")
}"""
)
self.assertEqual(len(lines), 2)
self.assertEqual(lines[0], {
'date': datetime(2000, 1, 1),
'amount': '-455.55',
'name': (
'Card transaction of 1234.56 USD issued by Paypal *XX CITY'
),
'note': (
'CARD-123456789: Card transaction of 1234.56 USD issued by'
' Paypal *XX CITY'
),
'partner_name': 'Paypal *XX',
'unique_import_id': 'DEBIT-CARD-123456789-946684800',
'amount_currency': '-567.89',
'currency_id': self.currency_usd.id,
})
self.assertEqual(lines[1], {
'date': datetime(2000, 1, 1),
'amount': '-1.23',
'name': 'Fee for CARD-123456789',
'note': 'Transaction fee for CARD-123456789',
'partner_name': 'Wise (former TransferWise)',
'unique_import_id': 'DEBIT-CARD-123456789-946684800-FEE',
})
self.assertEqual(
lines[0],
{
"date": datetime(2000, 1, 1),
"amount": "-455.55",
"name": ("Card transaction of 1234.56 USD issued by Paypal *XX CITY"),
"note": (
"CARD-123456789: Card transaction of 1234.56 USD issued by"
" Paypal *XX CITY"
),
"partner_name": "Paypal *XX",
"unique_import_id": "DEBIT-CARD-123456789-946684800",
"amount_currency": "-567.89",
"currency_id": self.currency_usd.id,
},
)
self.assertEqual(
lines[1],
{
"date": datetime(2000, 1, 1),
"amount": "-1.23",
"name": "Fee for CARD-123456789",
"note": "Transaction fee for CARD-123456789",
"partner_name": "Wise (former TransferWise)",
"unique_import_id": "DEBIT-CARD-123456789-946684800-FEE",
},
)
def test_transaction_parse_5(self):
lines = self.transferwise_parse_transaction("""{
lines = self.transferwise_parse_transaction(
"""{
"type": "DEBIT",
"date": "2000-01-01T00:00:00.000Z",
"amount": {
@@ -562,30 +578,38 @@ edF6byMgXSzgOWYuRPXwmHpBQV0GiexQUAxVyUzaVWfil69LaFfXaw==
"currency": "EUR"
},
"referenceNumber": "TRANSFER-123456789"
}""")
}"""
)
self.assertEqual(len(lines), 2)
self.assertEqual(lines[0], {
'date': datetime(2000, 1, 1),
'name': 'Invoice A from DD MMM YYYY',
'note': 'TRANSFER-123456789: Sent money to Jane Doe',
'partner_name': 'Jane Doe',
'account_number': '(ADBCDEF) 0000000000000000',
'amount': '-265.34',
'amount_currency': '-297.00',
'currency_id': self.currency_usd.id,
'unique_import_id': 'DEBIT-TRANSFER-123456789-946684800',
})
self.assertEqual(lines[1], {
'date': datetime(2000, 1, 1),
'name': 'Fee for TRANSFER-123456789',
'note': 'Transaction fee for TRANSFER-123456789',
'partner_name': 'Wise (former TransferWise)',
'amount': '-5.21',
'unique_import_id': 'DEBIT-TRANSFER-123456789-946684800-FEE',
})
self.assertEqual(
lines[0],
{
"date": datetime(2000, 1, 1),
"name": "Invoice A from DD MMM YYYY",
"note": "TRANSFER-123456789: Sent money to Jane Doe",
"partner_name": "Jane Doe",
"account_number": "(ADBCDEF) 0000000000000000",
"amount": "-265.34",
"amount_currency": "-297.00",
"currency_id": self.currency_usd.id,
"unique_import_id": "DEBIT-TRANSFER-123456789-946684800",
},
)
self.assertEqual(
lines[1],
{
"date": datetime(2000, 1, 1),
"name": "Fee for TRANSFER-123456789",
"note": "Transaction fee for TRANSFER-123456789",
"partner_name": "Wise (former TransferWise)",
"amount": "-5.21",
"unique_import_id": "DEBIT-TRANSFER-123456789-946684800-FEE",
},
)
def test_transaction_parse_6(self):
lines = self.transferwise_parse_transaction("""{
lines = self.transferwise_parse_transaction(
"""{
"type": "CREDIT",
"date": "2000-01-01T00:00:00.000Z",
"amount": {
@@ -606,18 +630,23 @@ edF6byMgXSzgOWYuRPXwmHpBQV0GiexQUAxVyUzaVWfil69LaFfXaw==
"currency": "EUR"
},
"referenceNumber": "TRANSFER-123456789"
}""")
}"""
)
self.assertEqual(len(lines), 1)
self.assertEqual(lines[0], {
'date': datetime(2000, 1, 1),
'name': 'Topped up balance',
'note': 'TRANSFER-123456789: Topped up balance',
'amount': '5000.00',
'unique_import_id': 'CREDIT-TRANSFER-123456789-946684800',
})
self.assertEqual(
lines[0],
{
"date": datetime(2000, 1, 1),
"name": "Topped up balance",
"note": "TRANSFER-123456789: Topped up balance",
"amount": "5000.00",
"unique_import_id": "CREDIT-TRANSFER-123456789-946684800",
},
)
def test_transaction_parse_7(self):
lines = self.transferwise_parse_transaction("""{
lines = self.transferwise_parse_transaction(
"""{
"type": "CREDIT",
"date": "2000-01-01T00:00:00.000Z",
"amount": {
@@ -657,20 +686,25 @@ edF6byMgXSzgOWYuRPXwmHpBQV0GiexQUAxVyUzaVWfil69LaFfXaw==
"currency": "EUR"
},
"referenceNumber": "BALANCE-123456789"
}""")
}"""
)
self.assertEqual(len(lines), 1)
self.assertEqual(lines[0], {
'date': datetime(2000, 1, 1),
'name': 'Converted 7.93 USD to 6.93 EUR',
'note': 'BALANCE-123456789: Converted 7.93 USD to 6.93 EUR',
'amount': '6.93',
'amount_currency': '7.93',
'currency_id': self.currency_usd.id,
'unique_import_id': 'CREDIT-BALANCE-123456789-946684800',
})
self.assertEqual(
lines[0],
{
"date": datetime(2000, 1, 1),
"name": "Converted 7.93 USD to 6.93 EUR",
"note": "BALANCE-123456789: Converted 7.93 USD to 6.93 EUR",
"amount": "6.93",
"amount_currency": "7.93",
"currency_id": self.currency_usd.id,
"unique_import_id": "CREDIT-BALANCE-123456789-946684800",
},
)
def test_transaction_parse_8(self):
lines = self.transferwise_parse_transaction("""{
lines = self.transferwise_parse_transaction(
"""{
"type": "DEBIT",
"date": "2000-01-01T00:00:00.000Z",
"amount": {
@@ -710,28 +744,36 @@ edF6byMgXSzgOWYuRPXwmHpBQV0GiexQUAxVyUzaVWfil69LaFfXaw==
"currency": "USD"
},
"referenceNumber": "BALANCE-123456789"
}""")
}"""
)
self.assertEqual(len(lines), 2)
self.assertEqual(lines[0], {
'date': datetime(2000, 1, 1),
'name': 'Converted 7.93 USD to 6.93 EUR',
'note': 'BALANCE-123456789: Converted 7.93 USD to 6.93 EUR',
'amount': '-7.88',
'amount_currency': '-6.93',
'currency_id': self.currency_eur.id,
'unique_import_id': 'DEBIT-BALANCE-123456789-946684800',
})
self.assertEqual(lines[1], {
'date': datetime(2000, 1, 1),
'name': 'Fee for BALANCE-123456789',
'note': 'Transaction fee for BALANCE-123456789',
'amount': '-0.05',
'partner_name': 'Wise (former TransferWise)',
'unique_import_id': 'DEBIT-BALANCE-123456789-946684800-FEE',
})
self.assertEqual(
lines[0],
{
"date": datetime(2000, 1, 1),
"name": "Converted 7.93 USD to 6.93 EUR",
"note": "BALANCE-123456789: Converted 7.93 USD to 6.93 EUR",
"amount": "-7.88",
"amount_currency": "-6.93",
"currency_id": self.currency_eur.id,
"unique_import_id": "DEBIT-BALANCE-123456789-946684800",
},
)
self.assertEqual(
lines[1],
{
"date": datetime(2000, 1, 1),
"name": "Fee for BALANCE-123456789",
"note": "Transaction fee for BALANCE-123456789",
"amount": "-0.05",
"partner_name": "Wise (former TransferWise)",
"unique_import_id": "DEBIT-BALANCE-123456789-946684800-FEE",
},
)
def test_transaction_parse_9(self):
lines = self.transferwise_parse_transaction("""{
lines = self.transferwise_parse_transaction(
"""{
"type": "CREDIT",
"date": "2000-01-01T00:00:00.000Z",
"amount": {
@@ -752,26 +794,34 @@ edF6byMgXSzgOWYuRPXwmHpBQV0GiexQUAxVyUzaVWfil69LaFfXaw==
"currency": "USD"
},
"referenceNumber": "TRANSFER-123456789"
}""")
}"""
)
self.assertEqual(len(lines), 2)
self.assertEqual(lines[0], {
'date': datetime(2000, 1, 1),
'name': 'Topped up balance',
'note': 'TRANSFER-123456789: Topped up balance',
'amount': '25.68',
'unique_import_id': 'CREDIT-TRANSFER-123456789-946684800',
})
self.assertEqual(lines[1], {
'date': datetime(2000, 1, 1),
'name': 'Fee for TRANSFER-123456789',
'note': 'Transaction fee for TRANSFER-123456789',
'amount': '-0.68',
'partner_name': 'Wise (former TransferWise)',
'unique_import_id': 'CREDIT-TRANSFER-123456789-946684800-FEE',
})
self.assertEqual(
lines[0],
{
"date": datetime(2000, 1, 1),
"name": "Topped up balance",
"note": "TRANSFER-123456789: Topped up balance",
"amount": "25.68",
"unique_import_id": "CREDIT-TRANSFER-123456789-946684800",
},
)
self.assertEqual(
lines[1],
{
"date": datetime(2000, 1, 1),
"name": "Fee for TRANSFER-123456789",
"note": "Transaction fee for TRANSFER-123456789",
"amount": "-0.68",
"partner_name": "Wise (former TransferWise)",
"unique_import_id": "CREDIT-TRANSFER-123456789-946684800-FEE",
},
)
def test_transaction_parse_10(self):
lines = self.transferwise_parse_transaction("""{
lines = self.transferwise_parse_transaction(
"""{
"type": "CREDIT",
"date": "2000-01-01T00:00:00.000Z",
"amount": {
@@ -795,21 +845,28 @@ edF6byMgXSzgOWYuRPXwmHpBQV0GiexQUAxVyUzaVWfil69LaFfXaw==
"currency": "USD"
},
"referenceNumber": "TRANSFER-123456789"
}""")
}"""
)
self.assertEqual(len(lines), 2)
self.assertEqual(lines[0], {
'date': datetime(2000, 1, 1),
'name': 'Sent money to Acme Inc.',
'note': 'TRANSFER-123456789: Sent money to Acme Inc.',
'partner_name': 'Acme Inc.',
'amount': '1800.00',
'unique_import_id': 'CREDIT-TRANSFER-123456789-946684800',
})
self.assertEqual(lines[1], {
'date': datetime(2000, 1, 1),
'name': 'Fee for TRANSFER-123456789',
'note': 'Transaction fee for TRANSFER-123456789',
'amount': '4.33',
'partner_name': 'Wise (former TransferWise)',
'unique_import_id': 'CREDIT-TRANSFER-123456789-946684800-FEE',
})
self.assertEqual(
lines[0],
{
"date": datetime(2000, 1, 1),
"name": "Sent money to Acme Inc.",
"note": "TRANSFER-123456789: Sent money to Acme Inc.",
"partner_name": "Acme Inc.",
"amount": "1800.00",
"unique_import_id": "CREDIT-TRANSFER-123456789-946684800",
},
)
self.assertEqual(
lines[1],
{
"date": datetime(2000, 1, 1),
"name": "Fee for TRANSFER-123456789",
"note": "Transaction fee for TRANSFER-123456789",
"amount": "4.33",
"partner_name": "Wise (former TransferWise)",
"unique_import_id": "CREDIT-TRANSFER-123456789-946684800-FEE",
},
)

View File

@@ -1,15 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<!--
Copyright 2019 Brainbean Apps (https://brainbeanapps.com)
Copyright 2021 CorporateHub (https://corporatehub.eu)
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
-->
<odoo>
<record model="ir.ui.view" id="online_bank_statement_provider_form">
<field name="name">online.bank.statement.provider.form</field>
<field name="model">online.bank.statement.provider</field>
<field name="inherit_id" ref="account_bank_statement_import_online.online_bank_statement_provider_form"/>
<field
name="inherit_id"
ref="account_bank_statement_import_online.online_bank_statement_provider_form"
/>
<field name="arch" type="xml">
<xpath expr="//page[@name='configuration']" position="inside">
<group attrs="{'invisible': [('service', '!=', 'transferwise')]}">
@@ -42,10 +44,7 @@
string="Private key"
password="True"
/>
<field
name="certificate_public_key"
string="Public key"
/>
<field name="certificate_public_key" string="Public key" />
<div col="2" colspan="2">
<button
name="button_transferwise_generate_key"
@@ -59,5 +58,4 @@
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1 @@
../../../../account_bank_statement_import_online_transferwise

View File

@@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)