From 4a5c8d35886ca9ce3acb7bc34e1924cd84a22710 Mon Sep 17 00:00:00 2001 From: Tonow-c2c Date: Wed, 13 Nov 2019 16:38:35 +0100 Subject: [PATCH 1/4] [FIX][12.0][account_move_transactionid_import] s/transaction_ref/ref Remove 'ref' from account.move.line preparation As 'ref' is related to account.move.ref it is not needed Fix multi move import multi_move_import on the parser allows to generate 1 account.move for each line in the imported file, instead of having all the lines on the same account.move --- .../models/account_journal.py | 10 +++++--- .../parser/file_parser.py | 25 ++++++++++++++----- .../parser/transactionid_file_parser.py | 2 +- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/account_move_base_import/models/account_journal.py b/account_move_base_import/models/account_journal.py index e6eabd01..c62ae71d 100644 --- a/account_move_base_import/models/account_journal.py +++ b/account_move_base_import/models/account_journal.py @@ -245,7 +245,6 @@ class AccountJournal(models.Model): 'debit_cash_basic': values['debit'], 'credit_cash_basic': values['credit'], 'balance_cash_basic': values['debit'] - values['credit'], - 'ref': move.ref, 'user_type_id': account.user_type_id.id, 'reconciled': False, }) @@ -277,11 +276,13 @@ class AccountJournal(models.Model): parser = new_move_parser(self, ftype=ftype, move_ref=filename) res = self.env['account.move'] for result_row_list in parser.parse(file_stream): - move = self._move_import(parser, file_stream, ftype=ftype) + move = self._move_import( + parser, file_stream, result_row_list=result_row_list, ftype=ftype + ) res |= move return res - def _move_import(self, parser, file_stream, ftype="csv"): + def _move_import(self, parser, file_stream, result_row_list=None, ftype="csv"): """Create a bank statement with the given profile and parser. It will fulfill the bank statement with the values of the file provided, but will not complete data (like finding the partner, or the right @@ -296,7 +297,8 @@ class AccountJournal(models.Model): move_obj = self.env['account.move'] move_line_obj = self.env['account.move.line'] attachment_obj = self.env['ir.attachment'] - result_row_list = parser.result_row_list + if result_row_list is None: + result_row_list = parser.result_row_list # Check all key are present in account.bank.statement.line!! if not result_row_list: raise UserError(_("Nothing to import: " diff --git a/account_move_base_import/parser/file_parser.py b/account_move_base_import/parser/file_parser.py index 8893b96b..3eaa3f32 100644 --- a/account_move_base_import/parser/file_parser.py +++ b/account_move_base_import/parser/file_parser.py @@ -57,6 +57,8 @@ class FileParser(AccountMoveImportParser): # Set in _parse_xls, from the contents of the file self.dialect = dialect self.move_ref = move_ref + self.parsed_file = None + self.current_line = 0 def _custom_format(self, *args, **kwargs): """No other work on data are needed in this parser.""" @@ -70,13 +72,24 @@ class FileParser(AccountMoveImportParser): """Launch the parsing through .csv, .xls or .xlsx depending on the given ftype """ - res = None - if self.ftype == 'csv': - res = self._parse_csv() + if self.parsed_file is None: + if self.ftype == 'csv': + self.parsed_file = self._parse_csv() + else: + self.parsed_file = self._parse_xls() + if self.support_multi_moves: + if len(self.parsed_file) <= self.current_line: + return False + else: + print(self.current_line) + self.result_row_list = self.parsed_file[ + self.current_line:self.current_line + 1 + ] + self.current_line += 1 + return True else: - res = self._parse_xls() - self.result_row_list = res - return True + self.result_row_list = self.parsed_file + return True def _validate(self, *args, **kwargs): """We check that all the key of the given file (means header) are diff --git a/account_move_transactionid_import/parser/transactionid_file_parser.py b/account_move_transactionid_import/parser/transactionid_file_parser.py index e57b4366..29c3c04e 100644 --- a/account_move_transactionid_import/parser/transactionid_file_parser.py +++ b/account_move_transactionid_import/parser/transactionid_file_parser.py @@ -66,5 +66,5 @@ class TransactionIDFileParser(FileParser): 'date_maturity': line.get('date', datetime.datetime.now().date()), 'credit': amount > 0.0 and amount or 0.0, 'debit': amount < 0.0 and -amount or 0.0, - 'transaction_ref': line.get('transaction_id', '/'), + 'ref': line.get('transaction_id', '/'), } From 5bbc9e439e2f2ea2f2ad0e10f26338900659e1dd Mon Sep 17 00:00:00 2001 From: Akim Juillerat Date: Tue, 19 Nov 2019 11:46:48 +0100 Subject: [PATCH 2/4] Use multi move import when importing using transaction_id With the drop of 'transaction_ref' field on account.move.line in order to ease the reconcilition using 'ref' on account.move, we need to import each line from the imported file in different account.moves. --- .../parser/transactionid_file_parser.py | 9 ++++ .../tests/test_transactionid_import.py | 53 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 account_move_transactionid_import/tests/test_transactionid_import.py diff --git a/account_move_transactionid_import/parser/transactionid_file_parser.py b/account_move_transactionid_import/parser/transactionid_file_parser.py index 29c3c04e..4febcdaa 100644 --- a/account_move_transactionid_import/parser/transactionid_file_parser.py +++ b/account_move_transactionid_import/parser/transactionid_file_parser.py @@ -32,6 +32,7 @@ class TransactionIDFileParser(FileParser): super().__init__( profile, extra_fields=conversion_dict, ftype=ftype, header=header, **kwargs) + self.support_multi_moves = True @classmethod def parser_for(cls, parser_name): @@ -68,3 +69,11 @@ class TransactionIDFileParser(FileParser): 'debit': amount < 0.0 and -amount or 0.0, 'ref': line.get('transaction_id', '/'), } + + def get_move_vals(self): + res = super().get_move_vals() + if 'ref' in res: + res.pop('ref') + if res.get('name') == '/': + res['name'] = self.move_ref + return res diff --git a/account_move_transactionid_import/tests/test_transactionid_import.py b/account_move_transactionid_import/tests/test_transactionid_import.py new file mode 100644 index 00000000..2bb209e3 --- /dev/null +++ b/account_move_transactionid_import/tests/test_transactionid_import.py @@ -0,0 +1,53 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +import base64 +import os + +from odoo.modules.module import get_module_resource +from odoo.addons.account_move_base_import.tests.test_base_import import TestCodaImport + + +class TestTransactionIdImport(TestCodaImport): + + + def test_multiline_csv(self): + """Test import from csv + """ + self.journal.write({'import_type': 'generic_csvxls_transaction'}) + file_name = get_module_resource( + 'account_move_transactionid_import', 'data', 'statement.csv' + ) + move_ids = self._import_file_multi(file_name) + self._validate_imported_moves(move_ids) + + def test_multiline_xls(self): + """Test import from xls + """ + self.journal.write({'import_type': 'generic_csvxls_transaction'}) + file_name = get_module_resource( + 'account_move_transactionid_import', 'data', 'statement.xls' + ) + move_ids = self._import_file_multi(file_name) + self._validate_imported_moves(move_ids) + + def _import_file_multi(self, file_name): + """ import a file using the wizard + return the create account.bank.statement object + """ + with open(file_name, 'rb') as f: + content = f.read() + self.wizard = self.import_wizard_obj.create({ + "journal_id": self.journal.id, + 'input_statement': base64.b64encode(content), + 'file_name': os.path.basename(file_name), + }) + res = self.wizard.import_statement() + return self.account_move_obj.browse(res['domain'][0][2]) + + def _validate_imported_moves(self, moves): + self.assertEqual(len(moves), 3) + transaction_ids = ['50969286', '51065326', '51179306'] + for i, move in enumerate(moves): + self.assertEqual(move.ref, transaction_ids[i]) + self.assertEqual(move.name, 'statement') + self.assertEqual(3, len(move.line_ids)) From 9fd9d764994810c9222e278756704c9dcd093fbb Mon Sep 17 00:00:00 2001 From: Tonow-c2c Date: Fri, 22 Nov 2019 08:33:01 +0100 Subject: [PATCH 3/4] Remove undefined fields balance_cash credit_cash debit_cash --- account_move_base_import/models/account_journal.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/account_move_base_import/models/account_journal.py b/account_move_base_import/models/account_journal.py index c62ae71d..79cdc1cd 100644 --- a/account_move_base_import/models/account_journal.py +++ b/account_move_base_import/models/account_journal.py @@ -242,9 +242,6 @@ class AccountJournal(models.Model): 'date': move.date, 'balance': values['debit'] - values['credit'], 'amount_residual_currency': 0, - 'debit_cash_basic': values['debit'], - 'credit_cash_basic': values['credit'], - 'balance_cash_basic': values['debit'] - values['credit'], 'user_type_id': account.user_type_id.id, 'reconciled': False, }) @@ -277,12 +274,16 @@ class AccountJournal(models.Model): res = self.env['account.move'] for result_row_list in parser.parse(file_stream): move = self._move_import( - parser, file_stream, result_row_list=result_row_list, ftype=ftype + parser, file_stream, + result_row_list=result_row_list, + ftype=ftype ) res |= move return res - def _move_import(self, parser, file_stream, result_row_list=None, ftype="csv"): + def _move_import( + self, parser, file_stream, result_row_list=None, ftype="csv" + ): """Create a bank statement with the given profile and parser. It will fulfill the bank statement with the values of the file provided, but will not complete data (like finding the partner, or the right From 851e0bce423a3675ecbf6c43f128a1c178fd229f Mon Sep 17 00:00:00 2001 From: Tonow-c2c Date: Fri, 22 Nov 2019 09:26:28 +0100 Subject: [PATCH 4/4] Pass Black on the files + pylint Move test data to tests/data --- .../models/account_journal.py | 265 ++++++++++-------- .../parser/file_parser.py | 112 +++++--- .../{demo => tests/data}/statement.csv | 0 .../{demo => tests/data}/statement.xls | Bin .../tests/test_base_import.py | 10 +- .../parser/transactionid_file_parser.py | 52 ++-- .../tests/data/completion_rule_data.xml | 16 ++ .../{ => tests}/data/statement.csv | 0 .../{ => tests}/data/statement.xls | Bin .../tests/test_transactionid_import.py | 40 +-- 10 files changed, 301 insertions(+), 194 deletions(-) rename account_move_base_import/{demo => tests/data}/statement.csv (100%) rename account_move_base_import/{demo => tests/data}/statement.xls (100%) create mode 100644 account_move_transactionid_import/tests/data/completion_rule_data.xml rename account_move_transactionid_import/{ => tests}/data/statement.csv (100%) rename account_move_transactionid_import/{ => tests}/data/statement.xls (100%) diff --git a/account_move_base_import/models/account_journal.py b/account_move_base_import/models/account_journal.py index 79cdc1cd..6b9a3658 100644 --- a/account_move_base_import/models/account_journal.py +++ b/account_move_base_import/models/account_journal.py @@ -12,63 +12,67 @@ from odoo.exceptions import UserError, ValidationError class AccountJournal(models.Model): - _name = 'account.journal' - _inherit = ['account.journal', 'mail.thread'] - _order = 'sequence' + _name = "account.journal" + _inherit = ["account.journal", "mail.thread"] + _order = "sequence" - used_for_import = fields.Boolean( - string="Journal used for import") + used_for_import = fields.Boolean(string="Journal used for import") commission_account_id = fields.Many2one( - comodel_name='account.account', - string='Commission account') + comodel_name="account.account", string="Commission account" + ) import_type = fields.Selection( - [('generic_csvxls_so', 'Generic .csv/.xls based on SO Name')], - string='Type of import', - default='generic_csvxls_so', + [("generic_csvxls_so", "Generic .csv/.xls based on SO Name")], + string="Type of import", + default="generic_csvxls_so", required=True, help="Choose here the method by which you want to import account " - "moves for this journal.") + "moves for this journal.", + ) - last_import_date = fields.Datetime( - string="Last Import Date") + last_import_date = fields.Datetime(string="Last Import Date") partner_id = fields.Many2one( - comodel_name='res.partner', - string='Bank/Payment Office partner', + comodel_name="res.partner", + string="Bank/Payment Office partner", help="Put a partner if you want to have it on the commission move " "(and optionaly on the counterpart of the intermediate/" - "banking move if you tick the corresponding checkbox).") + "banking move if you tick the corresponding checkbox).", + ) receivable_account_id = fields.Many2one( - comodel_name='account.account', - string='Receivable/Payable Account', + comodel_name="account.account", + string="Receivable/Payable Account", help="Choose a receivable/payable account to use as the default " - "debit/credit account.") + "debit/credit account.", + ) - used_for_completion = fields.Boolean( - string="Journal used for completion") + used_for_completion = fields.Boolean(string="Journal used for completion") rule_ids = fields.Many2many( - comodel_name='account.move.completion.rule', - string='Auto-completion rules', - relation='account_journal_completion_rule_rel') + comodel_name="account.move.completion.rule", + string="Auto-completion rules", + relation="account_journal_completion_rule_rel", + ) launch_import_completion = fields.Boolean( string="Launch completion after import", help="Tic that box to automatically launch the completion " - "on each imported file using this journal.") + "on each imported file using this journal.", + ) create_counterpart = fields.Boolean( string="Create Counterpart", help="Tick that box to automatically create the move counterpart", - default=True) + default=True, + ) split_counterpart = fields.Boolean( string="Split Counterpart", help="Two counterparts will be automatically created : one for " - "the refunds and one for the payments") + "the refunds and one for the payments", + ) @api.multi def _prepare_counterpart_line(self, move, amount, date): @@ -81,24 +85,24 @@ class AccountJournal(models.Model): credit = -amount debit = 0.0 counterpart_values = { - 'date_maturity': date, - 'credit': credit, - 'debit': debit, - 'partner_id': self.partner_id.id, - 'move_id': move.id, - 'account_id': account_id, - 'already_completed': True, - 'journal_id': self.id, - 'company_id': self.company_id.id, - 'currency_id': self.currency_id.id, - 'company_currency_id': self.company_id.currency_id.id, - 'amount_residual': amount, + "date_maturity": date, + "credit": credit, + "debit": debit, + "partner_id": self.partner_id.id, + "move_id": move.id, + "account_id": account_id, + "already_completed": True, + "journal_id": self.id, + "company_id": self.company_id.id, + "currency_id": self.currency_id.id, + "company_currency_id": self.company_id.currency_id.id, + "amount_residual": amount, } return counterpart_values @api.multi def _create_counterpart(self, parser, move): - move_line_obj = self.env['account.move.line'] + move_line_obj = self.env["account.move.line"] refund = 0.0 payment = 0.0 transfer_lines = [] @@ -114,16 +118,18 @@ class AccountJournal(models.Model): total_amount = refund + payment if total_amount: transfer_lines.append(total_amount) - counterpart_date = parser.get_move_vals().get('date') or \ - fields.Date.today() + counterpart_date = ( + parser.get_move_vals().get("date") or fields.Date.today() + ) transfer_line_count = len(transfer_lines) check_move_validity = False for amount in transfer_lines: transfer_line_count -= 1 if not transfer_line_count: check_move_validity = True - vals = self._prepare_counterpart_line(move, amount, - counterpart_date) + vals = self._prepare_counterpart_line( + move, amount, counterpart_date + ) move_line_obj.with_context( check_move_validity=check_move_validity ).create(vals) @@ -142,48 +148,53 @@ class AccountJournal(models.Model): statement ID :param: context: global context """ - move_line_obj = self.env['account.move.line'] + move_line_obj = self.env["account.move.line"] global_commission_amount = 0 for row in parser.result_row_list: global_commission_amount += float( - row.get('commission_amount', '0.0')) + row.get("commission_amount", "0.0") + ) partner_id = self.partner_id.id # Commission line if global_commission_amount > 0.0: - raise UserError(_('Commission amount should not be positive.')) + raise UserError(_("Commission amount should not be positive.")) elif global_commission_amount < 0.0: if not self.commission_account_id: raise UserError( - _('No commission account is set on the journal.')) + _("No commission account is set on the journal.") + ) else: commission_account_id = self.commission_account_id.id comm_values = { - 'name': _('Commission line'), - 'date_maturity': parser.get_move_vals().get('date') or - fields.Date.today(), - 'debit': -global_commission_amount, - 'partner_id': partner_id, - 'move_id': move.id, - 'account_id': commission_account_id, - 'already_completed': True, + "name": _("Commission line"), + "date_maturity": ( + parser.get_move_vals().get("date") + or fields.Date.today() + ), + "debit": -global_commission_amount, + "partner_id": partner_id, + "move_id": move.id, + "account_id": commission_account_id, + "already_completed": True, } - if (self.currency_id and - self.currency_id != self.company_id.currency_id): + if ( + self.currency_id + and self.currency_id != self.company_id.currency_id + ): # the commission we are reading is in the currency of the # journal: use the amount in the amount_currency field, and # set credit / debit to the value in company currency at # the date of the move. currency = self.currency_id.with_context(date=move.date) company_currency = self.company_id.currency_id - comm_values['amount_currency'] = comm_values['debit'] - comm_values['debit'] = currency.compute( - comm_values['debit'], - company_currency + comm_values["amount_currency"] = comm_values["debit"] + comm_values["debit"] = currency.compute( + comm_values["debit"], company_currency ) - comm_values['currency_id'] = currency.id - move_line_obj.with_context( - check_move_validity=False - ).create(comm_values) + comm_values["currency_id"] = currency.id + move_line_obj.with_context(check_move_validity=False).create( + comm_values + ) @api.multi def write_logs_after_import(self, move, num_lines): @@ -195,8 +206,9 @@ class AccountJournal(models.Model): :return: True """ self.message_post( - body=_('Move %s have been imported with %s ' - 'lines.') % (move.name, num_lines)) + body=_("Move %s have been imported with %s " "lines.") + % (move.name, num_lines) + ) return True def prepare_move_line_vals(self, parser_vals, move): @@ -211,40 +223,46 @@ class AccountJournal(models.Model): :return: dict of vals that will be passed to create method of statement line. """ - move_line_obj = self.env['account.move.line'] + move_line_obj = self.env["account.move.line"] values = parser_vals - if not values.get('account_id', False): - values['account_id'] = self.receivable_account_id.id - account = self.env['account.account'].browse(values['account_id']) - if (self.currency_id and - self.currency_id != self.company_id.currency_id): + if not values.get("account_id", False): + values["account_id"] = self.receivable_account_id.id + account = self.env["account.account"].browse(values["account_id"]) + if ( + self.currency_id + and self.currency_id != self.company_id.currency_id + ): # the debit and credit we are reading are in the currency of the # journal: use the amount in the amount_currency field, and set # credit / debit to the value in company currency at the date of # the move. currency = self.currency_id.with_context(date=move.date) company_currency = self.company_id.currency_id - values['amount_currency'] = values['debit'] - values['credit'] - values['debit'] = currency.compute(values['debit'], - company_currency) - values['credit'] = currency.compute(values['credit'], - company_currency) + values["amount_currency"] = values["debit"] - values["credit"] + values["debit"] = currency.compute( + values["debit"], company_currency + ) + values["credit"] = currency.compute( + values["credit"], company_currency + ) if account.reconcile: - values['amount_residual'] = values['debit'] - values['credit'] + values["amount_residual"] = values["debit"] - values["credit"] else: - values['amount_residual'] = 0 - values.update({ - 'company_id': self.company_id.id, - 'currency_id': self.currency_id.id, - 'company_currency_id': self.company_id.currency_id.id, - 'journal_id': self.id, - 'move_id': move.id, - 'date': move.date, - 'balance': values['debit'] - values['credit'], - 'amount_residual_currency': 0, - 'user_type_id': account.user_type_id.id, - 'reconciled': False, - }) + values["amount_residual"] = 0 + values.update( + { + "company_id": self.company_id.id, + "currency_id": self.currency_id.id, + "company_currency_id": self.company_id.currency_id.id, + "journal_id": self.id, + "move_id": move.id, + "date": move.date, + "balance": values["debit"] - values["credit"], + "amount_residual_currency": 0, + "user_type_id": account.user_type_id.id, + "reconciled": False, + } + ) values = move_line_obj._add_missing_default_values(values) return values @@ -252,9 +270,11 @@ class AccountJournal(models.Model): """Hook to build the values of the statement from the parser and the profile. """ - vals = {'journal_id': self.id, - 'currency_id': self.currency_id.id, - 'import_partner_id': self.partner_id.id} + vals = { + "journal_id": self.id, + "currency_id": self.currency_id.id, + "import_partner_id": self.partner_id.id, + } vals.update(parser.get_move_vals()) return vals @@ -267,23 +287,23 @@ class AccountJournal(models.Model): :param char: ftype represent the file extension (csv by default) :return: list: list of ids of the created account.bank.statement """ - filename = self._context.get('file_name', None) + filename = self._context.get("file_name", None) if filename: (filename, __) = os.path.splitext(filename) parser = new_move_parser(self, ftype=ftype, move_ref=filename) - res = self.env['account.move'] + res = self.env["account.move"] for result_row_list in parser.parse(file_stream): move = self._move_import( - parser, file_stream, + parser, + file_stream, result_row_list=result_row_list, - ftype=ftype + ftype=ftype, ) res |= move return res def _move_import( - self, parser, file_stream, result_row_list=None, ftype="csv" - ): + self, parser, file_stream, result_row_list=None, ftype="csv"): """Create a bank statement with the given profile and parser. It will fulfill the bank statement with the values of the file provided, but will not complete data (like finding the partner, or the right @@ -295,23 +315,26 @@ class AccountJournal(models.Model): :param char: ftype represent the file extension (csv by default) :return: ID of the created account.bank.statement """ - move_obj = self.env['account.move'] - move_line_obj = self.env['account.move.line'] - attachment_obj = self.env['ir.attachment'] + move_obj = self.env["account.move"] + move_line_obj = self.env["account.move.line"] + attachment_obj = self.env["ir.attachment"] if result_row_list is None: result_row_list = parser.result_row_list # Check all key are present in account.bank.statement.line!! if not result_row_list: - raise UserError(_("Nothing to import: " - "The file is empty")) + raise UserError(_("Nothing to import: " "The file is empty")) parsed_cols = list( parser.get_move_line_vals(result_row_list[0]).keys() ) for col in parsed_cols: if col not in move_line_obj._fields: raise UserError( - _("Missing column! Column %s you try to import is not " - "present in the move line!") % col) + _( + "Missing column! Column %s you try to import is not " + "present in the move line!" + ) + % col + ) move_vals = self.prepare_move_vals(result_row_list, parser) move = move_obj.create(move_vals) try: @@ -333,11 +356,11 @@ class AccountJournal(models.Model): move._amount_compute() # Attach data to the move attachment_data = { - 'name': 'statement file', - 'datas': file_stream, - 'datas_fname': "%s.%s" % (fields.Date.today(), ftype), - 'res_model': 'account.move', - 'res_id': move.id, + "name": "statement file", + "datas": file_stream, + "datas_fname": "%s.%s" % (fields.Date.today(), ftype), + "res_model": "account.move", + "res_id": move.id, } attachment_obj.create(attachment_data) # If user ask to launch completion at end of import, do it! @@ -351,9 +374,15 @@ class AccountJournal(models.Model): except Exception: error_type, error_value, trbk = sys.exc_info() st = "Error: %s\nDescription: %s\nTraceback:" % ( - error_type.__name__, error_value) - st += ''.join(traceback.format_tb(trbk, 30)) + error_type.__name__, + error_value, + ) + st += "".join(traceback.format_tb(trbk, 30)) raise ValidationError( - _("Statement import error " - "The statement cannot be created: %s") % st) + _( + "Statement import error " + "The statement cannot be created: %s" + ) + % st + ) return move diff --git a/account_move_base_import/parser/file_parser.py b/account_move_base_import/parser/file_parser.py index 3eaa3f32..9cc048d8 100644 --- a/account_move_base_import/parser/file_parser.py +++ b/account_move_base_import/parser/file_parser.py @@ -32,8 +32,15 @@ class FileParser(AccountMoveImportParser): format. """ - def __init__(self, journal, ftype='csv', extra_fields=None, header=None, - dialect=None, move_ref=None, **kwargs): + def __init__( + self, + journal, + ftype="csv", + extra_fields=None, + header=None, + dialect=None, + move_ref=None, + **kwargs): """ :param char: parse_name: The name of the parser :param char: ftype: extension of the file (could be csv, xls or @@ -44,11 +51,12 @@ class FileParser(AccountMoveImportParser): header """ super().__init__(journal, **kwargs) - if ftype in ('csv', 'xls', 'xlsx'): + if ftype in ("csv", "xls", "xlsx"): self.ftype = ftype[0:3] else: raise UserError( - _('Invalid file type %s. Please use csv, xls or xlsx') % ftype) + _("Invalid file type %s. Please use csv, xls or xlsx") % ftype + ) self.conversion_dict = extra_fields self.keys_to_validate = list(self.conversion_dict.keys()) self.fieldnames = header @@ -73,7 +81,7 @@ class FileParser(AccountMoveImportParser): given ftype """ if self.parsed_file is None: - if self.ftype == 'csv': + if self.ftype == "csv": self.parsed_file = self._parse_csv() else: self.parsed_file = self._parse_xls() @@ -81,9 +89,8 @@ class FileParser(AccountMoveImportParser): if len(self.parsed_file) <= self.current_line: return False else: - print(self.current_line) self.result_row_list = self.parsed_file[ - self.current_line:self.current_line + 1 + self.current_line: self.current_line + 1 ] self.current_line += 1 return True @@ -101,7 +108,7 @@ class FileParser(AccountMoveImportParser): parsed_cols = list(self.result_row_list[0].keys()) for col in self.keys_to_validate: if col not in parsed_cols: - raise UserError(_('Column %s not present in file') % col) + raise UserError(_("Column %s not present in file") % col) return True def _post(self, *args, **kwargs): @@ -115,9 +122,10 @@ class FileParser(AccountMoveImportParser): csv_file = tempfile.NamedTemporaryFile() csv_file.write(self.filebuffer) csv_file.flush() - with open(csv_file.name, 'rU') as fobj: - reader = UnicodeDictReader(fobj, fieldnames=self.fieldnames, - dialect=self.dialect) + with open(csv_file.name, "rU") as fobj: + reader = UnicodeDictReader( + fobj, fieldnames=self.fieldnames, dialect=self.dialect + ) return list(reader) def _parse_xls(self): @@ -143,26 +151,41 @@ class FileParser(AccountMoveImportParser): for rule in conversion_rules: if conversion_rules[rule] == datetime.datetime: try: - date_string = line[rule].split(' ')[0] - line[rule] = datetime.datetime.strptime(date_string, - '%Y-%m-%d') + date_string = line[rule].split(" ")[0] + line[rule] = datetime.datetime.strptime( + date_string, "%Y-%m-%d" + ) except ValueError as err: raise UserError( - _("Date format is not valid." - " It should be YYYY-MM-DD for column: %s" - " value: %s \n \n \n Please check the line with " - "ref: %s \n \n Detail: %s") % - (rule, line.get(rule, _('Missing')), - line.get('ref', line), repr(err))) + _( + "Date format is not valid." + " It should be YYYY-MM-DD for column: %s" + " value: %s \n \n \n Please check" + " the line with ref: %s \n \n Detail: %s" + ) + % ( + rule, + line.get(rule, _("Missing")), + line.get("ref", line), + repr(err), + ) + ) else: try: line[rule] = conversion_rules[rule](line[rule]) except Exception as err: raise UserError( - _("Value %s of column %s is not valid.\n Please " - "check the line with ref %s:\n \n Detail: %s") % - (line.get(rule, _('Missing')), rule, - line.get('ref', line), repr(err))) + _( + "Value %s of column %s is not valid.\n Please " + "check the line with ref %s:\n \n Detail: %s" + ) + % ( + line.get(rule, _("Missing")), + rule, + line.get("ref", line), + repr(err), + ) + ) return result_set def _from_xls(self, result_set, conversion_rules): @@ -173,26 +196,41 @@ class FileParser(AccountMoveImportParser): for rule in conversion_rules: if conversion_rules[rule] == datetime.datetime: try: - t_tuple = xlrd.xldate_as_tuple(line[rule], - self._datemode) + t_tuple = xlrd.xldate_as_tuple( + line[rule], self._datemode + ) line[rule] = datetime.datetime(*t_tuple) except Exception as err: raise UserError( - _("Date format is not valid. " - "Please modify the cell formatting to date " - "format for column: %s value: %s\n Please check " - "the line with ref: %s\n \n Detail: %s") % - (rule, line.get(rule, _('Missing')), - line.get('ref', line), repr(err))) + _( + "Date format is not valid. " + "Please modify the cell formatting to date " + "format for column: %s value: %s\n Please" + " check the line with ref: %s\n \n Detail: %s" + ) + % ( + rule, + line.get(rule, _("Missing")), + line.get("ref", line), + repr(err), + ) + ) else: try: line[rule] = conversion_rules[rule](line[rule]) except Exception as err: raise UserError( - _("Value %s of column %s is not valid.\n Please " - "check the line with ref %s:\n \n Detail: %s") % - (line.get(rule, _('Missing')), rule, - line.get('ref', line), repr(err))) + _( + "Value %s of column %s is not valid.\n Please " + "check the line with ref %s:\n \n Detail: %s" + ) + % ( + line.get(rule, _("Missing")), + rule, + line.get("ref", line), + repr(err), + ) + ) return result_set def _cast_rows(self, *args, **kwargs): @@ -200,6 +238,6 @@ class FileParser(AccountMoveImportParser): providen. We call here _from_xls or _from_csv depending on the self.ftype variable. """ - func = getattr(self, '_from_%s' % self.ftype) + func = getattr(self, "_from_%s" % self.ftype) res = func(self.result_row_list, self.conversion_dict) return res diff --git a/account_move_base_import/demo/statement.csv b/account_move_base_import/tests/data/statement.csv similarity index 100% rename from account_move_base_import/demo/statement.csv rename to account_move_base_import/tests/data/statement.csv diff --git a/account_move_base_import/demo/statement.xls b/account_move_base_import/tests/data/statement.xls similarity index 100% rename from account_move_base_import/demo/statement.xls rename to account_move_base_import/tests/data/statement.xls diff --git a/account_move_base_import/tests/test_base_import.py b/account_move_base_import/tests/test_base_import.py index 7eaf1609..57781ca2 100644 --- a/account_move_base_import/tests/test_base_import.py +++ b/account_move_base_import/tests/test_base_import.py @@ -53,7 +53,10 @@ class TestCodaImport(common.TransactionCase): """Test import from xls """ file_name = get_resource_path( - 'account_move_base_import', 'demo', 'statement.xls' + 'account_move_base_import', + 'tests', + 'data', + 'statement.xls' ) move = self._import_file(file_name) self._validate_imported_move(move) @@ -62,7 +65,10 @@ class TestCodaImport(common.TransactionCase): """Test import from csv """ file_name = get_resource_path( - 'account_move_base_import', 'demo', 'statement.csv' + 'account_move_base_import', + 'tests', + 'data', + 'statement.csv' ) move = self._import_file(file_name) self._validate_imported_move(move) diff --git a/account_move_transactionid_import/parser/transactionid_file_parser.py b/account_move_transactionid_import/parser/transactionid_file_parser.py index 4febcdaa..da71e104 100644 --- a/account_move_transactionid_import/parser/transactionid_file_parser.py +++ b/account_move_transactionid_import/parser/transactionid_file_parser.py @@ -3,7 +3,8 @@ import datetime from odoo.tools import ustr from odoo.addons.account_move_base_import.parser.file_parser import ( - FileParser, float_or_zero + FileParser, + float_or_zero, ) @@ -12,8 +13,13 @@ class TransactionIDFileParser(FileParser): bank statement. """ - def __init__(self, profile, ftype='csv', extra_fields=None, header=None, - **kwargs): + def __init__( + self, + profile, + ftype="csv", + extra_fields=None, + header=None, + **kwargs): """Add transaction_id in header keys :param char: profile: Reference to the profile :param char: ftype: extension of the file (could be csv or xls) @@ -23,15 +29,19 @@ class TransactionIDFileParser(FileParser): header """ conversion_dict = { - 'transaction_id': ustr, - 'label': ustr, - 'date': datetime.datetime, - 'amount': float_or_zero, - 'commission_amount': float_or_zero, + "transaction_id": ustr, + "label": ustr, + "date": datetime.datetime, + "amount": float_or_zero, + "commission_amount": float_or_zero, } super().__init__( - profile, extra_fields=conversion_dict, ftype=ftype, header=header, - **kwargs) + profile, + extra_fields=conversion_dict, + ftype=ftype, + header=header, + **kwargs + ) self.support_multi_moves = True @classmethod @@ -39,7 +49,7 @@ class TransactionIDFileParser(FileParser): """Used by the new_bank_statement_parser class factory. Return true if the providen name is generic_csvxls_transaction """ - return parser_name == 'generic_csvxls_transaction' + return parser_name == "generic_csvxls_transaction" def get_move_line_vals(self, line, *args, **kwargs): """This method must return a dict of vals that can be passed to create @@ -61,19 +71,19 @@ class TransactionIDFileParser(FileParser): In this generic parser, the commission is given for every line, so we store it for each one. """ - amount = line.get('amount', 0.0) + amount = line.get("amount", 0.0) return { - 'name': line.get('label', '/'), - 'date_maturity': line.get('date', datetime.datetime.now().date()), - 'credit': amount > 0.0 and amount or 0.0, - 'debit': amount < 0.0 and -amount or 0.0, - 'ref': line.get('transaction_id', '/'), + "name": line.get("label", "/"), + "date_maturity": line.get("date", datetime.datetime.now().date()), + "credit": amount > 0.0 and amount or 0.0, + "debit": amount < 0.0 and -amount or 0.0, + "ref": line.get("transaction_id", "/"), } def get_move_vals(self): res = super().get_move_vals() - if 'ref' in res: - res.pop('ref') - if res.get('name') == '/': - res['name'] = self.move_ref + if "ref" in res: + res.pop("ref") + if res.get("name") == "/": + res["name"] = self.move_ref return res diff --git a/account_move_transactionid_import/tests/data/completion_rule_data.xml b/account_move_transactionid_import/tests/data/completion_rule_data.xml new file mode 100644 index 00000000..45d680d2 --- /dev/null +++ b/account_move_transactionid_import/tests/data/completion_rule_data.xml @@ -0,0 +1,16 @@ + + + + + Match from Sales Order using transaction ID + 30 + get_from_transaction_id_and_so + + + + Match from Invoice using transaction ID + 40 + get_from_transaction_id_and_invoice + + + diff --git a/account_move_transactionid_import/data/statement.csv b/account_move_transactionid_import/tests/data/statement.csv similarity index 100% rename from account_move_transactionid_import/data/statement.csv rename to account_move_transactionid_import/tests/data/statement.csv diff --git a/account_move_transactionid_import/data/statement.xls b/account_move_transactionid_import/tests/data/statement.xls similarity index 100% rename from account_move_transactionid_import/data/statement.xls rename to account_move_transactionid_import/tests/data/statement.xls diff --git a/account_move_transactionid_import/tests/test_transactionid_import.py b/account_move_transactionid_import/tests/test_transactionid_import.py index 2bb209e3..816902e9 100644 --- a/account_move_transactionid_import/tests/test_transactionid_import.py +++ b/account_move_transactionid_import/tests/test_transactionid_import.py @@ -4,18 +4,21 @@ import base64 import os from odoo.modules.module import get_module_resource -from odoo.addons.account_move_base_import.tests.test_base_import import TestCodaImport +from odoo.addons.account_move_base_import.tests.test_base_import import ( + TestCodaImport, +) class TestTransactionIdImport(TestCodaImport): - - def test_multiline_csv(self): """Test import from csv """ - self.journal.write({'import_type': 'generic_csvxls_transaction'}) + self.journal.write({"import_type": "generic_csvxls_transaction"}) file_name = get_module_resource( - 'account_move_transactionid_import', 'data', 'statement.csv' + "account_move_transactionid_import", + "tests", + "data", + "statement.csv" ) move_ids = self._import_file_multi(file_name) self._validate_imported_moves(move_ids) @@ -23,9 +26,12 @@ class TestTransactionIdImport(TestCodaImport): def test_multiline_xls(self): """Test import from xls """ - self.journal.write({'import_type': 'generic_csvxls_transaction'}) + self.journal.write({"import_type": "generic_csvxls_transaction"}) file_name = get_module_resource( - 'account_move_transactionid_import', 'data', 'statement.xls' + "account_move_transactionid_import", + "tests", + "data", + "statement.xls" ) move_ids = self._import_file_multi(file_name) self._validate_imported_moves(move_ids) @@ -34,20 +40,22 @@ class TestTransactionIdImport(TestCodaImport): """ import a file using the wizard return the create account.bank.statement object """ - with open(file_name, 'rb') as f: + with open(file_name, "rb") as f: content = f.read() - self.wizard = self.import_wizard_obj.create({ - "journal_id": self.journal.id, - 'input_statement': base64.b64encode(content), - 'file_name': os.path.basename(file_name), - }) + self.wizard = self.import_wizard_obj.create( + { + "journal_id": self.journal.id, + "input_statement": base64.b64encode(content), + "file_name": os.path.basename(file_name), + } + ) res = self.wizard.import_statement() - return self.account_move_obj.browse(res['domain'][0][2]) + return self.account_move_obj.browse(res["domain"][0][2]) def _validate_imported_moves(self, moves): self.assertEqual(len(moves), 3) - transaction_ids = ['50969286', '51065326', '51179306'] + transaction_ids = ["50969286", "51065326", "51179306"] for i, move in enumerate(moves): self.assertEqual(move.ref, transaction_ids[i]) - self.assertEqual(move.name, 'statement') + self.assertEqual(move.name, "statement") self.assertEqual(3, len(move.line_ids))