From ff068d25bb328aa7dd764899dfe9bf34c449c679 Mon Sep 17 00:00:00 2001 From: Alexey Pelykh Date: Thu, 1 Aug 2019 18:52:12 +0300 Subject: [PATCH 01/10] [ADD] account_bank_statement_import_online_transferwise --- .../README.rst | 118 ++++ .../__init__.py | 3 + .../__manifest__.py | 24 + ...k_statement_import_online_transferwise.pot | 59 ++ .../models/__init__.py | 3 + ...ne_bank_statement_provider_transferwise.py | 277 +++++++++ .../readme/CONFIGURE.rst | 20 + .../readme/CONTRIBUTORS.rst | 3 + .../readme/DESCRIPTION.rst | 2 + .../readme/USAGE.rst | 6 + .../static/description/icon.png | Bin 0 -> 9455 bytes .../static/description/index.html | 461 +++++++++++++++ .../tests/__init__.py | 3 + ...nk_statement_import_online_transferwise.py | 551 ++++++++++++++++++ .../views/online_bank_statement_provider.xml | 43 ++ 15 files changed, 1573 insertions(+) create mode 100644 account_bank_statement_import_online_transferwise/README.rst create mode 100644 account_bank_statement_import_online_transferwise/__init__.py create mode 100644 account_bank_statement_import_online_transferwise/__manifest__.py create mode 100644 account_bank_statement_import_online_transferwise/i18n/account_bank_statement_import_online_transferwise.pot create mode 100644 account_bank_statement_import_online_transferwise/models/__init__.py create mode 100644 account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py create mode 100644 account_bank_statement_import_online_transferwise/readme/CONFIGURE.rst create mode 100644 account_bank_statement_import_online_transferwise/readme/CONTRIBUTORS.rst create mode 100644 account_bank_statement_import_online_transferwise/readme/DESCRIPTION.rst create mode 100644 account_bank_statement_import_online_transferwise/readme/USAGE.rst create mode 100644 account_bank_statement_import_online_transferwise/static/description/icon.png create mode 100644 account_bank_statement_import_online_transferwise/static/description/index.html create mode 100644 account_bank_statement_import_online_transferwise/tests/__init__.py create mode 100644 account_bank_statement_import_online_transferwise/tests/test_account_bank_statement_import_online_transferwise.py create mode 100644 account_bank_statement_import_online_transferwise/views/online_bank_statement_provider.xml diff --git a/account_bank_statement_import_online_transferwise/README.rst b/account_bank_statement_import_online_transferwise/README.rst new file mode 100644 index 00000000..cf9764b2 --- /dev/null +++ b/account_bank_statement_import_online_transferwise/README.rst @@ -0,0 +1,118 @@ +======================================== +Online Bank Statements: TransferWise.com +======================================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fbank--statement--import-lightgray.png?logo=github + :target: https://github.com/OCA/bank-statement-import/tree/12.0/account_bank_statement_import_online_transferwise + :alt: OCA/bank-statement-import +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/bank-statement-import-12-0/bank-statement-import-12-0-account_bank_statement_import_online_transferwise + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/174/12.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module provides online bank statements from +`TransferWise.com `__. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +To configure online bank statements provider: + +#. Go to *Invoicing > Configuration > Bank Accounts* +#. Open bank account to configure and edit it +#. Set *Bank Feeds* to *Online* +#. Select *TransferWise.com* as online bank statements provider in + *Online Bank Statements (OCA)* section +#. Save the bank account +#. Click on provider and configure provider-specific settings. + +or, alternatively: + +#. Go to *Invoicing > Overview* +#. Open settings of the corresponding journal account +#. Switch to *Bank Account* tab +#. Set *Bank Feeds* to *Online* +#. Select *TransferWise.com* as online bank statements provider in + *Online Bank Statements (OCA)* section +#. Save the bank account +#. Click on provider and configure provider-specific settings. + +Usage +===== + +To pull historical bank statements: + +#. Go to *Invoicing > Configuration > Bank Accounts* +#. Select specific bank accounts +#. Launch *Actions > Online Bank Statements Pull Wizard* +#. Configure date interval and click *Pull* + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* CorporateHub + +Contributors +~~~~~~~~~~~~ + +* `CorporateHub `__ + + * Alexey Pelykh + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-alexey-pelykh| image:: https://github.com/alexey-pelykh.png?size=40px + :target: https://github.com/alexey-pelykh + :alt: alexey-pelykh + +Current `maintainer `__: + +|maintainer-alexey-pelykh| + +This module is part of the `OCA/bank-statement-import `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/account_bank_statement_import_online_transferwise/__init__.py b/account_bank_statement_import_online_transferwise/__init__.py new file mode 100644 index 00000000..31660d6a --- /dev/null +++ b/account_bank_statement_import_online_transferwise/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import models diff --git a/account_bank_statement_import_online_transferwise/__manifest__.py b/account_bank_statement_import_online_transferwise/__manifest__.py new file mode 100644 index 00000000..fadec09e --- /dev/null +++ b/account_bank_statement_import_online_transferwise/__manifest__.py @@ -0,0 +1,24 @@ +# Copyright 2019 Brainbean Apps (https://brainbeanapps.com) +# Copyright 2020 CorporateHub (https://corporatehub.eu) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + 'name': 'Online Bank Statements: TransferWise.com', + 'version': '12.0.1.0.0', + '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 TransferWise.com', + 'depends': [ + 'account_bank_statement_import_online', + 'web_widget_dropdown_dynamic', + ], + 'data': [ + 'views/online_bank_statement_provider.xml', + ], + 'installable': True, +} diff --git a/account_bank_statement_import_online_transferwise/i18n/account_bank_statement_import_online_transferwise.pot b/account_bank_statement_import_online_transferwise/i18n/account_bank_statement_import_online_transferwise.pot new file mode 100644 index 00000000..f5d4679f --- /dev/null +++ b/account_bank_statement_import_online_transferwise/i18n/account_bank_statement_import_online_transferwise.pot @@ -0,0 +1,59 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_bank_statement_import_online_transferwise +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: account_bank_statement_import_online_transferwise +#: model_terms:ir.ui.view,arch_db:account_bank_statement_import_online_transferwise.online_bank_statement_provider_form +msgid "API base" +msgstr "" + +#. module: account_bank_statement_import_online_transferwise +#: model_terms:ir.ui.view,arch_db:account_bank_statement_import_online_transferwise.online_bank_statement_provider_form +msgid "API key" +msgstr "" + +#. module: account_bank_statement_import_online_transferwise +#: code:addons/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py:125 +#, python-format +msgid "Ending balance unavailable" +msgstr "" + +#. module: account_bank_statement_import_online_transferwise +#: code:addons/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py:240 +#, python-format +msgid "Fee for %s" +msgstr "" + +#. module: account_bank_statement_import_online_transferwise +#: code:addons/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py:271 +#, python-format +msgid "No API key specified!" +msgstr "" + +#. module: account_bank_statement_import_online_transferwise +#: model:ir.model,name:account_bank_statement_import_online_transferwise.model_online_bank_statement_provider +msgid "Online Bank Statement Provider" +msgstr "" + +#. module: account_bank_statement_import_online_transferwise +#: model_terms:ir.ui.view,arch_db:account_bank_statement_import_online_transferwise.online_bank_statement_provider_form +msgid "Profile" +msgstr "" + +#. module: account_bank_statement_import_online_transferwise +#: code:addons/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py:245 +#, python-format +msgid "Transaction fee for %s" +msgstr "" + diff --git a/account_bank_statement_import_online_transferwise/models/__init__.py b/account_bank_statement_import_online_transferwise/models/__init__.py new file mode 100644 index 00000000..3a96b707 --- /dev/null +++ b/account_bank_statement_import_online_transferwise/models/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import online_bank_statement_provider_transferwise diff --git a/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py b/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py new file mode 100644 index 00000000..e7a70a82 --- /dev/null +++ b/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py @@ -0,0 +1,277 @@ +# Copyright 2019 Brainbean Apps (https://brainbeanapps.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from dateutil.relativedelta import relativedelta +import dateutil.parser +from decimal import Decimal +import itertools +import json +import pytz +import urllib.parse +import urllib.request + +from odoo import models, api, _ +from odoo.exceptions import UserError + +TRANSFERWISE_API_BASE = 'https://api.transferwise.com' + + +class OnlineBankStatementProviderTransferwise(models.Model): + _inherit = 'online.bank.statement.provider' + + @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') + if not api_key: + return [] + try: + url = api_base + '/v1/profiles' + data = self._transferwise_retrieve(url, api_key) + except: + 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 + )) + + @api.model + def _get_available_services(self): + return super()._get_available_services() + [ + ('transferwise', 'TransferWise.com'), + ] + + @api.multi + def _obtain_statement_data(self, date_since, date_until): + self.ensure_one() + if self.service != 'transferwise': + return super()._obtain_statement_data( + date_since, + date_until, + ) # pragma: no cover + + api_base = self.api_base or TRANSFERWISE_API_BASE + api_key = self.password + 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) + if date_until.tzinfo: + 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, + ) + data = self._transferwise_retrieve(url, api_key) + borderless_account = data[0]['id'] + balance = list(filter( + lambda balance: balance['currency'] == currency, + data[0]['balances'] + )) + if not balance: + return None + + # Notes on /statement endpoint: + # - intervalStart <= date < intervalEnd + + # Get starting balance + starting_balance_timestamp = date_since.isoformat() + 'Z' + url = api_base + ( + '/v1/borderless-accounts/%s/statement.json' + + '?currency=%s&intervalStart=%s&intervalEnd=%s' + ) % ( + borderless_account, + currency, + starting_balance_timestamp, + starting_balance_timestamp, + ) + data = self._transferwise_retrieve(url, api_key) + balance_start = data['endOfStatementBalance']['value'] + + # Get statements, using 469 days (around 1 year 3 month) as step. + interval_step = relativedelta(days=469) + interval_start = date_since + interval_end = date_until + transactions = [] + balance_end = None + while interval_start < interval_end: + url = api_base + ( + '/v1/borderless-accounts/%s/statement.json' + + '?currency=%s&intervalStart=%s&intervalEnd=%s' + ) % ( + borderless_account, + currency, + interval_start.isoformat() + 'Z', + min( + interval_start + interval_step, interval_end + ).isoformat() + 'Z', + ) + data = self._transferwise_retrieve(url, api_key) + transactions += data['transactions'] + balance_end = data['endOfStatementBalance']['value'] + interval_start += interval_step + if balance_end is None: + raise UserError(_('Ending balance unavailable')) + + # Normalize transactions' date, sort by it, and get lines + transactions = map( + 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'] + ) + ))) + + 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) + return transaction + + @api.model + def _transferwise_transaction_to_lines(self, transaction): + 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()).copy_abs() + if amount_value.is_signed(): + fees_value = fees_value.copy_negate() + amount_value -= fees_value + unique_import_id = '%s-%s-%s' % ( + 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, + } + 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'], + }) + else: + 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'] + other_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'] + ) + 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 + ) + if other_amount_value and other_currency: + 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': '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']: + raise UserError( + content['error_description'] + if 'error_description' in content + else 'Unknown error' + ) + return content + + @api.model + def _transferwise_retrieve(self, url, api_key): + with self._transferwise_urlopen(url, api_key) as response: + content = response.read().decode( + response.headers.get_content_charset() + ) + return self._transferwise_validate(content) + + @api.model + def _transferwise_urlopen(self, url, api_key): + if not api_key: + raise UserError(_('No API key specified!')) + request = urllib.request.Request(url) + request.add_header( + 'Authorization', + 'Bearer %s' % api_key + ) + return urllib.request.urlopen(request) diff --git a/account_bank_statement_import_online_transferwise/readme/CONFIGURE.rst b/account_bank_statement_import_online_transferwise/readme/CONFIGURE.rst new file mode 100644 index 00000000..0a35ffe9 --- /dev/null +++ b/account_bank_statement_import_online_transferwise/readme/CONFIGURE.rst @@ -0,0 +1,20 @@ +To configure online bank statements provider: + +#. Go to *Invoicing > Configuration > Bank Accounts* +#. Open bank account to configure and edit it +#. Set *Bank Feeds* to *Online* +#. Select *TransferWise.com* as online bank statements provider in + *Online Bank Statements (OCA)* section +#. Save the bank account +#. Click on provider and configure provider-specific settings. + +or, alternatively: + +#. Go to *Invoicing > Overview* +#. Open settings of the corresponding journal account +#. Switch to *Bank Account* tab +#. Set *Bank Feeds* to *Online* +#. Select *TransferWise.com* as online bank statements provider in + *Online Bank Statements (OCA)* section +#. Save the bank account +#. Click on provider and configure provider-specific settings. diff --git a/account_bank_statement_import_online_transferwise/readme/CONTRIBUTORS.rst b/account_bank_statement_import_online_transferwise/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..724bc1d0 --- /dev/null +++ b/account_bank_statement_import_online_transferwise/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `CorporateHub `__ + + * Alexey Pelykh diff --git a/account_bank_statement_import_online_transferwise/readme/DESCRIPTION.rst b/account_bank_statement_import_online_transferwise/readme/DESCRIPTION.rst new file mode 100644 index 00000000..67f68254 --- /dev/null +++ b/account_bank_statement_import_online_transferwise/readme/DESCRIPTION.rst @@ -0,0 +1,2 @@ +This module provides online bank statements from +`TransferWise.com `__. diff --git a/account_bank_statement_import_online_transferwise/readme/USAGE.rst b/account_bank_statement_import_online_transferwise/readme/USAGE.rst new file mode 100644 index 00000000..03845f13 --- /dev/null +++ b/account_bank_statement_import_online_transferwise/readme/USAGE.rst @@ -0,0 +1,6 @@ +To pull historical bank statements: + +#. Go to *Invoicing > Configuration > Bank Accounts* +#. Select specific bank accounts +#. Launch *Actions > Online Bank Statements Pull Wizard* +#. Configure date interval and click *Pull* diff --git a/account_bank_statement_import_online_transferwise/static/description/icon.png b/account_bank_statement_import_online_transferwise/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/account_bank_statement_import_online_transferwise/static/description/index.html b/account_bank_statement_import_online_transferwise/static/description/index.html new file mode 100644 index 00000000..89d11dcb --- /dev/null +++ b/account_bank_statement_import_online_transferwise/static/description/index.html @@ -0,0 +1,461 @@ + + + + + + +Online Bank Statements: TransferWise.com + + + +
+

