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",
|
||||
"category": "Accounting",
|
||||
"summary": "Online bank statements update",
|
||||
"external_dependencies": {"python": ["odoo_test_helper"]},
|
||||
"depends": [
|
||||
"account",
|
||||
"account_statement_import",
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"multi_step_wizard",
|
||||
"web_widget_dropdown_dynamic",
|
||||
],
|
||||
"external_dependencies": {"python": ["xlrd"]},
|
||||
"external_dependencies": {"python": ["xlrd", "chardet"]},
|
||||
"data": [
|
||||
"security/ir.model.access.csv",
|
||||
"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()
|
||||
try:
|
||||
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:
|
||||
if self.env.context.get("account_statement_import_txt_xlsx_test"):
|
||||
raise
|
||||
_logger.warning("Sheet parser error", exc_info=True)
|
||||
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='"')
|
||||
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)
|
||||
currency_column = fields.Char(
|
||||
help=(
|
||||
@@ -63,9 +68,16 @@ class AccountStatementImportSheetMapping(models.Model):
|
||||
),
|
||||
)
|
||||
amount_column = fields.Char(
|
||||
required=True,
|
||||
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(
|
||||
help="Balance after transaction in journal's currency",
|
||||
)
|
||||
@@ -112,6 +124,19 @@ class AccountStatementImportSheetMapping(models.Model):
|
||||
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")
|
||||
def onchange_thousands_separator(self):
|
||||
if "dot" == self.float_thousands_sep == self.float_decimal_sep:
|
||||
|
||||
@@ -7,8 +7,10 @@ import logging
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from io import StringIO
|
||||
from os import path
|
||||
|
||||
from odoo import _, api, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -20,6 +22,14 @@ try:
|
||||
except (ImportError, IOError) as err: # pragma: no cover
|
||||
_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):
|
||||
_name = "account.statement.import.sheet.parser"
|
||||
@@ -43,7 +53,7 @@ class AccountStatementImportSheetParser(models.TransientModel):
|
||||
return list(next(csv_data))
|
||||
|
||||
@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"))
|
||||
currency_code = (journal.currency_id or journal.company_id.currency_id).name
|
||||
account_number = journal.bank_account_id.acc_number
|
||||
@@ -57,6 +67,11 @@ class AccountStatementImportSheetParser(models.TransientModel):
|
||||
last_line = lines[-1]
|
||||
data = {
|
||||
"date": first_line["timestamp"].date(),
|
||||
"name": _("%(code)s: %(filename)s")
|
||||
% {
|
||||
"code": journal.code,
|
||||
"filename": path.basename(filename),
|
||||
},
|
||||
}
|
||||
|
||||
if mapping.balance_column:
|
||||
@@ -69,7 +84,6 @@ class AccountStatementImportSheetParser(models.TransientModel):
|
||||
"balance_end_real": float(balance_end),
|
||||
}
|
||||
)
|
||||
|
||||
transactions = list(
|
||||
itertools.chain.from_iterable(
|
||||
map(lambda line: self._convert_line_to_transactions(line), lines)
|
||||
@@ -79,6 +93,50 @@ class AccountStatementImportSheetParser(models.TransientModel):
|
||||
|
||||
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):
|
||||
columns = dict()
|
||||
try:
|
||||
@@ -99,69 +157,44 @@ class AccountStatementImportSheetParser(models.TransientModel):
|
||||
csv_options["delimiter"] = csv_delimiter
|
||||
if mapping.quotechar:
|
||||
csv_options["quotechar"] = mapping.quotechar
|
||||
csv_or_xlsx = reader(
|
||||
StringIO(data_file.decode(mapping.file_encoding or "utf-8")),
|
||||
**csv_options
|
||||
try:
|
||||
decoded_file = data_file.decode(mapping.file_encoding or "utf-8")
|
||||
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)
|
||||
|
||||
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
|
||||
if isinstance(csv_or_xlsx, tuple):
|
||||
rows = range(1, csv_or_xlsx[1].nrows)
|
||||
@@ -183,66 +216,83 @@ class AccountStatementImportSheetParser(models.TransientModel):
|
||||
else:
|
||||
values = list(row)
|
||||
|
||||
timestamp = values[columns["timestamp_column"]]
|
||||
timestamp = self._get_values_from_column(
|
||||
values, columns, "timestamp_column"
|
||||
)
|
||||
currency = (
|
||||
values[columns["currency_column"]]
|
||||
if columns["currency_column"] is not None
|
||||
self._get_values_from_column(values, columns, "currency_column")
|
||||
if columns["currency_column"]
|
||||
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 = (
|
||||
values[columns["balance_column"]]
|
||||
if columns["balance_column"] is not None
|
||||
self._get_values_from_column(values, columns, "balance_column")
|
||||
if columns["balance_column"]
|
||||
else None
|
||||
)
|
||||
original_currency = (
|
||||
values[columns["original_currency_column"]]
|
||||
if columns["original_currency_column"] is not None
|
||||
self._get_values_from_column(
|
||||
values, columns, "original_currency_column"
|
||||
)
|
||||
if columns["original_currency_column"]
|
||||
else None
|
||||
)
|
||||
original_amount = (
|
||||
values[columns["original_amount_column"]]
|
||||
if columns["original_amount_column"] is not None
|
||||
self._get_values_from_column(values, columns, "original_amount_column")
|
||||
if columns["original_amount_column"]
|
||||
else None
|
||||
)
|
||||
debit_credit = (
|
||||
values[columns["debit_credit_column"]]
|
||||
if columns["debit_credit_column"] is not None
|
||||
self._get_values_from_column(values, columns, "debit_credit_column")
|
||||
if columns["debit_credit_column"]
|
||||
else None
|
||||
)
|
||||
transaction_id = (
|
||||
values[columns["transaction_id_column"]]
|
||||
if columns["transaction_id_column"] is not None
|
||||
self._get_values_from_column(values, columns, "transaction_id_column")
|
||||
if columns["transaction_id_column"]
|
||||
else None
|
||||
)
|
||||
description = (
|
||||
values[columns["description_column"]]
|
||||
if columns["description_column"] is not None
|
||||
self._get_values_from_column(values, columns, "description_column")
|
||||
if columns["description_column"]
|
||||
else None
|
||||
)
|
||||
notes = (
|
||||
values[columns["notes_column"]]
|
||||
if columns["notes_column"] is not None
|
||||
self._get_values_from_column(values, columns, "notes_column")
|
||||
if columns["notes_column"]
|
||||
else None
|
||||
)
|
||||
reference = (
|
||||
values[columns["reference_column"]]
|
||||
if columns["reference_column"] is not None
|
||||
self._get_values_from_column(values, columns, "reference_column")
|
||||
if columns["reference_column"]
|
||||
else None
|
||||
)
|
||||
partner_name = (
|
||||
values[columns["partner_name_column"]]
|
||||
if columns["partner_name_column"] is not None
|
||||
self._get_values_from_column(values, columns, "partner_name_column")
|
||||
if columns["partner_name_column"]
|
||||
else None
|
||||
)
|
||||
bank_name = (
|
||||
values[columns["bank_name_column"]]
|
||||
if columns["bank_name_column"] is not None
|
||||
self._get_values_from_column(values, columns, "bank_name_column")
|
||||
if columns["bank_name_column"]
|
||||
else None
|
||||
)
|
||||
bank_account = (
|
||||
values[columns["bank_account_column"]]
|
||||
if columns["bank_account_column"] is not None
|
||||
self._get_values_from_column(values, columns, "bank_account_column")
|
||||
if columns["bank_account_column"]
|
||||
else None
|
||||
)
|
||||
|
||||
@@ -252,7 +302,6 @@ class AccountStatementImportSheetParser(models.TransientModel):
|
||||
if isinstance(timestamp, str):
|
||||
timestamp = datetime.strptime(timestamp, mapping.timestamp_format)
|
||||
|
||||
amount = self._parse_decimal(amount, mapping)
|
||||
if balance:
|
||||
balance = self._parse_decimal(balance, mapping)
|
||||
else:
|
||||
@@ -359,9 +408,11 @@ class AccountStatementImportSheetParser(models.TransientModel):
|
||||
if transaction_id:
|
||||
note += _("Transaction ID: %s; ") % (transaction_id,)
|
||||
if note and notes:
|
||||
note = "{}\n{}".format(note, note.strip())
|
||||
note = "{}\n{}".format(notes, note.strip())
|
||||
elif note:
|
||||
note = note.strip()
|
||||
elif notes:
|
||||
note = notes
|
||||
if 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[
|
||||
"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):
|
||||
mode = "rt" if encoding else "rb"
|
||||
@@ -47,6 +56,7 @@ class TestAccountBankStatementImportTxtXlsx(common.TransactionCase):
|
||||
"type": "bank",
|
||||
"code": "BANK",
|
||||
"currency_id": self.currency_usd.id,
|
||||
"suspense_account_id": self.suspense_account.id,
|
||||
}
|
||||
)
|
||||
data = self._data_file("fixtures/sample_statement_en.csv", "utf-8")
|
||||
@@ -71,6 +81,7 @@ class TestAccountBankStatementImportTxtXlsx(common.TransactionCase):
|
||||
"type": "bank",
|
||||
"code": "BANK",
|
||||
"currency_id": self.currency_usd.id,
|
||||
"suspense_account_id": self.suspense_account.id,
|
||||
}
|
||||
)
|
||||
data = self._data_file("fixtures/empty_statement_en.csv", "utf-8")
|
||||
@@ -95,6 +106,7 @@ class TestAccountBankStatementImportTxtXlsx(common.TransactionCase):
|
||||
"type": "bank",
|
||||
"code": "BANK",
|
||||
"currency_id": self.currency_usd.id,
|
||||
"suspense_account_id": self.suspense_account.id,
|
||||
}
|
||||
)
|
||||
data = self._data_file("fixtures/sample_statement_en.xlsx")
|
||||
@@ -119,6 +131,7 @@ class TestAccountBankStatementImportTxtXlsx(common.TransactionCase):
|
||||
"type": "bank",
|
||||
"code": "BANK",
|
||||
"currency_id": self.currency_usd.id,
|
||||
"suspense_account_id": self.suspense_account.id,
|
||||
}
|
||||
)
|
||||
data = self._data_file("fixtures/empty_statement_en.xlsx")
|
||||
@@ -189,6 +202,7 @@ class TestAccountBankStatementImportTxtXlsx(common.TransactionCase):
|
||||
"type": "bank",
|
||||
"code": "BANK",
|
||||
"currency_id": self.currency_usd.id,
|
||||
"suspense_account_id": self.suspense_account.id,
|
||||
}
|
||||
)
|
||||
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.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):
|
||||
journal = self.AccountJournal.create(
|
||||
{
|
||||
@@ -219,6 +282,7 @@ class TestAccountBankStatementImportTxtXlsx(common.TransactionCase):
|
||||
"type": "bank",
|
||||
"code": "BANK",
|
||||
"currency_id": self.currency_usd.id,
|
||||
"suspense_account_id": self.suspense_account.id,
|
||||
}
|
||||
)
|
||||
data = self._data_file("fixtures/original_currency_empty.csv", "utf-8")
|
||||
@@ -247,6 +311,7 @@ class TestAccountBankStatementImportTxtXlsx(common.TransactionCase):
|
||||
"type": "bank",
|
||||
"code": "BANK",
|
||||
"currency_id": self.currency_usd.id,
|
||||
"suspense_account_id": self.suspense_account.id,
|
||||
}
|
||||
)
|
||||
statement_map = self.sample_statement_map.copy(
|
||||
@@ -282,6 +347,7 @@ class TestAccountBankStatementImportTxtXlsx(common.TransactionCase):
|
||||
"type": "bank",
|
||||
"code": "BANK",
|
||||
"currency_id": self.currency_usd.id,
|
||||
"suspense_account_id": self.suspense_account.id,
|
||||
}
|
||||
)
|
||||
statement_map = self.sample_statement_map.copy(
|
||||
@@ -316,6 +382,7 @@ class TestAccountBankStatementImportTxtXlsx(common.TransactionCase):
|
||||
"type": "bank",
|
||||
"code": "BANK",
|
||||
"currency_id": self.currency_usd.id,
|
||||
"suspense_account_id": self.suspense_account.id,
|
||||
}
|
||||
)
|
||||
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_end_real, 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>
|
||||
<field name="timestamp_format" />
|
||||
</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
|
||||
attrs="{'invisible': [('debit_credit_column', '=', False)]}"
|
||||
>
|
||||
@@ -53,20 +65,33 @@
|
||||
</group>
|
||||
</group>
|
||||
<group string="Columns">
|
||||
<field name="timestamp_column" />
|
||||
<field name="currency_column" />
|
||||
<field name="amount_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 colspan="4" col="2">
|
||||
<div class="alert alert-info" role="alert">
|
||||
<span
|
||||
class="fa fa-info-circle"
|
||||
/> Add the column names or column number (when the file has no header).
|
||||
You can concatenate multiple columns in the file into the same field, indicating the
|
||||
column names or numbers separated by comma.
|
||||
</div>
|
||||
</group>
|
||||
<group>
|
||||
<field name="timestamp_column" />
|
||||
<field name="currency_column" />
|
||||
<field name="amount_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>
|
||||
</sheet>
|
||||
</form>
|
||||
|
||||
@@ -42,6 +42,14 @@ class AccountStatementImportSheetMappingWizard(models.TransientModel):
|
||||
amount_column = fields.Char(
|
||||
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(
|
||||
help="Balance after transaction in journal's currency",
|
||||
)
|
||||
|
||||
@@ -50,7 +50,18 @@
|
||||
widget="dynamic_dropdown"
|
||||
values="statement_columns"
|
||||
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
|
||||
name="balance_column"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# generated from manifests external_dependencies
|
||||
odoo_test_helper
|
||||
chardet
|
||||
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