From 4ce2ad63cd4a82f9d1b6d3713f904ad9f0dbb2ad Mon Sep 17 00:00:00 2001 From: Alexey Pelykh Date: Sat, 28 Mar 2020 10:26:54 +0100 Subject: [PATCH] [IMP] account_bank_statement_import_txt_xlsx --- .../__manifest__.py | 33 +- .../data/map_data.xml | 25 ++ .../data/txt_map_data.xml | 53 --- .../migrations/12.0.2.0.0/post-migration.py | 98 ++++++ .../models/__init__.py | 6 +- .../models/account_bank_statement_import.py | 33 ++ ...unt_bank_statement_import_sheet_mapping.py | 188 ++++++++++ ...ount_bank_statement_import_sheet_parser.py | 331 ++++++++++++++++++ .../account_bank_statement_import_txt_map.py | 136 ------- .../models/account_journal.py | 17 +- .../readme/CONFIGURE.rst | 14 +- .../readme/CONTRIBUTORS.rst | 1 + .../readme/HISTORY.rst | 7 + .../readme/USAGE.rst | 3 +- .../security/ir.model.access.csv | 4 +- .../tests/__init__.py | 4 +- .../tests/fixtures/balance.csv | 3 + .../tests/fixtures/debit_credit.csv | 3 + .../tests/fixtures/empty_statement_en.csv | 1 + .../tests/fixtures/empty_statement_en.xlsx | Bin 0 -> 9041 bytes .../tests/fixtures/multi_currency.csv | 3 + .../tests/fixtures/original_currency.csv | 2 + .../{ => fixtures}/sample_statement_en.csv | 0 .../{ => fixtures}/sample_statement_en.xlsx | Bin ..._account_bank_statement_import_txt_xlsx.py | 327 +++++++++++++++++ .../tests/test_txt_statement_import.py | 99 ------ .../account_bank_statement_import.xml} | 10 +- ...nt_bank_statement_import_sheet_mapping.xml | 82 +++++ .../views/account_journal_views.xml | 16 - .../views/txt_map_views.xml | 70 ---- .../wizards/__init__.py | 5 +- ...k_statement_import_sheet_mapping_wizard.py | 191 ++++++++++ ..._statement_import_sheet_mapping_wizard.xml | 144 ++++++++ .../account_bank_statement_import_txt.py | 288 --------------- .../wizards/create_map_lines_from_file.py | 40 --- .../create_map_lines_from_file_views.xml | 29 -- oca_dependencies.txt | 2 + 37 files changed, 1494 insertions(+), 774 deletions(-) create mode 100644 account_bank_statement_import_txt_xlsx/data/map_data.xml delete mode 100644 account_bank_statement_import_txt_xlsx/data/txt_map_data.xml create mode 100644 account_bank_statement_import_txt_xlsx/migrations/12.0.2.0.0/post-migration.py create mode 100644 account_bank_statement_import_txt_xlsx/models/account_bank_statement_import.py create mode 100644 account_bank_statement_import_txt_xlsx/models/account_bank_statement_import_sheet_mapping.py create mode 100644 account_bank_statement_import_txt_xlsx/models/account_bank_statement_import_sheet_parser.py delete mode 100644 account_bank_statement_import_txt_xlsx/models/account_bank_statement_import_txt_map.py create mode 100644 account_bank_statement_import_txt_xlsx/readme/HISTORY.rst create mode 100644 account_bank_statement_import_txt_xlsx/tests/fixtures/balance.csv create mode 100644 account_bank_statement_import_txt_xlsx/tests/fixtures/debit_credit.csv create mode 100644 account_bank_statement_import_txt_xlsx/tests/fixtures/empty_statement_en.csv create mode 100644 account_bank_statement_import_txt_xlsx/tests/fixtures/empty_statement_en.xlsx create mode 100644 account_bank_statement_import_txt_xlsx/tests/fixtures/multi_currency.csv create mode 100644 account_bank_statement_import_txt_xlsx/tests/fixtures/original_currency.csv rename account_bank_statement_import_txt_xlsx/tests/{ => fixtures}/sample_statement_en.csv (100%) rename account_bank_statement_import_txt_xlsx/tests/{ => fixtures}/sample_statement_en.xlsx (100%) create mode 100644 account_bank_statement_import_txt_xlsx/tests/test_account_bank_statement_import_txt_xlsx.py delete mode 100644 account_bank_statement_import_txt_xlsx/tests/test_txt_statement_import.py rename account_bank_statement_import_txt_xlsx/{wizards/account_bank_statement_import_view.xml => views/account_bank_statement_import.xml} (55%) create mode 100644 account_bank_statement_import_txt_xlsx/views/account_bank_statement_import_sheet_mapping.xml delete mode 100644 account_bank_statement_import_txt_xlsx/views/account_journal_views.xml delete mode 100644 account_bank_statement_import_txt_xlsx/views/txt_map_views.xml create mode 100644 account_bank_statement_import_txt_xlsx/wizards/account_bank_statement_import_sheet_mapping_wizard.py create mode 100644 account_bank_statement_import_txt_xlsx/wizards/account_bank_statement_import_sheet_mapping_wizard.xml delete mode 100644 account_bank_statement_import_txt_xlsx/wizards/account_bank_statement_import_txt.py delete mode 100644 account_bank_statement_import_txt_xlsx/wizards/create_map_lines_from_file.py delete mode 100644 account_bank_statement_import_txt_xlsx/wizards/create_map_lines_from_file_views.xml create mode 100644 oca_dependencies.txt 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 0000000000000000000000000000000000000000..5ca47652c2d0e6661f9bf8f0bc5ee7e0ab5e21e7 GIT binary patch literal 9041 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∨jcF2*vX3Cti3NH(fX`Uw7mVfXh6|N3UVmh=}bGPjvg1rMeOry z4`Lb7a^H*NSm~-NeFd~+xz=`m-2+65Fl6dnAEtaGu!`!GYM&GI|aB;^v%@aCjgX-#` zM`16J>q3k7aK=^DPK_oG%c7w}%~e!FzN02Vb%+?C)w%`+M)ng_CAPAe7UnIxIdlkA zSdqRvjJkgatRR&l>JaJpT(u)f%$3|Y91X*O)KQ#jB&xfXW98kjd{67VT60eUSc1N& zISOt?2nO_(a|+uR!LSgTqDAwGAOY{+k{xHBD|J9+SfG~7J3jxBId(kKnQHH4v?3v8 z48O`&ID#5Ug{;(+rqn-ly6$rOYEY1J6TC&=V3^1@EX6srx?sxkSSRWpx4(O8+#@Jo zWttJ6Vj*+E)-w4Kkb_KKzBQgmMHXzfn?Ej zDd%UED`&eqJQw2p{k;SI!9ByRbv*!#I4>N1W;w0k$yPQ;?Jqlb>*qwkw_rQ)6)J-# zz=G#|BW6y5lq?a7G4WMbw1fxP$E z%+{|8e)Y!xP*8&VpAY|=Z~m*6U;T + + 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 + + +