Online Bank Statements: TransferWise.com

+ + +

Beta License: AGPL-3 OCA/bank-statement-import Translate me on Weblate Try me on Runbot

+

This module provides online bank statements from +TransferWise.com.

+

Table of contents

+ +
+

Configuration

+

To configure online bank statements provider:

+
    +
  1. Go to Invoicing > Configuration > Bank Accounts
  2. +
  3. Open bank account to configure and edit it
  4. +
  5. Set Bank Feeds to Online
  6. +
  7. Select TransferWise.com as online bank statements provider in +Online Bank Statements (OCA) section
  8. +
  9. Save the bank account
  10. +
  11. Click on provider and configure provider-specific settings.
  12. +
+

or, alternatively:

+
    +
  1. Go to Invoicing > Overview
  2. +
  3. Open settings of the corresponding journal account
  4. +
  5. Switch to Bank Account tab
  6. +
  7. Set Bank Feeds to Online
  8. +
  9. Select TransferWise.com as online bank statements provider in +Online Bank Statements (OCA) section
  10. +
  11. Save the bank account
  12. +
  13. Click on provider and configure provider-specific settings.
  14. +
+
+
+

Usage

+

To pull historical bank statements:

+
    +
  1. Go to Invoicing > Configuration > Bank Accounts
  2. +
  3. Select specific bank accounts
  4. +
  5. Launch Actions > Online Bank Statements Pull Wizard
  6. +
  7. Configure date interval and click Pull
  8. +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • CorporateHub
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

