From ce6a6d6852f35d635f2bce62ea495c37eca50ec1 Mon Sep 17 00:00:00 2001 From: Stefan Rijnhart Date: Tue, 28 Feb 2017 18:45:32 +0100 Subject: [PATCH 01/24] [ADD] Adyen statement import --- .../README.rst | 69 +++++++++ .../__init__.py | 1 + .../__openerp__.py | 18 +++ .../models/__init__.py | 2 + .../models/account_bank_statement_import.py | 139 ++++++++++++++++++ .../models/account_journal.py | 10 ++ .../test_files/adyen_test.xlsx | Bin 0 -> 17972 bytes .../test_files/adyen_test_credit_fees.xlsx | Bin 0 -> 18800 bytes .../tests/__init__.py | 1 + .../tests/test_import_adyen.py | 55 +++++++ .../views/account_journal.xml | 15 ++ 11 files changed, 310 insertions(+) create mode 100644 account_bank_statement_import_adyen/README.rst create mode 100644 account_bank_statement_import_adyen/__init__.py create mode 100644 account_bank_statement_import_adyen/__openerp__.py create mode 100644 account_bank_statement_import_adyen/models/__init__.py create mode 100644 account_bank_statement_import_adyen/models/account_bank_statement_import.py create mode 100644 account_bank_statement_import_adyen/models/account_journal.py create mode 100644 account_bank_statement_import_adyen/test_files/adyen_test.xlsx create mode 100644 account_bank_statement_import_adyen/test_files/adyen_test_credit_fees.xlsx create mode 100644 account_bank_statement_import_adyen/tests/__init__.py create mode 100644 account_bank_statement_import_adyen/tests/test_import_adyen.py create mode 100644 account_bank_statement_import_adyen/views/account_journal.xml diff --git a/account_bank_statement_import_adyen/README.rst b/account_bank_statement_import_adyen/README.rst new file mode 100644 index 00000000..9b6baaba --- /dev/null +++ b/account_bank_statement_import_adyen/README.rst @@ -0,0 +1,69 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +====================== +Adyen statement import +====================== + +This module processes Adyen transaction statements in xlsx format. You can +import the statements in a dedicated journal. Reconcile your sale invoices +with the credit transations. Reconcile the aggregated counterpart +transaction with the transaction in your real bank journal and register the +aggregated fee line containing commision and markup on the applicable +cost account. + +Configuration +============= + +Configure a pseudo bank journal by creating a new journal with a dedicated +Adyen clearing account as the default ledger account. Set your merchant +account string in the Advanced settings on the journal form. + +Usage +===== + +After installing this module, you can import your Adyen transaction statements +through Menu Finance -> Bank -> Import. Don't enter a journal in the import +wizard. + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/174/8.0 + + +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 smash it by providing detailed and welcomed feedback. + +Credits +======= + +Images +------ + +* Odoo Community Association: `Icon `_. + +Contributors +------------ + +* Stefan Rijnhart + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +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. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/account_bank_statement_import_adyen/__init__.py b/account_bank_statement_import_adyen/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/account_bank_statement_import_adyen/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/account_bank_statement_import_adyen/__openerp__.py b/account_bank_statement_import_adyen/__openerp__.py new file mode 100644 index 00000000..6f47e712 --- /dev/null +++ b/account_bank_statement_import_adyen/__openerp__.py @@ -0,0 +1,18 @@ +# coding: utf-8 +# © 2017 Opener BV () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +{ + 'name': 'Adyen statement import', + 'version': '8.0.1.0.0', + 'author': 'Opener BV, Odoo Community Association (OCA)', + 'category': 'Banking addons', + 'website': 'https://github.com/oca/bank-statement-import', + 'depends': [ + 'account_bank_statement_import', + 'account_bank_statement_clearing_account', + ], + 'data': [ + 'views/account_journal.xml', + ], + 'installable': True, +} diff --git a/account_bank_statement_import_adyen/models/__init__.py b/account_bank_statement_import_adyen/models/__init__.py new file mode 100644 index 00000000..ba1f4934 --- /dev/null +++ b/account_bank_statement_import_adyen/models/__init__.py @@ -0,0 +1,2 @@ +from . import account_bank_statement_import +from . import account_journal diff --git a/account_bank_statement_import_adyen/models/account_bank_statement_import.py b/account_bank_statement_import_adyen/models/account_bank_statement_import.py new file mode 100644 index 00000000..aae90b71 --- /dev/null +++ b/account_bank_statement_import_adyen/models/account_bank_statement_import.py @@ -0,0 +1,139 @@ +# coding: utf-8 +# © 2017 Opener BV () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from io import BytesIO +from openpyxl import load_workbook +from zipfile import BadZipfile + +from openerp import models, api +from openerp.exceptions import Warning as UserError +from openerp.tools.misc import DEFAULT_SERVER_DATE_FORMAT as DATEFMT +from openerp.tools.translate import _ +from openerp.addons.account_bank_statement_import.parserlib import ( + BankStatement) + + +class Import(models.TransientModel): + _inherit = 'account.bank.statement.import' + + @api.model + def _parse_file(self, data_file): + """Parse an Adyen xlsx file and map merchant account strings + to journals. """ + try: + statements = self.import_adyen_xlsx(data_file) + except ValueError: + return super(Import, self)._parse_file(data_file) + + for statement in statements: + merchant_id = statement['account_number'] + journal = self.env['account.journal'].search([ + ('adyen_merchant_account', '=', merchant_id)], limit=1) + if journal: + statement['adyen_journal_id'] = journal.id + else: + raise UserError( + _('Please create a journal with merchant account "%s"') % + merchant_id) + statement['account_number'] = False + return statements + + @api.model + def _import_statement(self, stmt_vals): + """ Propagate found journal to context, fromwhere it is picked up + in _get_journal """ + journal_id = stmt_vals.pop('adyen_journal_id', None) + if journal_id: + self = self.with_context(journal_id=journal_id) + return super(Import, self)._import_statement(stmt_vals) + + @api.model + def balance(self, row): + return -(row[15] or 0) + sum( + row[i] if row[i] else 0.0 + for i in (16, 17, 18, 19, 20)) + + @api.model + def import_adyen_transaction(self, statement, row): + transaction = statement.create_transaction() + transaction.value_date = row[6].strftime(DATEFMT) + transaction.transferred_amount = self.balance(row) + transaction.note = ( + '%s %s %s %s' % (row[2], row[3], row[4], row[21])) + transaction.message = "%s" % (row[3] or row[4] or row[9]) + return transaction + + @api.model + def import_adyen_xlsx(self, data_file): + statements = [] + statement = None + headers = False + fees = 0.0 + balance = 0.0 + payout = 0.0 + + with BytesIO() as buf: + buf.write(data_file) + try: + sheet = load_workbook(buf)._sheets[0] + except BadZipfile as e: + raise ValueError(e) + for row in sheet.rows: + row = [cell.value for cell in row] + if len(row) != 31: + raise ValueError( + 'Not an Adyen statement. Unexpected row length %s ' + 'instead of 31' % len(row)) + if not row[1]: + continue + if not headers: + if row[1] != 'Company Account': + raise ValueError( + 'Not an Adyen statement. Unexpected header "%s" ' + 'instead of "Company Account"', row[1]) + headers = True + continue + if not statement: + statement = BankStatement() + statements.append(statement) + statement.statement_id = '%s %s/%s' % ( + row[2], row[6].strftime('%Y'), int(row[23])) + statement.local_currency = row[14] + statement.local_account = row[2] + date = row[6].strftime(DATEFMT) + if not statement.date or statement.date > date: + statement.date = date + + row[8] = row[8].strip() + if row[8] == 'MerchantPayout': + payout -= self.balance(row) + else: + balance += self.balance(row) + self.import_adyen_transaction(statement, row) + fees += sum( + row[i] if row[i] else 0.0 + for i in (17, 18, 19, 20)) + + if not headers: + raise ValueError( + 'Not an Adyen statement. Did not encounter header row.') + + if fees: + transaction = statement.create_transaction() + transaction.value_date = max( + t.value_date for t in statement['transactions']) + transaction.transferred_amount = -fees + balance -= fees + transaction.message = 'Commision, markup etc. batch %s' % ( + int(row[23])) + + if statement['transactions'] and not payout: + raise UserError( + _('No payout detected in Adyen statement.')) + if self.env.user.company_id.currency_id.compare_amounts( + balance, payout) != 0: + raise UserError( + _('Parse error. Balance %s not equal to merchant ' + 'payout %s') % (balance, payout)) + + return statements diff --git a/account_bank_statement_import_adyen/models/account_journal.py b/account_bank_statement_import_adyen/models/account_journal.py new file mode 100644 index 00000000..d0c431fd --- /dev/null +++ b/account_bank_statement_import_adyen/models/account_journal.py @@ -0,0 +1,10 @@ +# coding: utf-8 +from openerp import fields, models + + +class Journal(models.Model): + _inherit = 'account.journal' + + adyen_merchant_account = fields.Char( + help=('Fill in the exact merchant account string to select this ' + 'journal when importing Adyen statements')) diff --git a/account_bank_statement_import_adyen/test_files/adyen_test.xlsx b/account_bank_statement_import_adyen/test_files/adyen_test.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..3adaa8dbf10f9a2a48f2e2538a5f15bc86756b46 GIT binary patch literal 17972 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 E08#;WUH||9 literal 0 HcmV?d00001 diff --git a/account_bank_statement_import_adyen/test_files/adyen_test_credit_fees.xlsx b/account_bank_statement_import_adyen/test_files/adyen_test_credit_fees.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..0443fceb3eae260220b96d33b06cf1e8032153ed GIT binary patch literal 18800 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 literal 0 HcmV?d00001 diff --git a/account_bank_statement_import_adyen/tests/__init__.py b/account_bank_statement_import_adyen/tests/__init__.py new file mode 100644 index 00000000..f53fb980 --- /dev/null +++ b/account_bank_statement_import_adyen/tests/__init__.py @@ -0,0 +1 @@ +from . import test_import_adyen diff --git a/account_bank_statement_import_adyen/tests/test_import_adyen.py b/account_bank_statement_import_adyen/tests/test_import_adyen.py new file mode 100644 index 00000000..d4e8a684 --- /dev/null +++ b/account_bank_statement_import_adyen/tests/test_import_adyen.py @@ -0,0 +1,55 @@ +# coding: utf-8 +from openerp.addons.account_bank_statement_import.tests import ( + TestStatementFile) + + +class TestImportAdyen(TestStatementFile): + def setUp(self): + super(TestImportAdyen, self).setUp() + self.journal = self.env['account.journal'].search( + [('type', '=', 'bank')], limit=1) + self.journal.default_debit_account_id.reconcile = True + self.journal.write({ + 'adyen_merchant_account': 'YOURCOMPANY_ACCOUNT', + 'update_posted': True, + }) + + def test_import_adyen(self): + self._test_statement_import( + 'account_bank_statement_import_adyen', 'adyen_test.xlsx', + 'YOURCOMPANY_ACCOUNT 2016/48') + statement = self.env['account.bank.statement'].search( + [], order='create_date desc', limit=1) + self.assertEqual(len(statement.line_ids), 22) + self.assertTrue( + self.env.user.company_id.currency_id.is_zero( + sum(line.amount for line in statement.line_ids))) + + account = self.env['account.account'].search([( + 'type', '=', 'receivable')], limit=1) + for line in statement.line_ids: + line.process_reconciliation([{ + 'debit': -line.amount if line.amount < 0 else 0, + 'credit': line.amount if line.amount > 0 else 0, + 'account_id': account.id}]) + + statement.button_confirm_bank() + self.assertEqual(statement.state, 'confirm') + lines = self.env['account.move.line'].search([ + ('account_id', '=', self.journal.default_debit_account_id.id), + ('statement_id', '=', statement.id)]) + reconcile = lines.mapped('reconcile_id') + self.assertEqual(len(reconcile), 1) + self.assertFalse(lines.mapped('reconcile_partial_id')) + self.assertEqual(lines, reconcile.line_id) + + statement.button_draft() + self.assertEqual(statement.state, 'draft') + self.assertFalse(lines.mapped('reconcile_partial_id')) + self.assertFalse(lines.mapped('reconcile_id')) + + def test_import_adyen_credit_fees(self): + self._test_statement_import( + 'account_bank_statement_import_adyen', + 'adyen_test_credit_fees.xlsx', + 'YOURCOMPANY_ACCOUNT 2016/8') diff --git a/account_bank_statement_import_adyen/views/account_journal.xml b/account_bank_statement_import_adyen/views/account_journal.xml new file mode 100644 index 00000000..2e6f2e6b --- /dev/null +++ b/account_bank_statement_import_adyen/views/account_journal.xml @@ -0,0 +1,15 @@ + + + + + Add Adyen merchant account + account.journal + + + + + + + + + From 9c7f36ad5dd86dc234ede8b3078fb96751c4990e Mon Sep 17 00:00:00 2001 From: Martin Pishpecki Date: Wed, 13 May 2020 17:05:05 +0200 Subject: [PATCH 02/24] [MIG] 12.0 account_bank_statement_import_adyen, account_bank_statement_clearing_account --- .../README.rst | 82 +++++--------- .../__manifest__.py | 24 +++++ .../__openerp__.py | 18 ---- .../models/account_bank_statement_import.py | 102 +++++++++--------- .../models/account_journal.py | 12 ++- .../readme/CONFIGURE.rst | 3 + .../readme/CONTRIBUTORS.rst | 2 + .../readme/CREDITS.rst | 0 .../readme/DESCRIPTION.rst | 10 ++ .../readme/HISTORY.rst | 0 .../readme/INSTALL.rst | 0 .../readme/ROADMAP.rst | 0 .../readme/USAGE.rst | 3 + .../test_files/adyen_test.xlsx | Bin 17972 -> 17127 bytes .../test_files/adyen_test_credit_fees.xlsx | Bin 18800 -> 18956 bytes .../test_files/adyen_test_invalid.xls | Bin 0 -> 17137 bytes .../tests/test_import_adyen.py | 93 ++++++++++++---- .../views/account_journal.xml | 24 ++--- 18 files changed, 209 insertions(+), 164 deletions(-) create mode 100644 account_bank_statement_import_adyen/__manifest__.py delete mode 100644 account_bank_statement_import_adyen/__openerp__.py create mode 100644 account_bank_statement_import_adyen/readme/CONFIGURE.rst create mode 100644 account_bank_statement_import_adyen/readme/CONTRIBUTORS.rst create mode 100644 account_bank_statement_import_adyen/readme/CREDITS.rst create mode 100644 account_bank_statement_import_adyen/readme/DESCRIPTION.rst create mode 100644 account_bank_statement_import_adyen/readme/HISTORY.rst create mode 100644 account_bank_statement_import_adyen/readme/INSTALL.rst create mode 100644 account_bank_statement_import_adyen/readme/ROADMAP.rst create mode 100644 account_bank_statement_import_adyen/readme/USAGE.rst create mode 100644 account_bank_statement_import_adyen/test_files/adyen_test_invalid.xls diff --git a/account_bank_statement_import_adyen/README.rst b/account_bank_statement_import_adyen/README.rst index 9b6baaba..38929e87 100644 --- a/account_bank_statement_import_adyen/README.rst +++ b/account_bank_statement_import_adyen/README.rst @@ -1,69 +1,35 @@ -.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg - :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html - :alt: License: AGPL-3 +**This file is going to be generated by oca-gen-addon-readme.** -====================== -Adyen statement import -====================== +*Manual changes will be overwritten.* -This module processes Adyen transaction statements in xlsx format. You can -import the statements in a dedicated journal. Reconcile your sale invoices -with the credit transations. Reconcile the aggregated counterpart -transaction with the transaction in your real bank journal and register the -aggregated fee line containing commision and markup on the applicable -cost account. +Please provide content in the ``readme`` directory: -Configuration -============= +* **DESCRIPTION.rst** (required) +* INSTALL.rst (optional) +* CONFIGURE.rst (optional) +* **USAGE.rst** (optional, highly recommended) +* DEVELOP.rst (optional) +* ROADMAP.rst (optional) +* HISTORY.rst (optional, recommended) +* **CONTRIBUTORS.rst** (optional, highly recommended) +* CREDITS.rst (optional) -Configure a pseudo bank journal by creating a new journal with a dedicated -Adyen clearing account as the default ledger account. Set your merchant -account string in the Advanced settings on the journal form. +Content of this README will also be drawn from the addon manifest, +from keys such as name, authors, maintainers, development_status, +and license. -Usage -===== - -After installing this module, you can import your Adyen transaction statements -through Menu Finance -> Bank -> Import. Don't enter a journal in the import -wizard. - -.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas - :alt: Try me on Runbot - :target: https://runbot.odoo-community.org/runbot/174/8.0 +A good, one sentence summary in the manifest is also highly recommended. -Bug Tracker -=========== +Automatic changelog generation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -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 smash it by providing detailed and welcomed feedback. +`HISTORY.rst` can be auto generated using `towncrier `_. -Credits -======= +Just put towncrier compatible changelog fragments into `readme/newsfragments` +and the changelog file will be automatically generated and updated when a new fragment is added. -Images ------- +Please refer to `towncrier` documentation to know more. -* Odoo Community Association: `Icon `_. - -Contributors ------------- - -* Stefan Rijnhart - -Maintainer ----------- - -.. image:: https://odoo-community.org/logo.png - :alt: Odoo Community Association - :target: https://odoo-community.org - -This module is maintained by the OCA. - -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. - -To contribute to this module, please visit https://odoo-community.org. +NOTE: the changelog will be automatically generated when using `/ocabot merge $option`. +If you need to run it manually, refer to `OCA/maintainer-tools README `_. diff --git a/account_bank_statement_import_adyen/__manifest__.py b/account_bank_statement_import_adyen/__manifest__.py new file mode 100644 index 00000000..4be97c2e --- /dev/null +++ b/account_bank_statement_import_adyen/__manifest__.py @@ -0,0 +1,24 @@ +# © 2017 Opener BV () +# © 2020 Vanmoof BV () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +{ + "name": "Adyen statement import", + "version": "12.0.1.0.0", + "author": "Opener BV, Vanmoof BV, Odoo Community Association (OCA)", + "category": "Banking addons", + "website": "https://github.com/oca/bank-statement-import", + "license": "AGPL-3", + "depends": [ + "account_bank_statement_import", + "account_bank_statement_clearing_account", + ], + "external_dependencies": { + "python": [ + "openpyxl", + ], + }, + "data": [ + "views/account_journal.xml", + ], + "installable": True, +} diff --git a/account_bank_statement_import_adyen/__openerp__.py b/account_bank_statement_import_adyen/__openerp__.py deleted file mode 100644 index 6f47e712..00000000 --- a/account_bank_statement_import_adyen/__openerp__.py +++ /dev/null @@ -1,18 +0,0 @@ -# coding: utf-8 -# © 2017 Opener BV () -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -{ - 'name': 'Adyen statement import', - 'version': '8.0.1.0.0', - 'author': 'Opener BV, Odoo Community Association (OCA)', - 'category': 'Banking addons', - 'website': 'https://github.com/oca/bank-statement-import', - 'depends': [ - 'account_bank_statement_import', - 'account_bank_statement_clearing_account', - ], - 'data': [ - 'views/account_journal.xml', - ], - 'installable': True, -} diff --git a/account_bank_statement_import_adyen/models/account_bank_statement_import.py b/account_bank_statement_import_adyen/models/account_bank_statement_import.py index aae90b71..a8d17f60 100644 --- a/account_bank_statement_import_adyen/models/account_bank_statement_import.py +++ b/account_bank_statement_import_adyen/models/account_bank_statement_import.py @@ -1,19 +1,15 @@ -# coding: utf-8 # © 2017 Opener BV () # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from io import BytesIO from openpyxl import load_workbook from zipfile import BadZipfile -from openerp import models, api -from openerp.exceptions import Warning as UserError -from openerp.tools.misc import DEFAULT_SERVER_DATE_FORMAT as DATEFMT -from openerp.tools.translate import _ -from openerp.addons.account_bank_statement_import.parserlib import ( - BankStatement) +from odoo import api, models, fields +from odoo.exceptions import UserError +from odoo.tools.translate import _ -class Import(models.TransientModel): +class AccountBankStatementImport(models.TransientModel): _inherit = 'account.bank.statement.import' @api.model @@ -21,31 +17,25 @@ class Import(models.TransientModel): """Parse an Adyen xlsx file and map merchant account strings to journals. """ try: - statements = self.import_adyen_xlsx(data_file) + return self.import_adyen_xlsx(data_file) except ValueError: - return super(Import, self)._parse_file(data_file) + return super(AccountBankStatementImport, self)._parse_file( + data_file) - for statement in statements: - merchant_id = statement['account_number'] + def _find_additional_data(self, currency_code, account_number): + """ Try to find journal by Adyen merchant account """ + if account_number: journal = self.env['account.journal'].search([ - ('adyen_merchant_account', '=', merchant_id)], limit=1) + ('adyen_merchant_account', '=', account_number)], limit=1) if journal: - statement['adyen_journal_id'] = journal.id - else: - raise UserError( - _('Please create a journal with merchant account "%s"') % - merchant_id) - statement['account_number'] = False - return statements - - @api.model - def _import_statement(self, stmt_vals): - """ Propagate found journal to context, fromwhere it is picked up - in _get_journal """ - journal_id = stmt_vals.pop('adyen_journal_id', None) - if journal_id: - self = self.with_context(journal_id=journal_id) - return super(Import, self)._import_statement(stmt_vals) + if self._context.get('journal_id', journal.id) != journal.id: + raise UserError( + _('Selected journal Merchant Account does not match ' + 'the import file Merchant Account ' + 'column: %s') % account_number) + self = self.with_context(journal_id=journal.id) + return super(AccountBankStatementImport, self)._find_additional_data( + currency_code, account_number) @api.model def balance(self, row): @@ -54,14 +44,16 @@ class Import(models.TransientModel): for i in (16, 17, 18, 19, 20)) @api.model - def import_adyen_transaction(self, statement, row): - transaction = statement.create_transaction() - transaction.value_date = row[6].strftime(DATEFMT) - transaction.transferred_amount = self.balance(row) - transaction.note = ( - '%s %s %s %s' % (row[2], row[3], row[4], row[21])) - transaction.message = "%s" % (row[3] or row[4] or row[9]) - return transaction + def import_adyen_transaction(self, statement, statement_id, row): + transaction_id = str(len(statement['transactions'])).zfill(4) + transaction = dict( + unique_import_id=statement_id + transaction_id, + date=fields.Date.from_string(row[6]), + amount=self.balance(row), + note='%s %s %s %s' % (row[2], row[3], row[4], row[21]), + name="%s" % (row[3] or row[4] or row[9]), + ) + statement['transactions'].append(transaction) @api.model def import_adyen_xlsx(self, data_file): @@ -71,6 +63,7 @@ class Import(models.TransientModel): fees = 0.0 balance = 0.0 payout = 0.0 + statement_id = None with BytesIO() as buf: buf.write(data_file) @@ -94,22 +87,24 @@ class Import(models.TransientModel): headers = True continue if not statement: - statement = BankStatement() + statement = {'transactions': []} statements.append(statement) - statement.statement_id = '%s %s/%s' % ( + statement_id = '%s %s/%s' % ( row[2], row[6].strftime('%Y'), int(row[23])) - statement.local_currency = row[14] - statement.local_account = row[2] - date = row[6].strftime(DATEFMT) - if not statement.date or statement.date > date: - statement.date = date + currency_code = row[14] + merchant_id = row[2] + statement['name'] = '%s %s/%s' % ( + row[2], row[6].year, row[23]) + date = fields.Date.from_string(row[6]) + if not statement.get('date') or statement.get('date') > date: + statement['date'] = date row[8] = row[8].strip() if row[8] == 'MerchantPayout': payout -= self.balance(row) else: balance += self.balance(row) - self.import_adyen_transaction(statement, row) + self.import_adyen_transaction(statement, statement_id, row) fees += sum( row[i] if row[i] else 0.0 for i in (17, 18, 19, 20)) @@ -119,13 +114,15 @@ class Import(models.TransientModel): 'Not an Adyen statement. Did not encounter header row.') if fees: - transaction = statement.create_transaction() - transaction.value_date = max( - t.value_date for t in statement['transactions']) - transaction.transferred_amount = -fees + transaction_id = str(len(statement['transactions'])).zfill(4) + transaction = dict( + unique_import_id=statement_id + transaction_id, + date=max(t['date'] for t in statement['transactions']), + amount=-fees, + name='Commission, markup etc. batch %s' % (int(row[23])), + ) balance -= fees - transaction.message = 'Commision, markup etc. batch %s' % ( - int(row[23])) + statement['transactions'].append(transaction) if statement['transactions'] and not payout: raise UserError( @@ -135,5 +132,4 @@ class Import(models.TransientModel): raise UserError( _('Parse error. Balance %s not equal to merchant ' 'payout %s') % (balance, payout)) - - return statements + return currency_code, merchant_id, statements diff --git a/account_bank_statement_import_adyen/models/account_journal.py b/account_bank_statement_import_adyen/models/account_journal.py index d0c431fd..44a3e7f8 100644 --- a/account_bank_statement_import_adyen/models/account_journal.py +++ b/account_bank_statement_import_adyen/models/account_journal.py @@ -1,5 +1,7 @@ -# coding: utf-8 -from openerp import fields, models +# © 2017 Opener BV () +# © 2020 Vanmoof BV () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import fields, models class Journal(models.Model): @@ -8,3 +10,9 @@ class Journal(models.Model): adyen_merchant_account = fields.Char( help=('Fill in the exact merchant account string to select this ' 'journal when importing Adyen statements')) + + def _get_bank_statements_available_import_formats(self): + res = super( + Journal, self)._get_bank_statements_available_import_formats() + res.append('adyen') + return res diff --git a/account_bank_statement_import_adyen/readme/CONFIGURE.rst b/account_bank_statement_import_adyen/readme/CONFIGURE.rst new file mode 100644 index 00000000..7df8a95e --- /dev/null +++ b/account_bank_statement_import_adyen/readme/CONFIGURE.rst @@ -0,0 +1,3 @@ +Configure a pseudo bank journal by creating a new journal with a dedicated +Adyen clearing account as the default ledger account. Set your merchant +account string in the Advanced settings on the journal form. diff --git a/account_bank_statement_import_adyen/readme/CONTRIBUTORS.rst b/account_bank_statement_import_adyen/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..58e9f494 --- /dev/null +++ b/account_bank_statement_import_adyen/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Stefan Rijnhart (https://opener.amsterdam) +* Martin Pishpecki (https://www.vanmoof.com) diff --git a/account_bank_statement_import_adyen/readme/CREDITS.rst b/account_bank_statement_import_adyen/readme/CREDITS.rst new file mode 100644 index 00000000..e69de29b diff --git a/account_bank_statement_import_adyen/readme/DESCRIPTION.rst b/account_bank_statement_import_adyen/readme/DESCRIPTION.rst new file mode 100644 index 00000000..c0832e16 --- /dev/null +++ b/account_bank_statement_import_adyen/readme/DESCRIPTION.rst @@ -0,0 +1,10 @@ +====================== +Adyen statement import +====================== + +This module processes Adyen transaction statements in xlsx format. You can +import the statements in a dedicated journal. Reconcile your sale invoices +with the credit transations. Reconcile the aggregated counterpart +transaction with the transaction in your real bank journal and register the +aggregated fee line containing commision and markup on the applicable +cost account. diff --git a/account_bank_statement_import_adyen/readme/HISTORY.rst b/account_bank_statement_import_adyen/readme/HISTORY.rst new file mode 100644 index 00000000..e69de29b diff --git a/account_bank_statement_import_adyen/readme/INSTALL.rst b/account_bank_statement_import_adyen/readme/INSTALL.rst new file mode 100644 index 00000000..e69de29b diff --git a/account_bank_statement_import_adyen/readme/ROADMAP.rst b/account_bank_statement_import_adyen/readme/ROADMAP.rst new file mode 100644 index 00000000..e69de29b diff --git a/account_bank_statement_import_adyen/readme/USAGE.rst b/account_bank_statement_import_adyen/readme/USAGE.rst new file mode 100644 index 00000000..70545a9f --- /dev/null +++ b/account_bank_statement_import_adyen/readme/USAGE.rst @@ -0,0 +1,3 @@ +After installing this module, you can import your Adyen transaction statements +through Menu Finance -> Bank -> Import. Don't enter a journal in the import +wizard. diff --git a/account_bank_statement_import_adyen/test_files/adyen_test.xlsx b/account_bank_statement_import_adyen/test_files/adyen_test.xlsx index 3adaa8dbf10f9a2a48f2e2538a5f15bc86756b46..b7cd4c6c287f961d57c8cc88e1b1e37eb065f01f 100644 GIT binary patch delta 15595 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 literal 17972 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 E08#;WUH||9 diff --git a/account_bank_statement_import_adyen/test_files/adyen_test_credit_fees.xlsx b/account_bank_statement_import_adyen/test_files/adyen_test_credit_fees.xlsx index 0443fceb3eae260220b96d33b06cf1e8032153ed..c4a786de4730536e198b1d366cb18d4f28282ec4 100644 GIT binary patch delta 9945 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 vQ1Jgp6i7~M~!OiUNyJyeY=brC(>-{II)2q6l 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 Qfc^Yge71%;^8FtDA4h+JegFUf literal 0 HcmV?d00001 diff --git a/account_bank_statement_import_adyen/tests/test_import_adyen.py b/account_bank_statement_import_adyen/tests/test_import_adyen.py index d4e8a684..acbadfdf 100644 --- a/account_bank_statement_import_adyen/tests/test_import_adyen.py +++ b/account_bank_statement_import_adyen/tests/test_import_adyen.py @@ -1,34 +1,49 @@ -# coding: utf-8 -from openerp.addons.account_bank_statement_import.tests import ( - TestStatementFile) +# © 2017 Opener BV () +# © 2020 Vanmoof BV () +# © 2015 Therp BV () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +import base64 +from odoo.exceptions import UserError +from odoo.tests.common import SavepointCase +from odoo.modules.module import get_module_resource -class TestImportAdyen(TestStatementFile): - def setUp(self): - super(TestImportAdyen, self).setUp() - self.journal = self.env['account.journal'].search( - [('type', '=', 'bank')], limit=1) - self.journal.default_debit_account_id.reconcile = True - self.journal.write({ +class TestImportAdyen(SavepointCase): + @classmethod + def setUpClass(cls): + super(TestImportAdyen, cls).setUpClass() + cls.journal = cls.env['account.journal'].create({ + 'company_id': cls.env.user.company_id.id, + 'name': 'Adyen test', + 'code': 'ADY', + 'type': 'bank', 'adyen_merchant_account': 'YOURCOMPANY_ACCOUNT', 'update_posted': True, + 'currency_id': cls.env.ref('base.USD').id, }) + # Enable reconcilation on the default journal account to trigger + # the functionality from account_bank_statement_clearing_account + cls.journal.default_debit_account_id.reconcile = True - def test_import_adyen(self): + def test_01_import_adyen(self): + """ Test that the Adyen statement can be imported and that the + lines on the default journal (clearing) account are fully reconciled + with each other """ self._test_statement_import( 'account_bank_statement_import_adyen', 'adyen_test.xlsx', 'YOURCOMPANY_ACCOUNT 2016/48') statement = self.env['account.bank.statement'].search( [], order='create_date desc', limit=1) + self.assertEqual(statement.journal_id, self.journal) self.assertEqual(len(statement.line_ids), 22) self.assertTrue( self.env.user.company_id.currency_id.is_zero( sum(line.amount for line in statement.line_ids))) account = self.env['account.account'].search([( - 'type', '=', 'receivable')], limit=1) + 'internal_type', '=', 'receivable')], limit=1) for line in statement.line_ids: - line.process_reconciliation([{ + line.process_reconciliation(new_aml_dicts=[{ 'debit': -line.amount if line.amount < 0 else 0, 'credit': line.amount if line.amount > 0 else 0, 'account_id': account.id}]) @@ -38,18 +53,56 @@ class TestImportAdyen(TestStatementFile): lines = self.env['account.move.line'].search([ ('account_id', '=', self.journal.default_debit_account_id.id), ('statement_id', '=', statement.id)]) - reconcile = lines.mapped('reconcile_id') + reconcile = lines.mapped('full_reconcile_id') self.assertEqual(len(reconcile), 1) - self.assertFalse(lines.mapped('reconcile_partial_id')) - self.assertEqual(lines, reconcile.line_id) + self.assertEqual(lines, reconcile.reconciled_line_ids) + # Reset the bank statement to see the counterpart lines being + # unreconciled statement.button_draft() - self.assertEqual(statement.state, 'draft') - self.assertFalse(lines.mapped('reconcile_partial_id')) - self.assertFalse(lines.mapped('reconcile_id')) + self.assertEqual(statement.state, 'open') + self.assertFalse(lines.mapped('matched_debit_ids')) + self.assertFalse(lines.mapped('matched_credit_ids')) + self.assertFalse(lines.mapped('full_reconcile_id')) - def test_import_adyen_credit_fees(self): + # Confirm the statement without the correct clearing account settings + self.journal.default_debit_account_id.reconcile = False + statement.button_confirm_bank() + self.assertEqual(statement.state, 'confirm') + self.assertFalse(lines.mapped('matched_debit_ids')) + self.assertFalse(lines.mapped('matched_credit_ids')) + self.assertFalse(lines.mapped('full_reconcile_id')) + + def test_02_import_adyen_credit_fees(self): + """ Import an Adyen statement with credit fees """ self._test_statement_import( 'account_bank_statement_import_adyen', 'adyen_test_credit_fees.xlsx', 'YOURCOMPANY_ACCOUNT 2016/8') + + def test_03_import_adyen_invalid(self): + """ Trying to hit that coverall target """ + with self.assertRaisesRegex(UserError, 'Could not make sense'): + self._test_statement_import( + 'account_bank_statement_import_adyen', + 'adyen_test_invalid.xls', + 'invalid') + + def _test_statement_import( + self, module_name, file_name, statement_name): + """Test correct creation of single statement.""" + statement_path = get_module_resource( + module_name, + 'test_files', + file_name + ) + statement_file = open(statement_path, 'rb').read() + import_wizard = self.env['account.bank.statement.import'].create({ + 'data_file': base64.b64encode(statement_file), + 'filename': file_name}) + import_wizard.import_file() + # statement name is account number + '-' + date of last line: + statements = self.env['account.bank.statement'].search( + [('name', '=', statement_name)]) + self.assertTrue(statements) + return statements diff --git a/account_bank_statement_import_adyen/views/account_journal.xml b/account_bank_statement_import_adyen/views/account_journal.xml index 2e6f2e6b..02f36f05 100644 --- a/account_bank_statement_import_adyen/views/account_journal.xml +++ b/account_bank_statement_import_adyen/views/account_journal.xml @@ -1,15 +1,13 @@ - - - - Add Adyen merchant account - account.journal - - - - - + + + Add Adyen merchant account + account.journal + + + + - - - + + + From 80d28d4d5787468f88ee04cfead226bcfa76a1c6 Mon Sep 17 00:00:00 2001 From: Stefan Rijnhart Date: Tue, 28 Feb 2017 18:45:32 +0100 Subject: [PATCH 03/24] [ADD] Adyen statement import --- .../__init__.py | 1 + .../__openerp__.py | 14 ++++ .../models/__init__.py | 1 + .../models/account_bank_statement.py | 80 +++++++++++++++++++ 4 files changed, 96 insertions(+) create mode 100644 account_bank_statement_clearing_account/__init__.py create mode 100644 account_bank_statement_clearing_account/__openerp__.py create mode 100644 account_bank_statement_clearing_account/models/__init__.py create mode 100644 account_bank_statement_clearing_account/models/account_bank_statement.py diff --git a/account_bank_statement_clearing_account/__init__.py b/account_bank_statement_clearing_account/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/account_bank_statement_clearing_account/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/account_bank_statement_clearing_account/__openerp__.py b/account_bank_statement_clearing_account/__openerp__.py new file mode 100644 index 00000000..f69fe697 --- /dev/null +++ b/account_bank_statement_clearing_account/__openerp__.py @@ -0,0 +1,14 @@ +# coding: utf-8 +# © 2017 Opener BV () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +{ + 'name': 'Reconcile entries from pseudo bank statements', + 'version': '8.0.1.0.0', + 'author': 'Opener B.V.', + 'category': 'Banking addons', + 'website': 'https://opener.am', + 'depends': [ + 'account_cancel', + ], + 'installable': True, +} diff --git a/account_bank_statement_clearing_account/models/__init__.py b/account_bank_statement_clearing_account/models/__init__.py new file mode 100644 index 00000000..0882dd26 --- /dev/null +++ b/account_bank_statement_clearing_account/models/__init__.py @@ -0,0 +1 @@ +from . import account_bank_statement diff --git a/account_bank_statement_clearing_account/models/account_bank_statement.py b/account_bank_statement_clearing_account/models/account_bank_statement.py new file mode 100644 index 00000000..06ee1e16 --- /dev/null +++ b/account_bank_statement_clearing_account/models/account_bank_statement.py @@ -0,0 +1,80 @@ +# coding: utf-8 +from openerp import api, models + + +class BankStatement(models.Model): + _inherit = 'account.bank.statement' + + @api.multi + def get_reconcile_clearing_account_lines(self): + if (self.journal_id.default_debit_account_id != + self.journal_id.default_credit_account_id or + not self.journal_id.default_debit_account_id.reconcile): + return False + account = self.journal_id.default_debit_account_id + currency = self.journal_id.currency or self.company_id.currency_id + + def get_bank_line(st_line): + for line in st_line.journal_entry_id.line_id: + if st_line.amount > 0: + compare_amount = st_line.amount + field = 'debit' + else: + compare_amount = -st_line.amount + field = 'credit' + if (line[field] and + not currency.compare_amounts( + line[field], compare_amount) and + line.account_id == account): + return line + return False + + move_lines = self.env['account.move.line'] + for st_line in self.line_ids: + bank_line = get_bank_line(st_line) + if not bank_line: + return False + move_lines += bank_line + balance = sum(line.debit - line.credit for line in move_lines) + if not currency.is_zero(balance): + return False + return move_lines + + @api.multi + def reconcile_clearing_account(self): + self.ensure_one() + lines = self.get_reconcile_clearing_account_lines() + if not lines: + return False + if any(line.reconcile_id or line.reconcile_partial_id + for line in lines): + return False + lines.reconcile_partial() + + @api.multi + def unreconcile_clearing_account(self): + self.ensure_one() + lines = self.get_reconcile_clearing_account_lines() + if not lines: + return False + reconciliation = lines[0].reconcile_id + if reconciliation and all( + line.reconcile_id == reconciliation + for line in lines) and all( + line in lines + for line in reconciliation.line_id): + reconciliation.unlink() + + @api.multi + def button_draft(self): + res = super(BankStatement, self).button_draft() + for statement in self: + statement.unreconcile_clearing_account() + return res + + @api.multi + def button_confirm_bank(self): + res = super(BankStatement, self).button_confirm_bank() + for statement in self: + statement.reconcile_clearing_account() + return res From 10d963964643c91d502d5e8011e16b9b2719b7dd Mon Sep 17 00:00:00 2001 From: Martin Pishpecki Date: Wed, 13 May 2020 17:05:05 +0200 Subject: [PATCH 04/24] [MIG] 12.0 account_bank_statement_import_adyen, account_bank_statement_clearing_account --- .../README.rst | 35 +++++++++++ .../__manifest__.py | 15 +++++ .../__openerp__.py | 14 ----- .../models/account_bank_statement.py | 61 +++++++++++-------- .../readme/CONFIGURE.rst | 4 ++ .../readme/CONTRIBUTORS.rst | 2 + .../readme/CREDITS.rst | 0 .../readme/DESCRIPTION.rst | 20 ++++++ .../readme/HISTORY.rst | 0 .../readme/INSTALL.rst | 0 .../readme/ROADMAP.rst | 0 .../readme/USAGE.rst | 0 12 files changed, 111 insertions(+), 40 deletions(-) create mode 100644 account_bank_statement_clearing_account/README.rst create mode 100644 account_bank_statement_clearing_account/__manifest__.py delete mode 100644 account_bank_statement_clearing_account/__openerp__.py create mode 100644 account_bank_statement_clearing_account/readme/CONFIGURE.rst create mode 100644 account_bank_statement_clearing_account/readme/CONTRIBUTORS.rst create mode 100644 account_bank_statement_clearing_account/readme/CREDITS.rst create mode 100644 account_bank_statement_clearing_account/readme/DESCRIPTION.rst create mode 100644 account_bank_statement_clearing_account/readme/HISTORY.rst create mode 100644 account_bank_statement_clearing_account/readme/INSTALL.rst create mode 100644 account_bank_statement_clearing_account/readme/ROADMAP.rst create mode 100644 account_bank_statement_clearing_account/readme/USAGE.rst diff --git a/account_bank_statement_clearing_account/README.rst b/account_bank_statement_clearing_account/README.rst new file mode 100644 index 00000000..38929e87 --- /dev/null +++ b/account_bank_statement_clearing_account/README.rst @@ -0,0 +1,35 @@ +**This file is going to be generated by oca-gen-addon-readme.** + +*Manual changes will be overwritten.* + +Please provide content in the ``readme`` directory: + +* **DESCRIPTION.rst** (required) +* INSTALL.rst (optional) +* CONFIGURE.rst (optional) +* **USAGE.rst** (optional, highly recommended) +* DEVELOP.rst (optional) +* ROADMAP.rst (optional) +* HISTORY.rst (optional, recommended) +* **CONTRIBUTORS.rst** (optional, highly recommended) +* CREDITS.rst (optional) + +Content of this README will also be drawn from the addon manifest, +from keys such as name, authors, maintainers, development_status, +and license. + +A good, one sentence summary in the manifest is also highly recommended. + + +Automatic changelog generation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +`HISTORY.rst` can be auto generated using `towncrier `_. + +Just put towncrier compatible changelog fragments into `readme/newsfragments` +and the changelog file will be automatically generated and updated when a new fragment is added. + +Please refer to `towncrier` documentation to know more. + +NOTE: the changelog will be automatically generated when using `/ocabot merge $option`. +If you need to run it manually, refer to `OCA/maintainer-tools README `_. diff --git a/account_bank_statement_clearing_account/__manifest__.py b/account_bank_statement_clearing_account/__manifest__.py new file mode 100644 index 00000000..04d914fc --- /dev/null +++ b/account_bank_statement_clearing_account/__manifest__.py @@ -0,0 +1,15 @@ +# © 2017 Opener BV () +# © 2020 Vanmoof BV () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +{ + "name": "Reconcile entries from pseudo bank statements", + "version": "12.0.1.0.0", + "author": "Opener B.V., Vanmoof BV, Odoo Community Association (OCA)", + "category": "Banking addons", + "website": "https://opener.am", + "license": "AGPL-3", + "depends": [ + "account_cancel", + ], + "installable": True, +} diff --git a/account_bank_statement_clearing_account/__openerp__.py b/account_bank_statement_clearing_account/__openerp__.py deleted file mode 100644 index f69fe697..00000000 --- a/account_bank_statement_clearing_account/__openerp__.py +++ /dev/null @@ -1,14 +0,0 @@ -# coding: utf-8 -# © 2017 Opener BV () -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -{ - 'name': 'Reconcile entries from pseudo bank statements', - 'version': '8.0.1.0.0', - 'author': 'Opener B.V.', - 'category': 'Banking addons', - 'website': 'https://opener.am', - 'depends': [ - 'account_cancel', - ], - 'installable': True, -} diff --git a/account_bank_statement_clearing_account/models/account_bank_statement.py b/account_bank_statement_clearing_account/models/account_bank_statement.py index 06ee1e16..eb30d373 100644 --- a/account_bank_statement_clearing_account/models/account_bank_statement.py +++ b/account_bank_statement_clearing_account/models/account_bank_statement.py @@ -1,5 +1,7 @@ -# coding: utf-8 -from openerp import api, models +# © 2017 Opener BV () +# © 2020 Vanmoof BV () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import api, models class BankStatement(models.Model): @@ -7,25 +9,26 @@ class BankStatement(models.Model): @api.multi def get_reconcile_clearing_account_lines(self): + """ If this statement qualifies for clearing account reconciliation, + return the relevant lines to (un)reconcile. This is the case if the + default journal account is reconcilable, each statement line has a + counterpart line on this account for the full amount and the sum of + the counterpart lines is zero. + """ + self.ensure_one() if (self.journal_id.default_debit_account_id != self.journal_id.default_credit_account_id or not self.journal_id.default_debit_account_id.reconcile): return False account = self.journal_id.default_debit_account_id - currency = self.journal_id.currency or self.company_id.currency_id + currency = self.journal_id.currency_id or self.company_id.currency_id def get_bank_line(st_line): - for line in st_line.journal_entry_id.line_id: - if st_line.amount > 0: - compare_amount = st_line.amount - field = 'debit' - else: - compare_amount = -st_line.amount - field = 'credit' - if (line[field] and - not currency.compare_amounts( - line[field], compare_amount) and - line.account_id == account): + for line in st_line.journal_entry_ids: + field = 'debit' if st_line.amount > 0 else 'credit' + if (line.account_id == account and + not currency.compare_amounts( + line[field], abs(st_line.amount))): return line return False @@ -42,31 +45,35 @@ class BankStatement(models.Model): @api.multi def reconcile_clearing_account(self): + """ If applicable, reconcile the clearing account lines in case + all lines are still unreconciled. """ self.ensure_one() lines = self.get_reconcile_clearing_account_lines() - if not lines: + if not lines or any( + li.matched_debit_ids or li.matched_credit_ids + for li in lines): return False - if any(line.reconcile_id or line.reconcile_partial_id - for line in lines): - return False - lines.reconcile_partial() + lines.reconcile() + return True @api.multi def unreconcile_clearing_account(self): + """ If applicable, unreconcile the clearing account lines + if still fully reconciled with each other. """ self.ensure_one() lines = self.get_reconcile_clearing_account_lines() if not lines: return False - reconciliation = lines[0].reconcile_id - if reconciliation and all( - line.reconcile_id == reconciliation - for line in lines) and all( - line in lines - for line in reconciliation.line_id): - reconciliation.unlink() + reconciliation = lines[0].full_reconcile_id + if reconciliation and lines == reconciliation.reconciled_line_ids: + lines.remove_move_reconcile() + return True + return False @api.multi def button_draft(self): + """ When setting the statement back to draft, unreconcile the + reconciliation on the clearing account """ res = super(BankStatement, self).button_draft() for statement in self: statement.unreconcile_clearing_account() @@ -74,6 +81,8 @@ class BankStatement(models.Model): @api.multi def button_confirm_bank(self): + """ When confirming the statement, trigger the reconciliation of + the lines on the clearing account (if applicable) """ res = super(BankStatement, self).button_confirm_bank() for statement in self: statement.reconcile_clearing_account() diff --git a/account_bank_statement_clearing_account/readme/CONFIGURE.rst b/account_bank_statement_clearing_account/readme/CONFIGURE.rst new file mode 100644 index 00000000..b29c2ce6 --- /dev/null +++ b/account_bank_statement_clearing_account/readme/CONFIGURE.rst @@ -0,0 +1,4 @@ +In order to enable the reconcilation of the counterparts of zero-balance +statement files from payment providers, you need to make sure that the journal +that is used for these statements have the same default debit account as their +default credit account, and this account is configured for reconciliation. diff --git a/account_bank_statement_clearing_account/readme/CONTRIBUTORS.rst b/account_bank_statement_clearing_account/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..58e9f494 --- /dev/null +++ b/account_bank_statement_clearing_account/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Stefan Rijnhart (https://opener.amsterdam) +* Martin Pishpecki (https://www.vanmoof.com) diff --git a/account_bank_statement_clearing_account/readme/CREDITS.rst b/account_bank_statement_clearing_account/readme/CREDITS.rst new file mode 100644 index 00000000..e69de29b diff --git a/account_bank_statement_clearing_account/readme/DESCRIPTION.rst b/account_bank_statement_clearing_account/readme/DESCRIPTION.rst new file mode 100644 index 00000000..7043715f --- /dev/null +++ b/account_bank_statement_clearing_account/readme/DESCRIPTION.rst @@ -0,0 +1,20 @@ +This is a technical modules that you can use to improve the processing of +statement files from payment providers. These statements usually consist +of lines that to be reconciled by customer debts, offset by lines that are +to be reconciled by the imbursements from the payment provider, corrected +for customer credits and the costs of the payment provider. Typically, the +balance of such a statement is zero. Effectively, the counterpart of each +statement line is made on a clearing account and you should keep track of +the balance of the clearing account to see if the payment provider still owes +you money. You can keep track of the account by reconciling each entry on it. + +That is where this module comes in. When importing such a statement, this +module reconciles all the counterparts on the clearing account with one +another. Reconciliation is executed when validating the statement. When +reopening the statement, the reconcilation is undone. + +Known issues +============ +This module does not come with its own tests because it depends on a +statement filter being installed. Instead, it is tested in +`account_bank_statement_import_adyen` diff --git a/account_bank_statement_clearing_account/readme/HISTORY.rst b/account_bank_statement_clearing_account/readme/HISTORY.rst new file mode 100644 index 00000000..e69de29b diff --git a/account_bank_statement_clearing_account/readme/INSTALL.rst b/account_bank_statement_clearing_account/readme/INSTALL.rst new file mode 100644 index 00000000..e69de29b diff --git a/account_bank_statement_clearing_account/readme/ROADMAP.rst b/account_bank_statement_clearing_account/readme/ROADMAP.rst new file mode 100644 index 00000000..e69de29b diff --git a/account_bank_statement_clearing_account/readme/USAGE.rst b/account_bank_statement_clearing_account/readme/USAGE.rst new file mode 100644 index 00000000..e69de29b From fc8b6db62d09cc9f2d49e7ca9e93fa1a0c0ca198 Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Wed, 17 Feb 2021 16:26:15 +0100 Subject: [PATCH 05/24] [IMP] : black, isort, prettier --- .../__manifest__.py | 4 +- .../models/account_bank_statement.py | 24 ++-- .../__manifest__.py | 10 +- .../models/account_bank_statement_import.py | 97 +++++++------- .../models/account_journal.py | 14 +- .../tests/test_import_adyen.py | 121 ++++++++++-------- .../views/account_journal.xml | 6 +- .../account_bank_statement_clearing_account | 1 + .../setup.py | 6 + .../account_bank_statement_import_adyen | 1 + .../setup.py | 6 + 11 files changed, 161 insertions(+), 129 deletions(-) create mode 120000 setup/account_bank_statement_clearing_account/odoo/addons/account_bank_statement_clearing_account create mode 100644 setup/account_bank_statement_clearing_account/setup.py create mode 120000 setup/account_bank_statement_import_adyen/odoo/addons/account_bank_statement_import_adyen create mode 100644 setup/account_bank_statement_import_adyen/setup.py diff --git a/account_bank_statement_clearing_account/__manifest__.py b/account_bank_statement_clearing_account/__manifest__.py index 04d914fc..5f6e364f 100644 --- a/account_bank_statement_clearing_account/__manifest__.py +++ b/account_bank_statement_clearing_account/__manifest__.py @@ -8,8 +8,6 @@ "category": "Banking addons", "website": "https://opener.am", "license": "AGPL-3", - "depends": [ - "account_cancel", - ], + "depends": ["account_cancel",], "installable": True, } diff --git a/account_bank_statement_clearing_account/models/account_bank_statement.py b/account_bank_statement_clearing_account/models/account_bank_statement.py index eb30d373..cf5ebadb 100644 --- a/account_bank_statement_clearing_account/models/account_bank_statement.py +++ b/account_bank_statement_clearing_account/models/account_bank_statement.py @@ -5,7 +5,7 @@ from odoo import api, models class BankStatement(models.Model): - _inherit = 'account.bank.statement' + _inherit = "account.bank.statement" @api.multi def get_reconcile_clearing_account_lines(self): @@ -16,23 +16,25 @@ class BankStatement(models.Model): the counterpart lines is zero. """ self.ensure_one() - if (self.journal_id.default_debit_account_id != - self.journal_id.default_credit_account_id or - not self.journal_id.default_debit_account_id.reconcile): + if ( + self.journal_id.default_debit_account_id + != self.journal_id.default_credit_account_id + or not self.journal_id.default_debit_account_id.reconcile + ): return False account = self.journal_id.default_debit_account_id currency = self.journal_id.currency_id or self.company_id.currency_id def get_bank_line(st_line): for line in st_line.journal_entry_ids: - field = 'debit' if st_line.amount > 0 else 'credit' - if (line.account_id == account and - not currency.compare_amounts( - line[field], abs(st_line.amount))): + field = "debit" if st_line.amount > 0 else "credit" + if line.account_id == account and not currency.compare_amounts( + line[field], abs(st_line.amount) + ): return line return False - move_lines = self.env['account.move.line'] + move_lines = self.env["account.move.line"] for st_line in self.line_ids: bank_line = get_bank_line(st_line) if not bank_line: @@ -50,8 +52,8 @@ class BankStatement(models.Model): self.ensure_one() lines = self.get_reconcile_clearing_account_lines() if not lines or any( - li.matched_debit_ids or li.matched_credit_ids - for li in lines): + li.matched_debit_ids or li.matched_credit_ids for li in lines + ): return False lines.reconcile() return True diff --git a/account_bank_statement_import_adyen/__manifest__.py b/account_bank_statement_import_adyen/__manifest__.py index 4be97c2e..de271032 100644 --- a/account_bank_statement_import_adyen/__manifest__.py +++ b/account_bank_statement_import_adyen/__manifest__.py @@ -12,13 +12,7 @@ "account_bank_statement_import", "account_bank_statement_clearing_account", ], - "external_dependencies": { - "python": [ - "openpyxl", - ], - }, - "data": [ - "views/account_journal.xml", - ], + "external_dependencies": {"python": ["openpyxl",],}, + "data": ["views/account_journal.xml",], "installable": True, } diff --git a/account_bank_statement_import_adyen/models/account_bank_statement_import.py b/account_bank_statement_import_adyen/models/account_bank_statement_import.py index a8d17f60..45e062de 100644 --- a/account_bank_statement_import_adyen/models/account_bank_statement_import.py +++ b/account_bank_statement_import_adyen/models/account_bank_statement_import.py @@ -1,16 +1,17 @@ # © 2017 Opener BV () # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from io import BytesIO -from openpyxl import load_workbook from zipfile import BadZipfile -from odoo import api, models, fields +from openpyxl import load_workbook + +from odoo import api, fields, models from odoo.exceptions import UserError from odoo.tools.translate import _ class AccountBankStatementImport(models.TransientModel): - _inherit = 'account.bank.statement.import' + _inherit = "account.bank.statement.import" @api.model def _parse_file(self, data_file): @@ -19,41 +20,46 @@ class AccountBankStatementImport(models.TransientModel): try: return self.import_adyen_xlsx(data_file) except ValueError: - return super(AccountBankStatementImport, self)._parse_file( - data_file) + return super(AccountBankStatementImport, self)._parse_file(data_file) def _find_additional_data(self, currency_code, account_number): """ Try to find journal by Adyen merchant account """ if account_number: - journal = self.env['account.journal'].search([ - ('adyen_merchant_account', '=', account_number)], limit=1) + journal = self.env["account.journal"].search( + [("adyen_merchant_account", "=", account_number)], limit=1 + ) if journal: - if self._context.get('journal_id', journal.id) != journal.id: + if self._context.get("journal_id", journal.id) != journal.id: raise UserError( - _('Selected journal Merchant Account does not match ' - 'the import file Merchant Account ' - 'column: %s') % account_number) + _( + "Selected journal Merchant Account does not match " + "the import file Merchant Account " + "column: %s" + ) + % account_number + ) self = self.with_context(journal_id=journal.id) return super(AccountBankStatementImport, self)._find_additional_data( - currency_code, account_number) + currency_code, account_number + ) @api.model def balance(self, row): return -(row[15] or 0) + sum( - row[i] if row[i] else 0.0 - for i in (16, 17, 18, 19, 20)) + row[i] if row[i] else 0.0 for i in (16, 17, 18, 19, 20) + ) @api.model def import_adyen_transaction(self, statement, statement_id, row): - transaction_id = str(len(statement['transactions'])).zfill(4) + transaction_id = str(len(statement["transactions"])).zfill(4) transaction = dict( unique_import_id=statement_id + transaction_id, date=fields.Date.from_string(row[6]), amount=self.balance(row), - note='%s %s %s %s' % (row[2], row[3], row[4], row[21]), + note="{} {} {} {}".format(row[2], row[3], row[4], row[21]), name="%s" % (row[3] or row[4] or row[9]), ) - statement['transactions'].append(transaction) + statement["transactions"].append(transaction) @api.model def import_adyen_xlsx(self, data_file): @@ -75,61 +81,62 @@ class AccountBankStatementImport(models.TransientModel): row = [cell.value for cell in row] if len(row) != 31: raise ValueError( - 'Not an Adyen statement. Unexpected row length %s ' - 'instead of 31' % len(row)) + "Not an Adyen statement. Unexpected row length %s " + "instead of 31" % len(row) + ) if not row[1]: continue if not headers: - if row[1] != 'Company Account': + if row[1] != "Company Account": raise ValueError( 'Not an Adyen statement. Unexpected header "%s" ' - 'instead of "Company Account"', row[1]) + 'instead of "Company Account"', + row[1], + ) headers = True continue if not statement: - statement = {'transactions': []} + statement = {"transactions": []} statements.append(statement) - statement_id = '%s %s/%s' % ( - row[2], row[6].strftime('%Y'), int(row[23])) + statement_id = "{} {}/{}".format( + row[2], + row[6].strftime("%Y"), + int(row[23]), + ) currency_code = row[14] merchant_id = row[2] - statement['name'] = '%s %s/%s' % ( - row[2], row[6].year, row[23]) + statement["name"] = "{} {}/{}".format(row[2], row[6].year, row[23]) date = fields.Date.from_string(row[6]) - if not statement.get('date') or statement.get('date') > date: - statement['date'] = date + if not statement.get("date") or statement.get("date") > date: + statement["date"] = date row[8] = row[8].strip() - if row[8] == 'MerchantPayout': + if row[8] == "MerchantPayout": payout -= self.balance(row) else: balance += self.balance(row) self.import_adyen_transaction(statement, statement_id, row) - fees += sum( - row[i] if row[i] else 0.0 - for i in (17, 18, 19, 20)) + fees += sum(row[i] if row[i] else 0.0 for i in (17, 18, 19, 20)) if not headers: - raise ValueError( - 'Not an Adyen statement. Did not encounter header row.') + raise ValueError("Not an Adyen statement. Did not encounter header row.") if fees: - transaction_id = str(len(statement['transactions'])).zfill(4) + transaction_id = str(len(statement["transactions"])).zfill(4) transaction = dict( unique_import_id=statement_id + transaction_id, - date=max(t['date'] for t in statement['transactions']), + date=max(t["date"] for t in statement["transactions"]), amount=-fees, - name='Commission, markup etc. batch %s' % (int(row[23])), + name="Commission, markup etc. batch %s" % (int(row[23])), ) balance -= fees - statement['transactions'].append(transaction) + statement["transactions"].append(transaction) - if statement['transactions'] and not payout: + if statement["transactions"] and not payout: + raise UserError(_("No payout detected in Adyen statement.")) + if self.env.user.company_id.currency_id.compare_amounts(balance, payout) != 0: raise UserError( - _('No payout detected in Adyen statement.')) - if self.env.user.company_id.currency_id.compare_amounts( - balance, payout) != 0: - raise UserError( - _('Parse error. Balance %s not equal to merchant ' - 'payout %s') % (balance, payout)) + _("Parse error. Balance %s not equal to merchant " "payout %s") + % (balance, payout) + ) return currency_code, merchant_id, statements diff --git a/account_bank_statement_import_adyen/models/account_journal.py b/account_bank_statement_import_adyen/models/account_journal.py index 44a3e7f8..32c213dd 100644 --- a/account_bank_statement_import_adyen/models/account_journal.py +++ b/account_bank_statement_import_adyen/models/account_journal.py @@ -5,14 +5,16 @@ from odoo import fields, models class Journal(models.Model): - _inherit = 'account.journal' + _inherit = "account.journal" adyen_merchant_account = fields.Char( - help=('Fill in the exact merchant account string to select this ' - 'journal when importing Adyen statements')) + help=( + "Fill in the exact merchant account string to select this " + "journal when importing Adyen statements" + ) + ) def _get_bank_statements_available_import_formats(self): - res = super( - Journal, self)._get_bank_statements_available_import_formats() - res.append('adyen') + res = super(Journal, self)._get_bank_statements_available_import_formats() + res.append("adyen") return res diff --git a/account_bank_statement_import_adyen/tests/test_import_adyen.py b/account_bank_statement_import_adyen/tests/test_import_adyen.py index acbadfdf..10e61e92 100644 --- a/account_bank_statement_import_adyen/tests/test_import_adyen.py +++ b/account_bank_statement_import_adyen/tests/test_import_adyen.py @@ -3,24 +3,27 @@ # © 2015 Therp BV () # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). import base64 + from odoo.exceptions import UserError -from odoo.tests.common import SavepointCase from odoo.modules.module import get_module_resource +from odoo.tests.common import SavepointCase class TestImportAdyen(SavepointCase): @classmethod def setUpClass(cls): super(TestImportAdyen, cls).setUpClass() - cls.journal = cls.env['account.journal'].create({ - 'company_id': cls.env.user.company_id.id, - 'name': 'Adyen test', - 'code': 'ADY', - 'type': 'bank', - 'adyen_merchant_account': 'YOURCOMPANY_ACCOUNT', - 'update_posted': True, - 'currency_id': cls.env.ref('base.USD').id, - }) + cls.journal = cls.env["account.journal"].create( + { + "company_id": cls.env.user.company_id.id, + "name": "Adyen test", + "code": "ADY", + "type": "bank", + "adyen_merchant_account": "YOURCOMPANY_ACCOUNT", + "update_posted": True, + "currency_id": cls.env.ref("base.USD").id, + } + ) # Enable reconcilation on the default journal account to trigger # the functionality from account_bank_statement_clearing_account cls.journal.default_debit_account_id.reconcile = True @@ -30,79 +33,91 @@ class TestImportAdyen(SavepointCase): lines on the default journal (clearing) account are fully reconciled with each other """ self._test_statement_import( - 'account_bank_statement_import_adyen', 'adyen_test.xlsx', - 'YOURCOMPANY_ACCOUNT 2016/48') - statement = self.env['account.bank.statement'].search( - [], order='create_date desc', limit=1) + "account_bank_statement_import_adyen", + "adyen_test.xlsx", + "YOURCOMPANY_ACCOUNT 2016/48", + ) + statement = self.env["account.bank.statement"].search( + [], order="create_date desc", limit=1 + ) self.assertEqual(statement.journal_id, self.journal) self.assertEqual(len(statement.line_ids), 22) self.assertTrue( self.env.user.company_id.currency_id.is_zero( - sum(line.amount for line in statement.line_ids))) + sum(line.amount for line in statement.line_ids) + ) + ) - account = self.env['account.account'].search([( - 'internal_type', '=', 'receivable')], limit=1) + account = self.env["account.account"].search( + [("internal_type", "=", "receivable")], limit=1 + ) for line in statement.line_ids: - line.process_reconciliation(new_aml_dicts=[{ - 'debit': -line.amount if line.amount < 0 else 0, - 'credit': line.amount if line.amount > 0 else 0, - 'account_id': account.id}]) + line.process_reconciliation( + new_aml_dicts=[ + { + "debit": -line.amount if line.amount < 0 else 0, + "credit": line.amount if line.amount > 0 else 0, + "account_id": account.id, + } + ] + ) statement.button_confirm_bank() - self.assertEqual(statement.state, 'confirm') - lines = self.env['account.move.line'].search([ - ('account_id', '=', self.journal.default_debit_account_id.id), - ('statement_id', '=', statement.id)]) - reconcile = lines.mapped('full_reconcile_id') + self.assertEqual(statement.state, "confirm") + lines = self.env["account.move.line"].search( + [ + ("account_id", "=", self.journal.default_debit_account_id.id), + ("statement_id", "=", statement.id), + ] + ) + reconcile = lines.mapped("full_reconcile_id") self.assertEqual(len(reconcile), 1) self.assertEqual(lines, reconcile.reconciled_line_ids) # Reset the bank statement to see the counterpart lines being # unreconciled statement.button_draft() - self.assertEqual(statement.state, 'open') - self.assertFalse(lines.mapped('matched_debit_ids')) - self.assertFalse(lines.mapped('matched_credit_ids')) - self.assertFalse(lines.mapped('full_reconcile_id')) + self.assertEqual(statement.state, "open") + self.assertFalse(lines.mapped("matched_debit_ids")) + self.assertFalse(lines.mapped("matched_credit_ids")) + self.assertFalse(lines.mapped("full_reconcile_id")) # Confirm the statement without the correct clearing account settings self.journal.default_debit_account_id.reconcile = False statement.button_confirm_bank() - self.assertEqual(statement.state, 'confirm') - self.assertFalse(lines.mapped('matched_debit_ids')) - self.assertFalse(lines.mapped('matched_credit_ids')) - self.assertFalse(lines.mapped('full_reconcile_id')) + self.assertEqual(statement.state, "confirm") + self.assertFalse(lines.mapped("matched_debit_ids")) + self.assertFalse(lines.mapped("matched_credit_ids")) + self.assertFalse(lines.mapped("full_reconcile_id")) def test_02_import_adyen_credit_fees(self): """ Import an Adyen statement with credit fees """ self._test_statement_import( - 'account_bank_statement_import_adyen', - 'adyen_test_credit_fees.xlsx', - 'YOURCOMPANY_ACCOUNT 2016/8') + "account_bank_statement_import_adyen", + "adyen_test_credit_fees.xlsx", + "YOURCOMPANY_ACCOUNT 2016/8", + ) def test_03_import_adyen_invalid(self): """ Trying to hit that coverall target """ - with self.assertRaisesRegex(UserError, 'Could not make sense'): + with self.assertRaisesRegex(UserError, "Could not make sense"): self._test_statement_import( - 'account_bank_statement_import_adyen', - 'adyen_test_invalid.xls', - 'invalid') + "account_bank_statement_import_adyen", + "adyen_test_invalid.xls", + "invalid", + ) - def _test_statement_import( - self, module_name, file_name, statement_name): + def _test_statement_import(self, module_name, file_name, statement_name): """Test correct creation of single statement.""" - statement_path = get_module_resource( - module_name, - 'test_files', - file_name + statement_path = get_module_resource(module_name, "test_files", file_name) + statement_file = open(statement_path, "rb").read() + import_wizard = self.env["account.bank.statement.import"].create( + {"data_file": base64.b64encode(statement_file), "filename": file_name} ) - statement_file = open(statement_path, 'rb').read() - import_wizard = self.env['account.bank.statement.import'].create({ - 'data_file': base64.b64encode(statement_file), - 'filename': file_name}) import_wizard.import_file() # statement name is account number + '-' + date of last line: - statements = self.env['account.bank.statement'].search( - [('name', '=', statement_name)]) + statements = self.env["account.bank.statement"].search( + [("name", "=", statement_name)] + ) self.assertTrue(statements) return statements diff --git a/account_bank_statement_import_adyen/views/account_journal.xml b/account_bank_statement_import_adyen/views/account_journal.xml index 02f36f05..bf845158 100644 --- a/account_bank_statement_import_adyen/views/account_journal.xml +++ b/account_bank_statement_import_adyen/views/account_journal.xml @@ -1,12 +1,12 @@ - + Add Adyen merchant account account.journal - + - + diff --git a/setup/account_bank_statement_clearing_account/odoo/addons/account_bank_statement_clearing_account b/setup/account_bank_statement_clearing_account/odoo/addons/account_bank_statement_clearing_account new file mode 120000 index 00000000..4ae7bb48 --- /dev/null +++ b/setup/account_bank_statement_clearing_account/odoo/addons/account_bank_statement_clearing_account @@ -0,0 +1 @@ +../../../../account_bank_statement_clearing_account \ No newline at end of file diff --git a/setup/account_bank_statement_clearing_account/setup.py b/setup/account_bank_statement_clearing_account/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/account_bank_statement_clearing_account/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/account_bank_statement_import_adyen/odoo/addons/account_bank_statement_import_adyen b/setup/account_bank_statement_import_adyen/odoo/addons/account_bank_statement_import_adyen new file mode 120000 index 00000000..2e39464e --- /dev/null +++ b/setup/account_bank_statement_import_adyen/odoo/addons/account_bank_statement_import_adyen @@ -0,0 +1 @@ +../../../../account_bank_statement_import_adyen \ No newline at end of file diff --git a/setup/account_bank_statement_import_adyen/setup.py b/setup/account_bank_statement_import_adyen/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/account_bank_statement_import_adyen/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From 9e2a82cca0568df1f777292b018874f3832b4332 Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Wed, 17 Feb 2021 16:35:49 +0100 Subject: [PATCH 06/24] [MIG] account_bank_statement_clearing_account Migrate to 13.0 --- .../README.rst | 123 ++++- .../__manifest__.py | 12 +- .../models/account_bank_statement.py | 17 +- .../readme/CONTRIBUTORS.rst | 1 + .../readme/USAGE.rst | 3 + .../static/description/index.html | 442 ++++++++++++++++++ 6 files changed, 557 insertions(+), 41 deletions(-) create mode 100644 account_bank_statement_clearing_account/static/description/index.html diff --git a/account_bank_statement_clearing_account/README.rst b/account_bank_statement_clearing_account/README.rst index 38929e87..830495f8 100644 --- a/account_bank_statement_clearing_account/README.rst +++ b/account_bank_statement_clearing_account/README.rst @@ -1,35 +1,110 @@ -**This file is going to be generated by oca-gen-addon-readme.** +============================================= +Reconcile entries from pseudo bank statements +============================================= -*Manual changes will be overwritten.* +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -Please provide content in the ``readme`` directory: +.. |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/13.0/account_bank_statement_clearing_account + :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-13-0/bank-statement-import-13-0-account_bank_statement_clearing_account + :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/13.0 + :alt: Try me on Runbot -* **DESCRIPTION.rst** (required) -* INSTALL.rst (optional) -* CONFIGURE.rst (optional) -* **USAGE.rst** (optional, highly recommended) -* DEVELOP.rst (optional) -* ROADMAP.rst (optional) -* HISTORY.rst (optional, recommended) -* **CONTRIBUTORS.rst** (optional, highly recommended) -* CREDITS.rst (optional) +|badge1| |badge2| |badge3| |badge4| |badge5| -Content of this README will also be drawn from the addon manifest, -from keys such as name, authors, maintainers, development_status, -and license. +This is a technical modules that you can use to improve the processing of +statement files from payment providers. These statements usually consist +of lines that to be reconciled by customer debts, offset by lines that are +to be reconciled by the imbursements from the payment provider, corrected +for customer credits and the costs of the payment provider. Typically, the +balance of such a statement is zero. Effectively, the counterpart of each +statement line is made on a clearing account and you should keep track of +the balance of the clearing account to see if the payment provider still owes +you money. You can keep track of the account by reconciling each entry on it. -A good, one sentence summary in the manifest is also highly recommended. +That is where this module comes in. When importing such a statement, this +module reconciles all the counterparts on the clearing account with one +another. Reconciliation is executed when validating the statement. When +reopening the statement, the reconcilation is undone. +Known issues +============ +This module does not come with its own tests because it depends on a +statement filter being installed. Instead, it is tested in +`account_bank_statement_import_adyen` -Automatic changelog generation -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +**Table of contents** -`HISTORY.rst` can be auto generated using `towncrier `_. +.. contents:: + :local: -Just put towncrier compatible changelog fragments into `readme/newsfragments` -and the changelog file will be automatically generated and updated when a new fragment is added. +Configuration +============= -Please refer to `towncrier` documentation to know more. +In order to enable the reconcilation of the counterparts of zero-balance +statement files from payment providers, you need to make sure that the journal +that is used for these statements have the same default debit account as their +default credit account, and this account is configured for reconciliation. -NOTE: the changelog will be automatically generated when using `/ocabot merge $option`. -If you need to run it manually, refer to `OCA/maintainer-tools README `_. +Usage +===== + +After installing this module, any statement where the sum of all debit and +credit lines match, and where the default journal account is reconcilable, will +reconcile all posted moved when the statement is confirmed. + +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 +~~~~~~~ + +* Opener B.V. +* Vanmoof BV + +Contributors +~~~~~~~~~~~~ + +* Stefan Rijnhart (https://opener.amsterdam) +* Martin Pishpecki (https://www.vanmoof.com) +* Ronald Portier (https://therp.nl) + +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. + +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_clearing_account/__manifest__.py b/account_bank_statement_clearing_account/__manifest__.py index 5f6e364f..91148a50 100644 --- a/account_bank_statement_clearing_account/__manifest__.py +++ b/account_bank_statement_clearing_account/__manifest__.py @@ -1,13 +1,13 @@ -# © 2017 Opener BV () -# © 2020 Vanmoof BV () -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# Copyright 2017 Opener BV () +# Copyright 2020 Vanmoof BV () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). { "name": "Reconcile entries from pseudo bank statements", - "version": "12.0.1.0.0", + "version": "13.0.1.0.0", "author": "Opener B.V., Vanmoof BV, Odoo Community Association (OCA)", "category": "Banking addons", - "website": "https://opener.am", + "website": "https://github.com/oca/bank-statement-import", "license": "AGPL-3", - "depends": ["account_cancel",], + "depends": ["account"], "installable": True, } diff --git a/account_bank_statement_clearing_account/models/account_bank_statement.py b/account_bank_statement_clearing_account/models/account_bank_statement.py index cf5ebadb..79baf5dc 100644 --- a/account_bank_statement_clearing_account/models/account_bank_statement.py +++ b/account_bank_statement_clearing_account/models/account_bank_statement.py @@ -1,13 +1,12 @@ -# © 2017 Opener BV () -# © 2020 Vanmoof BV () -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo import api, models +# Copyright 2017 Opener BV () +# Copyright 2020 Vanmoof BV () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import models class BankStatement(models.Model): _inherit = "account.bank.statement" - @api.multi def get_reconcile_clearing_account_lines(self): """ If this statement qualifies for clearing account reconciliation, return the relevant lines to (un)reconcile. This is the case if the @@ -45,7 +44,6 @@ class BankStatement(models.Model): return False return move_lines - @api.multi def reconcile_clearing_account(self): """ If applicable, reconcile the clearing account lines in case all lines are still unreconciled. """ @@ -58,7 +56,6 @@ class BankStatement(models.Model): lines.reconcile() return True - @api.multi def unreconcile_clearing_account(self): """ If applicable, unreconcile the clearing account lines if still fully reconciled with each other. """ @@ -72,16 +69,14 @@ class BankStatement(models.Model): return True return False - @api.multi - def button_draft(self): + def button_reopen(self): """ When setting the statement back to draft, unreconcile the reconciliation on the clearing account """ - res = super(BankStatement, self).button_draft() + res = super(BankStatement, self).button_reopen() for statement in self: statement.unreconcile_clearing_account() return res - @api.multi def button_confirm_bank(self): """ When confirming the statement, trigger the reconciliation of the lines on the clearing account (if applicable) """ diff --git a/account_bank_statement_clearing_account/readme/CONTRIBUTORS.rst b/account_bank_statement_clearing_account/readme/CONTRIBUTORS.rst index 58e9f494..f00439e5 100644 --- a/account_bank_statement_clearing_account/readme/CONTRIBUTORS.rst +++ b/account_bank_statement_clearing_account/readme/CONTRIBUTORS.rst @@ -1,2 +1,3 @@ * Stefan Rijnhart (https://opener.amsterdam) * Martin Pishpecki (https://www.vanmoof.com) +* Ronald Portier (https://therp.nl) diff --git a/account_bank_statement_clearing_account/readme/USAGE.rst b/account_bank_statement_clearing_account/readme/USAGE.rst index e69de29b..9f9ef0ac 100644 --- a/account_bank_statement_clearing_account/readme/USAGE.rst +++ b/account_bank_statement_clearing_account/readme/USAGE.rst @@ -0,0 +1,3 @@ +After installing this module, any statement where the sum of all debit and +credit lines match, and where the default journal account is reconcilable, will +reconcile all posted moved when the statement is confirmed. diff --git a/account_bank_statement_clearing_account/static/description/index.html b/account_bank_statement_clearing_account/static/description/index.html new file mode 100644 index 00000000..ba3839a4 --- /dev/null +++ b/account_bank_statement_clearing_account/static/description/index.html @@ -0,0 +1,442 @@ + + + + + + +Reconcile entries from pseudo bank statements + + + +
+

