diff --git a/addons/account/i18n/account.pot b/addons/account/i18n/account.pot index bd46e113f41..f5aea3f85f1 100644 --- a/addons/account/i18n/account.pot +++ b/addons/account/i18n/account.pot @@ -1339,6 +1339,13 @@ msgstr "" msgid "A tax fiscal position could be defined only one time on same taxes." msgstr "" +#. module: account +#. odoo-python +#: code:addons/account/models/account_move_line.py:0 +#, python-format +msgid "A temporary number can not be used in a real matching" +msgstr "" + #. module: account #: model:res.groups,name:account.group_warning_account msgid "A warning can be set on a partner (Account)" diff --git a/addons/account/models/account_account.py b/addons/account/models/account_account.py index 46f93b09a69..56d9ef87ae4 100644 --- a/addons/account/models/account_account.py +++ b/addons/account/models/account_account.py @@ -688,6 +688,7 @@ class AccountAccount(models.Model): WHERE full_reconcile_id IS NULL and account_id IN %s """ self.env.cr.execute(query, [tuple(self.ids)]) + self.env['account.move.line'].invalidate_model(['amount_residual', 'amount_residual_currency', 'reconciled']) def _toggle_reconcile_to_false(self): '''Toggle the `reconcile´ boolean from True -> False diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 4f57d6c1d1e..3a0d5ecc001 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -3758,6 +3758,7 @@ class AccountMove(models.Model): }) draft_reverse_moves.reversed_entry_id._reconcile_reversed_moves(draft_reverse_moves, self._context.get('move_reverse_cancel', False)) + to_post.line_ids._reconcile_marked() for invoice in to_post: invoice.message_subscribe([ diff --git a/addons/account/models/account_move_line.py b/addons/account/models/account_move_line.py index eab6de2610e..e623a117525 100644 --- a/addons/account/models/account_move_line.py +++ b/addons/account/models/account_move_line.py @@ -1,7 +1,8 @@ -import ast from collections import defaultdict from contextlib import contextmanager from datetime import date +import logging +import re from odoo import api, fields, models, Command, _ from odoo.exceptions import ValidationError, UserError @@ -13,6 +14,9 @@ from odoo.addons.web.controllers.utils import clean_action from odoo.addons.account.models.account_move import MAX_HASH_VERSION +_logger = logging.getLogger(__name__) + + class AccountMoveLine(models.Model): _name = "account.move.line" _inherit = "analytic.mixin" @@ -256,10 +260,10 @@ class AccountMoveLine(models.Model): ) matching_number = fields.Char( string="Matching #", - readonly=True, + copy=False, help="Matching number for this line, 'P' if it is only partially reconcile, or the name of " "the full reconcile if it exists.", - ) + ) # can also start with `I` for imports: see `_reconcile_marked` is_account_reconcile = fields.Boolean( string='Account Reconcile', related='account_id.reconcile', @@ -1340,6 +1344,23 @@ class AccountMoveLine(models.Model): if common_tags: raise ValidationError(_("Taxes exigible on payment and on invoice cannot be mixed on the same journal item if they share some tag.")) + @api.constrains('matching_number', 'matched_debit_ids', 'matched_credit_ids') + def _constrains_matching_number(self): + for line in self: + if line.matching_number: + if not re.match(r'^((P?\d+)|(I.+))$', line.matching_number): + raise Exception("Invalid matching number format") + elif line.matching_number.startswith('I') and (line.matched_debit_ids or line.matched_credit_ids): + raise ValidationError(_("A temporary number can not be used in a real matching")) + elif line.matching_number.startswith('P') and not (line.matched_debit_ids or line.matched_credit_ids): + raise Exception("Should have partials") + elif line.matching_number.startswith('P') and line.full_reconcile_id: + raise Exception("Should not be partial number") + elif line.full_reconcile_id and line.matching_number != str(line.full_reconcile_id.id): + raise Exception("Matching number should be the full reconcile") + elif line.matched_debit_ids or line.matched_credit_ids: + raise Exception("Should have number") + # ------------------------------------------------------------------------- # CRUD/ORM # ------------------------------------------------------------------------- @@ -2903,6 +2924,29 @@ class AccountMoveLine(models.Model): """ Undo a reconciliation """ (self.matched_debit_ids + self.matched_credit_ids).unlink() + def _reconcile_marked(self): + """Process the pending reconciliation of entries marked (i.e. uring imports). + + The entries can be marked using the string `I*` as matching number where `*` can be anything. + Once all the entries using identical numbers are posted, this function proceeds to do the real matching. + """ + temp_numbers = list({ + line.matching_number + for line in self + if line.matching_number and line.matching_number.startswith('I') + }) + if temp_numbers: + for _matching_number, account, lines in self._read_group( + domain=[('matching_number', 'in', temp_numbers)], + groupby=['matching_number', 'account_id'], + aggregates=['id:recordset'], + ): + if all(move.state == 'posted' for move in lines.move_id): + if not account.reconcile: + _logger.info("%s has reconciled lines, changing the config", account.display_name) + account.reconcile = True + lines.with_context(no_exchange_difference=True, no_cash_basis=True).reconcile() + # ------------------------------------------------------------------------- # ANALYTIC # ------------------------------------------------------------------------- diff --git a/addons/account/static/xls/aml_import_template.xlsx b/addons/account/static/xls/aml_import_template.xlsx index ddad540682e..be1f39af16e 100644 Binary files a/addons/account/static/xls/aml_import_template.xlsx and b/addons/account/static/xls/aml_import_template.xlsx differ diff --git a/addons/account/tests/test_account_move_reconcile.py b/addons/account/tests/test_account_move_reconcile.py index 144442daa9b..aabf4d5acfa 100644 --- a/addons/account/tests/test_account_move_reconcile.py +++ b/addons/account/tests/test_account_move_reconcile.py @@ -4772,3 +4772,24 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): ('move_id.move_type', '=', 'entry'), ]) self.assertFalse(caba_transfer_amls.move_id) + + def test_reconcile_import(self): + """Test that the import of matchings does a real matching upon posting""" + comp_curr = self.company_data['currency'] + + line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, comp_curr, '2016-01-01') + line_1.move_id.button_draft() + line_2 = self.create_line_for_reconciliation(-300.0, -300.0, comp_curr, '2016-01-01') + line_3 = self.create_line_for_reconciliation(-400.0, -400.0, comp_curr, '2016-01-01') + line_4 = self.create_line_for_reconciliation(-500.0, -500.0, comp_curr, '2016-01-01') + line_4.move_id.button_draft() + line_5 = self.create_line_for_reconciliation(200.0, 200.0, comp_curr, '2016-01-01') + (line_1 + line_2 + line_3).matching_number = 'I11111' + (line_4 + line_5).matching_number = 'I22222' + # posting triggers the matching of the imported values + (line_1 + line_4).move_id.action_post() + self.assertRegex(line_1.matching_number, r'^P\d+') + self.assertRegex(line_4.matching_number, r'^P\d+') + (line_1 + line_4).reconcile() + self.assertRegex(line_1.matching_number, r'^\d+') + self.assertTrue(line_1.full_reconcile_id)