Merge PR #569 into 15.0

Signed-off-by JordiBForgeFlow
This commit is contained in:
OCA-git-bot
2023-02-23 11:49:31 +00:00
15 changed files with 395 additions and 150 deletions

View File

@@ -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",

View File

@@ -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",

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View 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 Date Label Debit Credit Balance Partner Name Bank Account
2 12/15/2018 Credit 20.00 0.00 20.00 -10.00 John Doe 123456789
3 12/15/2018 Credit 13.50 0.00 -13.50 -23.50 John Doe 123456789
4 12/15/2018 Debit 33.50 -33.50 0.00 10.00 Azure Interior
5 12/15/2018 Debit 1500 1,500.00 0.00 1,510.00 Azure Interior

View File

@@ -0,0 +1 @@
"12/15/2018","Your payment","EUR","1,525.00","-1,000.00","Azure Interior","","INV0001"
1 12/15/2018 Your payment EUR 1,525.00 -1,000.00 Azure Interior INV0001

View File

@@ -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)

View File

@@ -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>

View File

@@ -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",
) )

View File

@@ -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"

View File

@@ -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
View File

@@ -0,0 +1 @@
odoo_test_helper