Reconcile entries from pseudo bank statements

+ + +

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

+

This is a technical modules that you can use to improve the processing of +statement files from payment providers. These statements usually consist +of lines that to be reconciled by customer debts, offset by lines that are +to be reconciled by the imbursements from the payment provider, corrected +for customer credits and the costs of the payment provider. Typically, the +balance of such a statement is zero. Effectively, the counterpart of each +statement line is made on a clearing account and you should keep track of +the balance of the clearing account to see if the payment provider still owes +you money. You can keep track of the account by reconciling each entry on it.

+

That is where this module comes in. When importing such a statement, this +module reconciles all the counterparts on the clearing account with one +another. Reconciliation is executed when validating the statement. When +reopening the statement, the reconcilation is undone.

+
+

Known issues

+

This module does not come with its own tests because it depends on a +statement filter being installed. Instead, it is tested in +account_bank_statement_import_adyen

+

Table of contents

+
+
+

Configuration

+

In order to enable the reconcilation of the counterparts of zero-balance +statement files from payment providers, you need to make sure that the journal +that is used for these statements have the same default debit account as their +default credit account, and this account is configured for reconciliation.

+
+
+

Usage

+

After installing this module, any statement where the sum of all debit and +credit lines match, and where the default journal account is reconcilable, will +reconcile all posted moved when the statement is confirmed.

