[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:
@@ -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)"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user