mirror of
https://github.com/OCA/bank-statement-import.git
synced 2025-01-20 12:37:43 +02:00
@@ -11,7 +11,6 @@
|
|||||||
"license": "AGPL-3",
|
"license": "AGPL-3",
|
||||||
"category": "Accounting",
|
"category": "Accounting",
|
||||||
"summary": "Online bank statements update",
|
"summary": "Online bank statements update",
|
||||||
"external_dependencies": {"python": ["odoo_test_helper"]},
|
|
||||||
"depends": [
|
"depends": [
|
||||||
"account",
|
"account",
|
||||||
"account_statement_import",
|
"account_statement_import",
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"multi_step_wizard",
|
"multi_step_wizard",
|
||||||
"web_widget_dropdown_dynamic",
|
"web_widget_dropdown_dynamic",
|
||||||
],
|
],
|
||||||
"external_dependencies": {"python": ["xlrd"]},
|
"external_dependencies": {"python": ["xlrd", "chardet"]},
|
||||||
"data": [
|
"data": [
|
||||||
"security/ir.model.access.csv",
|
"security/ir.model.access.csv",
|
||||||
"data/map_data.xml",
|
"data/map_data.xml",
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
# Copyright 2021 Tecnativa - Carlos Roca
|
|
||||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
|
||||||
|
|
||||||
from openupgradelib import openupgrade
|
|
||||||
|
|
||||||
_model_renames = [
|
|
||||||
(
|
|
||||||
"account.bank.statement.import.sheet.mapping",
|
|
||||||
"account.statement.import.sheet.mapping",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"account.bank.statement.import.sheet.parser",
|
|
||||||
"account.statement.import.sheet.parser",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"account.bank.statement.import.sheet.mapping.wizard",
|
|
||||||
"account.statement.import.sheet.mapping.wizard",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
_table_renames = [
|
|
||||||
(
|
|
||||||
"account_bank_statement_import_sheet_mapping",
|
|
||||||
"account_statement_import_sheet_mapping",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"account_bank_statement_import_sheet_parser",
|
|
||||||
"account_statement_import_sheet_parser",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"account_bank_statement_import_sheet_mapping_wizard",
|
|
||||||
"account_statement_import_sheet_mapping_wizard",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@openupgrade.migrate()
|
|
||||||
def migrate(env, version):
|
|
||||||
openupgrade.rename_models(env.cr, _model_renames)
|
|
||||||
openupgrade.rename_tables(env.cr, _table_renames)
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Copyright 2022 AppsToGROW - Henrik Norlin
|
||||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from openupgradelib import openupgrade
|
||||||
|
|
||||||
|
_fields_to_add = [
|
||||||
|
(
|
||||||
|
"amount_debit_column",
|
||||||
|
"account.statement.import.sheet.mapping",
|
||||||
|
"account_statement_import_sheet_mapping",
|
||||||
|
"char",
|
||||||
|
"varchar",
|
||||||
|
"account_statement_import_txt_xlsx",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"amount_credit_column",
|
||||||
|
"account.statement.import.sheet.mapping",
|
||||||
|
"account_statement_import_sheet_mapping",
|
||||||
|
"char",
|
||||||
|
"varchar",
|
||||||
|
"account_statement_import_txt_xlsx",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def add_fields_and_drop_not_null(env):
|
||||||
|
cr = env.cr
|
||||||
|
sql_debit_exists = """SELECT count(id) FROM ir_model_fields
|
||||||
|
WHERE name = 'amount_debit_column'
|
||||||
|
AND model = 'account.statement.import.sheet.mapping';"""
|
||||||
|
cr.execute(sql_debit_exists)
|
||||||
|
if cr.fetchone()[0] > 0:
|
||||||
|
openupgrade.add_fields(env, _fields_to_add)
|
||||||
|
cr.execute(
|
||||||
|
"""ALTER TABLE account_statement_import_sheet_mapping
|
||||||
|
ALTER COLUMN amount_column DROP NOT NULL;"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@openupgrade.migrate()
|
||||||
|
def migrate(env, version):
|
||||||
|
add_fields_and_drop_not_null(env)
|
||||||
@@ -28,9 +28,21 @@ class AccountStatementImport(models.TransientModel):
|
|||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
try:
|
try:
|
||||||
Parser = self.env["account.statement.import.sheet.parser"]
|
Parser = self.env["account.statement.import.sheet.parser"]
|
||||||
return Parser.parse(data_file, self.sheet_mapping_id)
|
return Parser.parse(
|
||||||
|
data_file, self.sheet_mapping_id, self.statement_filename
|
||||||
|
)
|
||||||
except BaseException:
|
except BaseException:
|
||||||
if self.env.context.get("account_statement_import_txt_xlsx_test"):
|
if self.env.context.get("account_statement_import_txt_xlsx_test"):
|
||||||
raise
|
raise
|
||||||
_logger.warning("Sheet parser error", exc_info=True)
|
_logger.warning("Sheet parser error", exc_info=True)
|
||||||
return super()._parse_file(data_file)
|
return super()._parse_file(data_file)
|
||||||
|
|
||||||
|
def _create_bank_statements(self, stmts_vals, result):
|
||||||
|
"""Set balance_end_real if not already provided by the file."""
|
||||||
|
res = super()._create_bank_statements(stmts_vals, result)
|
||||||
|
statements = self.env["account.bank.statement"].browse(result["statement_ids"])
|
||||||
|
for statement in statements:
|
||||||
|
if not statement.balance_end_real:
|
||||||
|
amount = sum(statement.line_ids.mapped("amount"))
|
||||||
|
statement.balance_end_real = statement.balance_start + amount
|
||||||
|
return res
|
||||||
|
|||||||
@@ -55,6 +55,11 @@ class AccountStatementImportSheetMapping(models.Model):
|
|||||||
)
|
)
|
||||||
quotechar = fields.Char(string="Text qualifier", size=1, default='"')
|
quotechar = fields.Char(string="Text qualifier", size=1, default='"')
|
||||||
timestamp_format = fields.Char(required=True)
|
timestamp_format = fields.Char(required=True)
|
||||||
|
no_header = fields.Boolean(
|
||||||
|
string="File does not contain header line",
|
||||||
|
help="When this occurs please indicate the column number in the Columns section "
|
||||||
|
"instead of the column name, considering that the first column is 0",
|
||||||
|
)
|
||||||
timestamp_column = fields.Char(required=True)
|
timestamp_column = fields.Char(required=True)
|
||||||
currency_column = fields.Char(
|
currency_column = fields.Char(
|
||||||
help=(
|
help=(
|
||||||
@@ -63,9 +68,16 @@ class AccountStatementImportSheetMapping(models.Model):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
amount_column = fields.Char(
|
amount_column = fields.Char(
|
||||||
required=True,
|
|
||||||
help="Amount of transaction in journal's currency",
|
help="Amount of transaction in journal's currency",
|
||||||
)
|
)
|
||||||
|
amount_debit_column = fields.Char(
|
||||||
|
string="Debit amount column",
|
||||||
|
help="Debit amount of transaction in journal's currency",
|
||||||
|
)
|
||||||
|
amount_credit_column = fields.Char(
|
||||||
|
string="Credit amount column",
|
||||||
|
help="Credit amount of transaction in journal's currency",
|
||||||
|
)
|
||||||
balance_column = fields.Char(
|
balance_column = fields.Char(
|
||||||
help="Balance after transaction in journal's currency",
|
help="Balance after transaction in journal's currency",
|
||||||
)
|
)
|
||||||
@@ -112,6 +124,19 @@ class AccountStatementImportSheetMapping(models.Model):
|
|||||||
help="Partner's bank account",
|
help="Partner's bank account",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_sql_constraints = [
|
||||||
|
(
|
||||||
|
"check_amount_columns",
|
||||||
|
(
|
||||||
|
"CHECK("
|
||||||
|
"amount_column IS NULL "
|
||||||
|
"OR (amount_debit_column IS NULL AND amount_credit_column IS NULL)"
|
||||||
|
")"
|
||||||
|
),
|
||||||
|
"Use amount_column OR (amount_debit_column AND amount_credit_column).",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
@api.onchange("float_thousands_sep")
|
@api.onchange("float_thousands_sep")
|
||||||
def onchange_thousands_separator(self):
|
def onchange_thousands_separator(self):
|
||||||
if "dot" == self.float_thousands_sep == self.float_decimal_sep:
|
if "dot" == self.float_thousands_sep == self.float_decimal_sep:
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import logging
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
from os import path
|
||||||
|
|
||||||
from odoo import _, api, models
|
from odoo import _, api, models
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -20,6 +22,14 @@ try:
|
|||||||
except (ImportError, IOError) as err: # pragma: no cover
|
except (ImportError, IOError) as err: # pragma: no cover
|
||||||
_logger.error(err)
|
_logger.error(err)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import chardet
|
||||||
|
except ImportError:
|
||||||
|
_logger.warning(
|
||||||
|
"chardet library not found, please install it "
|
||||||
|
"from http://pypi.python.org/pypi/chardet"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AccountStatementImportSheetParser(models.TransientModel):
|
class AccountStatementImportSheetParser(models.TransientModel):
|
||||||
_name = "account.statement.import.sheet.parser"
|
_name = "account.statement.import.sheet.parser"
|
||||||
@@ -43,7 +53,7 @@ class AccountStatementImportSheetParser(models.TransientModel):
|
|||||||
return list(next(csv_data))
|
return list(next(csv_data))
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def parse(self, data_file, mapping):
|
def parse(self, data_file, mapping, filename):
|
||||||
journal = self.env["account.journal"].browse(self.env.context.get("journal_id"))
|
journal = self.env["account.journal"].browse(self.env.context.get("journal_id"))
|
||||||
currency_code = (journal.currency_id or journal.company_id.currency_id).name
|
currency_code = (journal.currency_id or journal.company_id.currency_id).name
|
||||||
account_number = journal.bank_account_id.acc_number
|
account_number = journal.bank_account_id.acc_number
|
||||||
@@ -57,6 +67,11 @@ class AccountStatementImportSheetParser(models.TransientModel):
|
|||||||
last_line = lines[-1]
|
last_line = lines[-1]
|
||||||
data = {
|
data = {
|
||||||
"date": first_line["timestamp"].date(),
|
"date": first_line["timestamp"].date(),
|
||||||
|
"name": _("%(code)s: %(filename)s")
|
||||||
|
% {
|
||||||
|
"code": journal.code,
|
||||||
|
"filename": path.basename(filename),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if mapping.balance_column:
|
if mapping.balance_column:
|
||||||
@@ -69,7 +84,6 @@ class AccountStatementImportSheetParser(models.TransientModel):
|
|||||||
"balance_end_real": float(balance_end),
|
"balance_end_real": float(balance_end),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
transactions = list(
|
transactions = list(
|
||||||
itertools.chain.from_iterable(
|
itertools.chain.from_iterable(
|
||||||
map(lambda line: self._convert_line_to_transactions(line), lines)
|
map(lambda line: self._convert_line_to_transactions(line), lines)
|
||||||
@@ -79,6 +93,50 @@ class AccountStatementImportSheetParser(models.TransientModel):
|
|||||||
|
|
||||||
return currency_code, account_number, [data]
|
return currency_code, account_number, [data]
|
||||||
|
|
||||||
|
def _get_column_indexes(self, header, column_name, mapping):
|
||||||
|
column_indexes = []
|
||||||
|
if mapping[column_name] and "," in mapping[column_name]:
|
||||||
|
# We have to concatenate the values
|
||||||
|
column_names_or_indexes = mapping[column_name].split(",")
|
||||||
|
else:
|
||||||
|
column_names_or_indexes = [mapping[column_name]]
|
||||||
|
for column_name_or_index in column_names_or_indexes:
|
||||||
|
if not column_name_or_index:
|
||||||
|
continue
|
||||||
|
column_index = None
|
||||||
|
if mapping.no_header:
|
||||||
|
try:
|
||||||
|
column_index = int(column_name_or_index)
|
||||||
|
# pylint: disable=except-pass
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if column_index is not None:
|
||||||
|
column_indexes.append(column_index)
|
||||||
|
else:
|
||||||
|
if column_name_or_index:
|
||||||
|
column_indexes.append(header.index(column_name_or_index))
|
||||||
|
return column_indexes
|
||||||
|
|
||||||
|
def _get_column_names(self):
|
||||||
|
return [
|
||||||
|
"timestamp_column",
|
||||||
|
"currency_column",
|
||||||
|
"amount_column",
|
||||||
|
"amount_debit_column",
|
||||||
|
"amount_credit_column",
|
||||||
|
"balance_column",
|
||||||
|
"original_currency_column",
|
||||||
|
"original_amount_column",
|
||||||
|
"debit_credit_column",
|
||||||
|
"transaction_id_column",
|
||||||
|
"description_column",
|
||||||
|
"notes_column",
|
||||||
|
"reference_column",
|
||||||
|
"partner_name_column",
|
||||||
|
"bank_name_column",
|
||||||
|
"bank_account_column",
|
||||||
|
]
|
||||||
|
|
||||||
def _parse_lines(self, mapping, data_file, currency_code):
|
def _parse_lines(self, mapping, data_file, currency_code):
|
||||||
columns = dict()
|
columns = dict()
|
||||||
try:
|
try:
|
||||||
@@ -99,69 +157,44 @@ class AccountStatementImportSheetParser(models.TransientModel):
|
|||||||
csv_options["delimiter"] = csv_delimiter
|
csv_options["delimiter"] = csv_delimiter
|
||||||
if mapping.quotechar:
|
if mapping.quotechar:
|
||||||
csv_options["quotechar"] = mapping.quotechar
|
csv_options["quotechar"] = mapping.quotechar
|
||||||
csv_or_xlsx = reader(
|
try:
|
||||||
StringIO(data_file.decode(mapping.file_encoding or "utf-8")),
|
decoded_file = data_file.decode(mapping.file_encoding or "utf-8")
|
||||||
**csv_options
|
except UnicodeDecodeError:
|
||||||
|
# Try auto guessing the format
|
||||||
|
detected_encoding = chardet.detect(data_file).get("encoding", False)
|
||||||
|
if not detected_encoding:
|
||||||
|
raise UserError(
|
||||||
|
_("No valid encoding was found for the attached file")
|
||||||
|
) from None
|
||||||
|
decoded_file = data_file.decode(detected_encoding)
|
||||||
|
csv_or_xlsx = reader(StringIO(decoded_file), **csv_options)
|
||||||
|
header = False
|
||||||
|
if not mapping.no_header:
|
||||||
|
if isinstance(csv_or_xlsx, tuple):
|
||||||
|
header = [str(value) for value in csv_or_xlsx[1].row_values(0)]
|
||||||
|
else:
|
||||||
|
header = [value.strip() for value in next(csv_or_xlsx)]
|
||||||
|
for column_name in self._get_column_names():
|
||||||
|
columns[column_name] = self._get_column_indexes(
|
||||||
|
header, column_name, mapping
|
||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(csv_or_xlsx, tuple):
|
|
||||||
header = [str(value) for value in csv_or_xlsx[1].row_values(0)]
|
|
||||||
else:
|
|
||||||
header = [value.strip() for value in next(csv_or_xlsx)]
|
|
||||||
columns["timestamp_column"] = header.index(mapping.timestamp_column)
|
|
||||||
columns["currency_column"] = (
|
|
||||||
header.index(mapping.currency_column) if mapping.currency_column else None
|
|
||||||
)
|
|
||||||
columns["amount_column"] = header.index(mapping.amount_column)
|
|
||||||
columns["balance_column"] = (
|
|
||||||
header.index(mapping.balance_column) if mapping.balance_column else None
|
|
||||||
)
|
|
||||||
columns["original_currency_column"] = (
|
|
||||||
header.index(mapping.original_currency_column)
|
|
||||||
if mapping.original_currency_column
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
columns["original_amount_column"] = (
|
|
||||||
header.index(mapping.original_amount_column)
|
|
||||||
if mapping.original_amount_column
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
columns["debit_credit_column"] = (
|
|
||||||
header.index(mapping.debit_credit_column)
|
|
||||||
if mapping.debit_credit_column
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
columns["transaction_id_column"] = (
|
|
||||||
header.index(mapping.transaction_id_column)
|
|
||||||
if mapping.transaction_id_column
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
columns["description_column"] = (
|
|
||||||
header.index(mapping.description_column)
|
|
||||||
if mapping.description_column
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
columns["notes_column"] = (
|
|
||||||
header.index(mapping.notes_column) if mapping.notes_column else None
|
|
||||||
)
|
|
||||||
columns["reference_column"] = (
|
|
||||||
header.index(mapping.reference_column) if mapping.reference_column else None
|
|
||||||
)
|
|
||||||
columns["partner_name_column"] = (
|
|
||||||
header.index(mapping.partner_name_column)
|
|
||||||
if mapping.partner_name_column
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
columns["bank_name_column"] = (
|
|
||||||
header.index(mapping.bank_name_column) if mapping.bank_name_column else None
|
|
||||||
)
|
|
||||||
columns["bank_account_column"] = (
|
|
||||||
header.index(mapping.bank_account_column)
|
|
||||||
if mapping.bank_account_column
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
return self._parse_rows(mapping, currency_code, csv_or_xlsx, columns)
|
return self._parse_rows(mapping, currency_code, csv_or_xlsx, columns)
|
||||||
|
|
||||||
|
def _get_values_from_column(self, values, columns, column_name):
|
||||||
|
indexes = columns[column_name]
|
||||||
|
content_l = []
|
||||||
|
max_index = len(values) - 1
|
||||||
|
for index in indexes:
|
||||||
|
if isinstance(index, int):
|
||||||
|
if index <= max_index:
|
||||||
|
content_l.append(values[index])
|
||||||
|
else:
|
||||||
|
if index in values:
|
||||||
|
content_l.append(values[index])
|
||||||
|
if all(isinstance(content, str) for content in content_l):
|
||||||
|
return " ".join(content_l)
|
||||||
|
return content_l[0]
|
||||||
|
|
||||||
def _parse_rows(self, mapping, currency_code, csv_or_xlsx, columns): # noqa: C901
|
def _parse_rows(self, mapping, currency_code, csv_or_xlsx, columns): # noqa: C901
|
||||||
if isinstance(csv_or_xlsx, tuple):
|
if isinstance(csv_or_xlsx, tuple):
|
||||||
rows = range(1, csv_or_xlsx[1].nrows)
|
rows = range(1, csv_or_xlsx[1].nrows)
|
||||||
@@ -183,66 +216,83 @@ class AccountStatementImportSheetParser(models.TransientModel):
|
|||||||
else:
|
else:
|
||||||
values = list(row)
|
values = list(row)
|
||||||
|
|
||||||
timestamp = values[columns["timestamp_column"]]
|
timestamp = self._get_values_from_column(
|
||||||
|
values, columns, "timestamp_column"
|
||||||
|
)
|
||||||
currency = (
|
currency = (
|
||||||
values[columns["currency_column"]]
|
self._get_values_from_column(values, columns, "currency_column")
|
||||||
if columns["currency_column"] is not None
|
if columns["currency_column"]
|
||||||
else currency_code
|
else currency_code
|
||||||
)
|
)
|
||||||
amount = values[columns["amount_column"]]
|
|
||||||
|
def _decimal(column_name):
|
||||||
|
if columns[column_name]:
|
||||||
|
return self._parse_decimal(
|
||||||
|
self._get_values_from_column(values, columns, column_name),
|
||||||
|
mapping,
|
||||||
|
)
|
||||||
|
|
||||||
|
amount = _decimal("amount_column")
|
||||||
|
if not amount:
|
||||||
|
amount = abs(_decimal("amount_debit_column") or 0)
|
||||||
|
if not amount:
|
||||||
|
amount = -abs(_decimal("amount_credit_column") or 0)
|
||||||
|
|
||||||
balance = (
|
balance = (
|
||||||
values[columns["balance_column"]]
|
self._get_values_from_column(values, columns, "balance_column")
|
||||||
if columns["balance_column"] is not None
|
if columns["balance_column"]
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
original_currency = (
|
original_currency = (
|
||||||
values[columns["original_currency_column"]]
|
self._get_values_from_column(
|
||||||
if columns["original_currency_column"] is not None
|
values, columns, "original_currency_column"
|
||||||
|
)
|
||||||
|
if columns["original_currency_column"]
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
original_amount = (
|
original_amount = (
|
||||||
values[columns["original_amount_column"]]
|
self._get_values_from_column(values, columns, "original_amount_column")
|
||||||
if columns["original_amount_column"] is not None
|
if columns["original_amount_column"]
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
debit_credit = (
|
debit_credit = (
|
||||||
values[columns["debit_credit_column"]]
|
self._get_values_from_column(values, columns, "debit_credit_column")
|
||||||
if columns["debit_credit_column"] is not None
|
if columns["debit_credit_column"]
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
transaction_id = (
|
transaction_id = (
|
||||||
values[columns["transaction_id_column"]]
|
self._get_values_from_column(values, columns, "transaction_id_column")
|
||||||
if columns["transaction_id_column"] is not None
|
if columns["transaction_id_column"]
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
description = (
|
description = (
|
||||||
values[columns["description_column"]]
|
self._get_values_from_column(values, columns, "description_column")
|
||||||
if columns["description_column"] is not None
|
if columns["description_column"]
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
notes = (
|
notes = (
|
||||||
values[columns["notes_column"]]
|
self._get_values_from_column(values, columns, "notes_column")
|
||||||
if columns["notes_column"] is not None
|
if columns["notes_column"]
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
reference = (
|
reference = (
|
||||||
values[columns["reference_column"]]
|
self._get_values_from_column(values, columns, "reference_column")
|
||||||
if columns["reference_column"] is not None
|
if columns["reference_column"]
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
partner_name = (
|
partner_name = (
|
||||||
values[columns["partner_name_column"]]
|
self._get_values_from_column(values, columns, "partner_name_column")
|
||||||
if columns["partner_name_column"] is not None
|
if columns["partner_name_column"]
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
bank_name = (
|
bank_name = (
|
||||||
values[columns["bank_name_column"]]
|
self._get_values_from_column(values, columns, "bank_name_column")
|
||||||
if columns["bank_name_column"] is not None
|
if columns["bank_name_column"]
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
bank_account = (
|
bank_account = (
|
||||||
values[columns["bank_account_column"]]
|
self._get_values_from_column(values, columns, "bank_account_column")
|
||||||
if columns["bank_account_column"] is not None
|
if columns["bank_account_column"]
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -252,7 +302,6 @@ class AccountStatementImportSheetParser(models.TransientModel):
|
|||||||
if isinstance(timestamp, str):
|
if isinstance(timestamp, str):
|
||||||
timestamp = datetime.strptime(timestamp, mapping.timestamp_format)
|
timestamp = datetime.strptime(timestamp, mapping.timestamp_format)
|
||||||
|
|
||||||
amount = self._parse_decimal(amount, mapping)
|
|
||||||
if balance:
|
if balance:
|
||||||
balance = self._parse_decimal(balance, mapping)
|
balance = self._parse_decimal(balance, mapping)
|
||||||
else:
|
else:
|
||||||
@@ -359,9 +408,11 @@ class AccountStatementImportSheetParser(models.TransientModel):
|
|||||||
if transaction_id:
|
if transaction_id:
|
||||||
note += _("Transaction ID: %s; ") % (transaction_id,)
|
note += _("Transaction ID: %s; ") % (transaction_id,)
|
||||||
if note and notes:
|
if note and notes:
|
||||||
note = "{}\n{}".format(note, note.strip())
|
note = "{}\n{}".format(notes, note.strip())
|
||||||
elif note:
|
elif note:
|
||||||
note = note.strip()
|
note = note.strip()
|
||||||
|
elif notes:
|
||||||
|
note = notes
|
||||||
if note:
|
if note:
|
||||||
transaction["narration"] = note
|
transaction["narration"] = note
|
||||||
|
|
||||||
|
|||||||
5
account_statement_import_txt_xlsx/tests/fixtures/debit_credit_amount.csv
vendored
Normal file
5
account_statement_import_txt_xlsx/tests/fixtures/debit_credit_amount.csv
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"Date","Label","Debit","Credit","Balance","Partner Name","Bank Account"
|
||||||
|
"12/15/2018","Credit 20.00","0.00","20.00","-10.00","John Doe","123456789"
|
||||||
|
"12/15/2018","Credit 13.50","0.00","-13.50","-23.50","John Doe","123456789"
|
||||||
|
"12/15/2018","Debit 33.50","-33.50","0.00","10.00","Azure Interior",""
|
||||||
|
"12/15/2018","Debit 1500","1,500.00","0.00","1,510.00","Azure Interior",""
|
||||||
|
1
account_statement_import_txt_xlsx/tests/fixtures/original_currency_no_header.csv
vendored
Normal file
1
account_statement_import_txt_xlsx/tests/fixtures/original_currency_no_header.csv
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"12/15/2018","Your payment","EUR","1,525.00","-1,000.00","Azure Interior","","INV0001"
|
||||||
|
@@ -31,6 +31,15 @@ class TestAccountBankStatementImportTxtXlsx(common.TransactionCase):
|
|||||||
self.AccountStatementImportSheetMappingWizard = self.env[
|
self.AccountStatementImportSheetMappingWizard = self.env[
|
||||||
"account.statement.import.sheet.mapping.wizard"
|
"account.statement.import.sheet.mapping.wizard"
|
||||||
]
|
]
|
||||||
|
self.suspense_account = self.env["account.account"].create(
|
||||||
|
{
|
||||||
|
"code": "987654",
|
||||||
|
"name": "Suspense Account",
|
||||||
|
"user_type_id": self.env.ref(
|
||||||
|
"account.data_account_type_current_assets"
|
||||||
|
).id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def _data_file(self, filename, encoding=None):
|
def _data_file(self, filename, encoding=None):
|
||||||
mode = "rt" if encoding else "rb"
|
mode = "rt" if encoding else "rb"
|
||||||
@@ -47,6 +56,7 @@ class TestAccountBankStatementImportTxtXlsx(common.TransactionCase):
|
|||||||
"type": "bank",
|
"type": "bank",
|
||||||
"code": "BANK",
|
"code": "BANK",
|
||||||
"currency_id": self.currency_usd.id,
|
"currency_id": self.currency_usd.id,
|
||||||
|
"suspense_account_id": self.suspense_account.id,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
data = self._data_file("fixtures/sample_statement_en.csv", "utf-8")
|
data = self._data_file("fixtures/sample_statement_en.csv", "utf-8")
|
||||||
@@ -71,6 +81,7 @@ class TestAccountBankStatementImportTxtXlsx(common.TransactionCase):
|
|||||||
"type": "bank",
|
"type": "bank",
|
||||||
"code": "BANK",
|
"code": "BANK",
|
||||||
"currency_id": self.currency_usd.id,
|
"currency_id": self.currency_usd.id,
|
||||||
|
"suspense_account_id": self.suspense_account.id,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
data = self._data_file("fixtures/empty_statement_en.csv", "utf-8")
|
data = self._data_file("fixtures/empty_statement_en.csv", "utf-8")
|
||||||
@@ -95,6 +106,7 @@ class TestAccountBankStatementImportTxtXlsx(common.TransactionCase):
|
|||||||
"type": "bank",
|
"type": "bank",
|
||||||
"code": "BANK",
|
"code": "BANK",
|
||||||
"currency_id": self.currency_usd.id,
|
"currency_id": self.currency_usd.id,
|
||||||
|
"suspense_account_id": self.suspense_account.id,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
data = self._data_file("fixtures/sample_statement_en.xlsx")
|
data = self._data_file("fixtures/sample_statement_en.xlsx")
|
||||||
@@ -119,6 +131,7 @@ class TestAccountBankStatementImportTxtXlsx(common.TransactionCase):
|
|||||||
"type": "bank",
|
"type": "bank",
|
||||||
"code": "BANK",
|
"code": "BANK",
|
||||||
"currency_id": self.currency_usd.id,
|
"currency_id": self.currency_usd.id,
|
||||||
|
"suspense_account_id": self.suspense_account.id,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
data = self._data_file("fixtures/empty_statement_en.xlsx")
|
data = self._data_file("fixtures/empty_statement_en.xlsx")
|
||||||
@@ -189,6 +202,7 @@ class TestAccountBankStatementImportTxtXlsx(common.TransactionCase):
|
|||||||
"type": "bank",
|
"type": "bank",
|
||||||
"code": "BANK",
|
"code": "BANK",
|
||||||
"currency_id": self.currency_usd.id,
|
"currency_id": self.currency_usd.id,
|
||||||
|
"suspense_account_id": self.suspense_account.id,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
data = self._data_file("fixtures/original_currency.csv", "utf-8")
|
data = self._data_file("fixtures/original_currency.csv", "utf-8")
|
||||||
@@ -212,6 +226,55 @@ class TestAccountBankStatementImportTxtXlsx(common.TransactionCase):
|
|||||||
self.assertEqual(line.foreign_currency_id, self.currency_eur)
|
self.assertEqual(line.foreign_currency_id, self.currency_eur)
|
||||||
self.assertEqual(line.amount_currency, 1000.0)
|
self.assertEqual(line.amount_currency, 1000.0)
|
||||||
|
|
||||||
|
def test_original_currency_no_header(self):
|
||||||
|
no_header_statement_map = self.AccountStatementImportSheetMapping.create(
|
||||||
|
{
|
||||||
|
"name": "Sample Statement",
|
||||||
|
"float_thousands_sep": "comma",
|
||||||
|
"float_decimal_sep": "dot",
|
||||||
|
"delimiter": "comma",
|
||||||
|
"quotechar": '"',
|
||||||
|
"timestamp_format": "%m/%d/%Y",
|
||||||
|
"no_header": True,
|
||||||
|
"timestamp_column": "0",
|
||||||
|
"amount_column": "3",
|
||||||
|
"original_currency_column": "2",
|
||||||
|
"original_amount_column": "4",
|
||||||
|
"description_column": "1,7",
|
||||||
|
"partner_name_column": "5",
|
||||||
|
"bank_account_column": "6",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
journal = self.AccountJournal.create(
|
||||||
|
{
|
||||||
|
"name": "Bank",
|
||||||
|
"type": "bank",
|
||||||
|
"code": "BANK",
|
||||||
|
"currency_id": self.currency_usd.id,
|
||||||
|
"suspense_account_id": self.suspense_account.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
data = self._data_file("fixtures/original_currency_no_header.csv", "utf-8")
|
||||||
|
wizard = self.AccountStatementImport.with_context(journal_id=journal.id).create(
|
||||||
|
{
|
||||||
|
"statement_filename": "fixtures/original_currency.csv",
|
||||||
|
"statement_file": data,
|
||||||
|
"sheet_mapping_id": no_header_statement_map.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
wizard.with_context(
|
||||||
|
account_statement_import_txt_xlsx_test=True
|
||||||
|
).import_file_button()
|
||||||
|
statement = self.AccountBankStatement.search([("journal_id", "=", journal.id)])
|
||||||
|
self.assertEqual(len(statement), 1)
|
||||||
|
self.assertEqual(len(statement.line_ids), 1)
|
||||||
|
|
||||||
|
line = statement.line_ids
|
||||||
|
self.assertEqual(line.currency_id, self.currency_usd)
|
||||||
|
self.assertEqual(line.foreign_currency_id, self.currency_eur)
|
||||||
|
self.assertEqual(line.amount_currency, 1000.0)
|
||||||
|
self.assertEqual(line.payment_ref, "Your payment INV0001")
|
||||||
|
|
||||||
def test_original_currency_empty(self):
|
def test_original_currency_empty(self):
|
||||||
journal = self.AccountJournal.create(
|
journal = self.AccountJournal.create(
|
||||||
{
|
{
|
||||||
@@ -219,6 +282,7 @@ class TestAccountBankStatementImportTxtXlsx(common.TransactionCase):
|
|||||||
"type": "bank",
|
"type": "bank",
|
||||||
"code": "BANK",
|
"code": "BANK",
|
||||||
"currency_id": self.currency_usd.id,
|
"currency_id": self.currency_usd.id,
|
||||||
|
"suspense_account_id": self.suspense_account.id,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
data = self._data_file("fixtures/original_currency_empty.csv", "utf-8")
|
data = self._data_file("fixtures/original_currency_empty.csv", "utf-8")
|
||||||
@@ -247,6 +311,7 @@ class TestAccountBankStatementImportTxtXlsx(common.TransactionCase):
|
|||||||
"type": "bank",
|
"type": "bank",
|
||||||
"code": "BANK",
|
"code": "BANK",
|
||||||
"currency_id": self.currency_usd.id,
|
"currency_id": self.currency_usd.id,
|
||||||
|
"suspense_account_id": self.suspense_account.id,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
statement_map = self.sample_statement_map.copy(
|
statement_map = self.sample_statement_map.copy(
|
||||||
@@ -282,6 +347,7 @@ class TestAccountBankStatementImportTxtXlsx(common.TransactionCase):
|
|||||||
"type": "bank",
|
"type": "bank",
|
||||||
"code": "BANK",
|
"code": "BANK",
|
||||||
"currency_id": self.currency_usd.id,
|
"currency_id": self.currency_usd.id,
|
||||||
|
"suspense_account_id": self.suspense_account.id,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
statement_map = self.sample_statement_map.copy(
|
statement_map = self.sample_statement_map.copy(
|
||||||
@@ -316,6 +382,7 @@ class TestAccountBankStatementImportTxtXlsx(common.TransactionCase):
|
|||||||
"type": "bank",
|
"type": "bank",
|
||||||
"code": "BANK",
|
"code": "BANK",
|
||||||
"currency_id": self.currency_usd.id,
|
"currency_id": self.currency_usd.id,
|
||||||
|
"suspense_account_id": self.suspense_account.id,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
statement_map = self.sample_statement_map.copy(
|
statement_map = self.sample_statement_map.copy(
|
||||||
@@ -345,3 +412,41 @@ class TestAccountBankStatementImportTxtXlsx(common.TransactionCase):
|
|||||||
self.assertEqual(statement.balance_start, 10.0)
|
self.assertEqual(statement.balance_start, 10.0)
|
||||||
self.assertEqual(statement.balance_end_real, 1510.0)
|
self.assertEqual(statement.balance_end_real, 1510.0)
|
||||||
self.assertEqual(statement.balance_end, 1510.0)
|
self.assertEqual(statement.balance_end, 1510.0)
|
||||||
|
|
||||||
|
def test_debit_credit_amount(self):
|
||||||
|
journal = self.AccountJournal.create(
|
||||||
|
{
|
||||||
|
"name": "Bank",
|
||||||
|
"type": "bank",
|
||||||
|
"code": "BANK",
|
||||||
|
"currency_id": self.currency_usd.id,
|
||||||
|
"suspense_account_id": self.suspense_account.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
statement_map = self.sample_statement_map.copy(
|
||||||
|
{
|
||||||
|
"amount_debit_column": "Debit",
|
||||||
|
"amount_credit_column": "Credit",
|
||||||
|
"balance_column": "Balance",
|
||||||
|
"amount_column": None,
|
||||||
|
"original_currency_column": None,
|
||||||
|
"original_amount_column": None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
data = self._data_file("fixtures/debit_credit_amount.csv", "utf-8")
|
||||||
|
wizard = self.AccountStatementImport.with_context(journal_id=journal.id).create(
|
||||||
|
{
|
||||||
|
"statement_filename": "fixtures/debit_credit_amount.csv",
|
||||||
|
"statement_file": data,
|
||||||
|
"sheet_mapping_id": statement_map.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
wizard.with_context(
|
||||||
|
account_statement_import_txt_xlsx_test=True
|
||||||
|
).import_file_button()
|
||||||
|
statement = self.AccountBankStatement.search([("journal_id", "=", journal.id)])
|
||||||
|
self.assertEqual(len(statement), 1)
|
||||||
|
self.assertEqual(len(statement.line_ids), 4)
|
||||||
|
self.assertEqual(statement.balance_start, 10.0)
|
||||||
|
self.assertEqual(statement.balance_end_real, 1510.0)
|
||||||
|
self.assertEqual(statement.balance_end, 1510.0)
|
||||||
|
|||||||
@@ -39,6 +39,18 @@
|
|||||||
<group>
|
<group>
|
||||||
<field name="timestamp_format" />
|
<field name="timestamp_format" />
|
||||||
</group>
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="no_header" />
|
||||||
|
<div
|
||||||
|
class="alert alert-warning"
|
||||||
|
role="alert"
|
||||||
|
attrs="{'invisible': [('no_header', '=', False)]}"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="fa fa-info-circle"
|
||||||
|
/> indicate the column number in the Columns section. The first column is 0.
|
||||||
|
</div>
|
||||||
|
</group>
|
||||||
<group
|
<group
|
||||||
attrs="{'invisible': [('debit_credit_column', '=', False)]}"
|
attrs="{'invisible': [('debit_credit_column', '=', False)]}"
|
||||||
>
|
>
|
||||||
@@ -53,20 +65,33 @@
|
|||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
<group string="Columns">
|
<group string="Columns">
|
||||||
<field name="timestamp_column" />
|
<group colspan="4" col="2">
|
||||||
<field name="currency_column" />
|
<div class="alert alert-info" role="alert">
|
||||||
<field name="amount_column" />
|
<span
|
||||||
<field name="balance_column" />
|
class="fa fa-info-circle"
|
||||||
<field name="original_currency_column" />
|
/> Add the column names or column number (when the file has no header).
|
||||||
<field name="original_amount_column" />
|
You can concatenate multiple columns in the file into the same field, indicating the
|
||||||
<field name="debit_credit_column" />
|
column names or numbers separated by comma.
|
||||||
<field name="transaction_id_column" />
|
</div>
|
||||||
<field name="description_column" />
|
</group>
|
||||||
<field name="notes_column" />
|
<group>
|
||||||
<field name="reference_column" />
|
<field name="timestamp_column" />
|
||||||
<field name="partner_name_column" />
|
<field name="currency_column" />
|
||||||
<field name="bank_name_column" />
|
<field name="amount_column" />
|
||||||
<field name="bank_account_column" />
|
<field name="amount_debit_column" />
|
||||||
|
<field name="amount_credit_column" />
|
||||||
|
<field name="balance_column" />
|
||||||
|
<field name="original_currency_column" />
|
||||||
|
<field name="original_amount_column" />
|
||||||
|
<field name="debit_credit_column" />
|
||||||
|
<field name="transaction_id_column" />
|
||||||
|
<field name="description_column" />
|
||||||
|
<field name="notes_column" />
|
||||||
|
<field name="reference_column" />
|
||||||
|
<field name="partner_name_column" />
|
||||||
|
<field name="bank_name_column" />
|
||||||
|
<field name="bank_account_column" />
|
||||||
|
</group>
|
||||||
</group>
|
</group>
|
||||||
</sheet>
|
</sheet>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -42,6 +42,14 @@ class AccountStatementImportSheetMappingWizard(models.TransientModel):
|
|||||||
amount_column = fields.Char(
|
amount_column = fields.Char(
|
||||||
help="Amount of transaction in journal's currency",
|
help="Amount of transaction in journal's currency",
|
||||||
)
|
)
|
||||||
|
amount_debit_column = fields.Char(
|
||||||
|
string="Debit amount column",
|
||||||
|
help="Debit amount of transaction in journal's currency",
|
||||||
|
)
|
||||||
|
amount_credit_column = fields.Boolean(
|
||||||
|
string="Credit amount column",
|
||||||
|
help="Credit amount of transaction in journal's currency",
|
||||||
|
)
|
||||||
balance_column = fields.Char(
|
balance_column = fields.Char(
|
||||||
help="Balance after transaction in journal's currency",
|
help="Balance after transaction in journal's currency",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -50,7 +50,18 @@
|
|||||||
widget="dynamic_dropdown"
|
widget="dynamic_dropdown"
|
||||||
values="statement_columns"
|
values="statement_columns"
|
||||||
context="{'header': header}"
|
context="{'header': header}"
|
||||||
attrs="{'required': [('state', '=', 'final')]}"
|
/>
|
||||||
|
<field
|
||||||
|
name="amount_debit_column"
|
||||||
|
widget="dynamic_dropdown"
|
||||||
|
values="statement_columns"
|
||||||
|
context="{'header': header}"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="amount_credit_column"
|
||||||
|
widget="dynamic_dropdown"
|
||||||
|
values="statement_columns"
|
||||||
|
context="{'header': header}"
|
||||||
/>
|
/>
|
||||||
<field
|
<field
|
||||||
name="balance_column"
|
name="balance_column"
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
# generated from manifests external_dependencies
|
# generated from manifests external_dependencies
|
||||||
odoo_test_helper
|
chardet
|
||||||
xlrd
|
xlrd
|
||||||
|
|||||||
1
test-requirements.txt
Normal file
1
test-requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
odoo_test_helper
|
||||||
Reference in New Issue
Block a user