+
+
+

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

+
    +
  • Opener B.V.
  • +
  • Vanmoof BV
  • +
+
+
+

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.

+

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.

+
+
+
+ + From 302e0d895545561f697b3f75a4475e532ecb2620 Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Wed, 17 Feb 2021 16:55:26 +0100 Subject: [PATCH 07/24] [MIG] account_bank_statement_import_adyen Migrate to 13.0 --- .../readme/CREDITS.rst | 0 .../readme/HISTORY.rst | 0 .../readme/INSTALL.rst | 0 .../readme/ROADMAP.rst | 0 .../tests/__init__.py | 1 + .../tests/test_clearing_account.py | 120 +++++ .../README.rst | 110 ++++- .../__manifest__.py | 13 +- .../models/account_bank_statement_import.py | 119 +++-- .../models/account_journal.py | 10 +- .../readme/CONFIGURE.rst | 6 +- .../readme/CONTRIBUTORS.rst | 1 + .../readme/CREDITS.rst | 0 .../readme/DESCRIPTION.rst | 8 +- .../readme/HISTORY.rst | 0 .../readme/INSTALL.rst | 0 .../readme/ROADMAP.rst | 0 .../static/description/index.html | 442 ++++++++++++++++++ .../tests/test_import_adyen.py | 92 +--- 19 files changed, 747 insertions(+), 175 deletions(-) delete mode 100644 account_bank_statement_clearing_account/readme/CREDITS.rst delete mode 100644 account_bank_statement_clearing_account/readme/HISTORY.rst delete mode 100644 account_bank_statement_clearing_account/readme/INSTALL.rst delete mode 100644 account_bank_statement_clearing_account/readme/ROADMAP.rst create mode 100644 account_bank_statement_clearing_account/tests/__init__.py create mode 100644 account_bank_statement_clearing_account/tests/test_clearing_account.py delete mode 100644 account_bank_statement_import_adyen/readme/CREDITS.rst delete mode 100644 account_bank_statement_import_adyen/readme/HISTORY.rst delete mode 100644 account_bank_statement_import_adyen/readme/INSTALL.rst delete mode 100644 account_bank_statement_import_adyen/readme/ROADMAP.rst create mode 100644 account_bank_statement_import_adyen/static/description/index.html diff --git a/account_bank_statement_clearing_account/readme/CREDITS.rst b/account_bank_statement_clearing_account/readme/CREDITS.rst deleted file mode 100644 index e69de29b..00000000 diff --git a/account_bank_statement_clearing_account/readme/HISTORY.rst b/account_bank_statement_clearing_account/readme/HISTORY.rst deleted file mode 100644 index e69de29b..00000000 diff --git a/account_bank_statement_clearing_account/readme/INSTALL.rst b/account_bank_statement_clearing_account/readme/INSTALL.rst deleted file mode 100644 index e69de29b..00000000 diff --git a/account_bank_statement_clearing_account/readme/ROADMAP.rst b/account_bank_statement_clearing_account/readme/ROADMAP.rst deleted file mode 100644 index e69de29b..00000000 diff --git a/account_bank_statement_clearing_account/tests/__init__.py b/account_bank_statement_clearing_account/tests/__init__.py new file mode 100644 index 00000000..3e17ae02 --- /dev/null +++ b/account_bank_statement_clearing_account/tests/__init__.py @@ -0,0 +1 @@ +from . import test_clearing_account diff --git a/account_bank_statement_clearing_account/tests/test_clearing_account.py b/account_bank_statement_clearing_account/tests/test_clearing_account.py new file mode 100644 index 00000000..e698da0a --- /dev/null +++ b/account_bank_statement_clearing_account/tests/test_clearing_account.py @@ -0,0 +1,120 @@ +# Copyright 2017 Opener BV +# Copyright 2020 Vanmoof BV +# Copyright 2015-2021 Therp BV ) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo.tests.common import SavepointCase + + +class TestClearingAccount(SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.journal = cls.env["account.journal"].create( + { + "company_id": cls.env.user.company_id.id, + "name": "Clearing account test", + "code": "CAT", + "type": "bank", + "currency_id": cls.env.ref("base.USD").id, + } + ) + cls.partner_customer = cls.env["res.partner"].create({"name": "cutomer"}) + cls.partner_provider = cls.env["res.partner"].create({"name": "provider"}) + # Enable reconcilation on the default journal account to trigger + # the functionality from account_bank_statement_clearing_account + cls.journal.default_debit_account_id.reconcile = True + + def test_reconcile_unreconcile(self): + """Test that a bank statement that satiesfies the conditions, cab be + automatically reconciled and unreconciled on confirmation or reset of the + statement. + """ + account = self.journal.default_debit_account_id + statement = self.env["account.bank.statement"].create( + { + "name": "Test autoreconcile 2021-03-08", + "reference": "AUTO-2021-08-03", + "date": "2021-03-08", + "state": "open", + "journal_id": self.journal.id, + "line_ids": [ + ( + 0, + 0, + { + "name": "web sale", + "partner_id": self.partner_customer.id, + "amount": 100.00, + "account_id": account.id, + }, + ), + ( + 0, + 0, + { + "name": "transaction_fees", + "partner_id": False, + "amount": -1.25, + "account_id": account.id, + }, + ), + ( + 0, + 0, + { + "name": "due_from_provider", + "partner_id": self.partner_provider.id, + "amount": -98.75, + "account_id": account.id, + }, + ), + ], + } + ) + self.assertEqual(statement.journal_id, self.journal) + self.assertEqual(len(statement.line_ids), 3) + self.assertTrue( + self.env.user.company_id.currency_id.is_zero( + sum(line.amount for line in statement.line_ids) + ) + ) + account = self.env["account.account"].search( + [("internal_type", "=", "receivable")], limit=1 + ) + for line in statement.line_ids: + line.process_reconciliation( + new_aml_dicts=[ + { + "debit": -line.amount if line.amount < 0 else 0, + "credit": line.amount if line.amount > 0 else 0, + "account_id": account.id, + } + ] + ) + statement.button_confirm_bank() + self.assertEqual(statement.state, "confirm") + lines = self.env["account.move.line"].search( + [ + ("account_id", "=", self.journal.default_debit_account_id.id), + ("statement_id", "=", statement.id), + ] + ) + reconcile = lines.mapped("full_reconcile_id") + self.assertEqual(len(reconcile), 1) + self.assertEqual(lines, reconcile.reconciled_line_ids) + + # Reset the bank statement to see the counterpart lines being + # unreconciled + statement.button_reopen() + self.assertEqual(statement.state, "open") + self.assertFalse(lines.mapped("matched_debit_ids")) + self.assertFalse(lines.mapped("matched_credit_ids")) + self.assertFalse(lines.mapped("full_reconcile_id")) + + # Confirm the statement without the correct clearing account settings + self.journal.default_debit_account_id.reconcile = False + statement.button_confirm_bank() + self.assertEqual(statement.state, "confirm") + self.assertFalse(lines.mapped("matched_debit_ids")) + self.assertFalse(lines.mapped("matched_credit_ids")) + self.assertFalse(lines.mapped("full_reconcile_id")) diff --git a/account_bank_statement_import_adyen/README.rst b/account_bank_statement_import_adyen/README.rst index 38929e87..5a8d54c1 100644 --- a/account_bank_statement_import_adyen/README.rst +++ b/account_bank_statement_import_adyen/README.rst @@ -1,35 +1,97 @@ -**This file is going to be generated by oca-gen-addon-readme.** +====================== +Adyen statement import +====================== -*Manual changes will be overwritten.* +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -Please provide content in the ``readme`` directory: +.. |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/13.0/account_bank_statement_import_adyen + :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-13-0/bank-statement-import-13-0-account_bank_statement_import_adyen + :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/13.0 + :alt: Try me on Runbot -* **DESCRIPTION.rst** (required) -* INSTALL.rst (optional) -* CONFIGURE.rst (optional) -* **USAGE.rst** (optional, highly recommended) -* DEVELOP.rst (optional) -* ROADMAP.rst (optional) -* HISTORY.rst (optional, recommended) -* **CONTRIBUTORS.rst** (optional, highly recommended) -* CREDITS.rst (optional) +|badge1| |badge2| |badge3| |badge4| |badge5| -Content of this README will also be drawn from the addon manifest, -from keys such as name, authors, maintainers, development_status, -and license. +This module processes Adyen transaction statements, the settlement details report, +in excel or csv format. -A good, one sentence summary in the manifest is also highly recommended. +You can import the statements in a dedicated journal. Reconcile your sale invoices +with the credit transations. Reconcile the aggregated counterpart +transaction with the transaction in your real bank journal and register the +aggregated fee line containing commision and markup on the applicable +cost account. +**Table of contents** -Automatic changelog generation -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. contents:: + :local: -`HISTORY.rst` can be auto generated using `towncrier `_. +Configuration +============= -Just put towncrier compatible changelog fragments into `readme/newsfragments` -and the changelog file will be automatically generated and updated when a new fragment is added. +Configure a pseudo bank journal by creating a new journal linked to your Adyen +merchant account. Set your merchant account string in the Advanced settings +on the journal form. -Please refer to `towncrier` documentation to know more. +Usage +===== -NOTE: the changelog will be automatically generated when using `/ocabot merge $option`. -If you need to run it manually, refer to `OCA/maintainer-tools README `_. +After installing this module, you can import your Adyen transaction statements +through Menu Finance -> Bank -> Import. Don't enter a journal in the import +wizard. + +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 +~~~~~~~ + +* Opener BV +* Vanmoof BV + +Contributors +~~~~~~~~~~~~ + +* Stefan Rijnhart (https://opener.amsterdam) +* Martin Pishpecki (https://www.vanmoof.com) +* Ronald Portier (https://therp.nl) + +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. + +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_adyen/__manifest__.py b/account_bank_statement_import_adyen/__manifest__.py index de271032..bc24a336 100644 --- a/account_bank_statement_import_adyen/__manifest__.py +++ b/account_bank_statement_import_adyen/__manifest__.py @@ -1,18 +1,19 @@ -# © 2017 Opener BV () -# © 2020 Vanmoof BV () -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# Copyright 2017 Opener BV +# Copyright 2020 Vanmoof BV +# Copyright 2021 Therp BV . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). { "name": "Adyen statement import", - "version": "12.0.1.0.0", + "version": "13.0.1.0.0", "author": "Opener BV, Vanmoof BV, Odoo Community Association (OCA)", "category": "Banking addons", "website": "https://github.com/oca/bank-statement-import", "license": "AGPL-3", "depends": [ + "base_import", "account_bank_statement_import", "account_bank_statement_clearing_account", ], - "external_dependencies": {"python": ["openpyxl",],}, - "data": ["views/account_journal.xml",], + "data": ["views/account_journal.xml"], "installable": True, } diff --git a/account_bank_statement_import_adyen/models/account_bank_statement_import.py b/account_bank_statement_import_adyen/models/account_bank_statement_import.py index 45e062de..0ba9b7ba 100644 --- a/account_bank_statement_import_adyen/models/account_bank_statement_import.py +++ b/account_bank_statement_import_adyen/models/account_bank_statement_import.py @@ -1,13 +1,12 @@ -# © 2017 Opener BV () -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from io import BytesIO -from zipfile import BadZipfile +# Copyright 2017 Opener BV () +# Copyright 2021 Therp BV . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +import logging -from openpyxl import load_workbook - -from odoo import api, fields, models +from odoo import _, api, fields, models from odoo.exceptions import UserError -from odoo.tools.translate import _ + +_logger = logging.getLogger(__name__) class AccountBankStatementImport(models.TransientModel): @@ -15,15 +14,14 @@ class AccountBankStatementImport(models.TransientModel): @api.model def _parse_file(self, data_file): - """Parse an Adyen xlsx file and map merchant account strings - to journals. """ + """Parse an Adyen xlsx file and map merchant account strings to journals.""" try: return self.import_adyen_xlsx(data_file) except ValueError: - return super(AccountBankStatementImport, self)._parse_file(data_file) + return super()._parse_file(data_file) def _find_additional_data(self, currency_code, account_number): - """ Try to find journal by Adyen merchant account """ + """Try to find journal by Adyen merchant account.""" if account_number: journal = self.env["account.journal"].search( [("adyen_merchant_account", "=", account_number)], limit=1 @@ -39,14 +37,12 @@ class AccountBankStatementImport(models.TransientModel): % account_number ) self = self.with_context(journal_id=journal.id) - return super(AccountBankStatementImport, self)._find_additional_data( - currency_code, account_number - ) + return super()._find_additional_data(currency_code, account_number) @api.model def balance(self, row): - return -(row[15] or 0) + sum( - row[i] if row[i] else 0.0 for i in (16, 17, 18, 19, 20) + return -(float(row[15]) if row[15] else 0.0) + sum( + float(row[i]) if row[i] else 0.0 for i in (16, 17, 18, 19, 20) ) @api.model @@ -62,7 +58,7 @@ class AccountBankStatementImport(models.TransientModel): statement["transactions"].append(transaction) @api.model - def import_adyen_xlsx(self, data_file): + def parse_adyen_file(self, data_file): statements = [] statement = None headers = False @@ -71,52 +67,49 @@ class AccountBankStatementImport(models.TransientModel): payout = 0.0 statement_id = None - with BytesIO() as buf: - buf.write(data_file) - try: - sheet = load_workbook(buf)._sheets[0] - except BadZipfile as e: - raise ValueError(e) - for row in sheet.rows: - row = [cell.value for cell in row] - if len(row) != 31: - raise ValueError( - "Not an Adyen statement. Unexpected row length %s " - "instead of 31" % len(row) - ) - if not row[1]: - continue - if not headers: - if row[1] != "Company Account": - raise ValueError( - 'Not an Adyen statement. Unexpected header "%s" ' - 'instead of "Company Account"', - row[1], - ) - headers = True - continue - if not statement: - statement = {"transactions": []} - statements.append(statement) - statement_id = "{} {}/{}".format( - row[2], - row[6].strftime("%Y"), - int(row[23]), - ) - currency_code = row[14] - merchant_id = row[2] - statement["name"] = "{} {}/{}".format(row[2], row[6].year, row[23]) - date = fields.Date.from_string(row[6]) - if not statement.get("date") or statement.get("date") > date: - statement["date"] = date + import_model = self.env["base_import.import"] + importer = import_model.create( + {"file": data_file, "file_name": "Ayden settlemnt details"} + ) + rows = importer._read_file({}) - row[8] = row[8].strip() - if row[8] == "MerchantPayout": - payout -= self.balance(row) - else: - balance += self.balance(row) - self.import_adyen_transaction(statement, statement_id, row) - fees += sum(row[i] if row[i] else 0.0 for i in (17, 18, 19, 20)) + for row in rows: + if len(row) != 31: + raise ValueError( + "Not an Adyen statement. Unexpected row length %s " + "instead of 31" % len(row) + ) + if not row[1]: + continue + if not headers: + if row[1] != "Company Account": + raise ValueError( + 'Not an Adyen statement. Unexpected header "%s" ' + 'instead of "Company Account"', + row[1], + ) + headers = True + continue + if not statement: + statement = {"transactions": []} + statements.append(statement) + statement_id = "{merchant} {year}/{batch}".format( + merchant=row[2], year=row[6][:4], batch=row[23], + ) + currency_code = row[14] + merchant_id = row[2] + statement["name"] = statement_id + date = fields.Date.from_string(row[6]) + if not statement.get("date") or statement.get("date") > date: + statement["date"] = date + + row[8] = row[8].strip() + if row[8] == "MerchantPayout": + payout -= self.balance(row) + else: + balance += self.balance(row) + self.import_adyen_transaction(statement, statement_id, row) + fees += sum(float(row[i]) if row[i] else 0.0 for i in (17, 18, 19, 20)) if not headers: raise ValueError("Not an Adyen statement. Did not encounter header row.") diff --git a/account_bank_statement_import_adyen/models/account_journal.py b/account_bank_statement_import_adyen/models/account_journal.py index 32c213dd..605fc528 100644 --- a/account_bank_statement_import_adyen/models/account_journal.py +++ b/account_bank_statement_import_adyen/models/account_journal.py @@ -1,10 +1,10 @@ -# © 2017 Opener BV () -# © 2020 Vanmoof BV () -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# Copyright 2017 Opener BV () +# Copyright 2020 Vanmoof BV () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from odoo import fields, models -class Journal(models.Model): +class AccountJournal(models.Model): _inherit = "account.journal" adyen_merchant_account = fields.Char( @@ -15,6 +15,6 @@ class Journal(models.Model): ) def _get_bank_statements_available_import_formats(self): - res = super(Journal, self)._get_bank_statements_available_import_formats() + res = super()._get_bank_statements_available_import_formats() res.append("adyen") return res diff --git a/account_bank_statement_import_adyen/readme/CONFIGURE.rst b/account_bank_statement_import_adyen/readme/CONFIGURE.rst index 7df8a95e..0f7870bd 100644 --- a/account_bank_statement_import_adyen/readme/CONFIGURE.rst +++ b/account_bank_statement_import_adyen/readme/CONFIGURE.rst @@ -1,3 +1,3 @@ -Configure a pseudo bank journal by creating a new journal with a dedicated -Adyen clearing account as the default ledger account. Set your merchant -account string in the Advanced settings on the journal form. +Configure a pseudo bank journal by creating a new journal linked to your Adyen +merchant account. Set your merchant account string in the Advanced settings +on the journal form. diff --git a/account_bank_statement_import_adyen/readme/CONTRIBUTORS.rst b/account_bank_statement_import_adyen/readme/CONTRIBUTORS.rst index 58e9f494..f00439e5 100644 --- a/account_bank_statement_import_adyen/readme/CONTRIBUTORS.rst +++ b/account_bank_statement_import_adyen/readme/CONTRIBUTORS.rst @@ -1,2 +1,3 @@ * Stefan Rijnhart (https://opener.amsterdam) * Martin Pishpecki (https://www.vanmoof.com) +* Ronald Portier (https://therp.nl) diff --git a/account_bank_statement_import_adyen/readme/CREDITS.rst b/account_bank_statement_import_adyen/readme/CREDITS.rst deleted file mode 100644 index e69de29b..00000000 diff --git a/account_bank_statement_import_adyen/readme/DESCRIPTION.rst b/account_bank_statement_import_adyen/readme/DESCRIPTION.rst index c0832e16..c07218ef 100644 --- a/account_bank_statement_import_adyen/readme/DESCRIPTION.rst +++ b/account_bank_statement_import_adyen/readme/DESCRIPTION.rst @@ -1,9 +1,7 @@ -====================== -Adyen statement import -====================== +This module processes Adyen transaction statements, the settlement details report, +in excel or csv format. -This module processes Adyen transaction statements in xlsx format. You can -import the statements in a dedicated journal. Reconcile your sale invoices +You can import the statements in a dedicated journal. Reconcile your sale invoices with the credit transations. Reconcile the aggregated counterpart transaction with the transaction in your real bank journal and register the aggregated fee line containing commision and markup on the applicable diff --git a/account_bank_statement_import_adyen/readme/HISTORY.rst b/account_bank_statement_import_adyen/readme/HISTORY.rst deleted file mode 100644 index e69de29b..00000000 diff --git a/account_bank_statement_import_adyen/readme/INSTALL.rst b/account_bank_statement_import_adyen/readme/INSTALL.rst deleted file mode 100644 index e69de29b..00000000 diff --git a/account_bank_statement_import_adyen/readme/ROADMAP.rst b/account_bank_statement_import_adyen/readme/ROADMAP.rst deleted file mode 100644 index e69de29b..00000000 diff --git a/account_bank_statement_import_adyen/static/description/index.html b/account_bank_statement_import_adyen/static/description/index.html new file mode 100644 index 00000000..7d826df9 --- /dev/null +++ b/account_bank_statement_import_adyen/static/description/index.html @@ -0,0 +1,442 @@ + + + + + + +Adyen statement import + + + +
+

