From 4d00d1dbf43bcfbd8936eace43323df5f3a748e7 Mon Sep 17 00:00:00 2001 From: Jordi Ballester Alomar Date: Tue, 1 Oct 2019 05:43:11 +0200 Subject: [PATCH] [12.0][add] account_bank_statement_import_txt_xslx --- .../__init__.py | 2 + .../__manifest__.py | 28 ++ .../data/txt_map_data.xml | 53 ++++ .../models/__init__.py | 2 + .../account_bank_statement_import_txt_map.py | 136 +++++++++ .../models/account_journal.py | 19 ++ .../readme/CONFIGURE.rst | 12 + .../readme/CONTRIBUTORS.rst | 7 + .../readme/DESCRIPTION.rst | 2 + .../readme/USAGE.rst | 3 + .../security/ir.model.access.csv | 3 + .../tests/__init__.py | 1 + .../tests/sample_statement_en.csv | 3 + .../tests/sample_statement_en.xlsx | Bin 0 -> 5092 bytes .../tests/test_txt_statement_import.py | 99 ++++++ .../views/account_journal_views.xml | 16 + .../views/txt_map_views.xml | 70 +++++ .../wizards/__init__.py | 2 + .../account_bank_statement_import_txt.py | 288 ++++++++++++++++++ .../account_bank_statement_import_view.xml | 14 + .../wizards/create_map_lines_from_file.py | 40 +++ .../create_map_lines_from_file_views.xml | 29 ++ 22 files changed, 829 insertions(+) create mode 100644 account_bank_statement_import_txt_xlsx/__init__.py create mode 100644 account_bank_statement_import_txt_xlsx/__manifest__.py create mode 100644 account_bank_statement_import_txt_xlsx/data/txt_map_data.xml create mode 100644 account_bank_statement_import_txt_xlsx/models/__init__.py create 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/models/account_journal.py create mode 100644 account_bank_statement_import_txt_xlsx/readme/CONFIGURE.rst create mode 100644 account_bank_statement_import_txt_xlsx/readme/CONTRIBUTORS.rst create mode 100644 account_bank_statement_import_txt_xlsx/readme/DESCRIPTION.rst create mode 100644 account_bank_statement_import_txt_xlsx/readme/USAGE.rst create mode 100644 account_bank_statement_import_txt_xlsx/security/ir.model.access.csv create mode 100644 account_bank_statement_import_txt_xlsx/tests/__init__.py create mode 100644 account_bank_statement_import_txt_xlsx/tests/sample_statement_en.csv create mode 100644 account_bank_statement_import_txt_xlsx/tests/sample_statement_en.xlsx create mode 100644 account_bank_statement_import_txt_xlsx/tests/test_txt_statement_import.py create mode 100644 account_bank_statement_import_txt_xlsx/views/account_journal_views.xml create mode 100644 account_bank_statement_import_txt_xlsx/views/txt_map_views.xml create mode 100644 account_bank_statement_import_txt_xlsx/wizards/__init__.py create mode 100644 account_bank_statement_import_txt_xlsx/wizards/account_bank_statement_import_txt.py create mode 100644 account_bank_statement_import_txt_xlsx/wizards/account_bank_statement_import_view.xml create mode 100644 account_bank_statement_import_txt_xlsx/wizards/create_map_lines_from_file.py create mode 100644 account_bank_statement_import_txt_xlsx/wizards/create_map_lines_from_file_views.xml diff --git a/account_bank_statement_import_txt_xlsx/__init__.py b/account_bank_statement_import_txt_xlsx/__init__.py new file mode 100644 index 0000000..aee8895 --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizards diff --git a/account_bank_statement_import_txt_xlsx/__manifest__.py b/account_bank_statement_import_txt_xlsx/__manifest__.py new file mode 100644 index 0000000..08b8fba --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/__manifest__.py @@ -0,0 +1,28 @@ +# 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). +{ + "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", + "category": "Accounting", + "website": "https://github.com/OCA/bank-statement-import", + "author": " Eficent, Odoo Community Association (OCA)", + "license": "AGPL-3", + "installable": True, + "depends": [ + "account_bank_statement_import", + ], + "external_dependencies": { + "python": ["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", + ] +} 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 new file mode 100644 index 0000000..fb4c899 --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/data/txt_map_data.xml @@ -0,0 +1,53 @@ + + + + + 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/models/__init__.py b/account_bank_statement_import_txt_xlsx/models/__init__.py new file mode 100644 index 0000000..ff680f4 --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/models/__init__.py @@ -0,0 +1,2 @@ +from . import account_bank_statement_import_txt_map +from . import account_journal 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 new file mode 100644 index 0000000..9a6bc07 --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/models/account_bank_statement_import_txt_map.py @@ -0,0 +1,136 @@ +# 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 new file mode 100644 index 0000000..261df27 --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/models/account_journal.py @@ -0,0 +1,19 @@ +# 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 + + +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', + ) + + def _get_bank_statements_available_import_formats(self): + res = super()._get_bank_statements_available_import_formats() + res.append('Txt') + return res diff --git a/account_bank_statement_import_txt_xlsx/readme/CONFIGURE.rst b/account_bank_statement_import_txt_xlsx/readme/CONFIGURE.rst new file mode 100644 index 0000000..5c81f99 --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/readme/CONFIGURE.rst @@ -0,0 +1,12 @@ +* 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. + +* 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. diff --git a/account_bank_statement_import_txt_xlsx/readme/CONTRIBUTORS.rst b/account_bank_statement_import_txt_xlsx/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000..0e58309 --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/readme/CONTRIBUTORS.rst @@ -0,0 +1,7 @@ +* Alexis de Lattre +* Sebastien BEAU +* Tecnativa (https://www.tecnativa.com) + * Vicent Cubells + * Victor M.M. Torres +* Eficent (https://www.eficent.com) + * Jordi Ballester Alomar diff --git a/account_bank_statement_import_txt_xlsx/readme/DESCRIPTION.rst b/account_bank_statement_import_txt_xlsx/readme/DESCRIPTION.rst new file mode 100644 index 0000000..2564840 --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/readme/DESCRIPTION.rst @@ -0,0 +1,2 @@ +This module allows you to import the any TXT/CSV or XLSX file in Odoo as bank +statements. diff --git a/account_bank_statement_import_txt_xlsx/readme/USAGE.rst b/account_bank_statement_import_txt_xlsx/readme/USAGE.rst new file mode 100644 index 0000000..2a8fb28 --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/readme/USAGE.rst @@ -0,0 +1,3 @@ +To use this module, you need to: + +#. Go to your bank online and download your Bank Statement in TXT/CSV or XLSX 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 new file mode 100644 index 0000000..b282f31 --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/security/ir.model.access.csv @@ -0,0 +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 diff --git a/account_bank_statement_import_txt_xlsx/tests/__init__.py b/account_bank_statement_import_txt_xlsx/tests/__init__.py new file mode 100644 index 0000000..3260f75 --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/tests/__init__.py @@ -0,0 +1 @@ +from . import test_txt_statement_import diff --git a/account_bank_statement_import_txt_xlsx/tests/sample_statement_en.csv b/account_bank_statement_import_txt_xlsx/tests/sample_statement_en.csv new file mode 100644 index 0000000..c50bfc8 --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/tests/sample_statement_en.csv @@ -0,0 +1,3 @@ +"Date","Label","Currency","Amount","Amount Currency","Partner Name","Bank Account" +"12/15/2018","Your best supplier","USD","-33.50","0.0","John Doe","123456789" +"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.xlsx b/account_bank_statement_import_txt_xlsx/tests/sample_statement_en.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..b3365b205d4ff97fe88181c5654983dcfec100d2 GIT binary patch literal 5092 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 literal 0 HcmV?d00001 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 new file mode 100644 index 0000000..11ceb77 --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/tests/test_txt_statement_import.py @@ -0,0 +1,99 @@ +# 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/views/account_journal_views.xml b/account_bank_statement_import_txt_xlsx/views/account_journal_views.xml new file mode 100644 index 0000000..b7be6cd --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/views/account_journal_views.xml @@ -0,0 +1,16 @@ + + + + + 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 new file mode 100644 index 0000000..c7ab755 --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/views/txt_map_views.xml @@ -0,0 +1,70 @@ + + + + + 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 new file mode 100644 index 0000000..ec27082 --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/wizards/__init__.py @@ -0,0 +1,2 @@ +from . import create_map_lines_from_file +from . import account_bank_statement_import_txt diff --git a/account_bank_statement_import_txt_xlsx/wizards/account_bank_statement_import_txt.py b/account_bank_statement_import_txt_xlsx/wizards/account_bank_statement_import_txt.py new file mode 100644 index 0000000..228b431 --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/wizards/account_bank_statement_import_txt.py @@ -0,0 +1,288 @@ +# Copyright 2014-2017 Akretion (http://www.akretion.com). +# @author Alexis de Lattre +# @author Sébastien BEAU +# Copyright 2019 Eficent Business and IT Consulting Services, S.L. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import xlrd +import logging +import datetime as dtm +from datetime import datetime +from odoo import _, api, fields, models +from odoo.exceptions import UserError +import re +from io import StringIO +_logger = logging.getLogger(__name__) + +try: + import csv +except (ImportError, IOError) as err: + _logger.debug(err) + + +class AccountBankStatementImport(models.TransientModel): + _inherit = 'account.bank.statement.import' + + txt_map_id = fields.Many2one( + comodel_name='account.bank.statement.import.map', + string='Txt map', + readonly=True, + ) + + @api.model + def _get_txt_encoding(self): + if self.txt_map_id.file_encoding: + return self.txt_map_id.file_encoding + return 'utf-8-sig' + + @api.model + def _get_txt_str_data(self, data_file): + if not isinstance(data_file, str): + data_file = data_file.decode(self._get_txt_encoding()) + return data_file.strip() + + @api.model + def _txt_convert_amount(self, amount_str): + if not amount_str: + return 0.0 + if self.txt_map_id: + thousands, decimal = self.txt_map_id._get_separators() + else: + thousands, decimal = ',', '.' + valstr = re.sub(r'[^\d%s%s.-]' % (thousands, decimal), '', amount_str) + valstrdot = valstr.replace(thousands, '') + valstrdot = valstrdot.replace(decimal, '.') + return float(valstrdot) + + @api.model + def _check_xls(self, data_file): + # Try if it is an Excel file + headers = self.mapped('txt_map_id.map_line_ids.name') + try: + file_headers = [] + book = xlrd.open_workbook(file_contents=data_file) + xl_sheet = book.sheet_by_index(0) + row = xl_sheet.row(0) # 1st row + for idx, cell_obj in enumerate(row): + cell_type_str = xlrd.sheet.ctype_text.get(cell_obj.ctype, False) + if cell_type_str: + file_headers.append(cell_obj.value) + else: + return False + if any(item not in file_headers for item in headers): + raise UserError( + _("Headers of file to import and Txt map lines does not " + "match.")) + except xlrd.XLRDError as e: + return False + except Exception as e: + return False + return True + + @api.model + def _check_txt(self, data_file): + data_file = self._get_txt_str_data(data_file) + if not self.txt_map_id: + return False + headers = self.mapped('txt_map_id.map_line_ids.name') + file_headers = data_file.split('\n', 1)[0] + if any(item not in file_headers for item in headers): + raise UserError( + _("Headers of file to import and Txt map lines does not " + "match.")) + return True + + def _get_currency_fields(self): + return ['amount', 'amount_currency'] + + def _convert_txt_line_to_dict(self, idx, line): + rline = dict() + for item in range(len(line)): + txt_map = self.mapped('txt_map_id.map_line_ids')[item] + value = line[item] + if not txt_map.field_to_assign: + continue + if txt_map.date_format: + try: + value = fields.Date.to_string( + datetime.strptime(value, txt_map.date_format)) + except Exception: + raise UserError( + _("Date format of map file and Txt date does " + "not match.")) + rline[txt_map.field_to_assign] = value + for field in self._get_currency_fields(): + _logger.debug('Trying to convert %s to float' % rline[field]) + try: + rline[field] = self._txt_convert_amount(rline[field]) + except Exception: + raise UserError( + _("Value '%s' for the field '%s' on line %d, " + "cannot be converted to float") + % (rline[field], field, idx)) + return rline + + def _parse_txt_file(self, data_file): + data_file = self._get_txt_str_data(data_file) + f = StringIO(data_file) + f.seek(0) + raw_lines = [] + if not self.txt_map_id.quotechar: + reader = csv.reader(f, + delimiter=self.txt_map_id.delimiter or False) + else: + reader = csv.reader(f, + quotechar=self.txt_map_id.quotechar, + delimiter=self.txt_map_id.delimiter or False) + next(reader) # Drop header + for idx, line in enumerate(reader): + _logger.debug("Line %d: %s" % (idx, line)) + raw_lines.append(self._convert_txt_line_to_dict(idx, line)) + return raw_lines + + def _convert_xls_line_to_dict(self, row_idx, xl_sheet): + rline = dict() + for col_idx in range(0, xl_sheet.ncols): # Iterate through columns + txt_map = self.mapped('txt_map_id.map_line_ids')[col_idx] + cell_obj = xl_sheet.cell(row_idx, col_idx) # Get cell + ctype = xl_sheet.cell(row_idx, col_idx).ctype + value = cell_obj.value + if not txt_map.field_to_assign: + continue + if ctype == xlrd.XL_CELL_DATE: + ms_date_number = xl_sheet.cell(row_idx, col_idx).value + try: + year, month, day, hour, minute, \ + second = xlrd.xldate_as_tuple( + ms_date_number, 0) + except xlrd.XLDateError as e: + raise UserError( + _('An error was found translating a date ' + 'field from the file: %s') % e) + value = dtm.date(year, month, day) + value = value.strftime('%Y-%m-%d') + rline[txt_map.field_to_assign] = value + + return rline + + def _parse_xls_file(self, data_file): + try: + raw_lines = [] + book = xlrd.open_workbook(file_contents=data_file) + xl_sheet = book.sheet_by_index(0) + for row_idx in range(1, xl_sheet.nrows): + _logger.debug("Line %d" % row_idx) + raw_lines.append(self._convert_xls_line_to_dict( + row_idx, xl_sheet)) + except xlrd.XLRDError: + return False + except Exception as e: + return False + return raw_lines + + def _post_process_statement_line(self, raw_lines): + """ Enter your additional logic here. """ + return raw_lines + + def _get_journal(self): + journal_id = self.env.context.get('journal_id') + if not journal_id: + raise UserError(_('You must run this wizard from the journal')) + return self.env['account.journal'].browse(journal_id) + + def _get_currency_id(self, fline): + journal = self._get_journal() + line_currency_name = fline.get('currency', False) + currency = journal.currency_id or journal.company_id.currency_id + if line_currency_name and line_currency_name != currency.name: + currency = self.env['res.currency'].search( + [('name', '=', fline['currency'])], limit=1) + return currency.id + return False + + @api.model + def _get_partner_id(self, fline): + partner_name = fline.get('partner_name', False) + if partner_name: + partner = self.env['res.partner'].search([ + ('name', '=ilike', partner_name)]) + if partner and len(partner) == 1: + return partner.commercial_partner_id.id + return None + + def _prepare_txt_statement_line(self, fline): + currency_id = self._get_currency_id(fline) + return { + 'date': fline.get('date', False), + 'name': fline.get('name', ''), + 'ref': fline.get('ref', False), + 'note': fline.get('Notes', False), + 'amount': fline.get('amount', 0.0), + 'currency_id': self._get_currency_id(fline), + 'amount_currency': currency_id and fline.get( + 'amount_currency', 0.0) or 0.0, + 'partner_id': self._get_partner_id(fline), + 'account_number': fline.get('account_number', False), + } + + def _prepare_txt_statement(self, lines): + balance_end_real = 0.0 + for line in lines: + if 'amount' in line and line['amount']: + balance_end_real += line['amount'] + + return { + 'name': + _('%s Import %s > %s') + % (self.txt_map_id.name, + lines[0]['date'], lines[-1]['date']), + 'date': lines[-1]['date'], + 'balance_start': 0.0, + 'balance_end_real': balance_end_real, + } + + @api.model + def _parse_file(self, data_file): + """ Import a file in Txt CSV format """ + is_txt = False + is_xls = self._check_xls(data_file) + if not is_xls: + is_txt = self._check_txt(data_file) + if not is_txt and not is_xls: + return super(AccountBankStatementImport, self)._parse_file( + data_file) + if is_txt: + raw_lines = self._parse_txt_file(data_file) + else: + raw_lines = self._parse_xls_file(data_file) + final_lines = self._post_process_statement_line(raw_lines) + vals_bank_statement = self._prepare_txt_statement(final_lines) + transactions = [] + for fline in final_lines: + vals_line = self._prepare_txt_statement_line(fline) + _logger.debug("vals_line = %s" % vals_line) + transactions.append(vals_line) + vals_bank_statement['transactions'] = transactions + return None, None, [vals_bank_statement] + + @api.model + def _complete_txt_statement_line(self, line): + """ Enter additional logic here. """ + return None + + @api.model + def _complete_stmts_vals(self, stmts_vals, journal_id, account_number): + stmts_vals = super(AccountBankStatementImport, self). \ + _complete_stmts_vals(stmts_vals, journal_id, account_number) + for line in stmts_vals[0]['transactions']: + vals = self._complete_txt_statement_line(line) + if vals: + line.update(vals) + return stmts_vals + + @api.model + def default_get(self, fields): + res = super(AccountBankStatementImport, self).default_get(fields) + journal = self._get_journal() + res['txt_map_id'] = journal.statement_import_txt_map_id.id + return res diff --git a/account_bank_statement_import_txt_xlsx/wizards/account_bank_statement_import_view.xml b/account_bank_statement_import_txt_xlsx/wizards/account_bank_statement_import_view.xml new file mode 100644 index 0000000..14e682d --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/wizards/account_bank_statement_import_view.xml @@ -0,0 +1,14 @@ + + + + + account.bank.statement.import + + + +
  • Txt/XLSX file with Template:
  • +
    +
    +
    + +
    diff --git a/account_bank_statement_import_txt_xlsx/wizards/create_map_lines_from_file.py b/account_bank_statement_import_txt_xlsx/wizards/create_map_lines_from_file.py new file mode 100644 index 0000000..65d279e --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/wizards/create_map_lines_from_file.py @@ -0,0 +1,40 @@ +# 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 csv +import base64 +from odoo import api, fields, models +from io import StringIO + + +class WizardTxtMapCreate(models.TransientModel): + _name = 'wizard.txt.map.create' + _description = 'Wizard Txt Map Create' + + data_file = fields.Binary( + string='Bank Statement File', + required=True, + ) + filename = fields.Char() + + @api.multi + def create_map_lines(self): + statement_obj = self.env['account.bank.statement.import.map'] + data_file = base64.b64decode(self.data_file) + if not isinstance(data_file, str): + data_file = data_file.decode('utf-8-sig').strip() + file = StringIO(data_file) + file.seek(0) + reader = csv.reader(file) + headers = [] + for row in reader: + headers = row + break + lines = [] + for idx, title in enumerate(headers): + lines.append((0, 0, {'sequence': idx, 'name': title})) + if lines: + for statement in statement_obj.browse( + self.env.context.get('active_ids')): + statement.map_line_ids = lines diff --git a/account_bank_statement_import_txt_xlsx/wizards/create_map_lines_from_file_views.xml b/account_bank_statement_import_txt_xlsx/wizards/create_map_lines_from_file_views.xml new file mode 100644 index 0000000..d94d868 --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/wizards/create_map_lines_from_file_views.xml @@ -0,0 +1,29 @@ + + + + + Create Statement Map Lines + wizard.txt.map.create + +
    +

    Select a TXT/CSV or XLSX bank statement file to create all the map lines from headers file.

    +

    Download a bank statement from your bank and import it here.

    +

    All the txt map lines will be created automatically.

    + + +
    +
    + +
    +
    + + +