alexey-pelykh

+

This module is part of the OCA/bank-statement-import project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/account_bank_statement_import_online_transferwise/tests/__init__.py b/account_bank_statement_import_online_transferwise/tests/__init__.py new file mode 100644 index 00000000..130e465f --- /dev/null +++ b/account_bank_statement_import_online_transferwise/tests/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import test_account_bank_statement_import_online_transferwise diff --git a/account_bank_statement_import_online_transferwise/tests/test_account_bank_statement_import_online_transferwise.py b/account_bank_statement_import_online_transferwise/tests/test_account_bank_statement_import_online_transferwise.py new file mode 100644 index 00000000..8ab35755 --- /dev/null +++ b/account_bank_statement_import_online_transferwise/tests/test_account_bank_statement_import_online_transferwise.py @@ -0,0 +1,551 @@ +# Copyright 2019 Brainbean Apps (https://brainbeanapps.com) +# 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 unittest import mock + +from odoo.tests import common +from odoo import fields + +_module_ns = 'odoo.addons.account_bank_statement_import_online_transferwise' +_provider_class = ( + _module_ns + + '.models.online_bank_statement_provider_transferwise' + + '.OnlineBankStatementProviderTransferwise' +) + + +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'] + + Provider = self.OnlineBankStatementProvider + self.transferwise_parse_transaction = lambda payload: ( + Provider._transferwise_transaction_to_lines( + Provider._transferwise_preparse_transaction( + json.loads( + payload, + parse_float=Decimal, + ) + ) + ) + ) + + def test_values_transferwise_profile(self): + mocked_response = json.loads( + """[ + { + "id": 1234567890, + "type": "personal", + "details": { + "firstName": "Alexey", + "lastName": "Pelykh" + } + }, + { + "id": 1234567891, + "type": "business", + "details": { + "name": "Brainbean Apps OÜ" + } + } +]""", parse_float=Decimal) + values_transferwise_profile = [] + with mock.patch( + _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() + ) + self.assertEqual( + values_transferwise_profile, + [ + ('1234567890', 'Alexey Pelykh (personal)'), + ('1234567891', 'Brainbean Apps OÜ'), + ] + ) + + 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', + }) + + provider = journal.online_bank_statement_provider_id + provider.origin = '1234567891' + + def mock_response(url, api_key): + if '/borderless-accounts?profileId=1234567891' in url: + payload = """[ + { + "id": 42, + "balances": [ + { + "currency": "EUR" + } + ] + } +]""" + elif '/borderless-accounts/42/statement.json' in url: + payload = """{ + "transactions": [], + "endOfStatementBalance": { + "value": 42.00, + "currency": "EUR" + } +}""" + return json.loads(payload, parse_float=Decimal) + with mock.patch( + _provider_class + '._transferwise_retrieve', + side_effect=mock_response, + ): + data = provider._obtain_statement_data( + 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) + + def test_transaction_parse_1(self): + lines = self.transferwise_parse_transaction("""{ + "type": "CREDIT", + "date": "2000-01-01T00:00:00.000Z", + "amount": { + "value": 0.42, + "currency": "EUR" + }, + "totalFees": { + "value": 0.00, + "currency": "EUR" + }, + "details": { + "type": "DEPOSIT", + "description": "Received money from SENDER with reference REF-XYZ", + "senderName": "SENDER", + "senderAccount": "XX00 0000 0000 0000", + "paymentReference": "REF-XYZ" + }, + "exchangeDetails": null, + "runningBalance": { + "value": 0.42, + "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', + }) + + def test_transaction_parse_2(self): + lines = self.transferwise_parse_transaction("""{ + "type": "DEBIT", + "date": "2000-01-01T00:00:00.000Z", + "amount": { + "value": -200.60, + "currency": "EUR" + }, + "totalFees": { + "value": 0.60, + "currency": "EUR" + }, + "details": { + "type": "TRANSFER", + "description": "Sent money to John Doe", + "recipient": { + "name": "John Doe", + "bankAccount": "XX00 0000 0000 0000" + }, + "paymentReference": "INVOICE 42-01" + }, + "exchangeDetails": null, + "runningBalance": { + "value": 100.42, + "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': 'TransferWise', + 'unique_import_id': 'DEBIT-TRANSFER-123456789-946684800-FEE', + }) + + def test_transaction_parse_3(self): + lines = self.transferwise_parse_transaction("""{ + "type": "DEBIT", + "date": "2000-01-01T00:00:00.000Z", + "amount": { + "value": -123.45, + "currency": "USD" + }, + "totalFees": { + "value": 0.00, + "currency": "USD" + }, + "details": { + "type": "CARD", + "description": + "Card transaction of 1234.56 USD issued by Paypal *XX CITY", + "amount": { + "value": 1234.56, + "currency": "USD" + }, + "category": "Professional Services not elsewh", + "merchant": { + "name": "Paypal *XX", + "firstLine": null, + "postCode": "12345", + "city": "CITY", + "state": null, + "country": "GB", + "category": "Professional Services not elsewh" + } + }, + "exchangeDetails": null, + "runningBalance": { + "value": 0.00, + "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', + }) + + def test_transaction_parse_4(self): + lines = self.transferwise_parse_transaction("""{ + "type": "DEBIT", + "date": "2000-01-01T00:00:00.000Z", + "amount": { + "value": -456.78, + "currency": "EUR" + }, + "totalFees": { + "value": 1.23, + "currency": "EUR" + }, + "details": { + "type": "CARD", + "description": + "Card transaction of 1234.56 USD issued by Paypal *XX CITY", + "amount": { + "value": 1234.56, + "currency": "USD" + }, + "category": "Professional Services not elsewh", + "merchant": { + "name": "Paypal *XX", + "firstLine": null, + "postCode": "12345", + "city": "CITY", + "state": null, + "country": "GB", + "category": "Professional Services not elsewh" + } + }, + "exchangeDetails": { + "toAmount": { + "value": 567.89, + "currency": "USD" + }, + "fromAmount": { + "value": 456.78, + "currency": "EUR" + }, + "rate": 1.12260 + }, + "runningBalance": { + "value": 0.00, + "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': 'TransferWise', + 'unique_import_id': 'DEBIT-CARD-123456789-946684800-FEE', + }) + + def test_transaction_parse_5(self): + lines = self.transferwise_parse_transaction("""{ + "type": "DEBIT", + "date": "2000-01-01T00:00:00.000Z", + "amount": { + "value": -270.55, + "currency": "EUR" + }, + "totalFees": { + "value": 5.21, + "currency": "EUR" + }, + "details": { + "type": "TRANSFER", + "description": "Sent money to Jane Doe", + "recipient": { + "name": "Jane Doe", + "bankAccount": "(ADBCDEF) 0000000000000000" + }, + "paymentReference": "Invoice A from DD MMM YYYY" + }, + "exchangeDetails": { + "toAmount": { + "value": 297.00, + "currency": "USD" + }, + "fromAmount": { + "value": 265.34, + "currency": "EUR" + }, + "rate": 1.11930 + }, + "runningBalance": { + "value": 2360.43, + "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': 'TransferWise', + 'amount': '-5.21', + 'unique_import_id': 'DEBIT-TRANSFER-123456789-946684800-FEE', + }) + + def test_transaction_parse_6(self): + lines = self.transferwise_parse_transaction("""{ + "type": "CREDIT", + "date": "2000-01-01T00:00:00.000Z", + "amount": { + "value": 5000.00, + "currency": "EUR" + }, + "totalFees": { + "value": 0.00, + "currency": "EUR" + }, + "details": { + "type": "MONEY_ADDED", + "description": "Topped up balance" + }, + "exchangeDetails": null, + "runningBalance": { + "value": 7071.13, + "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', + }) + + def test_transaction_parse_7(self): + lines = self.transferwise_parse_transaction("""{ + "type": "CREDIT", + "date": "2000-01-01T00:00:00.000Z", + "amount": { + "value": 6.93, + "currency": "EUR" + }, + "totalFees": { + "value": 0.00, + "currency": "EUR" + }, + "details": { + "type": "CONVERSION", + "description": "Converted 7.93 USD to 6.93 EUR", + "sourceAmount": { + "value": 7.93, + "currency": "USD" + }, + "targetAmount": { + "value": 6.93, + "currency": "EUR" + }, + "rate": 0.87944162 + }, + "exchangeDetails": { + "toAmount": { + "value": 6.93, + "currency": "EUR" + }, + "fromAmount": { + "value": 7.93, + "currency": "USD" + }, + "rate": 0.87944 + }, + "runningBalance": { + "value": 255.00, + "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', + }) + + def test_transaction_parse_8(self): + lines = self.transferwise_parse_transaction("""{ + "type": "DEBIT", + "date": "2000-01-01T00:00:00.000Z", + "amount": { + "value": -7.93, + "currency": "USD" + }, + "totalFees": { + "value": 0.05, + "currency": "USD" + }, + "details": { + "type": "CONVERSION", + "description": "Converted 7.93 USD to 6.93 EUR", + "sourceAmount": { + "value": 7.93, + "currency": "USD" + }, + "targetAmount": { + "value": 6.93, + "currency": "EUR" + }, + "rate": 0.87944162 + }, + "exchangeDetails": { + "toAmount": { + "value": 6.93, + "currency": "EUR" + }, + "fromAmount": { + "value": 7.93, + "currency": "USD" + }, + "rate": 0.87944 + }, + "runningBalance": { + "value": 0.00, + "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': 'TransferWise', + 'unique_import_id': 'DEBIT-BALANCE-123456789-946684800-FEE', + }) diff --git a/account_bank_statement_import_online_transferwise/views/online_bank_statement_provider.xml b/account_bank_statement_import_online_transferwise/views/online_bank_statement_provider.xml new file mode 100644 index 00000000..013f15dc --- /dev/null +++ b/account_bank_statement_import_online_transferwise/views/online_bank_statement_provider.xml @@ -0,0 +1,43 @@ + + + + + + online.bank.statement.provider.form + online.bank.statement.provider + + + + + + + + + + + + + + + + + From f079a3331241a7adc1e292adc5e1892eb1505c2a Mon Sep 17 00:00:00 2001 From: Alexey Pelykh Date: Sat, 17 Oct 2020 20:10:35 +0300 Subject: [PATCH 02/10] [FIX] account_bank_statement_import_online_transferwise: fix fees with top-up --- .../__manifest__.py | 2 +- .../account_bank_statement_import_online_transferwise.pot | 8 ++++---- .../models/online_bank_statement_provider_transferwise.py | 5 ++--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/account_bank_statement_import_online_transferwise/__manifest__.py b/account_bank_statement_import_online_transferwise/__manifest__.py index fadec09e..e074ad5e 100644 --- a/account_bank_statement_import_online_transferwise/__manifest__.py +++ b/account_bank_statement_import_online_transferwise/__manifest__.py @@ -4,7 +4,7 @@ { 'name': 'Online Bank Statements: TransferWise.com', - 'version': '12.0.1.0.0', + 'version': '12.0.1.0.1', 'author': 'CorporateHub, ' 'Odoo Community Association (OCA)', diff --git a/account_bank_statement_import_online_transferwise/i18n/account_bank_statement_import_online_transferwise.pot b/account_bank_statement_import_online_transferwise/i18n/account_bank_statement_import_online_transferwise.pot index f5d4679f..8e904c69 100644 --- a/account_bank_statement_import_online_transferwise/i18n/account_bank_statement_import_online_transferwise.pot +++ b/account_bank_statement_import_online_transferwise/i18n/account_bank_statement_import_online_transferwise.pot @@ -24,19 +24,19 @@ msgid "API key" msgstr "" #. module: account_bank_statement_import_online_transferwise -#: code:addons/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py:125 +#: code:addons/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py:126 #, python-format msgid "Ending balance unavailable" msgstr "" #. module: account_bank_statement_import_online_transferwise -#: code:addons/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py:240 +#: code:addons/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py:239 #, python-format msgid "Fee for %s" msgstr "" #. module: account_bank_statement_import_online_transferwise -#: code:addons/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py:271 +#: code:addons/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py:270 #, python-format msgid "No API key specified!" msgstr "" @@ -52,7 +52,7 @@ msgid "Profile" msgstr "" #. module: account_bank_statement_import_online_transferwise -#: code:addons/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py:245 +#: code:addons/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py:244 #, python-format msgid "Transaction fee for %s" msgstr "" diff --git a/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py b/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py index e7a70a82..19266981 100644 --- a/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py +++ b/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py @@ -1,4 +1,5 @@ # Copyright 2019 Brainbean Apps (https://brainbeanapps.com) +# Copyright 2020 CorporateHub (https://corporatehub.eu) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from dateutil.relativedelta import relativedelta @@ -169,9 +170,7 @@ class OnlineBankStatementProviderTransferwise(models.Model): ) amount = transaction['amount'] amount_value = amount.get('value', 0) - fees_value = total_fees.get('value', Decimal()).copy_abs() - if amount_value.is_signed(): - fees_value = fees_value.copy_negate() + fees_value = total_fees.get('value', Decimal()).copy_negate() amount_value -= fees_value unique_import_id = '%s-%s-%s' % ( transaction['type'], From e2b6c60d2725f66d442fee284a7f699933a6d65f Mon Sep 17 00:00:00 2001 From: Alexey Pelykh Date: Sun, 18 Oct 2020 08:06:47 +0300 Subject: [PATCH 03/10] [FIX] account_bank_statement_import_online_transferwise: fees for refund and top-up --- .../__manifest__.py | 2 +- ...k_statement_import_online_transferwise.pot | 6 +- ...ne_bank_statement_provider_transferwise.py | 10 ++- ...nk_statement_import_online_transferwise.py | 85 +++++++++++++++++++ 4 files changed, 97 insertions(+), 6 deletions(-) diff --git a/account_bank_statement_import_online_transferwise/__manifest__.py b/account_bank_statement_import_online_transferwise/__manifest__.py index e074ad5e..a6c1185a 100644 --- a/account_bank_statement_import_online_transferwise/__manifest__.py +++ b/account_bank_statement_import_online_transferwise/__manifest__.py @@ -4,7 +4,7 @@ { 'name': 'Online Bank Statements: TransferWise.com', - 'version': '12.0.1.0.1', + 'version': '12.0.1.0.2', 'author': 'CorporateHub, ' 'Odoo Community Association (OCA)', diff --git a/account_bank_statement_import_online_transferwise/i18n/account_bank_statement_import_online_transferwise.pot b/account_bank_statement_import_online_transferwise/i18n/account_bank_statement_import_online_transferwise.pot index 8e904c69..58b0d392 100644 --- a/account_bank_statement_import_online_transferwise/i18n/account_bank_statement_import_online_transferwise.pot +++ b/account_bank_statement_import_online_transferwise/i18n/account_bank_statement_import_online_transferwise.pot @@ -30,13 +30,13 @@ msgid "Ending balance unavailable" msgstr "" #. module: account_bank_statement_import_online_transferwise -#: code:addons/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py:239 +#: code:addons/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py:245 #, python-format msgid "Fee for %s" msgstr "" #. module: account_bank_statement_import_online_transferwise -#: code:addons/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py:270 +#: code:addons/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py:276 #, python-format msgid "No API key specified!" msgstr "" @@ -52,7 +52,7 @@ msgid "Profile" msgstr "" #. module: account_bank_statement_import_online_transferwise -#: code:addons/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py:244 +#: code:addons/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py:250 #, python-format msgid "Transaction fee for %s" msgstr "" diff --git a/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py b/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py index 19266981..4ed88c11 100644 --- a/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py +++ b/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py @@ -154,6 +154,7 @@ class OnlineBankStatementProviderTransferwise(models.Model): @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') @@ -170,10 +171,15 @@ class OnlineBankStatementProviderTransferwise(models.Model): ) amount = transaction['amount'] amount_value = amount.get('value', 0) - fees_value = total_fees.get('value', Decimal()).copy_negate() + 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'], + transaction_type, reference_number, int(date.timestamp()), ) diff --git a/account_bank_statement_import_online_transferwise/tests/test_account_bank_statement_import_online_transferwise.py b/account_bank_statement_import_online_transferwise/tests/test_account_bank_statement_import_online_transferwise.py index 8ab35755..85651ffe 100644 --- a/account_bank_statement_import_online_transferwise/tests/test_account_bank_statement_import_online_transferwise.py +++ b/account_bank_statement_import_online_transferwise/tests/test_account_bank_statement_import_online_transferwise.py @@ -1,4 +1,5 @@ # Copyright 2019 Brainbean Apps (https://brainbeanapps.com) +# Copyright 2020 CorporateHub (https://corporatehub.eu) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from datetime import datetime @@ -549,3 +550,87 @@ class TestAccountBankAccountStatementImportOnlineTransferwise( 'partner_name': 'TransferWise', 'unique_import_id': 'DEBIT-BALANCE-123456789-946684800-FEE', }) + + def test_transaction_parse_9(self): + lines = self.transferwise_parse_transaction("""{ + "type": "CREDIT", + "date": "2000-01-01T00:00:00.000Z", + "amount": { + "value": 25.00, + "currency": "USD" + }, + "totalFees": { + "value": 0.68, + "currency": "USD" + }, + "details": { + "type": "MONEY_ADDED", + "description": "Topped up balance" + }, + "exchangeDetails": null, + "runningBalance": { + "value": 25.68, + "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': 'TransferWise', + 'unique_import_id': 'CREDIT-TRANSFER-123456789-946684800-FEE', + }) + + def test_transaction_parse_10(self): + lines = self.transferwise_parse_transaction("""{ + "type": "CREDIT", + "date": "2000-01-01T00:00:00.000Z", + "amount": { + "value": 1804.33, + "currency": "USD" + }, + "totalFees": { + "value": 4.33, + "currency": "USD" + }, + "details": { + "type": "TRANSFER", + "description": "Sent money to Acme Inc.", + "recipient": { + "name": "Acme Inc." + } + }, + "exchangeDetails": null, + "runningBalance": { + "value": 1804.33, + "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': 'TransferWise', + 'unique_import_id': 'CREDIT-TRANSFER-123456789-946684800-FEE', + }) From c63a31d5ab302e4b9895c26d9334ec37f2837a14 Mon Sep 17 00:00:00 2001 From: Alexey Pelykh Date: Thu, 22 Oct 2020 14:34:17 +0300 Subject: [PATCH 04/10] [FIX] account_bank_statement_import_online_transferwise: charset --- .../__manifest__.py | 2 +- ...k_statement_import_online_transferwise.pot | 8 +-- .../i18n/nl.po | 61 +++++++++++++++++++ ...ne_bank_statement_provider_transferwise.py | 7 ++- 4 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 account_bank_statement_import_online_transferwise/i18n/nl.po diff --git a/account_bank_statement_import_online_transferwise/__manifest__.py b/account_bank_statement_import_online_transferwise/__manifest__.py index a6c1185a..bea35228 100644 --- a/account_bank_statement_import_online_transferwise/__manifest__.py +++ b/account_bank_statement_import_online_transferwise/__manifest__.py @@ -4,7 +4,7 @@ { 'name': 'Online Bank Statements: TransferWise.com', - 'version': '12.0.1.0.2', + 'version': '12.0.1.0.3', 'author': 'CorporateHub, ' 'Odoo Community Association (OCA)', diff --git a/account_bank_statement_import_online_transferwise/i18n/account_bank_statement_import_online_transferwise.pot b/account_bank_statement_import_online_transferwise/i18n/account_bank_statement_import_online_transferwise.pot index 58b0d392..807eb499 100644 --- a/account_bank_statement_import_online_transferwise/i18n/account_bank_statement_import_online_transferwise.pot +++ b/account_bank_statement_import_online_transferwise/i18n/account_bank_statement_import_online_transferwise.pot @@ -24,19 +24,19 @@ msgid "API key" msgstr "" #. module: account_bank_statement_import_online_transferwise -#: code:addons/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py:126 +#: code:addons/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py:131 #, python-format msgid "Ending balance unavailable" msgstr "" #. module: account_bank_statement_import_online_transferwise -#: code:addons/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py:245 +#: code:addons/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py:250 #, python-format msgid "Fee for %s" msgstr "" #. module: account_bank_statement_import_online_transferwise -#: code:addons/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py:276 +#: code:addons/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py:281 #, python-format msgid "No API key specified!" msgstr "" @@ -52,7 +52,7 @@ msgid "Profile" msgstr "" #. module: account_bank_statement_import_online_transferwise -#: code:addons/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py:250 +#: code:addons/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py:255 #, python-format msgid "Transaction fee for %s" msgstr "" diff --git a/account_bank_statement_import_online_transferwise/i18n/nl.po b/account_bank_statement_import_online_transferwise/i18n/nl.po new file mode 100644 index 00000000..81d3e506 --- /dev/null +++ b/account_bank_statement_import_online_transferwise/i18n/nl.po @@ -0,0 +1,61 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_bank_statement_import_online_transferwise +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2021-01-22 22:44+0000\n" +"Last-Translator: Bosd \n" +"Language-Team: none\n" +"Language: nl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.3.2\n" + +#. module: account_bank_statement_import_online_transferwise +#: model_terms:ir.ui.view,arch_db:account_bank_statement_import_online_transferwise.online_bank_statement_provider_form +msgid "API base" +msgstr "API base" + +#. module: account_bank_statement_import_online_transferwise +#: model_terms:ir.ui.view,arch_db:account_bank_statement_import_online_transferwise.online_bank_statement_provider_form +msgid "API key" +msgstr "API Sleutel" + +#. module: account_bank_statement_import_online_transferwise +#: code:addons/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py:131 +#, python-format +msgid "Ending balance unavailable" +msgstr "Eindbalans is niet beschikbaar" + +#. module: account_bank_statement_import_online_transferwise +#: code:addons/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py:250 +#, fuzzy, python-format +msgid "Fee for %s" +msgstr "Transactievergoeding voor %s" + +#. module: account_bank_statement_import_online_transferwise +#: code:addons/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py:281 +#, python-format +msgid "No API key specified!" +msgstr "Geen API sleutel ingegeven!" + +#. module: account_bank_statement_import_online_transferwise +#: model:ir.model,name:account_bank_statement_import_online_transferwise.model_online_bank_statement_provider +msgid "Online Bank Statement Provider" +msgstr "Online Bankafschrift Provider" + +#. module: account_bank_statement_import_online_transferwise +#: model_terms:ir.ui.view,arch_db:account_bank_statement_import_online_transferwise.online_bank_statement_provider_form +msgid "Profile" +msgstr "Profiel" + +#. module: account_bank_statement_import_online_transferwise +#: code:addons/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py:255 +#, python-format +msgid "Transaction fee for %s" +msgstr "Transactievergoeding voor %s" diff --git a/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py b/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py index 4ed88c11..2006d880 100644 --- a/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py +++ b/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py @@ -14,6 +14,10 @@ import urllib.request from odoo import models, api, _ from odoo.exceptions import UserError +import logging +_logger = logging.getLogger(__name__) + + TRANSFERWISE_API_BASE = 'https://api.transferwise.com' @@ -30,6 +34,7 @@ class OnlineBankStatementProviderTransferwise(models.Model): url = api_base + '/v1/profiles' data = self._transferwise_retrieve(url, api_key) except: + _logger.warning('Unable to get profiles', exc_info=True) return [] return list(map( lambda entry: ( @@ -266,7 +271,7 @@ class OnlineBankStatementProviderTransferwise(models.Model): def _transferwise_retrieve(self, url, api_key): with self._transferwise_urlopen(url, api_key) as response: content = response.read().decode( - response.headers.get_content_charset() + response.headers.get_content_charset() or 'utf-8' ) return self._transferwise_validate(content) From 3dcd4ee101812245e747d48471449a0e0021ea8a Mon Sep 17 00:00:00 2001 From: Alexey Pelykh Date: Fri, 17 Sep 2021 07:40:25 +0200 Subject: [PATCH 05/10] [IMP] account_bank_statement_import_online_transferwise: support SCA --- .../__manifest__.py | 9 +- ...ne_bank_statement_provider_transferwise.py | 138 +++++++++++-- .../readme/CONFIGURE.rst | 4 +- .../readme/DESCRIPTION.rst | 3 +- .../readme/USAGE.rst | 6 + ...nk_statement_import_online_transferwise.py | 195 +++++++++++++++++- .../views/online_bank_statement_provider.xml | 22 +- 7 files changed, 342 insertions(+), 35 deletions(-) diff --git a/account_bank_statement_import_online_transferwise/__manifest__.py b/account_bank_statement_import_online_transferwise/__manifest__.py index bea35228..fc18078d 100644 --- a/account_bank_statement_import_online_transferwise/__manifest__.py +++ b/account_bank_statement_import_online_transferwise/__manifest__.py @@ -1,9 +1,9 @@ # Copyright 2019 Brainbean Apps (https://brainbeanapps.com) -# Copyright 2020 CorporateHub (https://corporatehub.eu) +# Copyright 2020-2021 CorporateHub (https://corporatehub.eu) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). { - 'name': 'Online Bank Statements: TransferWise.com', + 'name': 'Online Bank Statements: Wise.com (TransferWise.com)', 'version': '12.0.1.0.3', 'author': 'CorporateHub, ' @@ -12,11 +12,14 @@ 'website': 'https://github.com/OCA/bank-statement-import/', 'license': 'AGPL-3', 'category': 'Accounting', - 'summary': 'Online bank statements for TransferWise.com', + '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', ], diff --git a/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py b/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py index 2006d880..5c571728 100644 --- a/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py +++ b/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py @@ -1,7 +1,11 @@ # Copyright 2019 Brainbean Apps (https://brainbeanapps.com) -# Copyright 2020 CorporateHub (https://corporatehub.eu) +# 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 @@ -10,8 +14,9 @@ import json import pytz import urllib.parse import urllib.request +from urllib.error import HTTPError -from odoo import models, api, _ +from odoo import api, fields, models, _ from odoo.exceptions import UserError import logging @@ -24,6 +29,14 @@ TRANSFERWISE_API_BASE = 'https://api.transferwise.com' class OnlineBankStatementProviderTransferwise(models.Model): _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, + ) + @api.model def values_transferwise_profile(self): api_base = self.env.context.get('api_base') or TRANSFERWISE_API_BASE @@ -52,7 +65,7 @@ class OnlineBankStatementProviderTransferwise(models.Model): @api.model def _get_available_services(self): return super()._get_available_services() + [ - ('transferwise', 'TransferWise.com'), + ('transferwise', 'Wise.com (TransferWise.com)'), ] @api.multi @@ -66,6 +79,13 @@ class OnlineBankStatementProviderTransferwise(models.Model): api_base = self.api_base or TRANSFERWISE_API_BASE api_key = self.password + private_key = self.certificate_private_key + if private_key: + private_key = serialization.load_pem_private_key( + private_key.encode(), + password=None, + backend=default_backend(), + ) currency = ( self.currency_id or self.company_id.currency_id ).name @@ -79,7 +99,9 @@ class OnlineBankStatementProviderTransferwise(models.Model): url = api_base + '/v1/borderless-accounts?profileId=%s' % ( self.origin, ) - data = self._transferwise_retrieve(url, api_key) + 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, @@ -94,15 +116,16 @@ class OnlineBankStatementProviderTransferwise(models.Model): # Get starting balance starting_balance_timestamp = date_since.isoformat() + 'Z' url = api_base + ( - '/v1/borderless-accounts/%s/statement.json' + - '?currency=%s&intervalStart=%s&intervalEnd=%s' + '/v3/profiles/%s/borderless-accounts/%s/statement.json' + + '?currency=%s&intervalStart=%s&intervalEnd=%s&type=COMPACT' ) % ( + self.origin, borderless_account, currency, starting_balance_timestamp, starting_balance_timestamp, ) - data = self._transferwise_retrieve(url, api_key) + data = self._transferwise_retrieve(url, api_key, private_key) balance_start = data['endOfStatementBalance']['value'] # Get statements, using 469 days (around 1 year 3 month) as step. @@ -113,9 +136,10 @@ class OnlineBankStatementProviderTransferwise(models.Model): balance_end = None while interval_start < interval_end: url = api_base + ( - '/v1/borderless-accounts/%s/statement.json' + - '?currency=%s&intervalStart=%s&intervalEnd=%s' + '/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', @@ -123,7 +147,7 @@ class OnlineBankStatementProviderTransferwise(models.Model): interval_start + interval_step, interval_end ).isoformat() + 'Z', ) - data = self._transferwise_retrieve(url, api_key) + data = self._transferwise_retrieve(url, api_key, private_key) transactions += data['transactions'] balance_end = data['endOfStatementBalance']['value'] interval_start += interval_step @@ -250,7 +274,7 @@ class OnlineBankStatementProviderTransferwise(models.Model): 'name': _('Fee for %s') % reference_number, 'amount': str(fees_value), 'date': date, - 'partner_name': 'TransferWise', + 'partner_name': 'Wise (former TransferWise)', 'unique_import_id': '%s-FEE' % unique_import_id, 'note': _('Transaction fee for %s') % reference_number, }] @@ -268,20 +292,94 @@ class OnlineBankStatementProviderTransferwise(models.Model): return content @api.model - def _transferwise_retrieve(self, url, api_key): - with self._transferwise_urlopen(url, api_key) as response: - content = response.read().decode( - response.headers.get_content_charset() or 'utf-8' + def _transferwise_retrieve(self, url, api_key, private_key=None): + try: + with self._transferwise_urlopen(url, api_key) as response: + content = response.read().decode( + 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': + raise e + if not private_key: + 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(), ) + + with self._transferwise_urlopen( + url, + api_key, + one_time_token, + b64encode(signature).decode(), + ) as response: + content = response.read().decode( + response.headers.get_content_charset() or 'utf-8' + ) + return self._transferwise_validate(content) @api.model - def _transferwise_urlopen(self, url, api_key): + def _transferwise_urlopen(self, url, api_key, ott=None, signature=None): if not api_key: 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) return urllib.request.urlopen(request) + + @api.onchange('certificate_private_key', 'service') + def _onchange_transferwise_certificate_private_key(self): + if self.service != 'transferwise': + return + + self.certificate_public_key = False + if not self.certificate_private_key: + return + + try: + private_key = serialization.load_pem_private_key( + self.certificate_private_key.encode(), + 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')) + + @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(), + ) + self.certificate_private_key = private_key.private_bytes( + serialization.Encoding.PEM, + serialization.PrivateFormat.TraditionalOpenSSL, # a.k.a. PKCS#1 + serialization.NoEncryption(), + ).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): + for provider in self: + provider._transferwise_generate_key() diff --git a/account_bank_statement_import_online_transferwise/readme/CONFIGURE.rst b/account_bank_statement_import_online_transferwise/readme/CONFIGURE.rst index 0a35ffe9..cbd727f2 100644 --- a/account_bank_statement_import_online_transferwise/readme/CONFIGURE.rst +++ b/account_bank_statement_import_online_transferwise/readme/CONFIGURE.rst @@ -3,7 +3,7 @@ To configure online bank statements provider: #. Go to *Invoicing > Configuration > Bank Accounts* #. Open bank account to configure and edit it #. Set *Bank Feeds* to *Online* -#. Select *TransferWise.com* as online bank statements provider in +#. Select *Wise.com (TransferWise.com)* as online bank statements provider in *Online Bank Statements (OCA)* section #. Save the bank account #. Click on provider and configure provider-specific settings. @@ -14,7 +14,7 @@ or, alternatively: #. Open settings of the corresponding journal account #. Switch to *Bank Account* tab #. Set *Bank Feeds* to *Online* -#. Select *TransferWise.com* as online bank statements provider in +#. Select *Wise.com (TransferWise.com)* as online bank statements provider in *Online Bank Statements (OCA)* section #. Save the bank account #. Click on provider and configure provider-specific settings. diff --git a/account_bank_statement_import_online_transferwise/readme/DESCRIPTION.rst b/account_bank_statement_import_online_transferwise/readme/DESCRIPTION.rst index 67f68254..a023a686 100644 --- a/account_bank_statement_import_online_transferwise/readme/DESCRIPTION.rst +++ b/account_bank_statement_import_online_transferwise/readme/DESCRIPTION.rst @@ -1,2 +1,3 @@ This module provides online bank statements from -`TransferWise.com `__. +`Wise.com `__. +(formely `TransferWise.com `__). diff --git a/account_bank_statement_import_online_transferwise/readme/USAGE.rst b/account_bank_statement_import_online_transferwise/readme/USAGE.rst index 03845f13..0792163b 100644 --- a/account_bank_statement_import_online_transferwise/readme/USAGE.rst +++ b/account_bank_statement_import_online_transferwise/readme/USAGE.rst @@ -4,3 +4,9 @@ To pull historical bank statements: #. Select specific bank accounts #. Launch *Actions > Online Bank Statements Pull Wizard* #. Configure date interval and click *Pull* + +To configure Strong Customer Authentication: + +#. Go to provider-specific settings and either press *Generate Key* or paste +manually-generate private and public keys +#. Navigate to `Wise.com `__ and register the public key. diff --git a/account_bank_statement_import_online_transferwise/tests/test_account_bank_statement_import_online_transferwise.py b/account_bank_statement_import_online_transferwise/tests/test_account_bank_statement_import_online_transferwise.py index 85651ffe..3d378b19 100644 --- a/account_bank_statement_import_online_transferwise/tests/test_account_bank_statement_import_online_transferwise.py +++ b/account_bank_statement_import_online_transferwise/tests/test_account_bank_statement_import_online_transferwise.py @@ -1,5 +1,5 @@ # Copyright 2019 Brainbean Apps (https://brainbeanapps.com) -# Copyright 2020 CorporateHub (https://corporatehub.eu) +# Copyright 2020-2021 CorporateHub (https://corporatehub.eu) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from datetime import datetime @@ -7,6 +7,7 @@ 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 @@ -19,6 +20,29 @@ _provider_class = ( ) +class MockedResponse: + + class Headers(dict): + def get_content_charset(self): + return None + + def __init__(self, data=None, exception=None): + self.data = data + self.exception = exception + self.headers = MockedResponse.Headers() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def read(self): + if self.exception is not None: + raise self.exception() + return self.data + + class TestAccountBankAccountStatementImportOnlineTransferwise( common.TransactionCase ): @@ -47,6 +71,33 @@ class TestAccountBankAccountStatementImportOnlineTransferwise( ) ) ) + self.response_balance = MockedResponse(data="""[ + { + "id": 42, + "balances": [ + { + "currency": "EUR" + } + ] + } + ]""".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="""{ + "transactions": [], + "endOfStatementBalance": { + "value": 42.00, + "currency": "EUR" + } + }""".encode()) def test_values_transferwise_profile(self): mocked_response = json.loads( @@ -86,6 +137,28 @@ class TestAccountBankAccountStatementImportOnlineTransferwise( ] ) + def test_values_transferwise_profile_no_key(self): + 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', + side_effect=lambda: Exception(), + ): + 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', @@ -99,7 +172,7 @@ class TestAccountBankAccountStatementImportOnlineTransferwise( provider = journal.online_bank_statement_provider_id provider.origin = '1234567891' - def mock_response(url, api_key): + def mock_response(url, api_key, private_key=None): if '/borderless-accounts?profileId=1234567891' in url: payload = """[ { @@ -133,6 +206,112 @@ class TestAccountBankAccountStatementImportOnlineTransferwise( 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', + }) + + provider = journal.online_bank_statement_provider_id + provider.origin = '1234567891' + provider.password = 'API_KEY' + + with mock.patch( + _provider_class + '._transferwise_retrieve', + return_value=[], + ): + data = provider._obtain_statement_data( + 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', + }) + + provider = journal.online_bank_statement_provider_id + provider.origin = '1234567891' + provider.password = 'API_KEY' + + with common.Form(provider) as provider_form: + provider_form.certificate_private_key = """ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAxC7aYWigCwPIB4mfyLpsALYPnqDm3/IC8I/3GdEwfK8eqXoF +sU1BHnVytFycBDEObmJ2Acpxe8Dk61FnbWPrrl6rXVnXfIRqfFl94TvgwFsuwG7u +8crncD6gPfe1QGkEykHcfBURr74OcSE8590ngNJcKMGvac0cuyZ2/NEszTw7EFJg +obpMWjp0m5ItgZ/UNsPLR/D4gFE9vZz7a4+FYQMa9Wbv+xaVxUS6z9rQCJfUQx7N +iih4etIvAafbfAnX6rFv8PwPzz+XvexPWWJxnbS4iV1LN2atrPDxqw73g5hc3W88 +a0V2AVubtxhw9L2VK1VmRb/gnqsZpRXhDPSUIwIDAQABAoIBAQCMvnRLV80hudfC +mJh6YEvlgrfX/OVFmpFDVnVXHz2i5dugiHsXBS6HlIjzHlGLrEoHJTo19K/PscZJ +kEAcOYg2s5JLSY4PtcvTZDyr3tJSDdiPk8Z2zzOU0kkRy+lLyUv3cqKknlTu+PHR +daAFVCLoB4K4dqPKyq0nEuRgYgy7O42SPBY5DHgWYBKqkYGlTu+ImwpDD9unbv3e +mwvdcBCp9hAlYAArc1Ip/6aUkZdKxJYgVhovruaH309yuOmBfAEgguhsy3vR18t5 +IZXbAF3C6iXCQXi8l+S1NUu8XWPLEavldb+ZA2hI2L+NPSBVIYqhI4jDiI7lfs1c +HE8BRsRpAoGBAO6BnK3qD8sRvg6JsrBhppoIGsudOdpZ/KVp9ZpYCBJNJmfrkqLR +bWx1KF2UjAoYUmaKDTS2GP8JQd7X2n4T5LX8q+7iG9/wzdSWZYZuBOnjvWlNyJu4 +OiUKX4aEgdvZHiuEIin5xTP98/c5LTZXwM3bq8IrOXEz8LBLLPrTCGRvAoGBANKS +i3cn1jtVirJWbvhSIjjqhpfuZN0361FB6j1Aho+7z0WVd4NQjPQqA6cAqnWoa/kj +cX0X8Ncu5eHqf6CuW+HsQda3yp3bvCXi1Yc2nKBTHnWtMm721O4ZW6rbaALzBZYW +qeJr0m9pNlfCAL0INTcy7IVAtqcCJ/7CEN6Hjm2NAoGAIGSgKArDLFxziLvw9f29 +R+xT31WyVtKj+r9iaR0Ns5ag4bpgBxcUmodq/RLA1lopTt3vHzqgOHtEZATDGx6O +kJ0JqP8ys/6bpgTrMw/cQPv6bMPwvB2QYBmBkd6LWJWrgFOI5FSVEROrv+cXGetf +N1ZfhJakTZi1VuxO5p4k5KcCgYAZS9OHR/jbfeZAkFOabzt/POVYYSIq1SnmxBVg +sFy57aTzxgXqd4XHWzi/GjxgEBCQiGp8zaB4KUEih6o3YlrVZC1wnvmvRxNuNbbT +HINqWzHgjyLs46gmxlMVzm/LUuiL5EMaWTuZeLk3h63RB6hk7jAtvd1zaLXnS+b8 +5Kn+jQKBgQCDeMO6rvB2rbfqSbHvPPuTru1sPIsJBKm1YZpXTFI+VMjwtk7+meYb +UQnfZ1t5rjp9q4LEcRYuSa+PfifIkM6p+wMHVQhtltUCzXWWRYkLkmQrBWKu+qiP +edF6byMgXSzgOWYuRPXwmHpBQV0GiexQUAxVyUzaVWfil69LaFfXaw== +-----END RSA PRIVATE KEY----- + """ + + 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', + }) + + provider = journal.online_bank_statement_provider_id + provider.origin = '1234567891' + provider.password = 'API_KEY' + provider.button_transferwise_generate_key() + + with mock.patch( + 'urllib.request.urlopen', + side_effect=[ + self.response_balance, + self.response_ott, + self.response_transactions, + self.response_transactions, + self.response_transactions, + ], + ): + data = provider._obtain_statement_data( + 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) + def test_transaction_parse_1(self): lines = self.transferwise_parse_transaction("""{ "type": "CREDIT", @@ -216,7 +395,7 @@ class TestAccountBankAccountStatementImportOnlineTransferwise( 'amount': '-0.60', 'name': 'Fee for TRANSFER-123456789', 'note': 'Transaction fee for TRANSFER-123456789', - 'partner_name': 'TransferWise', + 'partner_name': 'Wise (former TransferWise)', 'unique_import_id': 'DEBIT-TRANSFER-123456789-946684800-FEE', }) @@ -342,7 +521,7 @@ class TestAccountBankAccountStatementImportOnlineTransferwise( 'amount': '-1.23', 'name': 'Fee for CARD-123456789', 'note': 'Transaction fee for CARD-123456789', - 'partner_name': 'TransferWise', + 'partner_name': 'Wise (former TransferWise)', 'unique_import_id': 'DEBIT-CARD-123456789-946684800-FEE', }) @@ -400,7 +579,7 @@ class TestAccountBankAccountStatementImportOnlineTransferwise( 'date': datetime(2000, 1, 1), 'name': 'Fee for TRANSFER-123456789', 'note': 'Transaction fee for TRANSFER-123456789', - 'partner_name': 'TransferWise', + 'partner_name': 'Wise (former TransferWise)', 'amount': '-5.21', 'unique_import_id': 'DEBIT-TRANSFER-123456789-946684800-FEE', }) @@ -547,7 +726,7 @@ class TestAccountBankAccountStatementImportOnlineTransferwise( 'name': 'Fee for BALANCE-123456789', 'note': 'Transaction fee for BALANCE-123456789', 'amount': '-0.05', - 'partner_name': 'TransferWise', + 'partner_name': 'Wise (former TransferWise)', 'unique_import_id': 'DEBIT-BALANCE-123456789-946684800-FEE', }) @@ -587,7 +766,7 @@ class TestAccountBankAccountStatementImportOnlineTransferwise( 'name': 'Fee for TRANSFER-123456789', 'note': 'Transaction fee for TRANSFER-123456789', 'amount': '-0.68', - 'partner_name': 'TransferWise', + 'partner_name': 'Wise (former TransferWise)', 'unique_import_id': 'CREDIT-TRANSFER-123456789-946684800-FEE', }) @@ -631,6 +810,6 @@ class TestAccountBankAccountStatementImportOnlineTransferwise( 'name': 'Fee for TRANSFER-123456789', 'note': 'Transaction fee for TRANSFER-123456789', 'amount': '4.33', - 'partner_name': 'TransferWise', + 'partner_name': 'Wise (former TransferWise)', 'unique_import_id': 'CREDIT-TRANSFER-123456789-946684800-FEE', }) diff --git a/account_bank_statement_import_online_transferwise/views/online_bank_statement_provider.xml b/account_bank_statement_import_online_transferwise/views/online_bank_statement_provider.xml index 013f15dc..5ded19b8 100644 --- a/account_bank_statement_import_online_transferwise/views/online_bank_statement_provider.xml +++ b/account_bank_statement_import_online_transferwise/views/online_bank_statement_provider.xml @@ -1,6 +1,7 @@ @@ -27,7 +28,7 @@ + + + +
+
+
From f5ae704071d35c6e3cb8e04ff5e9a2b31a8e4331 Mon Sep 17 00:00:00 2001 From: Alexey Pelykh Date: Tue, 11 May 2021 21:48:18 +0200 Subject: [PATCH 06/10] [IMP] account_bank_statement_import_online_transferwise: black, isort, prettier --- .../__manifest__.py | 33 +- ...ne_bank_statement_provider_transferwise.py | 349 +++++----- ...nk_statement_import_online_transferwise.py | 631 ++++++++++-------- .../views/online_bank_statement_provider.xml | 14 +- 4 files changed, 519 insertions(+), 508 deletions(-) diff --git a/account_bank_statement_import_online_transferwise/__manifest__.py b/account_bank_statement_import_online_transferwise/__manifest__.py index fc18078d..0afb9ef8 100644 --- a/account_bank_statement_import_online_transferwise/__manifest__.py +++ b/account_bank_statement_import_online_transferwise/__manifest__.py @@ -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, } diff --git a/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py b/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py index 5c571728..bcfc79fa 100644 --- a/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py +++ b/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py @@ -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): diff --git a/account_bank_statement_import_online_transferwise/tests/test_account_bank_statement_import_online_transferwise.py b/account_bank_statement_import_online_transferwise/tests/test_account_bank_statement_import_online_transferwise.py index 3d378b19..e41296e9 100644 --- a/account_bank_statement_import_online_transferwise/tests/test_account_bank_statement_import_online_transferwise.py +++ b/account_bank_statement_import_online_transferwise/tests/test_account_bank_statement_import_online_transferwise.py @@ -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", + }, + ) diff --git a/account_bank_statement_import_online_transferwise/views/online_bank_statement_provider.xml b/account_bank_statement_import_online_transferwise/views/online_bank_statement_provider.xml index 5ded19b8..48119e97 100644 --- a/account_bank_statement_import_online_transferwise/views/online_bank_statement_provider.xml +++ b/account_bank_statement_import_online_transferwise/views/online_bank_statement_provider.xml @@ -1,15 +1,17 @@ - + - online.bank.statement.provider.form online.bank.statement.provider - + @@ -42,10 +44,7 @@ string="Private key" password="True" /> - +