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..45cb3cab --- /dev/null +++ b/account_bank_statement_import_online_transferwise/__manifest__.py @@ -0,0 +1,18 @@ +# Copyright 2019 Brainbean Apps (https://brainbeanapps.com) +# Copyright 2020-2021 CorporateHub (https://corporatehub.eu) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Online Bank Statements: Wise.com (TransferWise.com)", + "version": "13.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 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/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..807eb499 --- /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: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: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:281 +#, 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: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/__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..f1260609 --- /dev/null +++ b/account_bank_statement_import_online_transferwise/models/online_bank_statement_provider_transferwise.py @@ -0,0 +1,347 @@ +# Copyright 2019 Brainbean Apps (https://brainbeanapps.com) +# Copyright 2020-2021 CorporateHub (https://corporatehub.eu) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +import itertools +import json +import logging +import urllib.parse +import urllib.request +from base64 import b64encode +from decimal import Decimal +from urllib.error import HTTPError + +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 + +_logger = logging.getLogger(__name__) + + +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 + 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 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, + ) + ) + + @api.model + def _get_available_services(self): + return super()._get_available_services() + [ + ("transferwise", "Wise.com (TransferWise.com)"), + ] + + 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 + 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 + + 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={}".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"]) + ) + 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 + ( + "/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, private_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 + ( + "/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", + ) + data = self._transferwise_retrieve(url, api_key, private_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): + 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 = "{}: {}".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 = "{}-{}-{}".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, + } + 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": "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"]: + raise UserError( + content["error_description"] + if "error_description" in content + else "Unknown error" + ) + return content + + @api.model + 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, 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) + 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 BaseException: + _logger.warning("Unable to parse key", exc_info=True) + raise UserError(_("Unable to parse key")) + + 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() + ) + + 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 new file mode 100644 index 00000000..cbd727f2 --- /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 *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. + +or, alternatively: + +#. Go to *Invoicing > Overview* +#. Open settings of the corresponding journal account +#. Switch to *Bank Account* tab +#. Set *Bank Feeds* to *Online* +#. 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/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..a023a686 --- /dev/null +++ b/account_bank_statement_import_online_transferwise/readme/DESCRIPTION.rst @@ -0,0 +1,3 @@ +This module provides online bank statements from +`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 new file mode 100644 index 00000000..0792163b --- /dev/null +++ b/account_bank_statement_import_online_transferwise/readme/USAGE.rst @@ -0,0 +1,12 @@ +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* + +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/static/description/icon.png b/account_bank_statement_import_online_transferwise/static/description/icon.png new file mode 100644 index 00000000..3a0328b5 Binary files /dev/null and b/account_bank_statement_import_online_transferwise/static/description/icon.png differ 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..e41296e9 --- /dev/null +++ b/account_bank_statement_import_online_transferwise/tests/test_account_bank_statement_import_online_transferwise.py @@ -0,0 +1,872 @@ +# Copyright 2019 Brainbean Apps (https://brainbeanapps.com) +# Copyright 2020-2021 CorporateHub (https://corporatehub.eu) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import json +from datetime import datetime +from decimal import Decimal +from unittest import mock +from urllib.error import HTTPError + +from dateutil.relativedelta import relativedelta + +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" +) + + +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 + + # pylint: disable=method-required-super + def read(self): + if self.exception is not None: + raise self.exception() + return self.data + + +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,) + ) + ) + ) + self.response_balance = MockedResponse( + data=b"""[ + { + "id": 42, + "balances": [ + { + "currency": "EUR" + } + ] + } + ]""" + ) + 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" + } + }""" + ) + + 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_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", + "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, private_key=None): + 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_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", + "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": "Wise (former 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": "Wise (former 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": "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( + """{ + "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": "Wise (former 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": "Wise (former 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": "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 new file mode 100644 index 00000000..48119e97 --- /dev/null +++ b/account_bank_statement_import_online_transferwise/views/online_bank_statement_provider.xml @@ -0,0 +1,61 @@ + + + + + online.bank.statement.provider.form + online.bank.statement.provider + + + + + + + + + + + + + + +
+
+
+
+
+
+
+
diff --git a/setup/account_bank_statement_import_online_transferwise/odoo/addons/account_bank_statement_import_online_transferwise b/setup/account_bank_statement_import_online_transferwise/odoo/addons/account_bank_statement_import_online_transferwise new file mode 120000 index 00000000..602b2166 --- /dev/null +++ b/setup/account_bank_statement_import_online_transferwise/odoo/addons/account_bank_statement_import_online_transferwise @@ -0,0 +1 @@ +../../../../account_bank_statement_import_online_transferwise \ No newline at end of file diff --git a/setup/account_bank_statement_import_online_transferwise/setup.py b/setup/account_bank_statement_import_online_transferwise/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/account_bank_statement_import_online_transferwise/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)