Adyen statement import

+ + +

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

+

This module processes Adyen transaction statements, the settlement details report, +in excel or csv format.

+

You can import the statements in a dedicated journal. Reconcile your sale invoices +with the credit transations. Reconcile the aggregated counterpart +transaction with the transaction in your real bank journal and register the +aggregated fee line containing commision and markup on the applicable +cost account.

+

Table of contents

+ +
+

Configuration

+

Configure a pseudo bank journal by creating a new journal linked to your Adyen +merchant account. Set your merchant account string in the Advanced settings +on the journal form.

+
+
+

Usage

+

After installing this module, you can import your Adyen transaction statements +through Menu Finance -> Bank -> Import. Don’t enter a journal in the import +wizard.

+
+
+

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

+
    +
  • Opener BV
  • +
  • Vanmoof BV
  • +
+
+ +
+

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.

+

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_adyen/tests/test_import_adyen.py b/account_bank_statement_import_adyen/tests/test_import_adyen.py index 10e61e92..1fc5418f 100644 --- a/account_bank_statement_import_adyen/tests/test_import_adyen.py +++ b/account_bank_statement_import_adyen/tests/test_import_adyen.py @@ -1,7 +1,7 @@ -# © 2017 Opener BV () -# © 2020 Vanmoof BV () -# © 2015 Therp BV () -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# Copyright 2017 Opener BV +# Copyright 2020 Vanmoof BV +# Copyright 2015-2021 Therp BV ) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). import base64 from odoo.exceptions import UserError @@ -12,7 +12,7 @@ from odoo.tests.common import SavepointCase class TestImportAdyen(SavepointCase): @classmethod def setUpClass(cls): - super(TestImportAdyen, cls).setUpClass() + super().setUpClass() cls.journal = cls.env["account.journal"].create( { "company_id": cls.env.user.company_id.id, @@ -20,7 +20,6 @@ class TestImportAdyen(SavepointCase): "code": "ADY", "type": "bank", "adyen_merchant_account": "YOURCOMPANY_ACCOUNT", - "update_posted": True, "currency_id": cls.env.ref("base.USD").id, } ) @@ -33,9 +32,7 @@ class TestImportAdyen(SavepointCase): lines on the default journal (clearing) account are fully reconciled with each other """ self._test_statement_import( - "account_bank_statement_import_adyen", - "adyen_test.xlsx", - "YOURCOMPANY_ACCOUNT 2016/48", + "adyen_test.xlsx", "YOURCOMPANY_ACCOUNT 2016/48", ) statement = self.env["account.bank.statement"].search( [], order="create_date desc", limit=1 @@ -48,76 +45,33 @@ class TestImportAdyen(SavepointCase): ) ) - account = self.env["account.account"].search( - [("internal_type", "=", "receivable")], limit=1 - ) - for line in statement.line_ids: - line.process_reconciliation( - new_aml_dicts=[ - { - "debit": -line.amount if line.amount < 0 else 0, - "credit": line.amount if line.amount > 0 else 0, - "account_id": account.id, - } - ] - ) - - statement.button_confirm_bank() - self.assertEqual(statement.state, "confirm") - lines = self.env["account.move.line"].search( - [ - ("account_id", "=", self.journal.default_debit_account_id.id), - ("statement_id", "=", statement.id), - ] - ) - reconcile = lines.mapped("full_reconcile_id") - self.assertEqual(len(reconcile), 1) - self.assertEqual(lines, reconcile.reconciled_line_ids) - - # Reset the bank statement to see the counterpart lines being - # unreconciled - statement.button_draft() - self.assertEqual(statement.state, "open") - self.assertFalse(lines.mapped("matched_debit_ids")) - self.assertFalse(lines.mapped("matched_credit_ids")) - self.assertFalse(lines.mapped("full_reconcile_id")) - - # Confirm the statement without the correct clearing account settings - self.journal.default_debit_account_id.reconcile = False - statement.button_confirm_bank() - self.assertEqual(statement.state, "confirm") - self.assertFalse(lines.mapped("matched_debit_ids")) - self.assertFalse(lines.mapped("matched_credit_ids")) - self.assertFalse(lines.mapped("full_reconcile_id")) - def test_02_import_adyen_credit_fees(self): """ Import an Adyen statement with credit fees """ self._test_statement_import( - "account_bank_statement_import_adyen", - "adyen_test_credit_fees.xlsx", - "YOURCOMPANY_ACCOUNT 2016/8", + "adyen_test_credit_fees.xlsx", "YOURCOMPANY_ACCOUNT 2016/8", ) def test_03_import_adyen_invalid(self): """ Trying to hit that coverall target """ with self.assertRaisesRegex(UserError, "Could not make sense"): self._test_statement_import( - "account_bank_statement_import_adyen", - "adyen_test_invalid.xls", - "invalid", + "adyen_test_invalid.xls", "invalid", ) - def _test_statement_import(self, module_name, file_name, statement_name): + def _test_statement_import(self, file_name, statement_name): """Test correct creation of single statement.""" - statement_path = get_module_resource(module_name, "test_files", file_name) - statement_file = open(statement_path, "rb").read() - import_wizard = self.env["account.bank.statement.import"].create( - {"data_file": base64.b64encode(statement_file), "filename": file_name} + testfile = get_module_resource( + "account_bank_statement_import_adyen", "test_files", file_name ) - import_wizard.import_file() - # statement name is account number + '-' + date of last line: - statements = self.env["account.bank.statement"].search( - [("name", "=", statement_name)] - ) - self.assertTrue(statements) - return statements + with open(testfile, "rb") as datafile: + data_file = base64.b64encode(datafile.read()) + import_wizard = self.env["account.bank.statement.import"].create( + {"attachment_ids": [(0, 0, {"name": "test file", "datas": data_file})]} + ) + import_wizard.import_file() + # statement name is account number + '-' + date of last line: + statements = self.env["account.bank.statement"].search( + [("name", "=", statement_name)] + ) + self.assertTrue(statements) + return statements From f637f44ebafeba32407d8d525400ba4ebce0d9af Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Wed, 17 Feb 2021 18:08:42 +0100 Subject: [PATCH 08/24] [ADD] account_bank_statement_import_online_adyen --- .../models/account_bank_statement_import.py | 29 +- .../tests/test_import_adyen.py | 7 +- .../README.rst | 118 +++++ .../__init__.py | 3 + .../__manifest__.py | 16 + .../models/__init__.py | 4 + .../models/account_journal.py | 30 ++ .../models/online_bank_statement_provider.py | 100 ++++ .../readme/CONFIGURE.rst | 27 + .../readme/CONTRIBUTORS.rst | 1 + .../readme/DESCRIPTION.rst | 1 + .../readme/USAGE.rst | 10 + .../static/description/icon.png | Bin 0 -> 11070 bytes .../static/description/index.html | 464 ++++++++++++++++++ .../tests/__init__.py | 2 + .../online_bank_statement_provider_dummy.py | 23 + .../tests/test_import_online.py | 66 +++ .../views/online_bank_statement_provider.xml | 28 ++ 18 files changed, 917 insertions(+), 12 deletions(-) create mode 100644 account_bank_statement_import_online_adyen/README.rst create mode 100644 account_bank_statement_import_online_adyen/__init__.py create mode 100644 account_bank_statement_import_online_adyen/__manifest__.py create mode 100644 account_bank_statement_import_online_adyen/models/__init__.py create mode 100644 account_bank_statement_import_online_adyen/models/account_journal.py create mode 100644 account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py create mode 100644 account_bank_statement_import_online_adyen/readme/CONFIGURE.rst create mode 100644 account_bank_statement_import_online_adyen/readme/CONTRIBUTORS.rst create mode 100644 account_bank_statement_import_online_adyen/readme/DESCRIPTION.rst create mode 100644 account_bank_statement_import_online_adyen/readme/USAGE.rst create mode 100644 account_bank_statement_import_online_adyen/static/description/icon.png create mode 100644 account_bank_statement_import_online_adyen/static/description/index.html create mode 100644 account_bank_statement_import_online_adyen/tests/__init__.py create mode 100644 account_bank_statement_import_online_adyen/tests/online_bank_statement_provider_dummy.py create mode 100644 account_bank_statement_import_online_adyen/tests/test_import_online.py create mode 100644 account_bank_statement_import_online_adyen/views/online_bank_statement_provider.xml diff --git a/account_bank_statement_import_adyen/models/account_bank_statement_import.py b/account_bank_statement_import_adyen/models/account_bank_statement_import.py index 0ba9b7ba..4df68c57 100644 --- a/account_bank_statement_import_adyen/models/account_bank_statement_import.py +++ b/account_bank_statement_import_adyen/models/account_bank_statement_import.py @@ -16,8 +16,18 @@ class AccountBankStatementImport(models.TransientModel): def _parse_file(self, data_file): """Parse an Adyen xlsx file and map merchant account strings to journals.""" try: - return self.import_adyen_xlsx(data_file) + try: + return self._parse_adyen_file(data_file) + except Exception as exc: + if self.env.context.get("account_bank_statement_import_adyen", False): + raise + _logger.info("Adyen parser error", exc_info=True) + raise ValueError("Not an adyen settlements file: %s" % exc) except ValueError: + _logger.debug( + _("Statement file was not a Adyen settlement details file."), + exc_info=True, + ) return super()._parse_file(data_file) def _find_additional_data(self, currency_code, account_number): @@ -40,25 +50,25 @@ class AccountBankStatementImport(models.TransientModel): return super()._find_additional_data(currency_code, account_number) @api.model - def balance(self, row): + def _balance(self, row): return -(float(row[15]) if row[15] else 0.0) + sum( float(row[i]) if row[i] else 0.0 for i in (16, 17, 18, 19, 20) ) @api.model - def import_adyen_transaction(self, statement, statement_id, row): + def _import_adyen_transaction(self, statement, statement_id, row): transaction_id = str(len(statement["transactions"])).zfill(4) transaction = dict( unique_import_id=statement_id + transaction_id, date=fields.Date.from_string(row[6]), - amount=self.balance(row), + amount=self._balance(row), note="{} {} {} {}".format(row[2], row[3], row[4], row[21]), name="%s" % (row[3] or row[4] or row[9]), ) statement["transactions"].append(transaction) @api.model - def parse_adyen_file(self, data_file): + def _parse_adyen_file(self, data_file): statements = [] statement = None headers = False @@ -70,6 +80,9 @@ class AccountBankStatementImport(models.TransientModel): import_model = self.env["base_import.import"] importer = import_model.create( {"file": data_file, "file_name": "Ayden settlemnt details"} + import_model = self.env["base_import.import"] + importer = import_model.create( + {"file": data_file, "file_name": "Ayden settlement details"} ) rows = importer._read_file({}) @@ -105,10 +118,10 @@ class AccountBankStatementImport(models.TransientModel): row[8] = row[8].strip() if row[8] == "MerchantPayout": - payout -= self.balance(row) + payout -= self._balance(row) else: - balance += self.balance(row) - self.import_adyen_transaction(statement, statement_id, row) + balance += self._balance(row) + self._import_adyen_transaction(statement, statement_id, row) fees += sum(float(row[i]) if row[i] else 0.0 for i in (17, 18, 19, 20)) if not headers: diff --git a/account_bank_statement_import_adyen/tests/test_import_adyen.py b/account_bank_statement_import_adyen/tests/test_import_adyen.py index 1fc5418f..ca75591a 100644 --- a/account_bank_statement_import_adyen/tests/test_import_adyen.py +++ b/account_bank_statement_import_adyen/tests/test_import_adyen.py @@ -23,9 +23,6 @@ class TestImportAdyen(SavepointCase): "currency_id": cls.env.ref("base.USD").id, } ) - # Enable reconcilation on the default journal account to trigger - # the functionality from account_bank_statement_clearing_account - cls.journal.default_debit_account_id.reconcile = True def test_01_import_adyen(self): """ Test that the Adyen statement can be imported and that the @@ -68,7 +65,9 @@ class TestImportAdyen(SavepointCase): import_wizard = self.env["account.bank.statement.import"].create( {"attachment_ids": [(0, 0, {"name": "test file", "datas": data_file})]} ) - import_wizard.import_file() + import_wizard.with_context( + {"account_bank_statement_import_adyen": True} + ).import_file() # statement name is account number + '-' + date of last line: statements = self.env["account.bank.statement"].search( [("name", "=", statement_name)] diff --git a/account_bank_statement_import_online_adyen/README.rst b/account_bank_statement_import_online_adyen/README.rst new file mode 100644 index 00000000..32b36ef1 --- /dev/null +++ b/account_bank_statement_import_online_adyen/README.rst @@ -0,0 +1,118 @@ +============================================ +Online Bank Statements: Adyen payment report +============================================ + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! 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/13.0/account_bank_statement_import_online_adyen + :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-13-0/bank-statement-import-13-0-account_bank_statement_import_online_adyen + :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/13.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module automates the download and import of Adyen payment reports. + +**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 *Adyen* 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 *Adyen* as online bank statements provider in + *Online Bank Statements (OCA)* section +#. Save the bank account +#. Click on provider and configure provider-specific settings. + +To obtain *Login* and *Key*: + +#. Open `Adyen website `_. + +Check also ``account_bank_statement_import_online`` configuration instructions +for more information. + +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* + +If historical data is not needed, then just simply wait for the scheduled +activity "Pull Online Bank Statements" to be executed for getting new +transactions. + +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 +~~~~~~~ + +* Ronald Portier (Therp BV) + +Contributors +~~~~~~~~~~~~ + +* Ronald Portier - Therp BV + +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. + +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_adyen/__init__.py b/account_bank_statement_import_online_adyen/__init__.py new file mode 100644 index 00000000..ad8c8648 --- /dev/null +++ b/account_bank_statement_import_online_adyen/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from . import models +from .tests import online_bank_statement_provider_dummy diff --git a/account_bank_statement_import_online_adyen/__manifest__.py b/account_bank_statement_import_online_adyen/__manifest__.py new file mode 100644 index 00000000..52ae345b --- /dev/null +++ b/account_bank_statement_import_online_adyen/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2021 - Therp BV . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Online Bank Statements: Adyen payment report", + "version": "13.0.1.0.0", + "category": "Account", + "website": "https://github.com/OCA/bank-statement-import", + "author": "Ronald Portier (Therp BV), Odoo Community Association (OCA)", + "license": "AGPL-3", + "installable": True, + "depends": [ + "account_bank_statement_import_adyen", + "account_bank_statement_import_online", + ], + "data": ["views/online_bank_statement_provider.xml"], +} diff --git a/account_bank_statement_import_online_adyen/models/__init__.py b/account_bank_statement_import_online_adyen/models/__init__.py new file mode 100644 index 00000000..56bd827c --- /dev/null +++ b/account_bank_statement_import_online_adyen/models/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import account_journal +from . import online_bank_statement_provider diff --git a/account_bank_statement_import_online_adyen/models/account_journal.py b/account_bank_statement_import_online_adyen/models/account_journal.py new file mode 100644 index 00000000..fb7572a3 --- /dev/null +++ b/account_bank_statement_import_online_adyen/models/account_journal.py @@ -0,0 +1,30 @@ +# Copyright 2021 Therp BV . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import api, models + + +class AccountJournal(models.Model): + _inherit = "account.journal" + + def write(self, vals): + """Do not reset a provider to file_import, if that will delete provider.""" + # TODO: In the future place this in super account_bank_statement_import_online. + for this in self: + is_online = this.bank_statements_source == "online" + if is_online and vals.get("bank_statements_source", "online") != "online": + vals.pop("bank_statements_source") + super(AccountJournal, this).write(vals) + return True + + @api.model + def _selection_online_bank_statement_provider(self): + res = super()._selection_online_bank_statement_provider() + res.append(("dummy_adyen", "Dummy Adyen")) + return res + + @api.model + def values_online_bank_statement_provider(self): + res = super().values_online_bank_statement_provider() + if self.user_has_groups("base.group_no_one"): + res += [("dummy_adyen", "Dummy Adyen")] + return res diff --git a/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py b/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py new file mode 100644 index 00000000..62a9babd --- /dev/null +++ b/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py @@ -0,0 +1,100 @@ +# Copyright 2021 Therp BV . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import logging +from html import escape + +import requests + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) + + +class OnlineBankStatementProvider(models.Model): + _inherit = "online.bank.statement.provider" + + download_file_name = fields.Char() + next_batch_number = fields.Integer() + + @api.model + def _selection_service(self): + res = super()._selection_service() + res.append(("dummy_adyen", "Dummy Adyen")) + return res + + @api.model + def _get_available_services(self): + return super()._get_available_services() + [("adyen", "Adyen payment report")] + + def _pull(self, date_since, date_until): # noqa: C901 + """Split between adyen providers and others.""" + adyen_providers = self.filtered(lambda r: r.service in ("adyen", "dummy_adyen")) + other_providers = self.filtered( + lambda r: r.service not in ("adyen", "dummy_adyen") + ) + if other_providers: + super(OnlineBankStatementProvider, other_providers)._pull( + date_since, date_until + ) + for provider in adyen_providers: + # TODO: incrementing batch number + is_scheduled = self.env.context.get("scheduled") + try: + data_file = self._adyen_get_settlement_details_file() + import_wizard = self.env["account.bank.statement.import"].create( + { + "attachment_ids": [ + (0, 0, {"name": "test file", "datas": data_file}) + ] + } + ) + import_wizard.with_context( + {"account_bank_statement_import_adyen": True} + ).import_file() + except BaseException as e: + if is_scheduled: + _logger.warning( + 'Online Bank Statement Provider "%s" failed to' + " obtain statement data" % (provider.name,), + exc_info=True, + ) + provider.message_post( + body=_( + "Failed to obtain statement data for period " + ": %s. See server logs for more details." + ) + % (escape(str(e)) or _("N/A"),), + subject=_("Issue with Online Bank Statement Provider"), + ) + break + raise + if is_scheduled: + provider._schedule_next_run() + + def _adyen_get_settlement_details_file(self): + """Retrieve daily generated settlement details file. + + The file could be retrieved with wget using: + $ wget \ + --http-user='[YourReportUser]@Company.[YourCompanyAccount]' \ + --http-password='[YourReportUserPassword]' \ + --quiet --no-check-certificate \ + https://ca-test.adyen.com/reports/download/MerchantAccount/ + + [YourMerchantAccount]/[ReportFileName]" + """ + batch_number = self.next_batch_number + download_file_name = self.download_file_name % batch_number + URL = "/".join( + [self.api_base, self.journal_id.adyen_merchant_account, download_file_name] + ) + response = requests.get(URL, auth=(self.username, self.password)) + if response.status_code == 200: + return response.content + else: + raise UserError(_("%s \n\n %s") % (response.status_code, response.text)) + + def _schedule_next_run(self): + """Set next run date and autoincrement batch number.""" + super()._schedule_next_run() + self.next_batch_number += 1 diff --git a/account_bank_statement_import_online_adyen/readme/CONFIGURE.rst b/account_bank_statement_import_online_adyen/readme/CONFIGURE.rst new file mode 100644 index 00000000..da06dd2c --- /dev/null +++ b/account_bank_statement_import_online_adyen/readme/CONFIGURE.rst @@ -0,0 +1,27 @@ +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 *Adyen* 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 *Adyen* as online bank statements provider in + *Online Bank Statements (OCA)* section +#. Save the bank account +#. Click on provider and configure provider-specific settings. + +To obtain *Login* and *Key*: + +#. Open `Adyen website `_. + +Check also ``account_bank_statement_import_online`` configuration instructions +for more information. diff --git a/account_bank_statement_import_online_adyen/readme/CONTRIBUTORS.rst b/account_bank_statement_import_online_adyen/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..6ee4d1d6 --- /dev/null +++ b/account_bank_statement_import_online_adyen/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Ronald Portier - Therp BV diff --git a/account_bank_statement_import_online_adyen/readme/DESCRIPTION.rst b/account_bank_statement_import_online_adyen/readme/DESCRIPTION.rst new file mode 100644 index 00000000..fc01c0c4 --- /dev/null +++ b/account_bank_statement_import_online_adyen/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module automates the download and import of Adyen payment reports. diff --git a/account_bank_statement_import_online_adyen/readme/USAGE.rst b/account_bank_statement_import_online_adyen/readme/USAGE.rst new file mode 100644 index 00000000..2785a201 --- /dev/null +++ b/account_bank_statement_import_online_adyen/readme/USAGE.rst @@ -0,0 +1,10 @@ +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* + +If historical data is not needed, then just simply wait for the scheduled +activity "Pull Online Bank Statements" to be executed for getting new +transactions. diff --git a/account_bank_statement_import_online_adyen/static/description/icon.png b/account_bank_statement_import_online_adyen/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..09847ed7696bcad441aac277011add9a9a82c01d GIT binary patch literal 11070 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 literal 0 HcmV?d00001 diff --git a/account_bank_statement_import_online_adyen/static/description/index.html b/account_bank_statement_import_online_adyen/static/description/index.html new file mode 100644 index 00000000..beccd682 --- /dev/null +++ b/account_bank_statement_import_online_adyen/static/description/index.html @@ -0,0 +1,464 @@ + + + + + + +Online Bank Statements: Adyen payment report + + + +
+

Online Bank Statements: Adyen payment report

+ + +

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

+

This module automates the download and import of Adyen payment reports.

+

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 Adyen 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 Adyen 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. +
+

To obtain Login and Key:

+
    +
  1. Open Adyen website.
  2. +
+

Check also account_bank_statement_import_online configuration instructions +for more information.

+
+
+

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. +
+

If historical data is not needed, then just simply wait for the scheduled +activity “Pull Online Bank Statements” to be executed for getting new +transactions.

+
+
+

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

+
    +
  • Ronald Portier (Therp BV)
  • +
+
+
+

Contributors

+
    +
  • Ronald Portier - Therp BV
  • +
+
+
+

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.

