[FIX] *adyen*: support xlsx as well as csv

This commit is contained in:
Ronald Portier
2022-02-25 09:31:44 +01:00
committed by Ronald Portier (Therp BV)
parent 318c67112b
commit cab6718eec
2 changed files with 39 additions and 27 deletions

View File

@@ -1,12 +1,14 @@
# Copyright 2017 Opener BV (<https://opener.amsterdam>) # Copyright 2017 Opener BV (<https://opener.amsterdam>)
# Copyright 2021 Therp BV <https://therp.nl>. # Copyright 2021-2022 Therp BV <https://therp.nl>.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). # 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 import logging
from odoo import _, fields, models from odoo import _, fields, models
from odoo.exceptions import UserError from odoo.exceptions import UserError
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__) # pylint: disable=invalid-name
COLUMNS = { COLUMNS = {
"Company Account": 1, "Company Account": 1,
@@ -43,6 +45,8 @@ COLUMNS = {
class AccountBankStatementImport(models.TransientModel): class AccountBankStatementImport(models.TransientModel):
"""Add import of Adyen statements."""
_inherit = "account.bank.statement.import" _inherit = "account.bank.statement.import"
def _parse_file(self, data_file): def _parse_file(self, data_file):
@@ -50,7 +54,7 @@ class AccountBankStatementImport(models.TransientModel):
try: try:
_logger.debug(_("Try parsing as Adyen settlement details.")) _logger.debug(_("Try parsing as Adyen settlement details."))
return self._parse_adyen_file(data_file) 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.") message = _("Statement file was not a Adyen settlement details file.")
if self.env.context.get("account_bank_statement_import_adyen", False): if self.env.context.get("account_bank_statement_import_adyen", False):
raise UserError(message) raise UserError(message)
@@ -58,7 +62,7 @@ class AccountBankStatementImport(models.TransientModel):
return super()._parse_file(data_file) return super()._parse_file(data_file)
def _find_additional_data(self, currency_code, account_number): 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: if account_number:
journal = self.env["account.journal"].search( journal = self.env["account.journal"].search(
[("adyen_merchant_account", "=", account_number)], limit=1 [("adyen_merchant_account", "=", account_number)], limit=1
@@ -73,33 +77,39 @@ class AccountBankStatementImport(models.TransientModel):
) )
% account_number % account_number
) )
self = self.with_context(journal_id=journal.id)
return super()._find_additional_data(currency_code, account_number) return super()._find_additional_data(currency_code, account_number)
def _parse_adyen_file(self, data_file): def _parse_adyen_file(self, data_file):
"""Parse file assuming it is an Adyen 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 statement = None
headers = False headers = False
batch_number = False
fees = 0.0 fees = 0.0
balance = 0.0 balance = 0.0
payout = 0.0 payout = 0.0
rows = self._get_rows(data_file) rows = self._get_rows(data_file)
num_rows = 0
for row in rows: 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: if len(row) < 24:
raise ValueError( raise ValueError(
"Not an Adyen statement. Unexpected row length %s " "Not an Adyen statement. Unexpected row length %s "
"less then minimum of 24" % len(row) "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: if not statement:
batch_number = self._get_value(row, "Batch Number")
statement = self._make_statement(row) statement = self._make_statement(row)
currency_code = self._get_value(row, "Net Currency") currency_code = self._get_value(row, "Net Currency")
merchant_id = self._get_value(row, "Merchant Account") merchant_id = self._get_value(row, "Merchant Account")
@@ -112,12 +122,14 @@ class AccountBankStatementImport(models.TransientModel):
balance += self._balance(row) balance += self._balance(row)
self._import_adyen_transaction(statement, row) self._import_adyen_transaction(statement, row)
fees += self._sum_fees(row) fees += self._sum_fees(row)
if not headers: 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: if fees:
balance -= fees balance -= fees
self._add_fees_transaction(statement, fees, row) self._add_fees_transaction(statement, fees, batch_number)
if statement["transactions"] and not payout: if statement["transactions"] and not payout:
raise UserError(_("No payout detected in Adyen statement.")) raise UserError(_("No payout detected in Adyen statement."))
if self.env.user.company_id.currency_id.compare_amounts(balance, payout) != 0: 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}) importer = import_model.create({"file": data_file, "file_name": filename})
return importer._read_file({"quoting": '"', "separator": ","}) 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): def _set_columns(self, row):
"""Set columns from headers. There MUST be a 'Company Account' header.""" """Set columns from headers. There MUST be a 'Company Account' header."""
seen_company_account = False seen_company_account = False
@@ -229,13 +248,12 @@ class AccountBankStatementImport(models.TransientModel):
"""get unique import ID for transaction.""" """get unique import ID for transaction."""
return statement["name"] + str(len(statement["transactions"])).zfill(4) 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.""" """Single transaction for all fees in statement."""
transaction = dict( transaction = dict(
unique_import_id=self._get_unique_import_id(statement), unique_import_id=self._get_unique_import_id(statement),
date=max(t["date"] for t in statement["transactions"]), date=max(t["date"] for t in statement["transactions"]),
amount=-fees, amount=-fees,
name="Commission, markup etc. batch %s" name="Commission, markup etc. batch %s" % batch_number,
% self._get_value(row, "Batch Number"),
) )
statement["transactions"].append(transaction) statement["transactions"].append(transaction)

View File

@@ -1,5 +1,6 @@
# Copyright 2021 Therp BV <https://therp.nl>. # Copyright 2021 Therp BV <https://therp.nl>.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
# pylint: disable=missing-docstring,invalid-name,protected-access
import base64 import base64
import logging import logging
from html import escape from html import escape
@@ -52,7 +53,8 @@ class OnlineBankStatementProvider(models.Model):
if is_scheduled: if is_scheduled:
_logger.warning( _logger.warning(
'Online Bank Statement Provider "%s" failed to' 'Online Bank Statement Provider "%s" failed to'
" obtain statement data" % (provider.name,), " obtain statement data",
provider.name,
exc_info=True, exc_info=True,
) )
provider.message_post( provider.message_post(
@@ -99,14 +101,6 @@ class OnlineBankStatementProvider(models.Model):
if response.status_code != 200: if response.status_code != 200:
raise UserError(_("%s \n\n %s") % (response.status_code, response.text)) raise UserError(_("%s \n\n %s") % (response.status_code, response.text))
_logger.debug(_("Headers returned by Adyen %s"), response.headers) _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) byte_count = len(response.content)
_logger.debug( _logger.debug(
_("Retrieved %d bytes from Adyen, starting with %s"), _("Retrieved %d bytes from Adyen, starting with %s"),