[IMP] account: unify reconciliation of import

Purpose
-------

Allow to reconcile entries during import easily.
* Have one uniform way of doing it
  - fix bugs only once
  - reduce number of fields
  - make it generic and not only for some imports
* avoid the need to post during imports
  - faster to import, can import bigger batches, because we do neither
    post or reconcile, both are among the most time consuming steps.
  - still allows to check the data before posting if needed
* make splitting import into smaller batches easier because the
  reconciliation is not cut/dependent on the batches anymore
* allow to reconcile entries in the future by using the same mechanism
  (i.e. cut-off)

Implementation
--------------

Allow setting the matching number to `I*` manually, where `*` can be
anything. When posting the last item with the same manually set number,
we will reconcile all the lines with the same number.

task-3593885

closes odoo/odoo#142017

Related: odoo/enterprise#50627
Signed-off-by: Quentin De Paoli <qdp@odoo.com>
This commit is contained in:
william-andre
2023-11-20 11:13:19 +00:00
parent afb33cb56e
commit 430f51fbd2
6 changed files with 77 additions and 3 deletions
+7
View File
@@ -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)"
+1
View File
@@ -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
+1
View File
@@ -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([
+47 -3
View File
@@ -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
# -------------------------------------------------------------------------
Binary file not shown.
@@ -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)