+

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_adyen/tests/__init__.py b/account_bank_statement_import_online_adyen/tests/__init__.py new file mode 100644 index 00000000..e889bae2 --- /dev/null +++ b/account_bank_statement_import_online_adyen/tests/__init__.py @@ -0,0 +1,2 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from . import test_import_online diff --git a/account_bank_statement_import_online_adyen/tests/online_bank_statement_provider_dummy.py b/account_bank_statement_import_online_adyen/tests/online_bank_statement_provider_dummy.py new file mode 100644 index 00000000..9c42c0aa --- /dev/null +++ b/account_bank_statement_import_online_adyen/tests/online_bank_statement_provider_dummy.py @@ -0,0 +1,23 @@ +# Copyright 2021 Therp BV . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +import base64 + +from odoo import models +from odoo.modules.module import get_module_resource + + +class OnlineBankStatementProviderDummy(models.Model): + _inherit = "online.bank.statement.provider" + + def _adyen_get_settlement_details_file(self): + """Get file from disk, instead of from url.""" + if self.service != "dummy_adyen": + # Not a dummy, get the regular adyen method. + return super()._adyen_get_settlement_details_file() + testfile = get_module_resource( + "account_bank_statement_import_adyen", "test_files", self.download_file_name + ) + with open(testfile, "rb") as datafile: + data_file = datafile.read() + data_file = base64.b64encode(data_file) + return data_file diff --git a/account_bank_statement_import_online_adyen/tests/test_import_online.py b/account_bank_statement_import_online_adyen/tests/test_import_online.py new file mode 100644 index 00000000..71c0d765 --- /dev/null +++ b/account_bank_statement_import_online_adyen/tests/test_import_online.py @@ -0,0 +1,66 @@ +# Copyright 2021 Therp BV . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from dateutil.relativedelta import relativedelta + +from odoo import fields + +from odoo.addons.account_bank_statement_import_adyen.tests.test_import_adyen import ( + TestImportAdyen, +) + + +class TestImportOnline(TestImportAdyen): + """Do the same tests as with the offline adyen import.""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.now = fields.Datetime.now() + cls.journal.write( + { + "bank_statements_source": "online", + "online_bank_statement_provider": "dummy_adyen", + } + ) + + def test_03_import_adyen_invalid(self): + """Override super test: online module test will return without statements.""" + with self.assertRaisesRegex(AssertionError, "account.bank.statement()"): + self._test_statement_import( + "adyen_test_invalid.xls", "invalid", + ) + + def _test_statement_import(self, file_name, statement_name): + """Test correct creation of single statement. + + Getting an adyen statement online should result in: + 1. A valid imported statement; + 2. An incremented batch number; + 3. The current date being set as the date_since in the provider. + """ + provider = self.journal.online_bank_statement_provider_id + provider.write( + { + "api_base": ( + "https://ca-test.adyen.com/reports/download/MerchantAccount" + ), + "download_file_name": file_name, + "interval_type": "days", + "interval_number": 1, + "service": "dummy_adyen", + "next_batch_number": 1, + } + ) + # Pull from yesterday, until today + yesterday = self.now - relativedelta(days=1) + provider.with_context(scheduled=True)._pull(yesterday, self.now) + # statement name is account number + '-' + date of last line: + statements = self.env["account.bank.statement"].search( + [("name", "=", statement_name)] + ) + self.assertTrue(statements) + self.assertEqual(len(statements), 1) + self.assertEqual(provider.next_batch_number, 2) + self.assertEqual(provider.last_successful_run, self.now) + self.assertTrue(provider.next_run > self.now) + return statements diff --git a/account_bank_statement_import_online_adyen/views/online_bank_statement_provider.xml b/account_bank_statement_import_online_adyen/views/online_bank_statement_provider.xml new file mode 100644 index 00000000..294c7769 --- /dev/null +++ b/account_bank_statement_import_online_adyen/views/online_bank_statement_provider.xml @@ -0,0 +1,28 @@ + + + + online.bank.statement.provider.form + online.bank.statement.provider + + + + + + + + + + + + + + From 2552e1e979310e35011be0ef5b858e9d6c560122 Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Tue, 30 Mar 2021 11:05:06 +0200 Subject: [PATCH 09/24] [FIX] absi- _paypal Module did not properly check input. If passed any csv file, paypal module would try to use it, even if clearly not a paypal file. --- .../models/account_bank_statement_import_paypal_parser.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/account_bank_statement_import_paypal/models/account_bank_statement_import_paypal_parser.py b/account_bank_statement_import_paypal/models/account_bank_statement_import_paypal_parser.py index 2a6f323d..395ff706 100644 --- a/account_bank_statement_import_paypal/models/account_bank_statement_import_paypal_parser.py +++ b/account_bank_statement_import_paypal/models/account_bank_statement_import_paypal_parser.py @@ -110,6 +110,8 @@ class AccountBankStatementImportPayPalParser(models.TransientModel): header = list(next(csv_data)) data_dict = self._data_dict_constructor(mapping, header) + if data_dict.get("currency_column") is None: + raise ValueError(_("No currency column, not a valid Paypal file")) return self._calculate_lines(csv_data, data_dict, mapping, currency_code) From 3afe152c02b1e9bdf3c51c0bfc171eb8c485142b Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Wed, 17 Mar 2021 13:28:41 +0100 Subject: [PATCH 10/24] [FIX] absi- _transfer_move. Correct test and duplicate class name --- .../models/account_bank_statement_import.py | 33 ++++++++- .../models/account_journal.py | 2 +- .../tests/test_statement.py | 69 ++++--------------- 3 files changed, 46 insertions(+), 58 deletions(-) diff --git a/account_bank_statement_import_transfer_move/models/account_bank_statement_import.py b/account_bank_statement_import_transfer_move/models/account_bank_statement_import.py index af145c46..e2ae6739 100644 --- a/account_bank_statement_import_transfer_move/models/account_bank_statement_import.py +++ b/account_bank_statement_import_transfer_move/models/account_bank_statement_import.py @@ -1,13 +1,44 @@ # Copyright 2020 Camptocamp SA # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo import models +from odoo import api, models class AccountBankStatementImport(models.TransientModel): _inherit = "account.bank.statement.import" + @api.model + def _parse_file(self, data_file): + """Enable testing of this functionality.""" + if self.env.context.get("account_bank_statement_import_transfer_move", False): + return ( + None, + "NL77ABNA0574908765", + [ + { + "balance_end_real": 15121.12, + "balance_start": 15568.27, + "date": "2014-01-05", + "name": "1234Test/1", + "transactions": [ + { + "account_number": "NL46ABNA0499998748", + "amount": -754.25, + "date": "2014-01-05", + "name": "Insurance policy 857239PERIOD 01.01.2014 - " + "31.12.2014", + "note": "MKB Insurance 859239PERIOD 01.01.2014 - " + "31.12.2014", + "partner_name": "INSURANCE COMPANY TESTX", + "ref": "435005714488-ABNO33052620", + }, + ], + } + ], + ) + return super()._parse_file(data_file) + def _create_bank_statements(self, stmts_vals): """ Create additional line in statement to set bank statement statement to 0 balance""" diff --git a/account_bank_statement_import_transfer_move/models/account_journal.py b/account_bank_statement_import_transfer_move/models/account_journal.py index 6bd9612a..92bcd270 100644 --- a/account_bank_statement_import_transfer_move/models/account_journal.py +++ b/account_bank_statement_import_transfer_move/models/account_journal.py @@ -4,7 +4,7 @@ from odoo import fields, models -class AccountBankStatementImport(models.Model): +class AccountJournal(models.Model): _inherit = "account.journal" diff --git a/account_bank_statement_import_transfer_move/tests/test_statement.py b/account_bank_statement_import_transfer_move/tests/test_statement.py index e81b7856..1c6ba3d8 100644 --- a/account_bank_statement_import_transfer_move/tests/test_statement.py +++ b/account_bank_statement_import_transfer_move/tests/test_statement.py @@ -1,8 +1,6 @@ # Copyright 2020 Camptocamp SA # Copyright 2020 Tecnativa - Pedro M. Baeza -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from unittest.mock import patch - +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo.tests.common import SavepointCase @@ -35,60 +33,19 @@ class TestGenerateBankStatement(SavepointCase): } ) - def _parse_file(self, data_file): - """Fake method for returning valuable data. Extracted from CAMT demo""" - return ( - None, - "NL77ABNA0574908765", - [ - { - "balance_end_real": 15121.12, - "balance_start": 15568.27, - "date": "2014-01-05", - "name": "1234Test/1", - "transactions": [ - { - "account_number": "NL46ABNA0499998748", - "amount": -754.25, - "date": "2014-01-05", - "name": "Insurance policy 857239PERIOD 01.01.2014 - " - "31.12.2014", - "note": "MKB Insurance 859239PERIOD 01.01.2014 - " - "31.12.2014", - "partner_name": "INSURANCE COMPANY TESTX", - "ref": "435005714488-ABNO33052620", - }, - ], - } - ], - ) - - def _get_bank_statements_available_import_formats(self): - """Fake method for returning a fake importer for not having errors.""" - return ["test"] - def _load_statement(self): - module = "odoo.addons.account_bank_statement_import" - with patch( - module - + ".account_journal.AccountJournal" - + "._get_bank_statements_available_import_formats", - self._get_bank_statements_available_import_formats, - ): - with patch( - module - + ".account_bank_statement_import" - + ".AccountBankStatementImport._parse_file", - self._parse_file, - ): - self.env["account.bank.statement.import"].create( - {"attachment_ids": [(0, 0, {"name": "test file", "datas": b""})]} - ).import_file() - bank_st_record = self.env["account.bank.statement"].search( - [("name", "=", "1234Test/1")], limit=1 - ) - statement_lines = bank_st_record.line_ids - return statement_lines + """Load fake statements, to test creation of extra line.""" + absi = self.env["account.bank.statement.import"].create( + {"attachment_ids": [(0, 0, {"name": "test file", "datas": b""})]} + ) + absi.with_context( + {"account_bank_statement_import_transfer_move": True} + ).import_file() + bank_st_record = self.env["account.bank.statement"].search( + [("name", "=", "1234Test/1")], limit=1 + ) + statement_lines = bank_st_record.line_ids + return statement_lines def test_statement_import(self): self.journal.transfer_line = True From dee4db1c6cbf56905e7f2da4d63345f6ff56a593 Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Mon, 29 Mar 2021 15:20:24 +0200 Subject: [PATCH 11/24] [IMP] absi- _adyen. Fully support csv files and reordered columns --- .../models/account_bank_statement_import.py | 253 ++++++++++++------ .../settlement_detail_report_batch_380.csv | 229 ++++++++++++++++ .../tests/test_import_adyen.py | 23 +- .../models/online_bank_statement_provider.py | 15 +- .../online_bank_statement_provider_dummy.py | 5 +- .../tests/test_import_online.py | 2 +- 6 files changed, 431 insertions(+), 96 deletions(-) create mode 100644 account_bank_statement_import_adyen/test_files/settlement_detail_report_batch_380.csv diff --git a/account_bank_statement_import_adyen/models/account_bank_statement_import.py b/account_bank_statement_import_adyen/models/account_bank_statement_import.py index 4df68c57..649d882a 100644 --- a/account_bank_statement_import_adyen/models/account_bank_statement_import.py +++ b/account_bank_statement_import_adyen/models/account_bank_statement_import.py @@ -3,31 +3,58 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). import logging -from odoo import _, api, fields, models +from odoo import _, fields, models from odoo.exceptions import UserError _logger = logging.getLogger(__name__) +COLUMNS = { + "Company Account": 1, + "Merchant Account": 2, + "Psp Reference": 3, + "Merchant Reference": 4, + "Payment Method": 5, # Not used at present + "Creation Date": 6, + "TimeZone": 7, # Not used at present + "Type": 8, + "Modification Reference": 9, + "Gross Currency": 10, # Not used at present + "Gross Debit (GC)": 11, # Not used at present + "Gross Credit (GC)": 12, # Not used at present + "Exchange Rate": 13, # Not used at present + "Net Currency": 14, + "Net Debit (NC)": 15, # Fee or Merchant Payout + "Net Credit (NC)": 16, + "Commission (NC)": 17, + "Markup (NC)": 18, + "Scheme Fees (NC)": 19, + "Interchange (NC)": 20, + "Payment Method Variant": 21, + "Modification Merchant Reference": 22, # Not used at present + "Batch Number": 23, + "Reserved4": 24, # Not used at present + "Reserved5": 25, # Not used at present + "Reserved6": 26, # Not used at present + "Reserved7": 27, # Not used at present + "Reserved8": 28, # Not used at present + "Reserved9": 29, # Not used at present + "Reserved10": 30, # Not used at present +} + class AccountBankStatementImport(models.TransientModel): _inherit = "account.bank.statement.import" - @api.model def _parse_file(self, data_file): """Parse an Adyen xlsx file and map merchant account strings to journals.""" try: - try: - return self._parse_adyen_file(data_file) - except Exception as exc: - if self.env.context.get("account_bank_statement_import_adyen", False): - raise - _logger.info("Adyen parser error", exc_info=True) - raise ValueError("Not an adyen settlements file: %s" % exc) - except ValueError: - _logger.debug( - _("Statement file was not a Adyen settlement details file."), - exc_info=True, - ) + _logger.debug(_("Try parsing as Adyen settlement details.")) + return self._parse_adyen_file(data_file) + except Exception: + message = _("Statement file was not a Adyen settlement details file.") + if self.env.context.get("account_bank_statement_import_adyen", False): + raise UserError(message) + _logger.debug(message, exc_info=True) return super()._parse_file(data_file) def _find_additional_data(self, currency_code, account_number): @@ -49,95 +76,48 @@ class AccountBankStatementImport(models.TransientModel): self = self.with_context(journal_id=journal.id) return super()._find_additional_data(currency_code, account_number) - @api.model - def _balance(self, row): - return -(float(row[15]) if row[15] else 0.0) + sum( - float(row[i]) if row[i] else 0.0 for i in (16, 17, 18, 19, 20) - ) - - @api.model - def _import_adyen_transaction(self, statement, statement_id, row): - transaction_id = str(len(statement["transactions"])).zfill(4) - transaction = dict( - unique_import_id=statement_id + transaction_id, - date=fields.Date.from_string(row[6]), - amount=self._balance(row), - note="{} {} {} {}".format(row[2], row[3], row[4], row[21]), - name="%s" % (row[3] or row[4] or row[9]), - ) - statement["transactions"].append(transaction) - - @api.model def _parse_adyen_file(self, data_file): - statements = [] + """Parse file assuming it is an Adyen file. + + An Excception will be thrown if file cannot be parsed. + """ statement = None headers = False fees = 0.0 balance = 0.0 payout = 0.0 - statement_id = None - - import_model = self.env["base_import.import"] - importer = import_model.create( - {"file": data_file, "file_name": "Ayden settlemnt details"} - import_model = self.env["base_import.import"] - importer = import_model.create( - {"file": data_file, "file_name": "Ayden settlement details"} - ) - rows = importer._read_file({}) - + rows = self._get_rows(data_file) for row in rows: - if len(row) != 31: + if len(row) < 24: raise ValueError( "Not an Adyen statement. Unexpected row length %s " - "instead of 31" % len(row) + "less then minimum of 24" % len(row) ) if not row[1]: continue if not headers: - if row[1] != "Company Account": - raise ValueError( - 'Not an Adyen statement. Unexpected header "%s" ' - 'instead of "Company Account"', - row[1], - ) + self._set_columns(row) headers = True continue if not statement: - statement = {"transactions": []} - statements.append(statement) - statement_id = "{merchant} {year}/{batch}".format( - merchant=row[2], year=row[6][:4], batch=row[23], - ) - currency_code = row[14] - merchant_id = row[2] - statement["name"] = statement_id - date = fields.Date.from_string(row[6]) - if not statement.get("date") or statement.get("date") > date: - statement["date"] = date - - row[8] = row[8].strip() - if row[8] == "MerchantPayout": + statement = self._make_statement(row) + currency_code = self._get_value(row, "Net Currency") + merchant_id = self._get_value(row, "Merchant Account") + else: + self._update_statement(statement, row) + row_type = self._get_value(row, "Type").strip() + if row_type == "MerchantPayout": payout -= self._balance(row) else: balance += self._balance(row) - self._import_adyen_transaction(statement, statement_id, row) - fees += sum(float(row[i]) if row[i] else 0.0 for i in (17, 18, 19, 20)) + self._import_adyen_transaction(statement, row) + fees += self._sum_fees(row) if not headers: raise ValueError("Not an Adyen statement. Did not encounter header row.") - if fees: - transaction_id = str(len(statement["transactions"])).zfill(4) - transaction = dict( - unique_import_id=statement_id + transaction_id, - date=max(t["date"] for t in statement["transactions"]), - amount=-fees, - name="Commission, markup etc. batch %s" % (int(row[23])), - ) balance -= fees - statement["transactions"].append(transaction) - + self._add_fees_transaction(statement, fees, row) if statement["transactions"] and not payout: raise UserError(_("No payout detected in Adyen statement.")) if self.env.user.company_id.currency_id.compare_amounts(balance, payout) != 0: @@ -145,4 +125,117 @@ class AccountBankStatementImport(models.TransientModel): _("Parse error. Balance %s not equal to merchant " "payout %s") % (balance, payout) ) - return currency_code, merchant_id, statements + return currency_code, merchant_id, [statement] + + def _get_rows(self, data_file): + """Get rows from data_file.""" + # Try to use original import file name. + filename = ( + self.attachment_ids[0].name + if len(self.attachment_ids) == 1 + else "Ayden settlement details" + ) + import_model = self.env["base_import.import"] + importer = import_model.create({"file": data_file, "file_name": filename}) + return importer._read_file({"quoting": '"', "separator": ","}) + + def _set_columns(self, row): + """Set columns from headers. There MUST be a 'Company Account' header.""" + seen_company_account = False + for num, header in enumerate(row): + if header == "Company Account": + seen_company_account = True + if header not in COLUMNS: + _logger.debug(_("Unknown header %s in Adyen statement headers"), header) + else: + COLUMNS[header] = num # Set the right number for the column. + if not seen_company_account: + raise ValueError( + _("Not an Adyen statement. Headers %s do not contain 'Company Account'") + % ", ".join(row) + ) + + def _get_value(self, row, column): + """Get the value from the righ column in the row.""" + return row[COLUMNS[column]] + + def _make_statement(self, row): + """Make statement on first transaction in file.""" + statement = {"transactions": []} + statement["name"] = "{merchant} {year}/{batch}".format( + merchant=self._get_value(row, "Merchant Account"), + year=self._get_value(row, "Creation Date")[:4], + batch=self._get_value(row, "Batch Number"), + ) + statement["date"] = self._get_transaction_date(row) + return statement + + def _get_transaction_date(self, row): + """Get transaction date in right format.""" + return fields.Date.from_string(self._get_value(row, "Creation Date")) + + def _update_statement(self, statement, row): + """Update statement from transaction row.""" + # Statement date is date of earliest transaction in file. + date = self._get_transaction_date(row) + if date < statement.get("date"): + statement["date"] = date + + def _balance(self, row): + return ( + -self._sum_amount_values(row, ("Net Debit (NC)",)) + + self._sum_amount_values(row, ("Net Credit (NC)",)) + + self._sum_fees(row) + ) + + def _sum_fees(self, row): + """Sum the amounts in the fees columns.""" + return self._sum_amount_values( + row, + ("Commission (NC)", "Markup (NC)", "Scheme Fees (NC)", "Interchange (NC)",), + ) + + def _sum_amount_values(self, row, columns): + """Sum the amounts from the columns passed.""" + amount = 0.0 + for column in columns: + value = self._get_value(row, column) + if value: + amount += float(value) + return amount + + def _import_adyen_transaction(self, statement, row): + """Add transaction from row to statements.""" + transaction = dict( + unique_import_id=self._get_unique_import_id(statement), + date=self._get_transaction_date(row), + amount=self._balance(row), + note="{} {} {} {}".format( + self._get_value(row, "Merchant Account"), + self._get_value(row, "Psp Reference"), + self._get_value(row, "Merchant Reference"), + self._get_value(row, "Payment Method Variant"), + ), + name="%s" + % ( + self._get_value(row, "Psp Reference") + or self._get_value(row, "Merchant Reference") + or self._get_value(row, "Modification Reference") + ), + ) + statement["transactions"].append(transaction) + + def _get_unique_import_id(self, statement): + """get unique import ID for transaction.""" + return statement["name"] + str(len(statement["transactions"])).zfill(4) + + def _add_fees_transaction(self, statement, fees, row): + """Single transaction for all fees in statement.""" + transaction = dict( + unique_import_id=self._get_unique_import_id(statement), + date=max(t["date"] for t in statement["transactions"]), + amount=-fees, + name="Commission, markup etc. batch %s" + % self._get_value(row, "Batch Number"), + ) + statement["transactions"].append(transaction) diff --git a/account_bank_statement_import_adyen/test_files/settlement_detail_report_batch_380.csv b/account_bank_statement_import_adyen/test_files/settlement_detail_report_batch_380.csv new file mode 100644 index 00000000..ff130166 --- /dev/null +++ b/account_bank_statement_import_adyen/test_files/settlement_detail_report_batch_380.csv @@ -0,0 +1,229 @@ +Company Account,Merchant Account,Psp Reference,Merchant Reference,Payment Method,Creation Date,TimeZone,Type,Modification Reference,Gross Currency,Gross Debit (GC),Gross Credit (GC),Exchange Rate,Net Currency,Net Debit (NC),Net Credit (NC),Commission (NC),Markup (NC),Scheme Fees (NC),Interchange (NC),Payment Method Variant,Batch Number,Reserved4,Reserved5,Reserved6,Reserved7,Reserved8,Reserved9,Reserved10,Modification Merchant Reference,Booking Date,Booking Date TimeZone +CompanyNL,YOURCOMPANY_ACCOUNT,1829098024817852,CM1000028854,mc,2021-01-05 00:21:32,CET,Settled,7429098024927090,USD,,31.45,1,USD,,31.15,,0.16,0.05,0.09,mcstandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4636098029442802,CM2000162856,visa,2021-01-05 00:29:21,CET,Settled,7936098029610473,USD,,21.95,1,USD,,21.74,,0.11,0.03,0.07,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1736098031315223,CM7000054248,visa,2021-01-05 00:32:20,CET,Settled,7436098031406030,USD,,26.45,1,USD,,26.2,,0.14,0.03,0.08,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1316098037405950,CM3000151362,directEbanking,2021-01-05 00:43:27,CET,Settled,1316098037405950,USD,,96.9,1,USD,,95.93,0.97,,,,directEbanking,380,,,,,,,,CM3000151362,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1656098094611035,CM2000162861,mc,2021-01-05 02:18:53,CET,Settled,7759098095331123,USD,,589.75,1,USD,,584.48,,3.07,1.02,1.18,mcstandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1316098263305981,CM2000162863,ideal,2021-01-05 06:59:37,CET,Settled,1316098263305981,USD,,26.45,1,USD,,26.2,0.25,,,,idealtriodos,380,,,,,,,,CM2000162863,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,6119098279552327,CM2000162864,ideal,2021-01-05 07:26:28,CET,Settled,6119098279552327,USD,,116.3,1,USD,,116.05,0.25,,,,idealing,380,,,,,,,,CM2000162864,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1516098280206459,CM1000028855,ideal,2021-01-05 07:27:26,CET,Settled,1516098280206459,USD,,26.45,1,USD,,26.2,0.25,,,,idealasn,380,,,,,,,,CM1000028855,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1719098288495570,CM7000054252,cartebancaire,2021-01-05 07:42:17,CET,Settled,2319098288620641,USD,,24.95,1,USD,,24.72,,0.15,0.01,0.07,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4616098301438837,CM7000054253,cartebancaire,2021-01-05 08:04:27,CET,Settled,5316098302004588,USD,,119.75,1,USD,,118.78,,0.72,0.01,0.24,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1816098304031443,CM1000028856,ideal,2021-01-05 08:07:05,CET,Settled,1816098304031443,USD,,66.45,1,USD,,66.2,0.25,,,,idealasn,380,,,,,,,,CM1000028856,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,6219098308593440,CM7000054254,cartebancaire,2021-01-05 08:16:46,CET,Settled,2416098309442597,USD,,79.85,1,USD,,79.2,,0.48,0.01,0.16,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1619093887226154,CM7000053968,bankTransfer_IBAN,2021-01-05 08:25:01,CET,Settled,1619093887226154,USD,,518.9,1,USD,,518.7,0.2,,,,bankTransfer_IBAN,380,,,,,,,,CM7000053968,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4316095884890882,CM1000028786,bankTransfer_NL,2021-01-05 08:25:18,CET,Settled,4316095884890882,USD,,1044.35,1,USD,,1044.15,0.2,,,,bankTransfer_NL,380,,,,,,,,CM1000028786,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1816098318146617,CM1000028857,ideal,2021-01-05 08:30:59,CET,Settled,1816098318146617,USD,,66.45,1,USD,,66.2,0.25,,,,idealing,380,,,,,,,,CM1000028857,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4556098318416786,CM2000162866,visa,2021-01-05 08:32:58,CET,Settled,7659098319786491,USD,,81.85,1,USD,,81.22,,0.43,0.04,0.16,visastandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1816098325040696,CM1000028858,ideal,2021-01-05 08:42:48,CET,Settled,1816098325040696,USD,,26.45,1,USD,,26.2,0.25,,,,idealasn,380,,,,,,,,CM1000028858,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1559098334575279,CM1000028859,bcmc_mobile,2021-01-05 08:57:39,CET,Settled,7759098334591982,USD,,26.45,1,USD,,26.22,,0.16,0.02,0.05,bcmc_mobile,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,6239098336305763,CM2000162873,visa,2021-01-05 09:02:04,CET,Settled,7936098337243280,USD,,565.8,1,USD,,561.55,,2.94,0.18,1.13,visastandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1359098338215734,CM18000001445,visa,2021-01-05 09:04:16,CET,Settled,7759098338563615,USD,,50.12,1,USD,,48.91,,0.26,0.2,0.75,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1539098339357892,CM3000151370,visa,2021-01-05 09:05:38,CET,Settled,7439098339386626,USD,,30.9,1,USD,,30.12,,0.16,0.16,0.46,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1339098339577947,CM7000054255,visa,2021-01-05 09:06:05,CET,Settled,7439098339659113,USD,,49.85,1,USD,,49.46,,0.26,0.03,0.1,visastandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1536098341845648,CM1000028860,visa,2021-01-05 09:11:00,CET,Settled,7936098342605952,USD,,555.35,1,USD,,550.51,,2.89,0.28,1.67,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1616098342598462,CM3000151371,directEbanking,2021-01-05 09:12:24,CET,Settled,1616098342598462,USD,,555.8,1,USD,,550.24,5.56,,,,directEbanking,380,,,,,,,,CM3000151371,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4646098344291577,CM17000002640,mc,2021-01-05 09:13:55,CET,Settled,7649098344354771,USD,,27.08,1,USD,,26.44,,0.14,0.09,0.41,mcsuperpremiumcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4726098345327373,CM7000054256,mc,2021-01-05 09:15:47,CET,Settled,7926098345475009,USD,,46.4,1,USD,,45.95,,0.24,0.07,0.14,mcstandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1856098346315573,CM2000162874,visa,2021-01-05 09:19:29,CET,Settled,7759098347699640,USD,,588.75,1,USD,,584.33,,3.06,0.18,1.18,visastandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4326098349237309,CM3000151372,visa,2021-01-05 09:28:08,CET,Settled,7429098352883956,USD,,475.95,1,USD,,471.8,,2.47,0.25,1.43,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1526098355393578,CM7000054259,visa,2021-01-05 09:33:49,CET,Settled,7426098356298725,USD,,518.9,1,USD,,508.41,,2.7,0.27,7.52,visacommercialcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1726098357414842,CM7000054260,bcmc_mobile,2021-01-05 09:35:48,CET,Settled,7426098357485040,USD,,156.3,1,USD,,155.29,,0.94,0.02,0.05,bcmc_mobile,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1816098357756292,CM3000151374,giropay,2021-01-05 09:39:00,CET,Settled,1816098357756292,USD,,46.85,1,USD,,46.04,0.81,,,,giropay,380,,,,,,,,CM3000151374,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1516098360214266,CM1000028861,ideal,2021-01-05 09:41:03,CET,Settled,1516098360214266,USD,,36.45,1,USD,,36.2,0.25,,,,idealasn,380,,,,,,,,CM1000028861,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,6219098360158827,CM3000151377,giropay,2021-01-05 09:43:16,CET,Settled,6219098360158827,USD,,36.9,1,USD,,36.22,0.68,,,,giropay,380,,,,,,,,CM3000151377,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4336098363026289,CM2000162876,visa,2021-01-05 09:45:05,CET,Settled,7936098363059485,USD,,34.9,1,USD,,34.62,,0.18,0.03,0.07,electron,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4616098365115461,CM2000162878,cartebancaire,2021-01-05 09:49:53,CET,Settled,7916098365186579,USD,,46.45,1,USD,,46.07,,0.28,0.01,0.09,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1619098368800567,CM7000054262,cartebancaire,2021-01-05 09:55:45,CET,Settled,7419098368870510,USD,,46.85,1,USD,,46.47,,0.28,0.01,0.09,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1516098372030965,CM1000028862,ideal,2021-01-05 10:00:57,CET,Settled,1516098372030965,USD,,46.4,1,USD,,46.15,0.25,,,,idealing,380,,,,,,,,CM1000028862,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1846098379029995,CM2000162881,visa,2021-01-05 10:11:50,CET,Settled,7549098379106006,USD,,31.9,1,USD,,31.6,,0.17,0.03,0.1,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4636098378101900,CM2000162880,visa,2021-01-05 10:12:27,CET,Settled,7936098379470528,USD,,529.85,1,USD,,525.86,,2.76,0.17,1.06,visasuperpremiumdebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4646080478327021,CM3000149254,visa,2021-01-05 10:17:31,CET,Refunded,4346098382059723,USD,19.95,,1,USD,19.95,,,,,,visastandardcredit,380,,,,,,,,CM3000149254,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1549098388405186,CM17000002641,visa,2021-01-05 10:27:52,CET,Settled,7749098388722759,USD,,487.6,1,USD,,476.46,,2.54,1.29,7.31,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1549098390299692,CM7000054263,visa,2021-01-05 10:31:52,CET,Settled,7749098391124118,USD,,538.85,1,USD,,534.8,,2.8,0.17,1.08,visastandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4616098392069723,CM7000054264,cartebancaire,2021-01-05 10:34:57,CET,Settled,7916098392205897,USD,,29.9,1,USD,,29.65,,0.18,0.01,0.06,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4516098401930718,CM2000162887,ideal,2021-01-05 10:50:48,CET,Settled,4516098401930718,USD,,66.35,1,USD,,66.1,0.25,,,,idealrabobank,380,,,,,,,,CM2000162887,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,6239098403242287,CM7000054268,visa,2021-01-05 10:52:47,CET,Settled,7439098403677975,USD,,483.72,1,USD,,472.97,,2.52,0.25,7.98,visacorporatecredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4316098407246061,CM1000028863,ideal,2021-01-05 10:59:17,CET,Settled,4316098407246061,USD,,6.5,1,USD,,6.25,0.25,,,,idealing,380,,,,,,,,CM1000028863,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4326098409899006,CM3000151391,visa,2021-01-05 11:03:26,CET,Settled,7426098410064121,USD,,141.95,1,USD,,140.7,,0.74,0.08,0.43,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1359098412271835,CM2000162890,mc,2021-01-05 11:07:34,CET,Settled,7759098412548011,USD,,181.75,1,USD,,180.1,,0.95,0.15,0.55,mcstandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1539098414025232,CM7000054274,visa,2021-01-05 11:10:17,CET,Settled,7439098414172511,USD,,34.95,1,USD,,34.64,,0.18,0.03,0.1,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1556098414930348,CM7000054275,visa,2021-01-05 11:12:29,CET,Settled,7559098415497986,USD,,478.95,1,USD,,475.35,,2.49,0.15,0.96,visastandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4536098415344183,CM7000054276,mc,2021-01-05 11:13:54,CET,Settled,7439098416347798,USD,,478.95,1,USD,,474.64,,2.49,0.38,1.44,mcstandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1849098416334181,CM7000054277,mc,2021-01-05 11:14:33,CET,Settled,7549098416733952,USD,,79.85,1,USD,,79.2,,0.42,0.07,0.16,mcstandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1336098417118690,CM2000162891,mc,2021-01-05 11:15:31,CET,Settled,7436098417314824,USD,,69.85,1,USD,,69.22,,0.36,0.06,0.21,mcstandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4756098417302497,CM2000162892,visa,2021-01-05 11:15:46,CET,Settled,7659098417466145,USD,,31.9,1,USD,,31.64,,0.17,0.03,0.06,electron,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,6249098418547686,CM3000151393,visa,2021-01-05 11:17:48,CET,Settled,7649098418680812,USD,,36.95,1,USD,,36.62,,0.19,0.03,0.11,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1619098421158771,CM3000151394,directEbanking,2021-01-05 11:23:00,CET,Settled,1619098421158771,USD,,475.95,1,USD,,471.19,4.76,,,,directEbanking,380,,,,,,,,CM3000151394,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1816098424350301,CM1000028864,ideal,2021-01-05 11:32:29,CET,Settled,1816098424350301,USD,,31.5,1,USD,,31.25,0.25,,,,idealtriodos,380,,,,,,,,CM1000028864,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4746098428746750,CM15000004736,visa,2021-01-05 11:35:45,CET,Settled,7749098429453067,USD,,430.95,1,USD,,427.71,,2.24,0.14,0.86,visastandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1519098432126040,CM3000151396,directEbanking,2021-01-05 11:41:32,CET,Settled,1519098432126040,USD,,555.8,1,USD,,550.24,5.56,,,,directEbanking,380,,,,,,,,CM3000151396,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1516098431938133,CM3000151395,directEbanking,2021-01-05 11:44:06,CET,Settled,1516098431938133,USD,,25.76,1,USD,,25.5,0.26,,,,directEbanking,380,,,,,,,,CM3000151395,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1546098438298972,CM7000054278,visa,2021-01-05 11:50:32,CET,Settled,7549098438328351,USD,,29.9,1,USD,,29.65,,0.16,0.03,0.06,visastandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1336098447468164,CM3000151400,visa,2021-01-05 12:06:23,CET,Settled,7439098447832487,USD,,515.9,1,USD,,512.03,,2.68,0.16,1.03,visastandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4716098449607813,CM1000028865,ideal,2021-01-05 12:09:58,CET,Settled,4716098449607813,USD,,66.45,1,USD,,66.2,0.25,,,,idealasn,380,,,,,,,,CM1000028865,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1649098450600333,CM7000054279,visa,2021-01-05 12:11:47,CET,Settled,7649098451071325,USD,,518.9,1,USD,,515,,2.7,0.16,1.04,visastandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1336098451682403,CM3000151401,visa,2021-01-05 12:14:02,CET,Settled,7436098452422094,USD,,475.95,1,USD,,472.38,,2.47,0.15,0.95,visastandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1719098456224257,CM1000028866,ideal,2021-01-05 12:20:53,CET,Settled,1719098456224257,USD,,26.45,1,USD,,26.2,0.25,,,,idealrabobank,380,,,,,,,,CM1000028866,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4746098458272701,CM7000054280,mc,2021-01-05 12:29:11,CET,Settled,7649098461512151,USD,,548.85,1,USD,,544.47,,2.85,0.43,1.1,mcstandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4616098461703368,CM1000028867,ideal,2021-01-05 12:31:21,CET,Settled,4616098461703368,USD,,66.45,1,USD,,66.2,0.25,,,,idealtriodos,380,,,,,,,,CM1000028867,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1749098464912244,CM2000162894,mc,2021-01-05 12:34:56,CET,Settled,7749098464964093,USD,,6.95,1,USD,,6.89,,0.04,0.01,0.01,mcstandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1756098464186658,CM20000002451,mc,2021-01-05 12:35:13,CET,Settled,7559098465135312,USD,,561.85,1,USD,,556.8,,2.92,0.44,1.69,mcstandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1756098478581449,CM2000162895,visa,2021-01-05 12:57:49,CET,Settled,7559098478696391,USD,,26.45,1,USD,,26.2,,0.14,0.03,0.08,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1529098480373559,CM2000162897,visa,2021-01-05 13:01:40,CET,Settled,7429098481008681,USD,,535.4,1,USD,,530.74,,2.78,0.27,1.61,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4816098481304518,CM1000028869,ideal,2021-01-05 13:03:17,CET,Settled,4816098481304518,USD,,515.45,1,USD,,515.2,0.25,,,,idealrabobank,380,,,,,,,,CM1000028869,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4556098484634014,CM7000054282,mc,2021-01-05 13:08:07,CET,Settled,7759098484873909,USD,,468.85,1,USD,,465.24,,2.44,0.23,0.94,mcstandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1519098493524448,CM3000151406,directEbanking,2021-01-05 13:23:37,CET,Settled,1519098493524448,USD,,26.9,1,USD,,26.63,0.27,,,,directEbanking,380,,,,,,,,CM3000151406,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4536098501532824,CM3000151407,visa,2021-01-05 13:35:55,CET,Settled,7936098501550114,USD,,26.9,1,USD,,26.65,,0.14,0.03,0.08,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1346098502166016,CM15000004737,visa,2021-01-05 13:37:28,CET,Settled,7549098502485178,USD,,51.95,1,USD,,51.55,,0.27,0.03,0.1,electron,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4516098503744553,CM3000151408,directEbanking,2021-01-05 13:39:59,CET,Settled,4516098503744553,USD,,26.9,1,USD,,26.63,0.27,,,,directEbanking,380,,,,,,,,CM3000151408,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1619098504352119,CM7000054284,cartebancaire,2021-01-05 13:42:59,CET,Settled,7419098505011440,USD,,149.75,1,USD,,148.54,,0.9,0.01,0.3,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4646098509498355,CM7000054285,mc,2021-01-05 13:49:43,CET,Settled,7649098509837982,USD,,548.85,1,USD,,543.94,,2.85,0.96,1.1,mcstandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1316098518102027,CM7000054288,cartebancaire,2021-01-05 14:05:26,CET,Settled,2419098518485053,USD,,82.85,1,USD,,82.18,,0.5,,0.17,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1716098520371467,CM7000054289,cartebancaire,2021-01-05 14:09:36,CET,Settled,7916098520830012,USD,,448.9,1,USD,,444.86,,2.69,,1.35,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1319098525213079,CM7000054290,cartebancaire,2021-01-05 14:17:08,CET,Settled,5416098525657484,USD,,591.75,1,USD,,587.01,,3.55,0.01,1.18,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1616098530359040,CM2000162903,directEbanking,2021-01-05 14:28:02,CET,Settled,1616098530359040,USD,,26.9,1,USD,,26.63,0.27,,,,directEbanking,380,,,,,,,,CM2000162903,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,6219098534473078,CM7000054291,cartebancaire,2021-01-05 14:33:16,CET,Settled,7916098534916500,USD,,69.95,1,USD,,69.38,,0.42,0.01,0.14,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1856098543755332,CM2000162908,visa,2021-01-05 14:47:13,CET,Settled,7559098544336766,USD,,545.4,1,USD,,541.3,,2.84,0.17,1.09,visastandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1516098545972663,CM1000028870,ideal,2021-01-05 14:50:37,CET,Settled,1516098545972663,USD,,545.4,1,USD,,545.15,0.25,,,,idealing,380,,,,,,,,CM1000028870,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1316098546477771,CM1000028871,ideal,2021-01-05 14:51:41,CET,Settled,1316098546477771,USD,,31.45,1,USD,,31.2,0.25,,,,idealing,380,,,,,,,,CM1000028871,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4716098547580789,CM1000028872,ideal,2021-01-05 14:55:11,CET,Settled,4716098547580789,USD,,585.3,1,USD,,585.05,0.25,,,,idealasn,380,,,,,,,,CM1000028872,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1359098553987017,CM2000162909,mc,2021-01-05 15:04:09,CET,Settled,7659098554493019,USD,,475.95,1,USD,,471.67,,2.47,0.38,1.43,mcstandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4616098558319300,CM7000054292,cartebancaire,2021-01-05 15:12:36,CET,Settled,2419098558828295,USD,,448.9,1,USD,,444.86,,2.69,,1.35,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4316098560947976,CM1000028873,ideal,2021-01-05 15:16:52,CET,Settled,4316098560947976,USD,,56.45,1,USD,,56.2,0.25,,,,idealtriodos,380,,,,,,,,CM1000028873,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1336098559877129,CM3000151416,visa,2021-01-05 15:17:04,CET,Settled,7936098562244607,USD,,68.84,1,USD,,68.3,,0.36,0.04,0.14,visastandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1316098564691485,CM7000054293,cartebancaire,2021-01-05 15:22:18,CET,Settled,2416098564712379,USD,,29.9,1,USD,,29.65,,0.18,0.01,0.06,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4626098566791803,CM15000004738,mc,2021-01-05 15:25:39,CET,Settled,7426098567395492,USD,,430.95,1,USD,,427.51,,2.24,0.34,0.86,mcstandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1819098565442906,CM3000151418,directEbanking,2021-01-05 15:25:56,CET,Settled,1819098565442906,USD,,31.95,1,USD,,31.63,0.32,,,,directEbanking,380,,,,,,,,CM3000151418,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,6219098567337430,CM3000151420,directEbanking,2021-01-05 15:28:44,CET,Settled,6219098567337430,USD,,545.85,1,USD,,540.39,5.46,,,,directEbanking,380,,,,,,,,CM3000151420,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,6219098570269216,CM7000054294,cartebancaire,2021-01-05 15:31:34,CET,Settled,5316098570350937,USD,,24.95,1,USD,,24.72,,0.15,0.01,0.07,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1529098572249212,CM14000007552,mc,2021-01-05 15:34:04,CET,Settled,7429098572448465,USD,,60.9,1,USD,,60.43,,0.32,0.03,0.12,mcstandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4345895611165986,CM300122499,mc,2021-01-05 15:36:57,CET,Refunded,6249098573617340,USD,75,,1,USD,75,,,,,,mcstandardcredit,380,,,,,,,,CM300122499,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4846098575089755,CM7000054295,visa,2021-01-05 15:39:25,CET,Settled,7549098575651482,USD,,99.8,1,USD,,99.04,,0.52,0.04,0.2,visastandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1619098584477539,CM7000054297,cartebancaire,2021-01-05 15:55:31,CET,Settled,7419098584590322,USD,,24.95,1,USD,,24.72,,0.15,0.01,0.07,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4756098584131257,CM3000151423,visa,2021-01-05 15:55:33,CET,Settled,7659098585336906,USD,,515.85,1,USD,,511.36,,2.68,0.26,1.55,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1559098598347829,CM2000162911,visa,2021-01-05 16:18:43,CET,Settled,7559098599235848,USD,,81.85,1,USD,,81.11,,0.43,0.06,0.25,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1619098601176056,CM7000054300,cartebancaire,2021-01-05 16:23:48,CET,Settled,2319098601657122,USD,,498.9,1,USD,,494.9,,2.99,0.01,1,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1716098602170803,CM7000054301,cartebancaire,2021-01-05 16:25:44,CET,Settled,7419098602620564,USD,,498.9,1,USD,,494.9,,2.99,0.01,1,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4546098602926519,CM3000151428,mc,2021-01-05 16:26:17,CET,Settled,7549098603771203,USD,,545.85,1,USD,,540.94,,2.84,0.43,1.64,mcstandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1319098608380340,CM2000162914,cartebancaire,2021-01-05 16:36:08,CET,Settled,2319098609032963,USD,,498.85,1,USD,,494.85,,2.99,0.01,1,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,6119098607954245,CM7000054305,cartebancaire,2021-01-05 16:36:08,CET,Settled,7419098609041681,USD,,568.8,1,USD,,563.68,,3.41,,1.71,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4846098611011177,CM3000151434,mc,2021-01-05 16:39:03,CET,Settled,7649098611430437,USD,,545.85,1,USD,,540.94,,2.84,0.43,1.64,mcstandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,6159098611216053,CM2000162917,visa,2021-01-05 16:39:34,CET,Settled,7559098611741815,USD,,944.95,1,USD,,937.87,,4.91,0.28,1.89,visastandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4746098616887887,CM20000002452,visa,2021-01-05 16:48:34,CET,Settled,7649098617149445,USD,,137.8,1,USD,,136.59,,0.72,0.08,0.41,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4316098616561235,CM1000028874,ideal,2021-01-05 16:48:56,CET,Settled,4316098616561235,USD,,585.3,1,USD,,585.05,0.25,,,,idealrabobank,380,,,,,,,,CM1000028874,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1315962210023024,CM700045716,cartebancaire,2021-01-05 16:50:11,CET,Refunded,1316098617350832,USD,461.5,,1,USD,461.5,,,,,,cartebancaire,380,,,,,,,,CM700045716,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1656098619840762,CM7000054306,mc,2021-01-05 16:54:03,CET,Settled,7659098620437669,USD,,508.8,1,USD,,504.73,,2.65,0.4,1.02,mcstandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4616098621031858,CM7000054307,cartebancaire,2021-01-05 16:57:14,CET,Settled,2416098621638476,USD,,518.85,1,USD,,514.69,,3.11,0.01,1.04,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1836098626077492,CM2000162919,visadankort,2021-01-05 17:04:12,CET,Settled,7436098626521458,USD,,479.95,1,USD,,476.34,,2.5,0.15,0.96,visadankort,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1616098632664453,CM1000028875,ideal,2021-01-05 17:15:04,CET,Settled,1616098632664453,USD,,26.45,1,USD,,26.2,0.25,,,,idealing,380,,,,,,,,CM1000028875,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4516098634301083,CM7000054308,cartebancaire,2021-01-05 17:19:50,CET,Settled,7419098635052389,USD,,423.9,1,USD,,420.5,,2.54,0.01,0.85,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1549098641389649,CM2000162922,mc,2021-01-05 17:29:43,CET,Settled,7549098641835796,USD,,552.85,1,USD,,547.89,,2.87,0.43,1.66,mcstandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1839098643550890,CM20000002453,mc,2021-01-05 17:33:13,CET,Settled,7439098643930630,USD,,120.8,1,USD,,119.87,,0.63,0.06,0.24,mcstandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,6119098644839953,CM3000151444,directEbanking,2021-01-05 17:36:39,CET,Settled,6119098644839953,USD,,50.85,1,USD,,50.34,0.51,,,,directEbanking,380,,,,,,,,CM3000151444,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4615982720540185,CM300134351,amex,2021-01-05 17:40:58,CET,Refunded,1816098648122627,USD,76.5,,1,USD,76.5,,,,,,amex,380,,,,,,,,CM300134351,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4646098645930352,CM2000162924,maestro,2021-01-05 17:41:15,CET,Settled,7649098648754767,USD,,569.8,1,USD,,565.57,,2.96,0.13,1.14,maestro,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1529098653630884,CM7000054309,mc,2021-01-05 17:50:23,CET,Settled,7429098654237844,USD,,568.8,1,USD,,564.25,,2.96,0.45,1.14,mcstandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4616098657908961,CM3000151449,directEbanking,2021-01-05 17:59:16,CET,Settled,4616098657908961,USD,,495.85,1,USD,,490.89,4.96,,,,directEbanking,380,,,,,,,,CM3000151449,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1316037899193429,CM300143509,directEbanking,2021-01-05 17:59:56,CET,Refunded,1816098659464421,USD,40,,1,USD,40.2,,0.2,,,,directEbanking,380,,,,,,,,CM300143509,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4316098660263282,CM1000028876,ideal,2021-01-05 18:00:55,CET,Settled,4316098660263282,USD,,56.45,1,USD,,56.2,0.25,,,,idealabn,380,,,,,,,,CM1000028876,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1816098662718979,CM7000054310,cartebancaire,2021-01-05 18:06:44,CET,Settled,7419098663303530,USD,,82.85,1,USD,,82.09,,0.5,0.01,0.25,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4526098663485664,CM15000004740,mc,2021-01-05 18:07:38,CET,Settled,7429098664581298,USD,,480.95,1,USD,,477.11,,2.5,0.38,0.96,mcpremiumdebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1519098664619084,CM7000054312,cartebancaire,2021-01-05 18:09:39,CET,Settled,2419098665064800,USD,,511.9,1,USD,,507.29,,3.07,,1.54,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1816057939971662,CM100027779,ideal,2021-01-05 18:16:35,CET,Refunded,1816098669505999,USD,419,,1,USD,419.2,,0.2,,,,idealing,380,,,,,,,,CM100027779,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1816098671105077,CM3000151454,directEbanking,2021-01-05 18:20:15,CET,Settled,1816098671105077,USD,,26.9,1,USD,,26.63,0.27,,,,directEbanking,380,,,,,,,,CM3000151454,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4336098673118875,CM14000007554,visa,2021-01-05 18:22:04,CET,Settled,7936098673241641,USD,,30.9,1,USD,,30.65,,0.16,0.03,0.06,electron,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1346098678442712,CM2000162927,mc,2021-01-05 18:30:51,CET,Settled,7549098678514694,USD,,26.45,1,USD,,26.19,,0.14,0.07,0.05,mcstandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1719098683770023,CM1000028879,ideal,2021-01-05 18:40:34,CET,Settled,1719098683770023,USD,,555.35,1,USD,,555.1,0.25,,,,idealing,380,,,,,,,,CM1000028879,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1536098687703419,CM2000162928,bcmc_mobile,2021-01-05 18:46:16,CET,Settled,7436098687766513,USD,,46.45,1,USD,,46.1,,0.28,0.02,0.05,bcmc_mobile,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4716098688894825,CM7000054314,cartebancaire,2021-01-05 18:49:46,CET,Settled,5316098689026329,USD,,41.55,1,USD,,40.92,,0.25,0.01,0.37,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1326098689284388,CM15000004743,mc,2021-01-05 18:49:58,CET,Settled,7429098689984352,USD,,590.75,1,USD,,586.04,,3.07,0.46,1.18,mcstandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1716098689923686,CM2000162929,ideal,2021-01-05 18:50:42,CET,Settled,1716098689923686,USD,,26.45,1,USD,,26.2,0.25,,,,idealasn,380,,,,,,,,CM2000162929,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1729098690327724,CM3000151457,mc,2021-01-05 18:50:48,CET,Settled,7429098690481562,USD,,46.85,1,USD,,46.4,,0.24,0.07,0.14,mcstandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1519098693348945,CM7000054315,cartebancaire,2021-01-05 18:57:03,CET,Settled,7419098693472693,USD,,49.9,1,USD,,49.49,,0.3,0.01,0.1,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4646098695089323,CM2000162931,visa,2021-01-05 18:58:45,CET,Settled,7649098695253291,USD,,120.85,1,USD,,119.79,,0.63,0.07,0.36,visapremiumcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4346098695635193,CM7000054316,mc,2021-01-05 18:59:28,CET,Settled,7649098695683588,USD,,39.9,1,USD,,39.55,,0.21,0.06,0.08,mcstandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1759098701345718,CM7000054318,bcmc_mobile,2021-01-05 19:09:01,CET,Settled,7759098701412051,USD,,76.4,1,USD,,75.87,,0.46,0.02,0.05,bcmc_mobile,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1516098702846445,CM1000028880,ideal,2021-01-05 19:12:30,CET,Settled,1516098702846445,USD,,26.5,1,USD,,26.25,0.25,,,,idealing,380,,,,,,,,CM1000028880,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4616098706070300,CM7000054319,cartebancaire,2021-01-05 19:19:07,CET,Settled,5416098706208941,USD,,44.95,1,USD,,44.58,,0.27,0.01,0.09,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1716098711894517,CM2000162933,cartebancaire,2021-01-05 19:28:37,CET,Settled,2319098712384350,USD,,513.9,1,USD,,509.27,,3.08,0.01,1.54,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1349098713988957,CM3000151465,visa,2021-01-05 19:30:33,CET,Settled,7649098714337109,USD,,495.85,1,USD,,491.53,,2.58,0.25,1.49,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1516098724985007,CM7000054321,cartebancaire,2021-01-05 19:49:26,CET,Settled,7416098725000549,USD,,49.85,1,USD,,49.44,,0.3,0.01,0.1,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1626098730490973,CM2000162936,visadankort,2021-01-05 19:57:44,CET,Settled,7426098730644701,USD,,25.95,1,USD,,25.75,,0.13,0.02,0.05,visadankort,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1759098739207500,CM7000054323,visa,2021-01-05 20:12:07,CET,Settled,7759098739276361,USD,,49.9,1,USD,,49.51,,0.26,0.03,0.1,visastandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4516098746917404,CM1000028882,ideal,2021-01-05 20:27:44,CET,Settled,4516098746917404,USD,,106.3,1,USD,,106.05,0.25,,,,idealrabobank,380,,,,,,,,CM1000028882,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4816098754982347,CM2000162937,directEbanking,2021-01-05 20:40:50,CET,Settled,4816098754982347,USD,,589.75,1,USD,,583.85,5.9,,,,directEbanking,380,,,,,,,,CM2000162937,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1639098765132529,CM3000151476,mc,2021-01-05 20:55:25,CET,Settled,7439098765255709,USD,,41.95,1,USD,,41.54,,0.22,0.06,0.13,mcstandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4826098775960486,CM7000054324,mc,2021-01-05 21:14:10,CET,Settled,7926098776500820,USD,,548.8,1,USD,,544.42,,2.85,0.43,1.1,mcstandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4516098776825980,CM1000028883,ideal,2021-01-05 21:15:31,CET,Settled,4516098776825980,USD,,46.45,1,USD,,46.2,0.25,,,,idealing,380,,,,,,,,CM1000028883,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1619098777850542,CM1000028885,ideal,2021-01-05 21:16:58,CET,Settled,1619098777850542,USD,,475.5,1,USD,,475.25,0.25,,,,idealtriodos,380,,,,,,,,CM1000028885,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1516098777260506,CM1000028884,ideal,2021-01-05 21:17:01,CET,Settled,1516098777260506,USD,,495.45,1,USD,,495.2,0.25,,,,idealrabobank,380,,,,,,,,CM1000028884,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1819098778201990,CM7000054326,cartebancaire,2021-01-05 21:18:59,CET,Settled,7416098778786085,USD,,99.9,1,USD,,99.09,,0.6,0.01,0.2,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1516098778907329,CM2000162939,giropay,2021-01-05 21:19:25,CET,Settled,1516098778907329,USD,,545.85,1,USD,,538.55,7.3,,,,giropay,380,,,,,,,,CM2000162939,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1719098782535640,CM1000028887,ideal,2021-01-05 21:24:58,CET,Settled,1719098782535640,USD,,475.5,1,USD,,475.25,0.25,,,,idealrabobank,380,,,,,,,,CM1000028887,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4736098783434390,CM1000028886,bcmc_mobile,2021-01-05 21:25:58,CET,Settled,7936098783588667,USD,,26.45,1,USD,,26.22,,0.16,0.02,0.05,bcmc_mobile,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4516098785683537,CM3000151480,directEbanking,2021-01-05 21:30:25,CET,Settled,4516098785683537,USD,,515.9,1,USD,,510.74,5.16,,,,directEbanking,380,,,,,,,,CM3000151480,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4856098786324808,CM7000054327,visa,2021-01-05 21:31:23,CET,Settled,7759098786834369,USD,,24.95,1,USD,,24.75,,0.13,0.02,0.05,visastandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1859098787109292,CM1000028888,bcmc_mobile,2021-01-05 21:32:08,CET,Settled,7759098787289522,USD,,515.45,1,USD,,512.29,,3.09,0.02,0.05,bcmc_mobile,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1839098789585718,CM7000054329,mc,2021-01-05 21:37:01,CET,Settled,7436098790217453,USD,,545.4,1,USD,,540.49,,2.84,0.43,1.64,mcstandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4326098790016921,CM20000002454,mc,2021-01-05 21:37:01,CET,Settled,7426098790215798,USD,,571.85,1,USD,,560.85,,2.97,1.45,6.58,mcstandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1716098792435313,CM3000151481,directEbanking,2021-01-05 21:42:07,CET,Settled,1716098792435313,USD,,46.9,1,USD,,46.43,0.47,,,,directEbanking,380,,,,,,,,CM3000151481,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4616098795263410,CM3000151483,directEbanking,2021-01-05 21:46:44,CET,Settled,4616098795263410,USD,,76.85,1,USD,,76.08,0.77,,,,directEbanking,380,,,,,,,,CM3000151483,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1316098795574641,CM1000028889,ideal,2021-01-05 21:47:20,CET,Settled,1316098795574641,USD,,46.45,1,USD,,46.2,0.25,,,,idealrabobank,380,,,,,,,,CM1000028889,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1656098797179298,CM1000028890,visa,2021-01-05 21:52:25,CET,Settled,7659098799454966,USD,,430.95,1,USD,,427.2,,2.24,0.22,1.29,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1319098800205881,CM2000162940,ideal,2021-01-05 21:54:35,CET,Settled,1319098800205881,USD,,465.4,1,USD,,465.15,0.25,,,,idealrabobank,380,,,,,,,,CM2000162940,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4616098801919765,CM1000028891,ideal,2021-01-05 22:00:31,CET,Settled,4616098801919765,USD,,51.5,1,USD,,51.25,0.25,,,,idealrabobank,380,,,,,,,,CM1000028891,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4616098803860017,CM7000054331,cartebancaire,2021-01-05 22:02:17,CET,Settled,7416098804619375,USD,,292.65,1,USD,,290.3,,1.76,,0.59,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1559098804925731,CM2000162941,mc,2021-01-05 22:02:37,CET,Settled,7759098805570275,USD,,549.85,1,USD,,543.95,,2.86,1.39,1.65,mcsuperpremiumcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,6119098806188504,CM2000162942,ideal,2021-01-05 22:04:04,CET,Settled,6119098806188504,USD,,46.45,1,USD,,46.2,0.25,,,,idealing,380,,,,,,,,CM2000162942,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4756098806465830,CM2000162943,mc,2021-01-05 22:04:52,CET,Settled,7759098806920040,USD,,565.8,1,USD,,560.72,,2.94,0.44,1.7,mcstandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,6129098808836025,CM7000054332,visa,2021-01-05 22:08:11,CET,Settled,7426098808917344,USD,,39.9,1,USD,,39.53,,0.21,0.04,0.12,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1856098811821154,CM1000028893,bcmc_mobile,2021-01-05 22:13:17,CET,Settled,7559098811975692,USD,,106.35,1,USD,,105.64,,0.64,0.02,0.05,bcmc_mobile,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4816098813893485,CM7000054333,cartebancaire,2021-01-05 22:17:44,CET,Settled,7916098813938027,USD,,34.95,1,USD,,34.66,,0.21,0.01,0.07,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4816098814282134,CM3000151485,directEbanking,2021-01-05 22:18:11,CET,Settled,4816098814282134,USD,,21.95,1,USD,,21.73,0.22,,,,directEbanking,380,,,,,,,,CM3000151485,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1819098817721438,CM1000028894,ideal,2021-01-05 22:23:31,CET,Settled,1819098817721438,USD,,46.4,1,USD,,46.15,0.25,,,,idealabn,380,,,,,,,,CM1000028894,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4616098818309976,CM1000028895,ideal,2021-01-05 22:24:18,CET,Settled,4616098818309976,USD,,475.5,1,USD,,475.25,0.25,,,,idealrabobank,380,,,,,,,,CM1000028895,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4536098821511581,CM17000002643,visa,2021-01-05 22:29:54,CET,Settled,7439098821946028,USD,,54.83,1,USD,,53.5,,0.29,0.22,0.82,visastandardcredit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4336098821908037,CM3000151488,visa,2021-01-05 22:30:26,CET,Settled,7439098822262839,USD,,515.9,1,USD,,508.59,,2.68,0.16,4.47,visacommercialdebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1556098823411971,CM3000151489,mc,2021-01-05 22:32:32,CET,Settled,7559098823527952,USD,,30.9,1,USD,,30.64,,0.16,0.04,0.06,mcstandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1716098823386675,CM7000054335,cartebancaire,2021-01-05 22:34:43,CET,Settled,7916098824278629,USD,,79.85,1,USD,,79.2,,0.48,0.01,0.16,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1816098825320671,CM1000028896,ideal,2021-01-05 22:35:55,CET,Settled,1816098825320671,USD,,86.4,1,USD,,86.15,0.25,,,,idealbunq,380,,,,,,,,CM1000028896,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1619097571396090,CM1000028824,bankTransfer_BE,2021-01-05 22:36:22,CET,Settled,1619097571396090,USD,,475.5,1,USD,,475.3,0.2,,,,bankTransfer_BE,380,,,,,,,,CM1000028824,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4816098610331689,CM2000161980,bankTransfer_BE,2021-01-05 22:36:24,CET,Settled,4816098610331689,USD,,508.45,1,USD,,508.25,0.2,,,,bankTransfer_BE,380,,,,,,,,CM2000161980,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4516098825789542,CM7000054337,cartebancaire,2021-01-05 22:38:10,CET,Settled,7916098826286190,USD,,122.75,1,USD,,121.76,,0.74,,0.25,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4546098829397452,CM2000162949,visadankort,2021-01-05 22:43:05,CET,Settled,7549098829859485,USD,,195.8,1,USD,,194.32,,1.02,0.07,0.39,visadankort,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1616096929077843,CM3000151232,bankTransfer_DE,2021-01-05 22:46:23,CET,Settled,1616096929077843,USD,,96.9,1,USD,,96.7,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000151232,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1716097806581567,CM3000151330,bankTransfer_DE,2021-01-05 22:46:25,CET,Settled,1716097806581567,USD,,136.9,1,USD,,136.7,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000151330,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4616093212265072,CM3000150712,bankTransfer_DE,2021-01-05 22:46:27,CET,Settled,4616093212265072,USD,,495.9,1,USD,,495.7,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000150712,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4716094450448429,CM2000162667,bankTransfer_DE,2021-01-05 22:46:27,CET,Settled,4716094450448429,USD,,475.95,1,USD,,475.75,0.2,,,,bankTransfer_DE,380,,,,,,,,CM2000162667,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4716092564273816,CM3000150631,bankTransfer_DE,2021-01-05 22:46:27,CET,Settled,4716092564273816,USD,,505.9,1,USD,,505.7,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000150631,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1719095902053041,CM3000151134,bankTransfer_DE,2021-01-05 22:46:29,CET,Settled,1719095902053041,USD,,21.95,1,USD,,21.75,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000151134,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1619097829807427,CM3000151335,bankTransfer_DE,2021-01-05 22:46:35,CET,Settled,1619097829807427,USD,,71.95,1,USD,,71.75,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000151335,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1519094275551766,CM3000150968,bankTransfer_DE,2021-01-05 22:46:36,CET,Settled,1519094275551766,USD,,475.95,1,USD,,475.75,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000150968,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1719094299491781,CM3000150979,bankTransfer_DE,2021-01-05 22:46:42,CET,Settled,1719094299491781,USD,,66.95,1,USD,,66.75,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000150979,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1716096664441385,CM3000151192,bankTransfer_DE,2021-01-05 22:46:59,CET,Settled,1716096664441385,USD,,26.95,1,USD,,26.75,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000151192,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1716097985927094,CM3000151359,bankTransfer_DE,2021-01-05 22:47:01,CET,Settled,1716097985927094,USD,,46.85,1,USD,,46.65,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000151359,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,6219093569570674,CM3000150828,bankTransfer_DE,2021-01-05 22:47:07,CET,Settled,6219093569570674,USD,,91.95,1,USD,,91.75,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000150828,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1619093490920383,CM3000150799,bankTransfer_DE,2021-01-05 22:47:17,CET,Settled,1619093490920383,USD,,465.9,1,USD,,465.7,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000150799,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4716095921917579,CM2000162731,bankTransfer_DE,2021-01-05 22:47:17,CET,Settled,4716095921917579,USD,,515.9,1,USD,,515.7,0.2,,,,bankTransfer_DE,380,,,,,,,,CM2000162731,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1316093661120967,CM2000162609,bankTransfer_DE,2021-01-05 22:47:17,CET,Settled,1316093661120967,USD,,515.9,1,USD,,515.7,0.2,,,,bankTransfer_DE,380,,,,,,,,CM2000162609,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1619094172177714,CM3000150919,bankTransfer_DE,2021-01-05 22:47:17,CET,Settled,1619094172177714,USD,,515.9,1,USD,,515.7,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000150919,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4816091036379804,CM3000150388,bankTransfer_DE,2021-01-05 22:47:18,CET,Settled,4816091036379804,USD,,545.85,1,USD,,545.65,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000150388,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1316095879575577,CM3000151130,bankTransfer_DE,2021-01-05 22:47:18,CET,Settled,1316095879575577,USD,,545.85,1,USD,,545.65,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000151130,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1619098212250449,CM3000151364,bankTransfer_DE,2021-01-05 22:47:20,CET,Settled,1619098212250449,USD,,26.9,1,USD,,26.7,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000151364,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1719082373216828,CM3000149556,bankTransfer_DE,2021-01-05 22:47:24,CET,Settled,1719082373216828,USD,,568.8,1,USD,,568.6,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000149556,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1316098329962825,CM2000162872,bankTransfer_DE,2021-01-05 22:47:28,CET,Settled,1316098329962825,USD,,475.95,1,USD,,475.75,0.2,,,,bankTransfer_DE,380,,,,,,,,CM2000162872,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1519098366040068,CM3000151379,bankTransfer_DE,2021-01-05 22:47:29,CET,Settled,1519098366040068,USD,,61.95,1,USD,,61.75,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000151379,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1319097730887677,CM2000162825,bankTransfer_DE,2021-01-05 22:47:30,CET,Settled,1319097730887677,USD,,475.95,1,USD,,475.75,0.2,,,,bankTransfer_DE,380,,,,,,,,CM2000162825,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1716096125751471,CM3000151166,bankTransfer_DE,2021-01-05 22:47:31,CET,Settled,1716096125751471,USD,,56.85,1,USD,,56.65,0.2,,,,bankTransfer_DE,380,,,,,,,,CM3000151166,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,6119097984495277,CM2000162849,bankTransfer_DE,2021-01-05 22:47:33,CET,Settled,6119097984495277,USD,,535.85,1,USD,,535.65,0.2,,,,bankTransfer_DE,380,,,,,,,,CM2000162849,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1519098833255984,CM7000054339,cartebancaire,2021-01-05 22:50:00,CET,Settled,2319098833417964,USD,,49.85,1,USD,,49.44,,0.3,0.01,0.1,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1316098837274028,CM7000054340,cartebancaire,2021-01-05 22:57:54,CET,Settled,7416098837989761,USD,,548.85,1,USD,,544.45,,3.29,0.01,1.1,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1719098838185283,CM7000054341,cartebancaire,2021-01-05 22:58:55,CET,Settled,2319098838335674,USD,,49.9,1,USD,,49.49,,0.3,0.01,0.1,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1319098839578639,CM1000028898,ideal,2021-01-05 22:59:38,CET,Settled,1319098839578639,USD,,21.5,1,USD,,21.25,0.25,,,,idealing,380,,,,,,,,CM1000028898,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1719098840081330,CM7000054342,cartebancaire,2021-01-05 23:02:06,CET,Settled,7419098840236918,USD,,29.9,1,USD,,29.65,,0.18,0.01,0.06,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1319098843583005,CM7000054343,cartebancaire,2021-01-05 23:08:26,CET,Settled,2316098844388595,USD,,79.85,1,USD,,79.12,,0.48,0.01,0.24,cartebancaire,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,1729098847315127,CM2000162952,mc,2021-01-05 23:13:50,CET,Settled,7426098848302184,USD,,469.9,1,USD,,465.7,,2.44,0.82,0.94,mcstandarddebit,380,,,,,,,,,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,4716098852529991,CM3000151494,directEbanking,2021-01-05 23:22:58,CET,Settled,4716098852529991,USD,,445.9,1,USD,,441.44,4.46,,,,directEbanking,380,,,,,,,,CM3000151494,2021-01-06 23:50:00,CET +CompanyNL,YOURCOMPANY_ACCOUNT,,,,2021-01-07 00:46:38,CET,Fee,Transaction Fees January 05 2021,,,,,USD,38.1,,,,,,,380,,,,,,,,,2021-01-07 00:46:38,CET +CompanyNL,YOURCOMPANY_ACCOUNT,,,,2021-01-07 00:46:38,CET,MerchantPayout,"TX635094301XT batch 380, YOURCOMPANY_ACCOUNT",,,,,USD,55308.07,,,,,,,380,,,,,,,,,2021-01-07 00:46:38,CET diff --git a/account_bank_statement_import_adyen/tests/test_import_adyen.py b/account_bank_statement_import_adyen/tests/test_import_adyen.py index ca75591a..66488ba2 100644 --- a/account_bank_statement_import_adyen/tests/test_import_adyen.py +++ b/account_bank_statement_import_adyen/tests/test_import_adyen.py @@ -50,11 +50,28 @@ class TestImportAdyen(SavepointCase): def test_03_import_adyen_invalid(self): """ Trying to hit that coverall target """ - with self.assertRaisesRegex(UserError, "Could not make sense"): + with self.assertRaisesRegex(UserError, "not a Adyen settlement details file"): self._test_statement_import( "adyen_test_invalid.xls", "invalid", ) + def test_04_import_adyen_csv(self): + """ Test that the Adyen statement can be imported in csv format.""" + self._test_statement_import( + "settlement_detail_report_batch_380.csv", "YOURCOMPANY_ACCOUNT 2021/380", + ) + statement = self.env["account.bank.statement"].search( + [], order="create_date desc", limit=1 + ) + self.assertEqual(statement.journal_id, self.journal) + # Csv lines has 229 lines. Minus 1 header. Plus 1 extra transaction line. + self.assertEqual(len(statement.line_ids), 229) + self.assertTrue( + self.env.user.company_id.currency_id.is_zero( + sum(line.amount for line in statement.line_ids) + ) + ) + def _test_statement_import(self, file_name, statement_name): """Test correct creation of single statement.""" testfile = get_module_resource( @@ -63,12 +80,12 @@ class TestImportAdyen(SavepointCase): with open(testfile, "rb") as datafile: data_file = base64.b64encode(datafile.read()) import_wizard = self.env["account.bank.statement.import"].create( - {"attachment_ids": [(0, 0, {"name": "test file", "datas": data_file})]} + {"attachment_ids": [(0, 0, {"name": file_name, "datas": data_file})]} ) import_wizard.with_context( {"account_bank_statement_import_adyen": True} ).import_file() - # statement name is account number + '-' + date of last line: + # statement name is account number + '-' + date of last line. statements = self.env["account.bank.statement"].search( [("name", "=", statement_name)] ) diff --git a/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py b/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py index 62a9babd..f938d7f1 100644 --- a/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py +++ b/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py @@ -38,16 +38,11 @@ class OnlineBankStatementProvider(models.Model): date_since, date_until ) for provider in adyen_providers: - # TODO: incrementing batch number is_scheduled = self.env.context.get("scheduled") try: - data_file = self._adyen_get_settlement_details_file() + data_file, filename = self._adyen_get_settlement_details_file() import_wizard = self.env["account.bank.statement.import"].create( - { - "attachment_ids": [ - (0, 0, {"name": "test file", "datas": data_file}) - ] - } + {"attachment_ids": [(0, 0, {"name": filename, "datas": data_file})]} ) import_wizard.with_context( {"account_bank_statement_import_adyen": True} @@ -84,13 +79,13 @@ class OnlineBankStatementProvider(models.Model): [YourMerchantAccount]/[ReportFileName]" """ batch_number = self.next_batch_number - download_file_name = self.download_file_name % batch_number + filename = self.download_file_name % batch_number URL = "/".join( - [self.api_base, self.journal_id.adyen_merchant_account, download_file_name] + [self.api_base, self.journal_id.adyen_merchant_account, filename] ) response = requests.get(URL, auth=(self.username, self.password)) if response.status_code == 200: - return response.content + return response.content, filename else: raise UserError(_("%s \n\n %s") % (response.status_code, response.text)) diff --git a/account_bank_statement_import_online_adyen/tests/online_bank_statement_provider_dummy.py b/account_bank_statement_import_online_adyen/tests/online_bank_statement_provider_dummy.py index 9c42c0aa..39adbe37 100644 --- a/account_bank_statement_import_online_adyen/tests/online_bank_statement_provider_dummy.py +++ b/account_bank_statement_import_online_adyen/tests/online_bank_statement_provider_dummy.py @@ -14,10 +14,11 @@ class OnlineBankStatementProviderDummy(models.Model): if self.service != "dummy_adyen": # Not a dummy, get the regular adyen method. return super()._adyen_get_settlement_details_file() + filename = self.download_file_name testfile = get_module_resource( - "account_bank_statement_import_adyen", "test_files", self.download_file_name + "account_bank_statement_import_adyen", "test_files", filename ) with open(testfile, "rb") as datafile: data_file = datafile.read() data_file = base64.b64encode(data_file) - return data_file + return data_file, filename diff --git a/account_bank_statement_import_online_adyen/tests/test_import_online.py b/account_bank_statement_import_online_adyen/tests/test_import_online.py index 71c0d765..9921e51f 100644 --- a/account_bank_statement_import_online_adyen/tests/test_import_online.py +++ b/account_bank_statement_import_online_adyen/tests/test_import_online.py @@ -54,7 +54,7 @@ class TestImportOnline(TestImportAdyen): # Pull from yesterday, until today yesterday = self.now - relativedelta(days=1) provider.with_context(scheduled=True)._pull(yesterday, self.now) - # statement name is account number + '-' + date of last line: + # statement name is account number + '-' + date of last line. statements = self.env["account.bank.statement"].search( [("name", "=", statement_name)] ) From d179f15f4bb38ce374d7402557197e481048dd8a Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Thu, 16 Dec 2021 15:49:24 +0100 Subject: [PATCH 12/24] [FIX] *_online_adyen: correct icon --- .../static/description/icon.png | Bin 11070 -> 2720 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/account_bank_statement_import_online_adyen/static/description/icon.png b/account_bank_statement_import_online_adyen/static/description/icon.png index 09847ed7696bcad441aac277011add9a9a82c01d..716dbd37cca93ec73ce2f21cad007f2ee8ab9ed3 100644 GIT binary patch literal 2720 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 From 20d37acfa6936ee57ed97c0d2b622450c7c4ae98 Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Thu, 16 Dec 2021 17:29:45 +0100 Subject: [PATCH 13/24] [FIX] *_online_adyen: make sure downloaded file contains valid base64 encode bytes --- .../models/online_bank_statement_provider.py | 14 +++++++++++--- .../account_bank_statement_import_online_adyen | 1 + .../setup.py | 6 ++++++ 3 files changed, 18 insertions(+), 3 deletions(-) create mode 120000 setup/account_bank_statement_import_online_adyen/odoo/addons/account_bank_statement_import_online_adyen create mode 100644 setup/account_bank_statement_import_online_adyen/setup.py diff --git a/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py b/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py index f938d7f1..5d982f16 100644 --- a/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py +++ b/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py @@ -84,10 +84,18 @@ class OnlineBankStatementProvider(models.Model): [self.api_base, self.journal_id.adyen_merchant_account, filename] ) response = requests.get(URL, auth=(self.username, self.password)) - if response.status_code == 200: - return response.content, filename - else: + if response.status_code != 200: raise UserError(_("%s \n\n %s") % (response.status_code, response.text)) + # Check base64 decoding and padding of response.content. + # Remember: response.text is unicode, response.content is in bytes. + byte_count = len(response.content) + _logger.debug( + _("Retrieved %d bytes, starting with %s"), byte_count, response.text[:32] + ) + # Make sure base64 encoded content contains multiple of 4 bytes. + byte_padding = b"=" * (byte_count % 4) + data_file = response.content + byte_padding + return data_file, filename def _schedule_next_run(self): """Set next run date and autoincrement batch number.""" diff --git a/setup/account_bank_statement_import_online_adyen/odoo/addons/account_bank_statement_import_online_adyen b/setup/account_bank_statement_import_online_adyen/odoo/addons/account_bank_statement_import_online_adyen new file mode 120000 index 00000000..fa446601 --- /dev/null +++ b/setup/account_bank_statement_import_online_adyen/odoo/addons/account_bank_statement_import_online_adyen @@ -0,0 +1 @@ +../../../../account_bank_statement_import_online_adyen \ No newline at end of file diff --git a/setup/account_bank_statement_import_online_adyen/setup.py b/setup/account_bank_statement_import_online_adyen/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/account_bank_statement_import_online_adyen/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From f780815b8ee4ce2705e207a0b49895aed25a8cb9 Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Fri, 17 Dec 2021 10:08:36 +0100 Subject: [PATCH 14/24] [IMP] *_online_adyen: more debugging on retrieved statement --- .../models/online_bank_statement_provider.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py b/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py index 5d982f16..99f73aa9 100644 --- a/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py +++ b/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py @@ -88,9 +88,17 @@ class OnlineBankStatementProvider(models.Model): raise UserError(_("%s \n\n %s") % (response.status_code, response.text)) # Check base64 decoding and padding of response.content. # Remember: response.text is unicode, response.content is in bytes. + text_count = len(response.text) + _logger.debug( + _("Retrieved %d length text from Adyen, starting with %s"), + text_count, + response.text[:64], + ) byte_count = len(response.content) _logger.debug( - _("Retrieved %d bytes, starting with %s"), byte_count, response.text[:32] + _("Retrieved %d bytes from Adyen, starting with %s"), + byte_count, + response.content[:64], ) # Make sure base64 encoded content contains multiple of 4 bytes. byte_padding = b"=" * (byte_count % 4) From 318c67112b8b9f8a0fae6540b241cd48f4c56022 Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Fri, 17 Dec 2021 12:17:46 +0100 Subject: [PATCH 15/24] [IMP] *_online_adyen base64 encode retrieved file --- .../models/online_bank_statement_provider.py | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py b/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py index 99f73aa9..04fbb1cc 100644 --- a/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py +++ b/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py @@ -1,5 +1,6 @@ # Copyright 2021 Therp BV . # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import base64 import logging from html import escape @@ -40,9 +41,9 @@ class OnlineBankStatementProvider(models.Model): for provider in adyen_providers: is_scheduled = self.env.context.get("scheduled") try: - data_file, filename = self._adyen_get_settlement_details_file() + attachment_vals = self._get_attachment_vals() import_wizard = self.env["account.bank.statement.import"].create( - {"attachment_ids": [(0, 0, {"name": filename, "datas": data_file})]} + {"attachment_ids": [(0, 0, attachment_vals)]} ) import_wizard.with_context( {"account_bank_statement_import_adyen": True} @@ -67,6 +68,17 @@ class OnlineBankStatementProvider(models.Model): if is_scheduled: provider._schedule_next_run() + def _get_attachment_vals(self): + """Retrieve settlement details and convert to attachment vals.""" + content, filename = self._adyen_get_settlement_details_file() + encoded_content = base64.encodebytes(content) + # Make sure base64 encoded content contains multiple of 4 bytes. + byte_count = len(encoded_content) + byte_padding = b"=" * (byte_count % 4) + data_file = encoded_content + byte_padding + attachment_vals = {"name": filename, "datas": data_file} + return attachment_vals + def _adyen_get_settlement_details_file(self): """Retrieve daily generated settlement details file. @@ -86,6 +98,7 @@ class OnlineBankStatementProvider(models.Model): response = requests.get(URL, auth=(self.username, self.password)) if response.status_code != 200: raise UserError(_("%s \n\n %s") % (response.status_code, response.text)) + _logger.debug(_("Headers returned by Adyen %s"), response.headers) # Check base64 decoding and padding of response.content. # Remember: response.text is unicode, response.content is in bytes. text_count = len(response.text) @@ -100,10 +113,7 @@ class OnlineBankStatementProvider(models.Model): byte_count, response.content[:64], ) - # Make sure base64 encoded content contains multiple of 4 bytes. - byte_padding = b"=" * (byte_count % 4) - data_file = response.content + byte_padding - return data_file, filename + return response.content, filename def _schedule_next_run(self): """Set next run date and autoincrement batch number.""" From cab6718eec72151a217c1a1413ff455311332dfd Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Fri, 25 Feb 2022 09:31:44 +0100 Subject: [PATCH 16/24] [FIX] *adyen*: support xlsx as well as csv --- .../models/account_bank_statement_import.py | 54 ++++++++++++------- .../models/online_bank_statement_provider.py | 12 ++--- 2 files changed, 39 insertions(+), 27 deletions(-) diff --git a/account_bank_statement_import_adyen/models/account_bank_statement_import.py b/account_bank_statement_import_adyen/models/account_bank_statement_import.py index 649d882a..64a4870e 100644 --- a/account_bank_statement_import_adyen/models/account_bank_statement_import.py +++ b/account_bank_statement_import_adyen/models/account_bank_statement_import.py @@ -1,12 +1,14 @@ # Copyright 2017 Opener BV () -# Copyright 2021 Therp BV . +# Copyright 2021-2022 Therp BV . # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +"""Add import of Adyen statements.""" +# pylint: disable=protected-access,no-self-use import logging from odoo import _, fields, models from odoo.exceptions import UserError -_logger = logging.getLogger(__name__) +_logger = logging.getLogger(__name__) # pylint: disable=invalid-name COLUMNS = { "Company Account": 1, @@ -43,6 +45,8 @@ COLUMNS = { class AccountBankStatementImport(models.TransientModel): + """Add import of Adyen statements.""" + _inherit = "account.bank.statement.import" def _parse_file(self, data_file): @@ -50,7 +54,7 @@ class AccountBankStatementImport(models.TransientModel): try: _logger.debug(_("Try parsing as Adyen settlement details.")) return self._parse_adyen_file(data_file) - except Exception: + except Exception: # pylint: disable=broad-except message = _("Statement file was not a Adyen settlement details file.") if self.env.context.get("account_bank_statement_import_adyen", False): raise UserError(message) @@ -58,7 +62,7 @@ class AccountBankStatementImport(models.TransientModel): return super()._parse_file(data_file) def _find_additional_data(self, currency_code, account_number): - """Try to find journal by Adyen merchant account.""" + """Check if journal passed in the context matches Adyen Merchant Account.""" if account_number: journal = self.env["account.journal"].search( [("adyen_merchant_account", "=", account_number)], limit=1 @@ -73,33 +77,39 @@ class AccountBankStatementImport(models.TransientModel): ) % account_number ) - self = self.with_context(journal_id=journal.id) return super()._find_additional_data(currency_code, account_number) def _parse_adyen_file(self, data_file): """Parse file assuming it is an Adyen file. - An Excception will be thrown if file cannot be parsed. + An Exception will be thrown if file cannot be parsed. """ statement = None headers = False + batch_number = False fees = 0.0 balance = 0.0 payout = 0.0 rows = self._get_rows(data_file) + num_rows = 0 for row in rows: + num_rows += 1 + if not row[1]: + continue + if not headers: + on_header_row = self._check_header_row(row) + if not on_header_row: + continue + self._set_columns(row) + headers = True + continue if len(row) < 24: raise ValueError( "Not an Adyen statement. Unexpected row length %s " "less then minimum of 24" % len(row) ) - if not row[1]: - continue - if not headers: - self._set_columns(row) - headers = True - continue if not statement: + batch_number = self._get_value(row, "Batch Number") statement = self._make_statement(row) currency_code = self._get_value(row, "Net Currency") merchant_id = self._get_value(row, "Merchant Account") @@ -112,12 +122,14 @@ class AccountBankStatementImport(models.TransientModel): balance += self._balance(row) self._import_adyen_transaction(statement, row) fees += self._sum_fees(row) - if not headers: - raise ValueError("Not an Adyen statement. Did not encounter header row.") + raise ValueError( + "Not an Adyen statement. Did not encounter header row in %d rows." + % (num_rows,) + ) if fees: balance -= fees - self._add_fees_transaction(statement, fees, row) + self._add_fees_transaction(statement, fees, batch_number) if statement["transactions"] and not payout: raise UserError(_("No payout detected in Adyen statement.")) if self.env.user.company_id.currency_id.compare_amounts(balance, payout) != 0: @@ -139,6 +151,13 @@ class AccountBankStatementImport(models.TransientModel): importer = import_model.create({"file": data_file, "file_name": filename}) return importer._read_file({"quoting": '"', "separator": ","}) + def _check_header_row(self, row): + """Header row is the first one with a "Company Account" header cell.""" + for cell in row: + if cell == "Company Account": + return True + return False + def _set_columns(self, row): """Set columns from headers. There MUST be a 'Company Account' header.""" seen_company_account = False @@ -229,13 +248,12 @@ class AccountBankStatementImport(models.TransientModel): """get unique import ID for transaction.""" return statement["name"] + str(len(statement["transactions"])).zfill(4) - def _add_fees_transaction(self, statement, fees, row): + def _add_fees_transaction(self, statement, fees, batch_number): """Single transaction for all fees in statement.""" transaction = dict( unique_import_id=self._get_unique_import_id(statement), date=max(t["date"] for t in statement["transactions"]), amount=-fees, - name="Commission, markup etc. batch %s" - % self._get_value(row, "Batch Number"), + name="Commission, markup etc. batch %s" % batch_number, ) statement["transactions"].append(transaction) diff --git a/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py b/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py index 04fbb1cc..4e6ef44d 100644 --- a/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py +++ b/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py @@ -1,5 +1,6 @@ # Copyright 2021 Therp BV . # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +# pylint: disable=missing-docstring,invalid-name,protected-access import base64 import logging from html import escape @@ -52,7 +53,8 @@ class OnlineBankStatementProvider(models.Model): if is_scheduled: _logger.warning( 'Online Bank Statement Provider "%s" failed to' - " obtain statement data" % (provider.name,), + " obtain statement data", + provider.name, exc_info=True, ) provider.message_post( @@ -99,14 +101,6 @@ class OnlineBankStatementProvider(models.Model): if response.status_code != 200: raise UserError(_("%s \n\n %s") % (response.status_code, response.text)) _logger.debug(_("Headers returned by Adyen %s"), response.headers) - # Check base64 decoding and padding of response.content. - # Remember: response.text is unicode, response.content is in bytes. - text_count = len(response.text) - _logger.debug( - _("Retrieved %d length text from Adyen, starting with %s"), - text_count, - response.text[:64], - ) byte_count = len(response.content) _logger.debug( _("Retrieved %d bytes from Adyen, starting with %s"), From 2edfc66a9b3dd13d9dafcb2c2eef4d4815278039 Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Mon, 7 Mar 2022 13:29:04 +0100 Subject: [PATCH 17/24] [IMP] *import_adyen: log rows read and transaction count --- .../models/account_bank_statement_import.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/account_bank_statement_import_adyen/models/account_bank_statement_import.py b/account_bank_statement_import_adyen/models/account_bank_statement_import.py index 64a4870e..7b122799 100644 --- a/account_bank_statement_import_adyen/models/account_bank_statement_import.py +++ b/account_bank_statement_import_adyen/models/account_bank_statement_import.py @@ -92,6 +92,7 @@ class AccountBankStatementImport(models.TransientModel): payout = 0.0 rows = self._get_rows(data_file) num_rows = 0 + num_transactions = 0 for row in rows: num_rows += 1 if not row[1]: @@ -120,6 +121,7 @@ class AccountBankStatementImport(models.TransientModel): payout -= self._balance(row) else: balance += self._balance(row) + num_transactions += 1 self._import_adyen_transaction(statement, row) fees += self._sum_fees(row) if not headers: @@ -137,6 +139,11 @@ class AccountBankStatementImport(models.TransientModel): _("Parse error. Balance %s not equal to merchant " "payout %s") % (balance, payout) ) + _logger.info( + _("Processed %d rows from Adyen statement file with %d transactions"), + num_rows, + num_transactions, + ) return currency_code, merchant_id, [statement] def _get_rows(self, data_file): From 0f54cfbbe9cd57d2039e35cad44e524902a5fefb Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Thu, 17 Mar 2022 13:23:54 +0100 Subject: [PATCH 18/24] [IMP] *_import_adyen: Support statements without payout --- .../models/account_bank_statement_import.py | 5 +++- .../settlement_detail_report_batch_238.csv | 4 +++ .../tests/test_import_adyen.py | 27 +++++++++++++++++-- 3 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 account_bank_statement_import_adyen/test_files/settlement_detail_report_batch_238.csv diff --git a/account_bank_statement_import_adyen/models/account_bank_statement_import.py b/account_bank_statement_import_adyen/models/account_bank_statement_import.py index 7b122799..dffb4439 100644 --- a/account_bank_statement_import_adyen/models/account_bank_statement_import.py +++ b/account_bank_statement_import_adyen/models/account_bank_statement_import.py @@ -84,6 +84,7 @@ class AccountBankStatementImport(models.TransientModel): An Exception will be thrown if file cannot be parsed. """ + # pylint: disable=too-many-locals,too-many-branches statement = None headers = False batch_number = False @@ -133,7 +134,7 @@ class AccountBankStatementImport(models.TransientModel): balance -= fees self._add_fees_transaction(statement, fees, batch_number) if statement["transactions"] and not payout: - raise UserError(_("No payout detected in Adyen statement.")) + _logger.info(_("No payout detected in Adyen statement.")) if self.env.user.company_id.currency_id.compare_amounts(balance, payout) != 0: raise UserError( _("Parse error. Balance %s not equal to merchant " "payout %s") @@ -169,6 +170,8 @@ class AccountBankStatementImport(models.TransientModel): """Set columns from headers. There MUST be a 'Company Account' header.""" seen_company_account = False for num, header in enumerate(row): + if not header.strip(): + continue # Ignore empty columns. if header == "Company Account": seen_company_account = True if header not in COLUMNS: diff --git a/account_bank_statement_import_adyen/test_files/settlement_detail_report_batch_238.csv b/account_bank_statement_import_adyen/test_files/settlement_detail_report_batch_238.csv new file mode 100644 index 00000000..164262cc --- /dev/null +++ b/account_bank_statement_import_adyen/test_files/settlement_detail_report_batch_238.csv @@ -0,0 +1,4 @@ +Company Account,Merchant Account,Psp Reference,Merchant Reference,Payment Method,Creation Date,TimeZone,Type,Modification Reference,Gross Currency,Gross Debit (GC),Gross Credit (GC),Exchange Rate,Net Currency,Net Debit (NC),Net Credit (NC),Commission (NC),Markup (NC),Scheme Fees (NC),Interchange (NC),Payment Method Variant,Modification Merchant Reference,Batch Number,Reserved4,Reserved5,Reserved6,Reserved7,Reserved8,Reserved9,Reserved10 +CompanyNL,YOURCOMPANY_ACCOUNT,,,,2022-02-22 00:24:23,CET,Balancetransfer,Balancetransfer,,,,,EUR,,454331.99,,,,,,Balancetransfer from batch 237,238,,,,,,, +CompanyNL,YOURCOMPANY_ACCOUNT,,,,2022-03-02 02:21:49,CET,Fee,Transaction Fees March 01 2022,,,,,EUR,0.09,,,,,,,,238,,,,,,, +CompanyNL,YOURCOMPANY_ACCOUNT,,,,2022-03-02 02:21:49,CET,Balancetransfer,Balancetransfer,,,,,EUR,454331.90,,,,,,,Balancetransfer to batch 239,238,,,,,,, diff --git a/account_bank_statement_import_adyen/tests/test_import_adyen.py b/account_bank_statement_import_adyen/tests/test_import_adyen.py index 66488ba2..ed6e57b8 100644 --- a/account_bank_statement_import_adyen/tests/test_import_adyen.py +++ b/account_bank_statement_import_adyen/tests/test_import_adyen.py @@ -1,7 +1,8 @@ # Copyright 2017 Opener BV # Copyright 2020 Vanmoof BV -# Copyright 2015-2021 Therp BV ) +# Copyright 2015-2022 Therp BV ) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +"""Run test imports of Adyen files.""" import base64 from odoo.exceptions import UserError @@ -10,6 +11,8 @@ from odoo.tests.common import SavepointCase class TestImportAdyen(SavepointCase): + """Run test imports of Adyen files.""" + @classmethod def setUpClass(cls): super().setUpClass() @@ -72,6 +75,23 @@ class TestImportAdyen(SavepointCase): ) ) + def test_05_import_adyen_csv(self): + """ Test that the Adyen statement without Merchant Payoutcan be imported.""" + self._test_statement_import( + "settlement_detail_report_batch_238.csv", "YOURCOMPANY_ACCOUNT 2022/238", + ) + statement = self.env["account.bank.statement"].search( + [], order="create_date desc", limit=1 + ) + self.assertEqual(statement.journal_id, self.journal) + # Csv lines has 4 lines. Minus 1 header. No extra transaction line. + self.assertEqual(len(statement.line_ids), 3) + self.assertTrue( + self.env.user.company_id.currency_id.is_zero( + sum(line.amount for line in statement.line_ids) + ) + ) + def _test_statement_import(self, file_name, statement_name): """Test correct creation of single statement.""" testfile = get_module_resource( @@ -83,7 +103,10 @@ class TestImportAdyen(SavepointCase): {"attachment_ids": [(0, 0, {"name": file_name, "datas": data_file})]} ) import_wizard.with_context( - {"account_bank_statement_import_adyen": True} + { + "account_bank_statement_import_adyen": True, + "journal_id": self.journal.id, + } ).import_file() # statement name is account number + '-' + date of last line. statements = self.env["account.bank.statement"].search( From 88b8b572a8d801d1cdc7a57d05ffc0537e325c2a Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Thu, 17 Mar 2022 15:58:25 +0100 Subject: [PATCH 19/24] [IMP] *_online_adyen: Directly parse retrieved adyen file Prevent other statement import modules from processing the file. Without this for instance the Enterprise account_bank_statement_import_csv module will process a retrieved cv file. In addition this is more efficient as it saves a decoding step. --- .../models/online_bank_statement_provider.py | 25 +++++++++++++------ .../online_bank_statement_provider_dummy.py | 8 +++--- .../tests/test_import_online.py | 7 +++++- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py b/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py index 4e6ef44d..97d3467d 100644 --- a/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py +++ b/account_bank_statement_import_online_adyen/models/online_bank_statement_provider.py @@ -42,13 +42,7 @@ class OnlineBankStatementProvider(models.Model): for provider in adyen_providers: is_scheduled = self.env.context.get("scheduled") try: - attachment_vals = self._get_attachment_vals() - import_wizard = self.env["account.bank.statement.import"].create( - {"attachment_ids": [(0, 0, attachment_vals)]} - ) - import_wizard.with_context( - {"account_bank_statement_import_adyen": True} - ).import_file() + self._import_adyen_file() except BaseException as e: if is_scheduled: _logger.warning( @@ -70,6 +64,21 @@ class OnlineBankStatementProvider(models.Model): if is_scheduled: provider._schedule_next_run() + def _import_adyen_file(self): + """Import Adyen file using functionality from manual Adyen import module.""" + self.ensure_one() + content, attachment_vals = self._get_attachment_vals() + wizard = ( + self.env["account.bank.statement.import"] + .with_context({"journal_id": self.journal_id.id}) + .create({"attachment_ids": [(0, 0, attachment_vals)]}) + ) + currency_code, account_number, stmts_vals = wizard._parse_adyen_file(content) + wizard._check_parsed_data(stmts_vals, account_number) + _currency, journal = wizard._find_additional_data(currency_code, account_number) + stmts_vals = wizard._complete_stmts_vals(stmts_vals, journal, account_number) + wizard._create_bank_statements(stmts_vals) + def _get_attachment_vals(self): """Retrieve settlement details and convert to attachment vals.""" content, filename = self._adyen_get_settlement_details_file() @@ -79,7 +88,7 @@ class OnlineBankStatementProvider(models.Model): byte_padding = b"=" * (byte_count % 4) data_file = encoded_content + byte_padding attachment_vals = {"name": filename, "datas": data_file} - return attachment_vals + return content, attachment_vals def _adyen_get_settlement_details_file(self): """Retrieve daily generated settlement details file. diff --git a/account_bank_statement_import_online_adyen/tests/online_bank_statement_provider_dummy.py b/account_bank_statement_import_online_adyen/tests/online_bank_statement_provider_dummy.py index 39adbe37..9b028cb4 100644 --- a/account_bank_statement_import_online_adyen/tests/online_bank_statement_provider_dummy.py +++ b/account_bank_statement_import_online_adyen/tests/online_bank_statement_provider_dummy.py @@ -1,12 +1,13 @@ -# Copyright 2021 Therp BV . +# Copyright 2021-2022 Therp BV . # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). -import base64 - +"""Dummy provider gets files from file-system, instead of from api.""" from odoo import models from odoo.modules.module import get_module_resource class OnlineBankStatementProviderDummy(models.Model): + """Dummy provider gets files from file-system, instead of from api.""" + _inherit = "online.bank.statement.provider" def _adyen_get_settlement_details_file(self): @@ -20,5 +21,4 @@ class OnlineBankStatementProviderDummy(models.Model): ) with open(testfile, "rb") as datafile: data_file = datafile.read() - data_file = base64.b64encode(data_file) return data_file, filename diff --git a/account_bank_statement_import_online_adyen/tests/test_import_online.py b/account_bank_statement_import_online_adyen/tests/test_import_online.py index 9921e51f..64d42de4 100644 --- a/account_bank_statement_import_online_adyen/tests/test_import_online.py +++ b/account_bank_statement_import_online_adyen/tests/test_import_online.py @@ -1,9 +1,11 @@ -# Copyright 2021 Therp BV . +# Copyright 2021-2022 Therp BV . # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +"""Test online Adyen reusing tests for manual import.""" from dateutil.relativedelta import relativedelta from odoo import fields +# pylint: disable=import-error from odoo.addons.account_bank_statement_import_adyen.tests.test_import_adyen import ( TestImportAdyen, ) @@ -14,6 +16,8 @@ class TestImportOnline(TestImportAdyen): @classmethod def setUpClass(cls): + """Setup online journal.""" + # pylint: disable=invalid-name super().setUpClass() cls.now = fields.Datetime.now() cls.journal.write( @@ -53,6 +57,7 @@ class TestImportOnline(TestImportAdyen): ) # Pull from yesterday, until today yesterday = self.now - relativedelta(days=1) + # pylint: disable=protected-access provider.with_context(scheduled=True)._pull(yesterday, self.now) # statement name is account number + '-' + date of last line. statements = self.env["account.bank.statement"].search( From c96f2e90d9079c1bef970a57e3839a3be3ae9af9 Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Thu, 17 Mar 2022 19:39:31 +0100 Subject: [PATCH 20/24] [FIX] *adyen*: Test file currency should be same as on journal --- .../test_files/settlement_detail_report_batch_238.csv | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/account_bank_statement_import_adyen/test_files/settlement_detail_report_batch_238.csv b/account_bank_statement_import_adyen/test_files/settlement_detail_report_batch_238.csv index 164262cc..7060b966 100644 --- a/account_bank_statement_import_adyen/test_files/settlement_detail_report_batch_238.csv +++ b/account_bank_statement_import_adyen/test_files/settlement_detail_report_batch_238.csv @@ -1,4 +1,4 @@ Company Account,Merchant Account,Psp Reference,Merchant Reference,Payment Method,Creation Date,TimeZone,Type,Modification Reference,Gross Currency,Gross Debit (GC),Gross Credit (GC),Exchange Rate,Net Currency,Net Debit (NC),Net Credit (NC),Commission (NC),Markup (NC),Scheme Fees (NC),Interchange (NC),Payment Method Variant,Modification Merchant Reference,Batch Number,Reserved4,Reserved5,Reserved6,Reserved7,Reserved8,Reserved9,Reserved10 -CompanyNL,YOURCOMPANY_ACCOUNT,,,,2022-02-22 00:24:23,CET,Balancetransfer,Balancetransfer,,,,,EUR,,454331.99,,,,,,Balancetransfer from batch 237,238,,,,,,, -CompanyNL,YOURCOMPANY_ACCOUNT,,,,2022-03-02 02:21:49,CET,Fee,Transaction Fees March 01 2022,,,,,EUR,0.09,,,,,,,,238,,,,,,, -CompanyNL,YOURCOMPANY_ACCOUNT,,,,2022-03-02 02:21:49,CET,Balancetransfer,Balancetransfer,,,,,EUR,454331.90,,,,,,,Balancetransfer to batch 239,238,,,,,,, +CompanyNL,YOURCOMPANY_ACCOUNT,,,,2022-02-22 00:24:23,CET,Balancetransfer,Balancetransfer,,,,,USD,,454331.99,,,,,,Balancetransfer from batch 237,238,,,,,,, +CompanyNL,YOURCOMPANY_ACCOUNT,,,,2022-03-02 02:21:49,CET,Fee,Transaction Fees March 01 2022,,,,,USD,0.09,,,,,,,,238,,,,,,, +CompanyNL,YOURCOMPANY_ACCOUNT,,,,2022-03-02 02:21:49,CET,Balancetransfer,Balancetransfer,,,,,USD,454331.90,,,,,,,Balancetransfer to batch 239,238,,,,,,, From 71ac84018535910fe1dff28aab568feec41accd5 Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Fri, 18 Mar 2022 00:11:13 +0100 Subject: [PATCH 21/24] [FIX] *_paypal: remove external dependency csv csv is part of the standard library. Defining this as an external dependency crashes runboat --- .../models/account_bank_statement_import_paypal_parser.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/account_bank_statement_import_paypal/models/account_bank_statement_import_paypal_parser.py b/account_bank_statement_import_paypal/models/account_bank_statement_import_paypal_parser.py index 395ff706..791fda95 100644 --- a/account_bank_statement_import_paypal/models/account_bank_statement_import_paypal_parser.py +++ b/account_bank_statement_import_paypal/models/account_bank_statement_import_paypal_parser.py @@ -5,6 +5,7 @@ import itertools import logging +from csv import reader from datetime import datetime from decimal import Decimal from io import StringIO @@ -16,11 +17,6 @@ from odoo import _, api, models _logger = logging.getLogger(__name__) -try: - from csv import reader -except (ImportError, IOError) as err: - _logger.error(err) - class AccountBankStatementImportPayPalParser(models.TransientModel): _name = "account.bank.statement.import.paypal.parser" From 2d85ab34aa8d240acbda90c62acac7e89c3acf28 Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Fri, 18 Mar 2022 16:05:29 +0100 Subject: [PATCH 22/24] [IMP] *_import_adyen: more information retained Put more information in the generated statement line. Also put the for users more recognizable merchant ref in the name (Label) field, and put the psp reference in ref. With a small refactoring the information retrieved can also more easily be customized. --- .../models/account_bank_statement_import.py | 60 ++++++++++++------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/account_bank_statement_import_adyen/models/account_bank_statement_import.py b/account_bank_statement_import_adyen/models/account_bank_statement_import.py index dffb4439..c4b938e0 100644 --- a/account_bank_statement_import_adyen/models/account_bank_statement_import.py +++ b/account_bank_statement_import_adyen/models/account_bank_statement_import.py @@ -54,10 +54,10 @@ class AccountBankStatementImport(models.TransientModel): try: _logger.debug(_("Try parsing as Adyen settlement details.")) return self._parse_adyen_file(data_file) - except Exception: # pylint: disable=broad-except + except Exception as exc: # pylint: disable=broad-except message = _("Statement file was not a Adyen settlement details file.") if self.env.context.get("account_bank_statement_import_adyen", False): - raise UserError(message) + raise UserError(message) from exc _logger.debug(message, exc_info=True) return super()._parse_file(data_file) @@ -123,7 +123,9 @@ class AccountBankStatementImport(models.TransientModel): else: balance += self._balance(row) num_transactions += 1 - self._import_adyen_transaction(statement, row) + transaction = self._get_adyen_transaction(row) + transaction["unique_import_id"] = self._get_unique_import_id(statement) + statement["transactions"].append(transaction) fees += self._sum_fees(row) if not headers: raise ValueError( @@ -233,26 +235,40 @@ class AccountBankStatementImport(models.TransientModel): amount += float(value) return amount - def _import_adyen_transaction(self, statement, row): - """Add transaction from row to statements.""" - transaction = dict( - unique_import_id=self._get_unique_import_id(statement), - date=self._get_transaction_date(row), - amount=self._balance(row), - note="{} {} {} {}".format( - self._get_value(row, "Merchant Account"), - self._get_value(row, "Psp Reference"), - self._get_value(row, "Merchant Reference"), - self._get_value(row, "Payment Method Variant"), - ), - name="%s" - % ( - self._get_value(row, "Psp Reference") - or self._get_value(row, "Merchant Reference") - or self._get_value(row, "Modification Reference") - ), + def _get_adyen_transaction(self, row): + """Get transaction from row. + + This can easily be overwritten in custom modules to add extra information. + """ + merchant_account = self._get_value(row, "Merchant Account") + psp_reference = self._get_value(row, "Psp Reference") + merchant_reference = self._get_value(row, "Merchant Reference") + payment_method = self._get_value(row, "Payment Method Variant") + modification_reference = self._get_value(row, "Modification Reference") + transaction = { + "date": self._get_transaction_date(row), + "amount": self._balance(row), + } + transaction["note"] = " ".join( + [ + part + for part in [ + merchant_account, + psp_reference, + merchant_reference, + payment_method, + ] + if part + ] ) - statement["transactions"].append(transaction) + transaction["name"] = ( + merchant_reference or psp_reference or modification_reference + ) + transaction["ref"] = ( + psp_reference or modification_reference or merchant_reference + ) + transaction["transaction_type"] = self._get_value(row, "Type") + return transaction def _get_unique_import_id(self, statement): """get unique import ID for transaction.""" From a3f584779116736ecad9fe791edfda827934cfef Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Mon, 28 Mar 2022 12:46:39 +0200 Subject: [PATCH 23/24] [IMP] *_adyen: Check merchant account as early as possible --- .../models/account_bank_statement_import.py | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/account_bank_statement_import_adyen/models/account_bank_statement_import.py b/account_bank_statement_import_adyen/models/account_bank_statement_import.py index c4b938e0..ea3674b3 100644 --- a/account_bank_statement_import_adyen/models/account_bank_statement_import.py +++ b/account_bank_statement_import_adyen/models/account_bank_statement_import.py @@ -61,24 +61,6 @@ class AccountBankStatementImport(models.TransientModel): _logger.debug(message, exc_info=True) return super()._parse_file(data_file) - def _find_additional_data(self, currency_code, account_number): - """Check if journal passed in the context matches Adyen Merchant Account.""" - if account_number: - journal = self.env["account.journal"].search( - [("adyen_merchant_account", "=", account_number)], limit=1 - ) - if journal: - if self._context.get("journal_id", journal.id) != journal.id: - raise UserError( - _( - "Selected journal Merchant Account does not match " - "the import file Merchant Account " - "column: %s" - ) - % account_number - ) - return super()._find_additional_data(currency_code, account_number) - def _parse_adyen_file(self, data_file): """Parse file assuming it is an Adyen file. @@ -111,10 +93,11 @@ class AccountBankStatementImport(models.TransientModel): "less then minimum of 24" % len(row) ) if not statement: + merchant_account = self._get_value(row, "Merchant Account") + self._validate_merchant_account(merchant_account) batch_number = self._get_value(row, "Batch Number") statement = self._make_statement(row) currency_code = self._get_value(row, "Net Currency") - merchant_id = self._get_value(row, "Merchant Account") else: self._update_statement(statement, row) row_type = self._get_value(row, "Type").strip() @@ -147,7 +130,26 @@ class AccountBankStatementImport(models.TransientModel): num_rows, num_transactions, ) - return currency_code, merchant_id, [statement] + return currency_code, merchant_account, [statement] + + def _validate_merchant_account(self, merchant_account): + """Check wether merchant account exist, and belongs to the correct journal.""" + journal = self.env["account.journal"].search( + [("adyen_merchant_account", "=", merchant_account)], limit=1 + ) + if not journal: + raise UserError( + _("No journal refers to Merchant Account %s") % merchant_account + ) + if self._context.get("journal_id", journal.id) != journal.id: + raise UserError( + _( + "Selected journal Merchant Account does not match " + "the import file Merchant Account " + "column: %s" + ) + % merchant_account + ) def _get_rows(self, data_file): """Get rows from data_file.""" From 7a4da01ea05b15b6152ce51953935223c6a0b0f9 Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Mon, 28 Mar 2022 17:58:54 +0200 Subject: [PATCH 24/24] [RFR] *_import_adyen: refactor Move parsing code to separate class; Break up large and unwieldy parsing method in separate methods. --- .../__manifest__.py | 2 +- .../__manifest__.py | 2 +- .../models/__init__.py | 1 + .../models/account_bank_statement_import.py | 250 +-------------- ...ount_bank_statement_import_adyen_parser.py | 286 ++++++++++++++++++ 5 files changed, 294 insertions(+), 247 deletions(-) create mode 100644 account_bank_statement_import_adyen/models/account_bank_statement_import_adyen_parser.py diff --git a/account_bank_statement_clearing_account/__manifest__.py b/account_bank_statement_clearing_account/__manifest__.py index 91148a50..d5269800 100644 --- a/account_bank_statement_clearing_account/__manifest__.py +++ b/account_bank_statement_clearing_account/__manifest__.py @@ -6,7 +6,7 @@ "version": "13.0.1.0.0", "author": "Opener B.V., Vanmoof BV, Odoo Community Association (OCA)", "category": "Banking addons", - "website": "https://github.com/oca/bank-statement-import", + "website": "https://github.com/OCA/bank-statement-import", "license": "AGPL-3", "depends": ["account"], "installable": True, diff --git a/account_bank_statement_import_adyen/__manifest__.py b/account_bank_statement_import_adyen/__manifest__.py index bc24a336..91064694 100644 --- a/account_bank_statement_import_adyen/__manifest__.py +++ b/account_bank_statement_import_adyen/__manifest__.py @@ -7,7 +7,7 @@ "version": "13.0.1.0.0", "author": "Opener BV, Vanmoof BV, Odoo Community Association (OCA)", "category": "Banking addons", - "website": "https://github.com/oca/bank-statement-import", + "website": "https://github.com/OCA/bank-statement-import", "license": "AGPL-3", "depends": [ "base_import", diff --git a/account_bank_statement_import_adyen/models/__init__.py b/account_bank_statement_import_adyen/models/__init__.py index ba1f4934..7ce2f087 100644 --- a/account_bank_statement_import_adyen/models/__init__.py +++ b/account_bank_statement_import_adyen/models/__init__.py @@ -1,2 +1,3 @@ from . import account_bank_statement_import +from . import account_bank_statement_import_adyen_parser from . import account_journal diff --git a/account_bank_statement_import_adyen/models/account_bank_statement_import.py b/account_bank_statement_import_adyen/models/account_bank_statement_import.py index ea3674b3..0e232a33 100644 --- a/account_bank_statement_import_adyen/models/account_bank_statement_import.py +++ b/account_bank_statement_import_adyen/models/account_bank_statement_import.py @@ -5,44 +5,11 @@ # pylint: disable=protected-access,no-self-use import logging -from odoo import _, fields, models +from odoo import _, models from odoo.exceptions import UserError _logger = logging.getLogger(__name__) # pylint: disable=invalid-name -COLUMNS = { - "Company Account": 1, - "Merchant Account": 2, - "Psp Reference": 3, - "Merchant Reference": 4, - "Payment Method": 5, # Not used at present - "Creation Date": 6, - "TimeZone": 7, # Not used at present - "Type": 8, - "Modification Reference": 9, - "Gross Currency": 10, # Not used at present - "Gross Debit (GC)": 11, # Not used at present - "Gross Credit (GC)": 12, # Not used at present - "Exchange Rate": 13, # Not used at present - "Net Currency": 14, - "Net Debit (NC)": 15, # Fee or Merchant Payout - "Net Credit (NC)": 16, - "Commission (NC)": 17, - "Markup (NC)": 18, - "Scheme Fees (NC)": 19, - "Interchange (NC)": 20, - "Payment Method Variant": 21, - "Modification Merchant Reference": 22, # Not used at present - "Batch Number": 23, - "Reserved4": 24, # Not used at present - "Reserved5": 25, # Not used at present - "Reserved6": 26, # Not used at present - "Reserved7": 27, # Not used at present - "Reserved8": 28, # Not used at present - "Reserved9": 29, # Not used at present - "Reserved10": 30, # Not used at present -} - class AccountBankStatementImport(models.TransientModel): """Add import of Adyen statements.""" @@ -52,7 +19,6 @@ class AccountBankStatementImport(models.TransientModel): def _parse_file(self, data_file): """Parse an Adyen xlsx file and map merchant account strings to journals.""" try: - _logger.debug(_("Try parsing as Adyen settlement details.")) return self._parse_adyen_file(data_file) except Exception as exc: # pylint: disable=broad-except message = _("Statement file was not a Adyen settlement details file.") @@ -62,94 +28,11 @@ class AccountBankStatementImport(models.TransientModel): return super()._parse_file(data_file) def _parse_adyen_file(self, data_file): - """Parse file assuming it is an Adyen file. - - An Exception will be thrown if file cannot be parsed. - """ - # pylint: disable=too-many-locals,too-many-branches - statement = None - headers = False - batch_number = False - fees = 0.0 - balance = 0.0 - payout = 0.0 + """Just parse the adyen file.""" + _logger.debug(_("Try parsing as Adyen settlement details.")) + parser = self.env["account.bank.statement.import.adyen.parser"] rows = self._get_rows(data_file) - num_rows = 0 - num_transactions = 0 - for row in rows: - num_rows += 1 - if not row[1]: - continue - if not headers: - on_header_row = self._check_header_row(row) - if not on_header_row: - continue - self._set_columns(row) - headers = True - continue - if len(row) < 24: - raise ValueError( - "Not an Adyen statement. Unexpected row length %s " - "less then minimum of 24" % len(row) - ) - if not statement: - merchant_account = self._get_value(row, "Merchant Account") - self._validate_merchant_account(merchant_account) - batch_number = self._get_value(row, "Batch Number") - statement = self._make_statement(row) - currency_code = self._get_value(row, "Net Currency") - else: - self._update_statement(statement, row) - row_type = self._get_value(row, "Type").strip() - if row_type == "MerchantPayout": - payout -= self._balance(row) - else: - balance += self._balance(row) - num_transactions += 1 - transaction = self._get_adyen_transaction(row) - transaction["unique_import_id"] = self._get_unique_import_id(statement) - statement["transactions"].append(transaction) - fees += self._sum_fees(row) - if not headers: - raise ValueError( - "Not an Adyen statement. Did not encounter header row in %d rows." - % (num_rows,) - ) - if fees: - balance -= fees - self._add_fees_transaction(statement, fees, batch_number) - if statement["transactions"] and not payout: - _logger.info(_("No payout detected in Adyen statement.")) - if self.env.user.company_id.currency_id.compare_amounts(balance, payout) != 0: - raise UserError( - _("Parse error. Balance %s not equal to merchant " "payout %s") - % (balance, payout) - ) - _logger.info( - _("Processed %d rows from Adyen statement file with %d transactions"), - num_rows, - num_transactions, - ) - return currency_code, merchant_account, [statement] - - def _validate_merchant_account(self, merchant_account): - """Check wether merchant account exist, and belongs to the correct journal.""" - journal = self.env["account.journal"].search( - [("adyen_merchant_account", "=", merchant_account)], limit=1 - ) - if not journal: - raise UserError( - _("No journal refers to Merchant Account %s") % merchant_account - ) - if self._context.get("journal_id", journal.id) != journal.id: - raise UserError( - _( - "Selected journal Merchant Account does not match " - "the import file Merchant Account " - "column: %s" - ) - % merchant_account - ) + return parser.parse_rows(rows) def _get_rows(self, data_file): """Get rows from data_file.""" @@ -162,126 +45,3 @@ class AccountBankStatementImport(models.TransientModel): import_model = self.env["base_import.import"] importer = import_model.create({"file": data_file, "file_name": filename}) return importer._read_file({"quoting": '"', "separator": ","}) - - def _check_header_row(self, row): - """Header row is the first one with a "Company Account" header cell.""" - for cell in row: - if cell == "Company Account": - return True - return False - - def _set_columns(self, row): - """Set columns from headers. There MUST be a 'Company Account' header.""" - seen_company_account = False - for num, header in enumerate(row): - if not header.strip(): - continue # Ignore empty columns. - if header == "Company Account": - seen_company_account = True - if header not in COLUMNS: - _logger.debug(_("Unknown header %s in Adyen statement headers"), header) - else: - COLUMNS[header] = num # Set the right number for the column. - if not seen_company_account: - raise ValueError( - _("Not an Adyen statement. Headers %s do not contain 'Company Account'") - % ", ".join(row) - ) - - def _get_value(self, row, column): - """Get the value from the righ column in the row.""" - return row[COLUMNS[column]] - - def _make_statement(self, row): - """Make statement on first transaction in file.""" - statement = {"transactions": []} - statement["name"] = "{merchant} {year}/{batch}".format( - merchant=self._get_value(row, "Merchant Account"), - year=self._get_value(row, "Creation Date")[:4], - batch=self._get_value(row, "Batch Number"), - ) - statement["date"] = self._get_transaction_date(row) - return statement - - def _get_transaction_date(self, row): - """Get transaction date in right format.""" - return fields.Date.from_string(self._get_value(row, "Creation Date")) - - def _update_statement(self, statement, row): - """Update statement from transaction row.""" - # Statement date is date of earliest transaction in file. - date = self._get_transaction_date(row) - if date < statement.get("date"): - statement["date"] = date - - def _balance(self, row): - return ( - -self._sum_amount_values(row, ("Net Debit (NC)",)) - + self._sum_amount_values(row, ("Net Credit (NC)",)) - + self._sum_fees(row) - ) - - def _sum_fees(self, row): - """Sum the amounts in the fees columns.""" - return self._sum_amount_values( - row, - ("Commission (NC)", "Markup (NC)", "Scheme Fees (NC)", "Interchange (NC)",), - ) - - def _sum_amount_values(self, row, columns): - """Sum the amounts from the columns passed.""" - amount = 0.0 - for column in columns: - value = self._get_value(row, column) - if value: - amount += float(value) - return amount - - def _get_adyen_transaction(self, row): - """Get transaction from row. - - This can easily be overwritten in custom modules to add extra information. - """ - merchant_account = self._get_value(row, "Merchant Account") - psp_reference = self._get_value(row, "Psp Reference") - merchant_reference = self._get_value(row, "Merchant Reference") - payment_method = self._get_value(row, "Payment Method Variant") - modification_reference = self._get_value(row, "Modification Reference") - transaction = { - "date": self._get_transaction_date(row), - "amount": self._balance(row), - } - transaction["note"] = " ".join( - [ - part - for part in [ - merchant_account, - psp_reference, - merchant_reference, - payment_method, - ] - if part - ] - ) - transaction["name"] = ( - merchant_reference or psp_reference or modification_reference - ) - transaction["ref"] = ( - psp_reference or modification_reference or merchant_reference - ) - transaction["transaction_type"] = self._get_value(row, "Type") - return transaction - - def _get_unique_import_id(self, statement): - """get unique import ID for transaction.""" - return statement["name"] + str(len(statement["transactions"])).zfill(4) - - def _add_fees_transaction(self, statement, fees, batch_number): - """Single transaction for all fees in statement.""" - transaction = dict( - unique_import_id=self._get_unique_import_id(statement), - date=max(t["date"] for t in statement["transactions"]), - amount=-fees, - name="Commission, markup etc. batch %s" % batch_number, - ) - statement["transactions"].append(transaction) diff --git a/account_bank_statement_import_adyen/models/account_bank_statement_import_adyen_parser.py b/account_bank_statement_import_adyen/models/account_bank_statement_import_adyen_parser.py new file mode 100644 index 00000000..3186a412 --- /dev/null +++ b/account_bank_statement_import_adyen/models/account_bank_statement_import_adyen_parser.py @@ -0,0 +1,286 @@ +# Copyright 2017 Opener BV () +# Copyright 2021-2022 Therp BV . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +"""Add import of Adyen statements.""" +# pylint: disable=protected-access,no-self-use +import logging + +from odoo import _, fields, models +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) # pylint: disable=invalid-name + +COLUMNS = { + "Company Account": 1, + "Merchant Account": 2, + "Psp Reference": 3, + "Merchant Reference": 4, + "Payment Method": 5, # Not used at present + "Creation Date": 6, + "TimeZone": 7, # Not used at present + "Type": 8, + "Modification Reference": 9, + "Gross Currency": 10, # Not used at present + "Gross Debit (GC)": 11, # Not used at present + "Gross Credit (GC)": 12, # Not used at present + "Exchange Rate": 13, # Not used at present + "Net Currency": 14, + "Net Debit (NC)": 15, # Fee or Merchant Payout + "Net Credit (NC)": 16, + "Commission (NC)": 17, + "Markup (NC)": 18, + "Scheme Fees (NC)": 19, + "Interchange (NC)": 20, + "Payment Method Variant": 21, + "Modification Merchant Reference": 22, # Not used at present + "Batch Number": 23, + "Reserved4": 24, # Not used at present + "Reserved5": 25, # Not used at present + "Reserved6": 26, # Not used at present + "Reserved7": 27, # Not used at present + "Reserved8": 28, # Not used at present + "Reserved9": 29, # Not used at present + "Reserved10": 30, # Not used at present +} + + +class AccountBankStatementImportAdyenParser(models.TransientModel): + """Parse Adyen statement files for bank import.""" + + _name = "account.bank.statement.import.adyen.parser" + _description = "Account Bank Statement Import Adyen Parser" + + def parse_rows(self, rows): + """Parse rows generated from an Adyen file. + + An Exception will be thrown if file cannot be parsed. + """ + statement = None + fees = 0.0 + balance = 0.0 + payout = 0.0 + num_rows = self._process_headers(rows) + for row in rows: + num_rows += 1 + if not self._is_transaction_row(row): + continue + if not statement: + statement = self._make_statement(row) + statement_info = self._get_statement_info(row) + row_type = self._get_value(row, "Type").strip() + if row_type == "MerchantPayout": + payout -= self._balance(row) + else: + balance += self._balance(row) + transaction = self._get_transaction(row) + self._append_transaction(statement, transaction) + fees += self._sum_fees(row) + if fees: + balance -= fees + self._append_fees_transaction( + statement, fees, statement_info["batch_number"] + ) + self._validate_statement(statement, payout, balance) + _logger.info( + _("Processed %d rows from Adyen statement file with %d transactions"), + num_rows, + len(statement["transactions"]), + ) + return ( + statement_info["currency_code"], + statement_info["merchant_account"], + [statement], + ) + + def _process_headers(self, rows): + """Process the headers in the generated rows.""" + num_rows = 0 + for row in rows: + num_rows += 1 + if not row[1]: + continue + on_header_row = self._check_header_row(row) + if not on_header_row: + continue + self._set_columns(row) + return num_rows + raise ValueError( + "Not an Adyen statement. Did not encounter header row in %d rows." + % (num_rows,) + ) + + def _is_transaction_row(self, row): + """Check wether row is a not empty and valid transaction row.""" + if not row[1]: + return False + if len(row) < 24: + raise ValueError( + "Not an Adyen statement. Unexpected row length %s " + "less then minimum of 24" % len(row) + ) + return True + + def _get_statement_info(self, row): + """Get general information for statement.""" + merchant_account = self._get_value(row, "Merchant Account") + self._validate_merchant_account(merchant_account) + batch_number = self._get_value(row, "Batch Number") + currency_code = self._get_value(row, "Net Currency") + return { + "merchant_account": merchant_account, + "batch_number": batch_number, + "currency_code": currency_code, + } + + def _validate_merchant_account(self, merchant_account): + """Check wether merchant account exist, and belongs to the correct journal.""" + journal = self.env["account.journal"].search( + [("adyen_merchant_account", "=", merchant_account)], limit=1 + ) + if not journal: + raise UserError( + _("No journal refers to Merchant Account %s") % merchant_account + ) + if self._context.get("journal_id", journal.id) != journal.id: + raise UserError( + _( + "Selected journal Merchant Account does not match " + "the import file Merchant Account " + "column: %s" + ) + % merchant_account + ) + + def _check_header_row(self, row): + """Header row is the first one with a "Company Account" header cell.""" + for cell in row: + if cell == "Company Account": + return True + return False + + def _set_columns(self, row): + """Set columns from headers. There MUST be a 'Company Account' header.""" + seen_company_account = False + for num, header in enumerate(row): + if not header.strip(): + continue # Ignore empty columns. + if header == "Company Account": + seen_company_account = True + if header not in COLUMNS: + _logger.debug(_("Unknown header %s in Adyen statement headers"), header) + else: + COLUMNS[header] = num # Set the right number for the column. + if not seen_company_account: + raise ValueError( + _("Not an Adyen statement. Headers %s do not contain 'Company Account'") + % ", ".join(row) + ) + + def _validate_statement(self, statement, payout, balance): + """Check wether statement valid: balanced. Log when no payout.""" + if statement["transactions"] and not payout: + _logger.info(_("No payout detected in Adyen statement.")) + if self.env.user.company_id.currency_id.compare_amounts(balance, payout) != 0: + raise UserError( + _("Parse error. Balance %s not equal to merchant " "payout %s") + % (balance, payout) + ) + + def _get_value(self, row, column): + """Get the value from the righ column in the row.""" + return row[COLUMNS[column]] + + def _make_statement(self, row): + """Make statement on first transaction in file.""" + statement = {"transactions": []} + statement["name"] = "{merchant} {year}/{batch}".format( + merchant=self._get_value(row, "Merchant Account"), + year=self._get_value(row, "Creation Date")[:4], + batch=self._get_value(row, "Batch Number"), + ) + statement["date"] = self._get_transaction_date(row) + return statement + + def _get_transaction_date(self, row): + """Get transaction date in right format.""" + return fields.Date.from_string(self._get_value(row, "Creation Date")) + + def _balance(self, row): + return ( + -self._sum_amount_values(row, ("Net Debit (NC)",)) + + self._sum_amount_values(row, ("Net Credit (NC)",)) + + self._sum_fees(row) + ) + + def _sum_fees(self, row): + """Sum the amounts in the fees columns.""" + return self._sum_amount_values( + row, + ("Commission (NC)", "Markup (NC)", "Scheme Fees (NC)", "Interchange (NC)",), + ) + + def _sum_amount_values(self, row, columns): + """Sum the amounts from the columns passed.""" + amount = 0.0 + for column in columns: + value = self._get_value(row, column) + if value: + amount += float(value) + return amount + + def _get_transaction(self, row): + """Get transaction from row. + + This can easily be overwritten in custom modules to add extra information. + """ + merchant_account = self._get_value(row, "Merchant Account") + psp_reference = self._get_value(row, "Psp Reference") + merchant_reference = self._get_value(row, "Merchant Reference") + payment_method = self._get_value(row, "Payment Method Variant") + modification_reference = self._get_value(row, "Modification Reference") + transaction = { + "date": self._get_transaction_date(row), + "amount": self._balance(row), + } + transaction["note"] = " ".join( + [ + part + for part in [ + merchant_account, + psp_reference, + merchant_reference, + payment_method, + ] + if part + ] + ) + transaction["name"] = ( + merchant_reference or psp_reference or modification_reference + ) + transaction["ref"] = ( + psp_reference or modification_reference or merchant_reference + ) + transaction["transaction_type"] = self._get_value(row, "Type") + return transaction + + def _append_fees_transaction(self, statement, fees, batch_number): + """Single transaction for all fees in statement.""" + max_date = max(t["date"] for t in statement["transactions"]) + transaction = { + "date": max_date, + "amount": -fees, + "name": "Commission, markup etc. batch %s" % batch_number, + } + self._append_transaction(statement, transaction) + + def _append_transaction(self, statement, transaction): + """Add transaction with unique import id to statement.""" + # Statement date is date of earliest transaction in file. + if transaction["date"] < statement.get("date"): + statement["date"] = transaction["date"] + transaction["unique_import_id"] = self._get_unique_import_id(statement) + statement["transactions"].append(transaction) + + def _get_unique_import_id(self, statement): + """get unique import ID for transaction.""" + return statement["name"] + str(len(statement["transactions"])).zfill(4)