diff --git a/account_bank_statement_import_txt_xlsx/__manifest__.py b/account_bank_statement_import_txt_xlsx/__manifest__.py index 08b8fba3..6d1f50c3 100644 --- a/account_bank_statement_import_txt_xlsx/__manifest__.py +++ b/account_bank_statement_import_txt_xlsx/__manifest__.py @@ -1,28 +1,35 @@ -# Copyright 2014-2017 Akretion (http://www.akretion.com). -# @author Alexis de Lattre -# @author Sébastien BEAU -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +# Copyright 2019 ForgeFlow, S.L. +# Copyright 2020 Brainbean Apps (https://brainbeanapps.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + { "name": "Account Bank Statement Import TXT XLSX", - 'summary': 'Import TXT/CSV or XLSX files as Bank Statements in Odoo', - "version": "12.0.1.0.1", + "summary": "Import TXT/CSV or XLSX files as Bank Statements in Odoo", + "version": "12.0.2.0.0", "category": "Accounting", "website": "https://github.com/OCA/bank-statement-import", - "author": " Eficent, Odoo Community Association (OCA)", + "author": + "ForgeFlow, " + "Brainbean Apps, " + "Odoo Community Association (OCA)", "license": "AGPL-3", "installable": True, "depends": [ "account_bank_statement_import", + "multi_step_wizard", + "web_widget_dropdown_dynamic", ], "external_dependencies": { - "python": ["xlrd"], + "python": [ + "csv", + "xlrd", + ] }, "data": [ "security/ir.model.access.csv", - "data/txt_map_data.xml", - "wizards/create_map_lines_from_file_views.xml", - "wizards/account_bank_statement_import_view.xml", - "views/account_journal_views.xml", - "views/txt_map_views.xml", + "data/map_data.xml", + "views/account_bank_statement_import_sheet_mapping.xml", + "views/account_bank_statement_import.xml", + "wizards/account_bank_statement_import_sheet_mapping_wizard.xml", ] } diff --git a/account_bank_statement_import_txt_xlsx/data/map_data.xml b/account_bank_statement_import_txt_xlsx/data/map_data.xml new file mode 100644 index 00000000..3025d9c9 --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/data/map_data.xml @@ -0,0 +1,25 @@ + + + + + + Sample Statement + comma + dot + comma + " + %m/%d/%Y + Date + Amount + Currency + Amount Currency + Label + Partner Name + Bank Account + + + diff --git a/account_bank_statement_import_txt_xlsx/data/txt_map_data.xml b/account_bank_statement_import_txt_xlsx/data/txt_map_data.xml deleted file mode 100644 index fb4c899a..00000000 --- a/account_bank_statement_import_txt_xlsx/data/txt_map_data.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - - - Sample Statement - comma - dot - - - - Date - 0 - - date - %m/%d/%Y - - - Label - 1 - - name - - - Currency - 2 - - currency - - - Amount - 3 - - amount - - - Amount Currency - 4 - - amount_currency - - - Partner Name - 5 - - partner_name - - - Bank Account - 6 - - account_number - - diff --git a/account_bank_statement_import_txt_xlsx/migrations/12.0.2.0.0/post-migration.py b/account_bank_statement_import_txt_xlsx/migrations/12.0.2.0.0/post-migration.py new file mode 100644 index 00000000..49e0e6a4 --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/migrations/12.0.2.0.0/post-migration.py @@ -0,0 +1,98 @@ +# Copyright 2020 Brainbean Apps (https://brainbeanapps.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from openupgradelib import openupgrade + + +@openupgrade.migrate() +def migrate(env, version): + openupgrade.logged_query( + env.cr, + """ +WITH _mappings AS ( + SELECT + m.id, + l.field_to_assign, + l.name, + l.date_format + FROM + account_bank_statement_import_map AS m + RIGHT OUTER JOIN ( + SELECT + *, + ROW_NUMBER() OVER ( + PARTITION BY map_parent_id, field_to_assign + ORDER BY id ASC + ) AS row_number + FROM account_bank_statement_import_map_line + WHERE field_to_assign IS NOT NULL + ) AS l ON m.id = l.map_parent_id AND l.row_number = 1 +) + +INSERT INTO account_bank_statement_import_sheet_mapping ( + name, + float_thousands_sep, + float_decimal_sep, + file_encoding, + delimiter, + quotechar, + timestamp_format, + timestamp_column, + amount_column, + original_currency_column, + original_amount_column, + description_column, + reference_column, + notes_column, + partner_name_column, + bank_account_column +) +SELECT + m.name, + m.float_thousands_sep, + m.float_decimal_sep, + m.file_encoding, + ( + CASE + WHEN m.delimiter='.' THEN 'dot' + WHEN m.delimiter=',' THEN 'comma' + WHEN m.delimiter=';' THEN 'semicolon' + WHEN m.delimiter='' THEN 'n/a' + WHEN m.delimiter='\t' THEN 'tab' + WHEN m.delimiter=' ' THEN 'space' + ELSE 'n/a' + END + ) AS delimiter, + m.quotechar, + COALESCE(_date.date_format, '%m/%d/%Y') AS timestamp_format, + COALESCE(_date.name, 'Date') AS timestamp_column, + COALESCE(_amount.name, 'Amount') AS amount_column, + _o_currency.name AS original_currency_column, + _o_amount.name AS original_amount_column, + _description.name AS description_column, + _ref.name AS reference_column, + _notes.name AS notes_column, + _p_name.name AS partner_name_column, + _bank_acc.name AS bank_account_column +FROM + account_bank_statement_import_map AS m +LEFT JOIN _mappings AS _date + ON m.id = _date.id AND _date.field_to_assign = 'date' +LEFT JOIN _mappings AS _description + ON m.id = _description.id AND _description.field_to_assign = 'name' +LEFT JOIN _mappings AS _o_currency + ON m.id = _o_currency.id AND _o_currency.field_to_assign = 'currency' +LEFT JOIN _mappings AS _amount + ON m.id = _amount.id AND _amount.field_to_assign = 'amount' +LEFT JOIN _mappings AS _o_amount + ON m.id = _o_amount.id AND _o_amount.field_to_assign = 'amount_currency' +LEFT JOIN _mappings AS _ref + ON m.id = _ref.id AND _ref.field_to_assign = 'ref' +LEFT JOIN _mappings AS _notes + ON m.id = _notes.id AND _notes.field_to_assign = 'note' +LEFT JOIN _mappings AS _p_name + ON m.id = _p_name.id AND _p_name.field_to_assign = 'partner_name' +LEFT JOIN _mappings AS _bank_acc + ON m.id = _bank_acc.id AND _bank_acc.field_to_assign = 'account_number'; + """ + ) diff --git a/account_bank_statement_import_txt_xlsx/models/__init__.py b/account_bank_statement_import_txt_xlsx/models/__init__.py index ff680f4c..cddf714a 100644 --- a/account_bank_statement_import_txt_xlsx/models/__init__.py +++ b/account_bank_statement_import_txt_xlsx/models/__init__.py @@ -1,2 +1,6 @@ -from . import account_bank_statement_import_txt_map +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import account_bank_statement_import_sheet_mapping +from . import account_bank_statement_import_sheet_parser +from . import account_bank_statement_import from . import account_journal diff --git a/account_bank_statement_import_txt_xlsx/models/account_bank_statement_import.py b/account_bank_statement_import_txt_xlsx/models/account_bank_statement_import.py new file mode 100644 index 00000000..dd869e9b --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/models/account_bank_statement_import.py @@ -0,0 +1,33 @@ +# Copyright 2020 Brainbean Apps (https://brainbeanapps.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + +import logging +_logger = logging.getLogger(__name__) + + +class AccountBankStatementImport(models.TransientModel): + _inherit = 'account.bank.statement.import' + + sheet_mapping_id = fields.Many2one( + string='Sheet mapping', + comodel_name='account.bank.statement.import.sheet.mapping', + ) + + @api.multi + def _parse_file(self, data_file): + self.ensure_one() + try: + Parser = self.env['account.bank.statement.import.sheet.parser'] + return Parser.parse( + self.sheet_mapping_id, + data_file, + self.filename + ) + except: + if self.env.context.get( + 'account_bank_statement_import_txt_xlsx_test'): + raise + _logger.warning('Sheet parser error', exc_info=True) + return super()._parse_file(data_file) diff --git a/account_bank_statement_import_txt_xlsx/models/account_bank_statement_import_sheet_mapping.py b/account_bank_statement_import_txt_xlsx/models/account_bank_statement_import_sheet_mapping.py new file mode 100644 index 00000000..2e91fdae --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/models/account_bank_statement_import_sheet_mapping.py @@ -0,0 +1,188 @@ +# Copyright 2019 ForgeFlow, S.L. +# Copyright 2020 Brainbean Apps (https://brainbeanapps.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class AccountBankStatementImportSheetMapping(models.Model): + _name = 'account.bank.statement.import.sheet.mapping' + _description = 'Account Bank Statement Import Sheet Mapping' + + name = fields.Char( + required=True, + ) + float_thousands_sep = fields.Selection( + string='Thousands Separator', + selection=[ + ('dot', 'dot (.)'), + ('comma', 'comma (,)'), + ('none', 'none'), + ], + default='dot', + ) + float_decimal_sep = fields.Selection( + string='Decimals Separator', + selection=[ + ('dot', 'dot (.)'), + ('comma', 'comma (,)'), + ('none', 'none'), + ], + default='comma', + ) + file_encoding = fields.Selection( + string='Encoding', + selection=[ + ('utf-8', 'UTF-8'), + ('utf-8-sig', 'UTF-8 (with BOM)'), + ('utf-16', 'UTF-16'), + ('utf-16-sig', 'UTF-16 (with BOM)'), + ('windows-1252', 'Western (Windows-1252)'), + ('iso-8859-1', 'Western (Latin-1 / ISO 8859-1)'), + ('iso-8859-2', 'Central European (Latin-2 / ISO 8859-2)'), + ('iso-8859-4', 'Baltic (Latin-4 / ISO 8859-4)'), + ('big5', 'Traditional Chinese (big5)'), + ('gb18030', 'Unified Chinese (gb18030)'), + ('shift_jis', 'Japanese (Shift JIS)'), + ('windows-1251', 'Cyrillic (Windows-1251)'), + ('koi8_r', 'Cyrillic (KOI8-R)'), + ('koi8_u', 'Cyrillic (KOI8-U)'), + ], + default='utf-8', + ) + delimiter = fields.Selection( + string='Delimiter', + selection=[ + ('dot', 'dot (.)'), + ('comma', 'comma (,)'), + ('semicolon', 'semicolon (;)'), + ('tab', 'tab'), + ('space', 'space'), + ('n/a', 'N/A'), + ], + default='comma', + ) + quotechar = fields.Char( + string='Text qualifier', + size=1, + default='"', + ) + timestamp_format = fields.Char( + string='Timestamp Format', + required=True, + ) + timestamp_column = fields.Char( + string='Timestamp column', + required=True, + ) + currency_column = fields.Char( + string='Currency column', + help=( + 'In case statement is multi-currency, column to get currency of ' + 'transaction from' + ), + ) + amount_column = fields.Char( + string='Amount column', + required=True, + help='Amount of transaction in journal\'s currency', + ) + balance_column = fields.Char( + string='Balance column', + help='Balance after transaction in journal\'s currency', + ) + original_currency_column = fields.Char( + string='Original currency column', + help=( + 'In case statement provides original currency for transactions ' + 'with automatic currency conversion, column to get original ' + 'currency of transaction from' + ), + ) + original_amount_column = fields.Char( + string='Original amount column', + help=( + 'In case statement provides original currency for transactions ' + 'with automatic currency conversion, column to get original ' + 'transaction amount in original transaction currency from' + ), + ) + debit_credit_column = fields.Char( + string='Debit/credit column', + help=( + 'Some statement formats use absolute amount value and indicate sign' + 'of the transaction by specifying if it was a debit or a credit one' + ), + ) + debit_value = fields.Char( + string='Debit value', + help='Value of debit/credit column that indicates if it\'s a debit', + default='D', + ) + credit_value = fields.Char( + string='Credit value', + help='Value of debit/credit column that indicates if it\'s a credit', + default='C', + ) + transaction_id_column = fields.Char( + string='Unique transaction ID column', + ) + description_column = fields.Char( + string='Description column', + ) + notes_column = fields.Char( + string='Notes column', + ) + reference_column = fields.Char( + string='Reference column', + ) + partner_name_column = fields.Char( + string='Partner Name column', + ) + bank_name_column = fields.Char( + string='Bank Name column', + help='Partner\'s bank', + ) + bank_account_column = fields.Char( + string='Bank Account column', + help='Partner\'s bank account', + ) + + @api.onchange('float_thousands_sep') + def onchange_thousands_separator(self): + if 'dot' == self.float_thousands_sep == self.float_decimal_sep: + self.float_decimal_sep = 'comma' + elif 'comma' == self.float_thousands_sep == self.float_decimal_sep: + self.float_decimal_sep = 'dot' + + @api.onchange('float_decimal_sep') + def onchange_decimal_separator(self): + if 'dot' == self.float_thousands_sep == self.float_decimal_sep: + self.float_thousands_sep = 'comma' + elif 'comma' == self.float_thousands_sep == self.float_decimal_sep: + self.float_thousands_sep = 'dot' + + @api.multi + def _get_float_separators(self): + self.ensure_one() + separators = { + 'dot': '.', + 'comma': ',', + 'none': '', + } + return (separators[self.float_thousands_sep], + separators[self.float_decimal_sep]) + + @api.model + def _decode_column_delimiter_character(self, delimiter): + return ({ + 'dot': '.', + 'comma': ',', + 'semicolon': ';', + 'tab': '\t', + 'space': ' ', + }).get(delimiter) + + @api.multi + def _get_column_delimiter_character(self): + return self._decode_column_delimiter_character(self.delimiter) diff --git a/account_bank_statement_import_txt_xlsx/models/account_bank_statement_import_sheet_parser.py b/account_bank_statement_import_txt_xlsx/models/account_bank_statement_import_sheet_parser.py new file mode 100644 index 00000000..89257845 --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/models/account_bank_statement_import_sheet_parser.py @@ -0,0 +1,331 @@ +# Copyright 2019 ForgeFlow, S.L. +# Copyright 2020 Brainbean Apps (https://brainbeanapps.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, models, _ + +from datetime import datetime +from decimal import Decimal +from io import StringIO +from os import path +import itertools + +import logging +_logger = logging.getLogger(__name__) + +try: + from csv import reader + import xlrd + from xlrd.xldate import xldate_as_datetime +except (ImportError, IOError) as err: # pragma: no cover + _logger.error(err) + + +class AccountBankStatementImportSheetParser(models.TransientModel): + _name = 'account.bank.statement.import.sheet.parser' + _description = 'Account Bank Statement Import Sheet Parser' + + @api.model + def parse_header(self, data_file, encoding, csv_options): + try: + workbook = xlrd.open_workbook( + file_contents=data_file, + encoding_override=encoding if encoding else None, + ) + sheet = workbook.sheet_by_index(0) + values = sheet.row_values(0) + return [str(value) for value in values] + except xlrd.XLRDError: + pass + + data = StringIO(data_file.decode(encoding or 'utf-8')) + csv_data = reader(data, **csv_options) + return list(next(csv_data)) + + @api.model + def parse(self, mapping, data_file, 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 + + name = _('%s: %s') % ( + journal.code, + path.basename(filename), + ) + lines = self._parse_lines(mapping, data_file, currency_code) + if not lines: + return currency_code, account_number, [{ + 'name': name, + 'transactions': [], + }] + + lines = list(sorted( + lines, + key=lambda line: line['timestamp'] + )) + first_line = lines[0] + last_line = lines[-1] + data = { + 'name': name, + 'date': first_line['timestamp'].date(), + } + + if mapping.balance_column: + balance_start = first_line['balance'] + balance_start -= first_line['amount'] + balance_end = last_line['balance'] + data.update({ + 'balance_start': float(balance_start), + 'balance_end_real': float(balance_end), + }) + + transactions = list(itertools.chain.from_iterable(map( + lambda line: self._convert_line_to_transactions(line), + lines + ))) + data.update({ + 'transactions': transactions, + }) + + return currency_code, account_number, [data] + + def _parse_lines(self, mapping, data_file, currency_code): + try: + workbook = xlrd.open_workbook( + file_contents=data_file, + encoding_override=( + mapping.file_encoding if mapping.file_encoding else None + ), + ) + csv_or_xlsx = (workbook, workbook.sheet_by_index(0),) + except xlrd.XLRDError: + csv_options = {} + csv_delimiter = mapping._get_column_delimiter_character() + if csv_delimiter: + 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 + ) + + if isinstance(csv_or_xlsx, tuple): + header = [str(value) for value in csv_or_xlsx[1].row_values(0)] + else: + header = list(next(csv_or_xlsx)) + timestamp_column = header.index(mapping.timestamp_column) + currency_column = header.index(mapping.currency_column) \ + if mapping.currency_column else None + amount_column = header.index(mapping.amount_column) + balance_column = header.index(mapping.balance_column) \ + if mapping.balance_column else None + original_currency_column = ( + header.index(mapping.original_currency_column) + if mapping.original_currency_column else None + ) + original_amount_column = ( + header.index(mapping.original_amount_column) + if mapping.original_amount_column else None + ) + debit_credit_column = header.index(mapping.debit_credit_column) \ + if mapping.debit_credit_column else None + transaction_id_column = header.index(mapping.transaction_id_column) \ + if mapping.transaction_id_column else None + description_column = header.index(mapping.description_column) \ + if mapping.description_column else None + notes_column = header.index(mapping.notes_column) \ + if mapping.notes_column else None + reference_column = header.index(mapping.reference_column) \ + if mapping.reference_column else None + partner_name_column = header.index(mapping.partner_name_column) \ + if mapping.partner_name_column else None + bank_name_column = header.index(mapping.bank_name_column) \ + if mapping.bank_name_column else None + bank_account_column = header.index(mapping.bank_account_column) \ + if mapping.bank_account_column else None + + if isinstance(csv_or_xlsx, tuple): + rows = range(1, csv_or_xlsx[1].nrows) + else: + rows = csv_or_xlsx + + lines = [] + for row in rows: + if isinstance(csv_or_xlsx, tuple): + book = csv_or_xlsx[0] + sheet = csv_or_xlsx[1] + values = [] + for col_index in range(sheet.row_len(row)): + cell_type = sheet.cell_type(row, col_index) + cell_value = sheet.cell_value(row, col_index) + if cell_type == xlrd.XL_CELL_DATE: + cell_value = xldate_as_datetime(cell_value, book.datemode) + values.append(cell_value) + else: + values = list(row) + + timestamp = values[timestamp_column] + currency = values[currency_column] \ + if currency_column is not None else currency_code + amount = values[amount_column] + balance = values[balance_column] \ + if balance_column is not None else None + original_currency = values[original_currency_column] \ + if original_currency_column is not None else None + original_amount = values[original_amount_column] \ + if original_amount_column is not None else None + debit_credit = values[debit_credit_column] \ + if debit_credit_column is not None else None + transaction_id = values[transaction_id_column] \ + if transaction_id_column is not None else None + description = values[description_column] \ + if description_column is not None else None + notes = values[notes_column] \ + if notes_column is not None else None + reference = values[reference_column] \ + if reference_column is not None else None + partner_name = values[partner_name_column] \ + if partner_name_column is not None else None + bank_name = values[bank_name_column] \ + if bank_name_column is not None else None + bank_account = values[bank_account_column] \ + if bank_account_column is not None else None + + if currency != currency_code: + continue + + if isinstance(timestamp, str): + timestamp = datetime.strptime( + timestamp, + mapping.timestamp_format + ) + + amount = self._parse_decimal(amount, mapping) + if balance is not None: + balance = self._parse_decimal(balance, mapping) + + if debit_credit is not None: + amount = amount.copy_abs() + if debit_credit == mapping.debit_value: + amount = -amount + + if original_currency is None: + original_currency = currency + original_amount = amount + elif original_currency == currency: + original_amount = amount + + original_amount = self._parse_decimal(original_amount, mapping) + + line = { + 'timestamp': timestamp, + 'amount': amount, + 'currency': currency, + 'original_amount': original_amount, + 'original_currency': original_currency, + } + if balance is not None: + line['balance'] = balance + if transaction_id is not None: + line['transaction_id'] = transaction_id + if description is not None: + line['description'] = description + if notes is not None: + line['notes'] = notes + if reference is not None: + line['reference'] = reference + if partner_name is not None: + line['partner_name'] = partner_name + if bank_name is not None: + line['bank_name'] = bank_name + if bank_account is not None: + line['bank_account'] = bank_account + lines.append(line) + return lines + + @api.model + def _convert_line_to_transactions(self, line): + """Hook for extension""" + timestamp = line['timestamp'] + amount = line['amount'] + currency = line['currency'] + original_amount = line['original_amount'] + original_currency = line['original_currency'] + transaction_id = line.get('transaction_id') + description = line.get('description') + notes = line.get('notes') + reference = line.get('reference') + partner_name = line.get('partner_name') + bank_name = line.get('bank_name') + bank_account = line.get('bank_account') + + transaction = { + 'date': timestamp, + 'amount': str(amount), + } + if currency != original_currency: + original_currency = self.env['res.currency'].search( + [('name', '=', original_currency)], + limit=1, + ) + if original_currency: + transaction.update({ + 'amount_currency': str(original_amount), + 'currency_id': original_currency.id, + }) + + if transaction_id: + transaction['unique_import_id'] = '%s-%s' % ( + transaction_id, + int(timestamp.timestamp()), + ) + + transaction['name'] = description or _('N/A') + if reference: + transaction['ref'] = reference + + note = '' + if bank_name: + note += _('Bank: %s; ') % ( + bank_name, + ) + if bank_account: + note += _('Account: %s; ') % ( + bank_account, + ) + if transaction_id: + note += _('Transaction ID: %s; ') % ( + transaction_id, + ) + if note and notes: + note = '%s\n%s' % ( + note, + note.strip(), + ) + elif note: + note = note.strip() + if note: + transaction['note'] = note + + if partner_name: + transaction['partner_name'] = partner_name + if bank_account: + transaction['account_number'] = bank_account + + return [transaction] + + @api.model + def _parse_decimal(self, value, mapping): + if isinstance(value, Decimal): + return value + elif isinstance(value, float): + return Decimal(value) + thousands, decimal = mapping._get_float_separators() + value = value.replace(thousands, '') + value = value.replace(decimal, '.') + return Decimal(value) diff --git a/account_bank_statement_import_txt_xlsx/models/account_bank_statement_import_txt_map.py b/account_bank_statement_import_txt_xlsx/models/account_bank_statement_import_txt_map.py deleted file mode 100644 index 9a6bc07f..00000000 --- a/account_bank_statement_import_txt_xlsx/models/account_bank_statement_import_txt_map.py +++ /dev/null @@ -1,136 +0,0 @@ -# Copyright 2019 Tecnativa - Vicent Cubells -# Copyright 2019 Eficent Business and IT Consulting Services, S.L. -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -from odoo import fields, models, api - - -class AccountBankStatementImportTxtMap(models.Model): - _name = 'account.bank.statement.import.map' - _description = 'Account Bank Statement Import Txt Map' - - name = fields.Char( - required=True, - ) - map_line_ids = fields.One2many( - comodel_name='account.bank.statement.import.map.line', - inverse_name='map_parent_id', - string="Map lines", - required=True, - copy=True, - ) - float_thousands_sep = fields.Selection( - [('dot', 'dot (.)'), - ('comma', 'comma (,)'), - ('none', 'none'), - ], - string='Thousands separator', - # forward compatibility: this was the value assumed - # before the field was added. - default='dot', - required=True - ) - float_decimal_sep = fields.Selection( - [('dot', 'dot (.)'), - ('comma', 'comma (,)'), - ('none', 'none'), - ], - string='Decimals separator', - # forward compatibility: this was the value assumed - # before the field was added. - default='comma', - required=True - ) - file_encoding = fields.Selection( - string='Encoding', - selection=[ - ('utf-8', 'UTF-8'), - ('utf-16 ', 'UTF-16'), - ('windows-1252', 'Windows-1252'), - ('latin1', 'latin1'), - ('latin2', 'latin2'), - ('big5', 'big5'), - ('gb18030', 'gb18030'), - ('shift_jis', 'shift_jis'), - ('windows-1251', 'windows-1251'), - ('koir8_r', 'koir9_r'), - ], - default='utf-8', - ) - delimiter = fields.Selection( - string='Separated by', - selection=[ - ('.', 'dot (.)'), - (',', 'comma (,)'), - (';', 'semicolon (;)'), - ('', 'none'), - ('\t', 'Tab'), - (' ', 'Space'), - ], - default=',', - ) - quotechar = fields.Char(string='String delimiter', size=1, - default='"') - - @api.onchange('float_thousands_sep') - def onchange_thousands_separator(self): - if 'dot' == self.float_thousands_sep == self.float_decimal_sep: - self.float_decimal_sep = 'comma' - elif 'comma' == self.float_thousands_sep == self.float_decimal_sep: - self.float_decimal_sep = 'dot' - - @api.onchange('float_decimal_sep') - def onchange_decimal_separator(self): - if 'dot' == self.float_thousands_sep == self.float_decimal_sep: - self.float_thousands_sep = 'comma' - elif 'comma' == self.float_thousands_sep == self.float_decimal_sep: - self.float_thousands_sep = 'dot' - - def _get_separators(self): - separators = {'dot': '.', - 'comma': ',', - 'none': '', - } - return (separators[self.float_thousands_sep], - separators[self.float_decimal_sep]) - - -class AccountBankStatementImportTxtMapLine(models.Model): - _name = 'account.bank.statement.import.map.line' - _description = 'Account Bank Statement Import Txt Map Line' - _order = "sequence asc, id asc" - - sequence = fields.Integer( - string="Field number", - required=True, - ) - name = fields.Char( - string="Header Name", - required=True, - ) - map_parent_id = fields.Many2one( - comodel_name='account.bank.statement.import.map', - required=True, - ondelete='cascade', - ) - field_to_assign = fields.Selection( - selection=[ - ('date', 'Date'), - ('name', 'Label'), - ('currency', 'Currency'), - ('amount', 'Amount in the journal currency'), - ('amount_currency', 'Amount in foreign currency'), - ('ref', 'Reference'), - ('note', 'Notes'), - ('partner_name', 'Name'), - ('account_number', 'Bank Account Number'), - ], - string="Statement Field to Assign", - ) - date_format = fields.Selection( - selection=[ - ('%d/%m/%Y', 'i.e. 15/12/2019'), - ('%m/%d/%Y', 'i.e. 12/15/2019'), - ], - string="Date Format", - ) diff --git a/account_bank_statement_import_txt_xlsx/models/account_journal.py b/account_bank_statement_import_txt_xlsx/models/account_journal.py index 261df272..9281be07 100644 --- a/account_bank_statement_import_txt_xlsx/models/account_journal.py +++ b/account_bank_statement_import_txt_xlsx/models/account_journal.py @@ -1,19 +1,14 @@ -# Copyright 2019 Tecnativa - Vicent Cubells -# Copyright 2019 Eficent Business and IT Consulting Services, S.L. -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +# Copyright 2019 ForgeFlow, S.L. +# Copyright 2020 Brainbean Apps (https://brainbeanapps.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo import fields, models +from odoo import models class AccountJournal(models.Model): - _inherit = "account.journal" - - statement_import_txt_map_id = fields.Many2one( - comodel_name='account.bank.statement.import.map', - string='Statement Import Txt Map', - ) + _inherit = 'account.journal' def _get_bank_statements_available_import_formats(self): res = super()._get_bank_statements_available_import_formats() - res.append('Txt') + res.append('TXT/CSV/XSLX') return res diff --git a/account_bank_statement_import_txt_xlsx/readme/CONFIGURE.rst b/account_bank_statement_import_txt_xlsx/readme/CONFIGURE.rst index 5c81f99c..ca7a8b50 100644 --- a/account_bank_statement_import_txt_xlsx/readme/CONFIGURE.rst +++ b/account_bank_statement_import_txt_xlsx/readme/CONFIGURE.rst @@ -1,12 +1,4 @@ -* Create or go to a bank journal where you want to import the txt statement. -* Edit that journal and set a Txt map in **Statement Import Map** section in **Advanced - Settings** tab. +To create TXT/CSV/XLSX statement sheet columns mapping: -* Now you can import Text based statements in that journal. - -Note: if existent Txt Map does not fit to your file to import, you can -create another map in **Invoicing > Configuration > Accounting > -Statement Import Map**. - -You can import headers from any Txt file in **Action > Create Map -Lines** and set every line with which field of statement have to match. +#. Open *Invoicing > Configuration > Accounting > Statement Sheet Mappings* +#. Create mapping(s) according to your online banking software statement format diff --git a/account_bank_statement_import_txt_xlsx/readme/CONTRIBUTORS.rst b/account_bank_statement_import_txt_xlsx/readme/CONTRIBUTORS.rst index 0e583091..04a803d5 100644 --- a/account_bank_statement_import_txt_xlsx/readme/CONTRIBUTORS.rst +++ b/account_bank_statement_import_txt_xlsx/readme/CONTRIBUTORS.rst @@ -5,3 +5,4 @@ * Victor M.M. Torres * Eficent (https://www.eficent.com) * Jordi Ballester Alomar +* Alexey Pelykh diff --git a/account_bank_statement_import_txt_xlsx/readme/HISTORY.rst b/account_bank_statement_import_txt_xlsx/readme/HISTORY.rst new file mode 100644 index 00000000..fe7b1add --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/readme/HISTORY.rst @@ -0,0 +1,7 @@ +12.0.2.0.0 +~~~~~~~~~~ + +* [BREAKING] New mapping, please review mappings after upgrade. +* [BREAKING] Different bank accounts have to be used per each currency. +* [ADD] Support for both Statement and Activity reports. +* [ADD] Separate fee and currency exchange parsing. diff --git a/account_bank_statement_import_txt_xlsx/readme/USAGE.rst b/account_bank_statement_import_txt_xlsx/readme/USAGE.rst index 2a8fb28a..1ed86b40 100644 --- a/account_bank_statement_import_txt_xlsx/readme/USAGE.rst +++ b/account_bank_statement_import_txt_xlsx/readme/USAGE.rst @@ -1,3 +1,4 @@ To use this module, you need to: -#. Go to your bank online and download your Bank Statement in TXT/CSV or XLSX format. +#. Get statement in TXT/CSV or XLSX from your online banking software +#. Go to Odoo and and import the statement file, selecting corresponding format diff --git a/account_bank_statement_import_txt_xlsx/security/ir.model.access.csv b/account_bank_statement_import_txt_xlsx/security/ir.model.access.csv index b282f31b..1e4598ba 100644 --- a/account_bank_statement_import_txt_xlsx/security/ir.model.access.csv +++ b/account_bank_statement_import_txt_xlsx/security/ir.model.access.csv @@ -1,3 +1,3 @@ "id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" -access_account_bank_statement_import_map,map manager,model_account_bank_statement_import_map,account.group_account_manager,1,1,1,1 -access_account_bank_statement_import_map_line,map line manager,model_account_bank_statement_import_map_line,account.group_account_manager,1,1,1,1 +access_account_bank_statement_import_sheet_mapping_manager,account.bank.statement.import.sheet.mapping:account.group_account_manager,model_account_bank_statement_import_sheet_mapping,account.group_account_manager,1,1,1,1 +access_account_bank_statement_import_sheet_mapping_user,account.bank.statement.import.sheet.mapping:account.group_account_user,model_account_bank_statement_import_sheet_mapping,account.group_account_user,1,0,0,0 diff --git a/account_bank_statement_import_txt_xlsx/tests/__init__.py b/account_bank_statement_import_txt_xlsx/tests/__init__.py index 3260f753..3073e38a 100644 --- a/account_bank_statement_import_txt_xlsx/tests/__init__.py +++ b/account_bank_statement_import_txt_xlsx/tests/__init__.py @@ -1 +1,3 @@ -from . import test_txt_statement_import +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import test_account_bank_statement_import_txt_xlsx diff --git a/account_bank_statement_import_txt_xlsx/tests/fixtures/balance.csv b/account_bank_statement_import_txt_xlsx/tests/fixtures/balance.csv new file mode 100644 index 00000000..c6a000f0 --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/tests/fixtures/balance.csv @@ -0,0 +1,3 @@ +"Date","Label","Amount","Balance","Partner Name","Bank Account" +"12/15/2018","Your best supplier","-33.50","-23.50","John Doe","123456789" +"12/15/2018","Your payment","1,533.50","1,510.00","Azure Interior","" diff --git a/account_bank_statement_import_txt_xlsx/tests/fixtures/debit_credit.csv b/account_bank_statement_import_txt_xlsx/tests/fixtures/debit_credit.csv new file mode 100644 index 00000000..936e32aa --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/tests/fixtures/debit_credit.csv @@ -0,0 +1,3 @@ +"Date","Label","Amount","D/C","Balance","Partner Name","Bank Account" +"12/15/2018","Your best supplier","33.50","D","-23.50","John Doe","123456789" +"12/15/2018","Your payment","-1,533.50","C","1,510.00","Azure Interior","" diff --git a/account_bank_statement_import_txt_xlsx/tests/fixtures/empty_statement_en.csv b/account_bank_statement_import_txt_xlsx/tests/fixtures/empty_statement_en.csv new file mode 100644 index 00000000..696c59ba --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/tests/fixtures/empty_statement_en.csv @@ -0,0 +1 @@ +"Date","Label","Currency","Amount","Amount Currency","Partner Name","Bank Account" diff --git a/account_bank_statement_import_txt_xlsx/tests/fixtures/empty_statement_en.xlsx b/account_bank_statement_import_txt_xlsx/tests/fixtures/empty_statement_en.xlsx new file mode 100644 index 00000000..5ca47652 Binary files /dev/null and b/account_bank_statement_import_txt_xlsx/tests/fixtures/empty_statement_en.xlsx differ diff --git a/account_bank_statement_import_txt_xlsx/tests/fixtures/multi_currency.csv b/account_bank_statement_import_txt_xlsx/tests/fixtures/multi_currency.csv new file mode 100644 index 00000000..c64bd180 --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/tests/fixtures/multi_currency.csv @@ -0,0 +1,3 @@ +"Date","Label","Currency","Amount","Partner Name","Bank Account" +"12/15/2018","Your best supplier","USD","-33.50","John Doe","123456789" +"12/15/2018","Your payment","EUR","1,525.00","Azure Interior","" diff --git a/account_bank_statement_import_txt_xlsx/tests/fixtures/original_currency.csv b/account_bank_statement_import_txt_xlsx/tests/fixtures/original_currency.csv new file mode 100644 index 00000000..e025cf08 --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/tests/fixtures/original_currency.csv @@ -0,0 +1,2 @@ +"Date","Label","Currency","Amount","Amount Currency","Partner Name","Bank Account" +"12/15/2018","Your payment","EUR","1,525.00","1,000.00","Azure Interior","" diff --git a/account_bank_statement_import_txt_xlsx/tests/sample_statement_en.csv b/account_bank_statement_import_txt_xlsx/tests/fixtures/sample_statement_en.csv similarity index 100% rename from account_bank_statement_import_txt_xlsx/tests/sample_statement_en.csv rename to account_bank_statement_import_txt_xlsx/tests/fixtures/sample_statement_en.csv diff --git a/account_bank_statement_import_txt_xlsx/tests/sample_statement_en.xlsx b/account_bank_statement_import_txt_xlsx/tests/fixtures/sample_statement_en.xlsx similarity index 100% rename from account_bank_statement_import_txt_xlsx/tests/sample_statement_en.xlsx rename to account_bank_statement_import_txt_xlsx/tests/fixtures/sample_statement_en.xlsx diff --git a/account_bank_statement_import_txt_xlsx/tests/test_account_bank_statement_import_txt_xlsx.py b/account_bank_statement_import_txt_xlsx/tests/test_account_bank_statement_import_txt_xlsx.py new file mode 100644 index 00000000..9f8709bb --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/tests/test_account_bank_statement_import_txt_xlsx.py @@ -0,0 +1,327 @@ +# Copyright 2019 ForgeFlow, S.L. +# Copyright 2020 Brainbean Apps (https://brainbeanapps.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields +from odoo.exceptions import UserError +from odoo.tests import common + +from base64 import b64encode +from os import path + + +class TestAccountBankStatementImportTxtXlsx(common.TransactionCase): + def setUp(self): + super().setUp() + + self.now = fields.Datetime.now() + self.currency_eur = self.env.ref('base.EUR') + self.currency_usd = self.env.ref('base.USD') + self.sample_statement_map = self.env.ref( + 'account_bank_statement_import_txt_xlsx.sample_statement_map' + ) + self.AccountJournal = self.env['account.journal'] + self.AccountBankStatement = self.env['account.bank.statement'] + self.AccountBankStatementImport = self.env[ + 'account.bank.statement.import' + ] + self.AccountBankStatementImportSheetMapping = self.env[ + 'account.bank.statement.import.sheet.mapping' + ] + self.AccountBankStatementImportSheetMappingWizard = self.env[ + 'account.bank.statement.import.sheet.mapping.wizard' + ] + + def _data_file(self, filename, encoding=None): + mode = 'rt' if encoding else 'rb' + with open(path.join(path.dirname(__file__), filename), mode) as file: + data = file.read() + if encoding: + data = data.encode(encoding) + return b64encode(data) + + def test_import_csv_file(self): + journal = self.AccountJournal.create({ + 'name': 'Bank', + 'type': 'bank', + 'code': 'BANK', + 'currency_id': self.currency_usd.id, + }) + wizard = self.AccountBankStatementImport.with_context({ + 'journal_id': journal.id, + }).create({ + 'filename': 'fixtures/sample_statement_en.csv', + 'data_file': self._data_file( + 'fixtures/sample_statement_en.csv', + 'utf-8' + ), + 'sheet_mapping_id': self.sample_statement_map.id, + }) + wizard.with_context({ + 'journal_id': journal.id, + 'account_bank_statement_import_txt_xlsx_test': True, + }).import_file() + statement = self.AccountBankStatement.search([ + ('journal_id', '=', journal.id), + ]) + self.assertEqual(len(statement), 1) + self.assertEqual(len(statement.line_ids), 2) + + def test_import_empty_csv_file(self): + journal = self.AccountJournal.create({ + 'name': 'Bank', + 'type': 'bank', + 'code': 'BANK', + 'currency_id': self.currency_usd.id, + }) + wizard = self.AccountBankStatementImport.with_context({ + 'journal_id': journal.id, + }).create({ + 'filename': 'fixtures/empty_statement_en.csv', + 'data_file': self._data_file( + 'fixtures/empty_statement_en.csv', + 'utf-8' + ), + 'sheet_mapping_id': self.sample_statement_map.id, + }) + with self.assertRaises(UserError): + wizard.with_context({ + 'journal_id': journal.id, + 'account_bank_statement_import_txt_xlsx_test': True, + }).import_file() + statement = self.AccountBankStatement.search([ + ('journal_id', '=', journal.id), + ]) + self.assertEqual(len(statement), 0) + + def test_import_xlsx_file(self): + journal = self.AccountJournal.create({ + 'name': 'Bank', + 'type': 'bank', + 'code': 'BANK', + 'currency_id': self.currency_usd.id, + }) + wizard = self.AccountBankStatementImport.with_context({ + 'journal_id': journal.id, + }).create({ + 'filename': 'fixtures/sample_statement_en.xlsx', + 'data_file': self._data_file('fixtures/sample_statement_en.xlsx'), + 'sheet_mapping_id': self.sample_statement_map.id, + }) + wizard.with_context({ + 'journal_id': journal.id, + 'account_bank_statement_import_txt_xlsx_test': True, + }).import_file() + statement = self.AccountBankStatement.search([ + ('journal_id', '=', journal.id), + ]) + self.assertEqual(len(statement), 1) + self.assertEqual(len(statement.line_ids), 2) + + def test_import_empty_xlsx_file(self): + journal = self.AccountJournal.create({ + 'name': 'Bank', + 'type': 'bank', + 'code': 'BANK', + 'currency_id': self.currency_usd.id, + }) + wizard = self.AccountBankStatementImport.with_context({ + 'journal_id': journal.id, + }).create({ + 'filename': 'fixtures/empty_statement_en.xlsx', + 'data_file': self._data_file('fixtures/empty_statement_en.xlsx'), + 'sheet_mapping_id': self.sample_statement_map.id, + }) + with self.assertRaises(UserError): + wizard.with_context({ + 'journal_id': journal.id, + 'account_bank_statement_import_txt_xlsx_test': True, + }).import_file() + statement = self.AccountBankStatement.search([ + ('journal_id', '=', journal.id), + ]) + self.assertEqual(len(statement), 0) + + def test_mapping_import_wizard_xlsx(self): + with common.Form( + self.AccountBankStatementImportSheetMappingWizard) as form: + form.filename = 'fixtures/empty_statement_en.xlsx' + form.data_file = self._data_file( + 'fixtures/empty_statement_en.xlsx' + ) + self.assertEqual(len(form.header), 90) + self.assertEqual( + len( + self.AccountBankStatementImportSheetMappingWizard + .with_context( + header=form.header, + ).statement_columns() + ), + 7 + ) + form.timestamp_column = 'Date' + form.amount_column = 'Amount' + wizard = form.save() + wizard.import_mapping() + + def test_mapping_import_wizard_csv(self): + with common.Form( + self.AccountBankStatementImportSheetMappingWizard) as form: + form.filename = 'fixtures/empty_statement_en.csv' + form.data_file = self._data_file( + 'fixtures/empty_statement_en.csv' + ) + self.assertEqual(len(form.header), 90) + self.assertEqual( + len( + self.AccountBankStatementImportSheetMappingWizard + .with_context( + header=form.header, + ).statement_columns() + ), + 7 + ) + form.timestamp_column = 'Date' + form.amount_column = 'Amount' + wizard = form.save() + wizard.import_mapping() + + def test_original_currency(self): + journal = self.AccountJournal.create({ + 'name': 'Bank', + 'type': 'bank', + 'code': 'BANK', + 'currency_id': self.currency_usd.id, + }) + wizard = self.AccountBankStatementImport.with_context({ + 'journal_id': journal.id, + }).create({ + 'filename': 'fixtures/original_currency.csv', + 'data_file': self._data_file( + 'fixtures/original_currency.csv', + 'utf-8' + ), + 'sheet_mapping_id': self.sample_statement_map.id, + }) + wizard.with_context({ + 'journal_id': journal.id, + 'account_bank_statement_import_txt_xlsx_test': True, + }).import_file() + 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_eur) + self.assertEqual(line.amount_currency, 1000.0) + + def test_multi_currency(self): + journal = self.AccountJournal.create({ + 'name': 'Bank', + 'type': 'bank', + 'code': 'BANK', + 'currency_id': self.currency_usd.id, + }) + statement_map = self.sample_statement_map.copy({ + 'currency_column': 'Currency', + 'original_currency_column': None, + 'original_amount_column': None, + }) + wizard = self.AccountBankStatementImport.with_context({ + 'journal_id': journal.id, + }).create({ + 'filename': 'fixtures/multi_currency.csv', + 'data_file': self._data_file( + 'fixtures/multi_currency.csv', + 'utf-8' + ), + 'sheet_mapping_id': statement_map.id, + }) + wizard.with_context({ + 'journal_id': journal.id, + 'account_bank_statement_import_txt_xlsx_test': True, + }).import_file() + 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.assertFalse(line.currency_id) + self.assertEqual(line.amount, -33.5) + + def test_balance(self): + journal = self.AccountJournal.create({ + 'name': 'Bank', + 'type': 'bank', + 'code': 'BANK', + 'currency_id': self.currency_usd.id, + }) + statement_map = self.sample_statement_map.copy({ + 'balance_column': 'Balance', + 'original_currency_column': None, + 'original_amount_column': None, + }) + wizard = self.AccountBankStatementImport.with_context({ + 'journal_id': journal.id, + }).create({ + 'filename': 'fixtures/balance.csv', + 'data_file': self._data_file( + 'fixtures/balance.csv', + 'utf-8' + ), + 'sheet_mapping_id': statement_map.id, + }) + wizard.with_context({ + 'journal_id': journal.id, + 'account_bank_statement_import_txt_xlsx_test': True, + }).import_file() + statement = self.AccountBankStatement.search([ + ('journal_id', '=', journal.id), + ]) + self.assertEqual(len(statement), 1) + self.assertEqual(len(statement.line_ids), 2) + 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(self): + journal = self.AccountJournal.create({ + 'name': 'Bank', + 'type': 'bank', + 'code': 'BANK', + 'currency_id': self.currency_usd.id, + }) + statement_map = self.sample_statement_map.copy({ + 'balance_column': 'Balance', + 'original_currency_column': None, + 'original_amount_column': None, + 'debit_credit_column': 'D/C', + 'debit_value': 'D', + 'credit_value': 'C', + }) + wizard = self.AccountBankStatementImport.with_context({ + 'journal_id': journal.id, + }).create({ + 'filename': 'fixtures/debit_credit.csv', + 'data_file': self._data_file( + 'fixtures/debit_credit.csv', + 'utf-8' + ), + 'sheet_mapping_id': statement_map.id, + }) + wizard.with_context({ + 'journal_id': journal.id, + 'account_bank_statement_import_txt_xlsx_test': True, + }).import_file() + statement = self.AccountBankStatement.search([ + ('journal_id', '=', journal.id), + ]) + self.assertEqual(len(statement), 1) + self.assertEqual(len(statement.line_ids), 2) + self.assertEqual(statement.balance_start, 10.0) + self.assertEqual(statement.balance_end_real, 1510.0) + self.assertEqual(statement.balance_end, 1510.0) diff --git a/account_bank_statement_import_txt_xlsx/tests/test_txt_statement_import.py b/account_bank_statement_import_txt_xlsx/tests/test_txt_statement_import.py deleted file mode 100644 index 11ceb77e..00000000 --- a/account_bank_statement_import_txt_xlsx/tests/test_txt_statement_import.py +++ /dev/null @@ -1,99 +0,0 @@ -# Copyright 2019 Tecnativa - Vicent Cubells -# Copyright 2019 Eficent Business and IT Consulting Services, S.L. -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -import os -import base64 -from odoo.tests import common - - -class TestTxtFile(common.TransactionCase): - - def setUp(self): - super(TestTxtFile, self).setUp() - - self.map = self.env['account.bank.statement.import.map'].create({ - 'name': 'Txt Map Test', - }) - usd = self.env.ref('base.USD') - self.journal = self.env['account.journal'].create({ - 'name': 'Txt Bank', - 'type': 'bank', - 'code': 'TXT', - 'currency_id': ( - usd.id if self.env.user.company_id.currency_id != usd - else False - ), - }) - - def _do_import_xlsx(self, file_name): - file_name = os.path.join(os.path.dirname(__file__), file_name) - with open(file_name, 'rb') as fin: - data = fin.read() - return data - - def _do_import(self, file_name): - file_name = os.path.join(os.path.dirname(__file__), file_name) - return open(file_name).read() - - def test_import_header(self): - file = self._do_import('sample_statement_en.csv') - file = base64.b64encode(file.encode("utf-8")) - wizard = self.env['wizard.txt.map.create'].with_context({ - 'journal_id': self.journal.id, - 'active_ids': [self.map.id], - }).create({'data_file': file}) - wizard.create_map_lines() - self.assertEqual(len(self.map.map_line_ids.ids), 7) - - def test_import_txt_file(self): - # Current statements before to run the wizard - old_statements = self.env['account.bank.statement'].search([]) - # This journal is for Txt statements - txt_map = self.env.ref( - 'account_bank_statement_import_txt_xlsx.txt_map' - ) - self.journal.statement_import_txt_map_id = txt_map.id - file = self._do_import('sample_statement_en.csv') - file = base64.b64encode(file.encode("utf-8")) - wizard = self.env['account.bank.statement.import'].with_context({ - 'journal_id': self.journal.id, - }).create({'data_file': file}) - wizard.import_file() - staments_now = self.env['account.bank.statement'].search([]) - statement = staments_now - old_statements - self.assertEqual(len(statement.line_ids), 2) - self.assertEqual(len(statement.mapped('line_ids').filtered( - lambda x: x.partner_id)), 1) - self.assertAlmostEqual( - sum(statement.mapped('line_ids.amount')), 1491.50 - ) - self.assertAlmostEqual( - sum(statement.mapped('line_ids.amount_currency')), 1000.00 - ) - - def test_import_xlsx_file(self): - # Current statements before to run the wizard - old_statements = self.env['account.bank.statement'].search([]) - # This journal is for Txt statements - txt_map = self.env.ref( - 'account_bank_statement_import_txt_xlsx.txt_map' - ) - self.journal.statement_import_txt_map_id = txt_map.id - file = self._do_import_xlsx('sample_statement_en.xlsx') - file = base64.b64encode(file) - wizard = self.env['account.bank.statement.import'].with_context({ - 'journal_id': self.journal.id, - }).create({'data_file': file}) - wizard.import_file() - staments_now = self.env['account.bank.statement'].search([]) - statement = staments_now - old_statements - self.assertEqual(len(statement.line_ids), 2) - self.assertEqual(len(statement.mapped('line_ids').filtered( - lambda x: x.partner_id)), 1) - self.assertAlmostEqual( - sum(statement.mapped('line_ids.amount')), 1491.50 - ) - self.assertAlmostEqual( - sum(statement.mapped('line_ids.amount_currency')), 1000.00 - ) diff --git a/account_bank_statement_import_txt_xlsx/wizards/account_bank_statement_import_view.xml b/account_bank_statement_import_txt_xlsx/views/account_bank_statement_import.xml similarity index 55% rename from account_bank_statement_import_txt_xlsx/wizards/account_bank_statement_import_view.xml rename to account_bank_statement_import_txt_xlsx/views/account_bank_statement_import.xml index 14e682d0..71e0e36e 100644 --- a/account_bank_statement_import_txt_xlsx/wizards/account_bank_statement_import_view.xml +++ b/account_bank_statement_import_txt_xlsx/views/account_bank_statement_import.xml @@ -1,12 +1,20 @@ + + account.bank.statement.import account.bank.statement.import -
  • Txt/XLSX file with Template:
  • +
  • + TXT/CSV/XLSX mapping: +
  • diff --git a/account_bank_statement_import_txt_xlsx/views/account_bank_statement_import_sheet_mapping.xml b/account_bank_statement_import_txt_xlsx/views/account_bank_statement_import_sheet_mapping.xml new file mode 100644 index 00000000..38eaa92b --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/views/account_bank_statement_import_sheet_mapping.xml @@ -0,0 +1,82 @@ + + + + + + account.bank.statement.import.sheet.mapping.form + account.bank.statement.import.sheet.mapping + + + + + + + + + account.bank.statement.import.sheet.mapping.tree + account.bank.statement.import.sheet.mapping + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    + + + Statement Sheet Mappings + account.bank.statement.import.sheet.mapping + form + tree,form + + + + +
    diff --git a/account_bank_statement_import_txt_xlsx/views/account_journal_views.xml b/account_bank_statement_import_txt_xlsx/views/account_journal_views.xml deleted file mode 100644 index b7be6cd2..00000000 --- a/account_bank_statement_import_txt_xlsx/views/account_journal_views.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - account.journal - - - - - - - - - - - diff --git a/account_bank_statement_import_txt_xlsx/views/txt_map_views.xml b/account_bank_statement_import_txt_xlsx/views/txt_map_views.xml deleted file mode 100644 index c7ab7550..00000000 --- a/account_bank_statement_import_txt_xlsx/views/txt_map_views.xml +++ /dev/null @@ -1,70 +0,0 @@ - - - - - account.bank.statement.import.map - - - - - - - - - account.bank.statement.import.map - -
    - - - - - - - - - - - - -
    - - - account.bank.statement.import.map.line - - - - - - - - - - - - account.bank.statement.import.map.line - -
    - - - - - - - -
    -
    -
    - - - Statement Import Mapping - account.bank.statement.import.map - form - tree,form - - - - -
    diff --git a/account_bank_statement_import_txt_xlsx/wizards/__init__.py b/account_bank_statement_import_txt_xlsx/wizards/__init__.py index ec27082d..2cb91b64 100644 --- a/account_bank_statement_import_txt_xlsx/wizards/__init__.py +++ b/account_bank_statement_import_txt_xlsx/wizards/__init__.py @@ -1,2 +1,3 @@ -from . import create_map_lines_from_file -from . import account_bank_statement_import_txt +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import account_bank_statement_import_sheet_mapping_wizard diff --git a/account_bank_statement_import_txt_xlsx/wizards/account_bank_statement_import_sheet_mapping_wizard.py b/account_bank_statement_import_txt_xlsx/wizards/account_bank_statement_import_sheet_mapping_wizard.py new file mode 100644 index 00000000..115f1b73 --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/wizards/account_bank_statement_import_sheet_mapping_wizard.py @@ -0,0 +1,191 @@ +# Copyright 2019 ForgeFlow, S.L. +# Copyright 2020 Brainbean Apps (https://brainbeanapps.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models, _ + +from base64 import b64decode +import json +from os import path + + +class AccountBankStatementImportSheetMappingWizard(models.TransientModel): + _name = 'account.bank.statement.import.sheet.mapping.wizard' + _description = 'Account Bank Statement Import Sheet Mapping Wizard' + _inherit = ['multi.step.wizard.mixin'] + + data_file = fields.Binary( + string='Bank Statement File', + required=True, + ) + filename = fields.Char() + header = fields.Char() + file_encoding = fields.Selection( + string='Encoding', + selection=lambda self: self._selection_file_encoding(), + ) + delimiter = fields.Selection( + string='Delimiter', + selection=lambda self: self._selection_delimiter(), + ) + quotechar = fields.Char( + string='Text qualifier', + size=1, + ) + timestamp_column = fields.Char( + string='Timestamp column', + ) + currency_column = fields.Char( + string='Currency column', + help=( + 'In case statement is multi-currency, column to get currency of ' + 'transaction from' + ), + ) + amount_column = fields.Char( + string='Amount column', + help='Amount of transaction in journal\'s currency', + ) + balance_column = fields.Char( + string='Balance column', + help='Balance after transaction in journal\'s currency', + ) + original_currency_column = fields.Char( + string='Original currency column', + help=( + 'In case statement provides original currency for transactions ' + 'with automatic currency conversion, column to get original ' + 'currency of transaction from' + ), + ) + original_amount_column = fields.Char( + string='Original amount column', + help=( + 'In case statement provides original currency for transactions ' + 'with automatic currency conversion, column to get original ' + 'transaction amount in original transaction currency from' + ), + ) + debit_credit_column = fields.Char( + string='Debit/credit column', + help=( + 'Some statement formats use absolute amount value and indicate sign' + 'of the transaction by specifying if it was a debit or a credit one' + ), + ) + debit_value = fields.Char( + string='Debit value', + help='Value of debit/credit column that indicates if it\'s a debit', + default='D', + ) + credit_value = fields.Char( + string='Credit value', + help='Value of debit/credit column that indicates if it\'s a credit', + default='C', + ) + transaction_id_column = fields.Char( + string='Unique transaction ID column', + ) + description_column = fields.Char( + string='Description column', + ) + notes_column = fields.Char( + string='Notes column', + ) + reference_column = fields.Char( + string='Reference column', + ) + partner_name_column = fields.Char( + string='Partner Name column', + ) + bank_name_column = fields.Char( + string='Bank Name column', + help='Partner\'s bank', + ) + bank_account_column = fields.Char( + string='Bank Account column', + help='Partner\'s bank account', + ) + + @api.model + def _selection_file_encoding(self): + return self.env['account.bank.statement.import.sheet.mapping']._fields[ + 'file_encoding' + ].selection + + @api.model + def _selection_delimiter(self): + return self.env['account.bank.statement.import.sheet.mapping']._fields[ + 'delimiter' + ].selection + + @api.onchange('data_file') + def _onchange_data_file(self): + Parser = self.env['account.bank.statement.import.sheet.parser'] + Mapping = self.env['account.bank.statement.import.sheet.mapping'] + if not self.data_file: + return + csv_options = {} + if self.delimiter: + csv_options['delimiter'] = \ + Mapping._decode_column_delimiter_character(self.delimiter) + if self.quotechar: + csv_options['quotechar'] = self.quotechar + header = Parser.parse_header( + b64decode(self.data_file), + self.file_encoding, + csv_options + ) + self.header = json.dumps(header) + + @api.model + def statement_columns(self): + header = self.env.context.get('header') + if not header: + return [] + return [(x, x) for x in json.loads(header)] + + @api.multi + def _get_mapping_values(self): + """Hook for extension""" + self.ensure_one() + return { + 'name': _('Mapping from %s') % path.basename(self.filename), + 'float_thousands_sep': 'comma', + 'float_decimal_sep': 'dot', + 'file_encoding': self.file_encoding, + 'delimiter': self.delimiter, + 'quotechar': self.quotechar, + 'timestamp_format': '%d/%m/%Y', + 'timestamp_column': self.timestamp_column, + 'currency_column': self.currency_column, + 'amount_column': self.amount_column, + 'balance_column': self.balance_column, + 'original_currency_column': self.original_currency_column, + 'original_amount_column': self.original_amount_column, + 'debit_credit_column': self.debit_credit_column, + 'debit_value': self.debit_value, + 'credit_value': self.credit_value, + 'transaction_id_column': self.transaction_id_column, + 'description_column': self.description_column, + 'notes_column': self.notes_column, + 'reference_column': self.reference_column, + 'partner_name_column': self.partner_name_column, + 'bank_name_column': self.bank_name_column, + 'bank_account_column': self.bank_account_column, + } + + @api.multi + def import_mapping(self): + self.ensure_one() + mapping = self.env['account.bank.statement.import.sheet.mapping']\ + .create(self._get_mapping_values()) + return { + 'type': 'ir.actions.act_window', + 'name': _('Imported Mapping'), + 'res_model': 'account.bank.statement.import.sheet.mapping', + 'res_id': mapping.id, + 'view_mode': 'form', + 'view_id': False, + 'target': 'new', + } diff --git a/account_bank_statement_import_txt_xlsx/wizards/account_bank_statement_import_sheet_mapping_wizard.xml b/account_bank_statement_import_txt_xlsx/wizards/account_bank_statement_import_sheet_mapping_wizard.xml new file mode 100644 index 00000000..9a5f372b --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/wizards/account_bank_statement_import_sheet_mapping_wizard.xml @@ -0,0 +1,144 @@ + + + + + + account.bank.statement.import.sheet.mapping.wizard.form + account.bank.statement.import.sheet.mapping.wizard + primary + + + +

    Select a statement file to import mapping

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + btn-default + Cancel + + +