mirror of
https://github.com/OCA/bank-statement-import.git
synced 2025-01-20 12:37:43 +02:00
[FIX] *adyen*: support xlsx as well as csv
This commit is contained in:
committed by
Ronald Portier (Therp BV)
parent
318c67112b
commit
cab6718eec
@@ -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)
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
Reference in New Issue
Block a user