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 0000000000..aee8895e7a --- /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 0000000000..08b8fba33d --- /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 0000000000..fb4c899afa --- /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 0000000000..ff680f4cb0 --- /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 0000000000..9a6bc07f65 --- /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 0000000000..261df272ad --- /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 0000000000..5c81f99c39 --- /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 0000000000..0e58309108 --- /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 0000000000..2564840495 --- /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 0000000000..2a8fb28a67 --- /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 0000000000..b282f31ba8 --- /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 0000000000..3260f75378 --- /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 0000000000..c50bfc83c8 --- /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 0000000000..b3365b205d Binary files /dev/null and b/account_bank_statement_import_txt_xlsx/tests/sample_statement_en.xlsx differ 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 0000000000..88b559790c --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/tests/test_txt_statement_import.py @@ -0,0 +1,100 @@ +# 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 +from io import StringIO +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 0000000000..b7be6cd234 --- /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 0000000000..c7ab75506d --- /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 0000000000..ec27082d37 --- /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 0000000000..5ba300c296 --- /dev/null +++ b/account_bank_statement_import_txt_xlsx/wizards/account_bank_statement_import_txt.py @@ -0,0 +1,289 @@ +# 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 time +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 0000000000..14e682d090 --- /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 0000000000..65d279ed81 --- /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 0000000000..d94d868c0f --- /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.

    + + +
    +
    + +
    +
    + + +