diff --git a/addons/account/data/account_data.xml b/addons/account/data/account_data.xml index 628649f58e8..4ca719fa04c 100644 --- a/addons/account/data/account_data.xml +++ b/addons/account/data/account_data.xml @@ -97,18 +97,6 @@ Command.create({'value': 'percent', 'value_amount': 100.0, 'nb_days': 30})]"/> - - - Account reconcile sequence - account.reconcile - A - - - - - Validated diff --git a/addons/account/models/account_full_reconcile.py b/addons/account/models/account_full_reconcile.py index d701816f756..a27c46839ec 100644 --- a/addons/account/models/account_full_reconcile.py +++ b/addons/account/models/account_full_reconcile.py @@ -6,7 +6,6 @@ class AccountFullReconcile(models.Model): _name = "account.full.reconcile" _description = "Full Reconcile" - name = fields.Char(string='Number', required=True, copy=False, default=lambda self: self.env['ir.sequence'].next_by_code('account.reconcile')) partial_reconcile_ids = fields.One2many('account.partial.reconcile', 'full_reconcile_id', string='Reconciliation Parts') reconciled_line_ids = fields.One2many('account.move.line', 'full_reconcile_id', string='Matched Journal Items') exchange_move_id = fields.Many2one('account.move', index="btree_not_null") @@ -35,3 +34,10 @@ class AccountFullReconcile(models.Model): moves_to_reverse._reverse_moves(default_values_list, cancel=True) return res + + @api.model_create_multi + def create(self, vals_list): + fulls = super().create(vals_list) + for full in fulls: + full.reconciled_line_ids.matching_number = str(full.id) + return fulls diff --git a/addons/account/models/account_move_line.py b/addons/account/models/account_move_line.py index 268d15efd63..7d3640789d2 100644 --- a/addons/account/models/account_move_line.py +++ b/addons/account/models/account_move_line.py @@ -256,7 +256,7 @@ class AccountMoveLine(models.Model): ) matching_number = fields.Char( string="Matching #", - compute='_compute_matching_number', store=True, + readonly=True, help="Matching number for this line, 'P' if it is only partially reconcile, or the name of " "the full reconcile if it exists.", ) @@ -666,16 +666,6 @@ class AccountMoveLine(models.Model): if line.currency_id == line.company_id.currency_id: line.amount_currency = line.balance - @api.depends('full_reconcile_id.name', 'matched_debit_ids', 'matched_credit_ids') - def _compute_matching_number(self): - for record in self: - if record.full_reconcile_id: - record.matching_number = record.full_reconcile_id.name - elif record.matched_debit_ids or record.matched_credit_ids: - record.matching_number = 'P' - else: - record.matching_number = None - @api.depends_context('order_cumulated_balance', 'domain_cumulated_balance') def _compute_cumulated_balance(self): if not self.env.context.get('order_cumulated_balance'): @@ -2948,13 +2938,22 @@ class AccountMoveLine(models.Model): def _all_reconciled_lines(self): reconciliation_lines = self.filtered(lambda x: x.account_id.reconcile or x.account_id.account_type in ('asset_cash', 'liability_credit_card')) - current_lines = reconciliation_lines - current_partials = self.env['account.partial.reconcile'] - while current_lines: - current_partials = (current_lines.matched_debit_ids + current_lines.matched_credit_ids) - current_partials - current_lines = (current_partials.debit_move_id + current_partials.credit_move_id) - current_lines - reconciliation_lines += current_lines - return reconciliation_lines + self.env['account.partial.reconcile'].flush_model() + self.env.cr.execute(""" + WITH RECURSIVE partials (current_id) AS ( + SELECT line.id + FROM account_move_line line + WHERE id = ANY(%s) + + UNION + + SELECT CASE WHEN partial.debit_move_id = p.current_id THEN partial.credit_move_id ELSE partial.debit_move_id END + FROM partials p + JOIN account_partial_reconcile partial ON partial.debit_move_id = p.current_id OR partial.credit_move_id = p.current_id + ) + SELECT current_id FROM partials; + """, [reconciliation_lines.ids]) + return self.browse(r[0] for r in self.env.cr.fetchall()) def _get_attachment_domains(self): self.ensure_one() diff --git a/addons/account/models/account_partial_reconcile.py b/addons/account/models/account_partial_reconcile.py index 26098971bff..d86198049e7 100644 --- a/addons/account/models/account_partial_reconcile.py +++ b/addons/account/models/account_partial_reconcile.py @@ -8,7 +8,6 @@ from datetime import date class AccountPartialReconcile(models.Model): _name = "account.partial.reconcile" _description = "Partial Reconcile" - _rec_name = "id" # ==== Reconciliation fields ==== debit_move_id = fields.Many2one( @@ -106,6 +105,7 @@ class AccountPartialReconcile(models.Model): # Retrieve the matching number to unlink. full_to_unlink = self.full_reconcile_id + all_reconciled = self.debit_move_id + self.credit_move_id # Retrieve the CABA entries to reverse. moves_to_reverse = self.env['account.move'].search([('tax_cash_basis_rec_id', 'in', self.ids)]) @@ -126,8 +126,57 @@ class AccountPartialReconcile(models.Model): } for move in moves_to_reverse] moves_to_reverse._reverse_moves(default_values_list, cancel=True) + self._update_matching_number(all_reconciled) return res + @api.model_create_multi + def create(self, vals_list): + partials = super().create(vals_list) + self._update_matching_number(partials.debit_move_id + partials.credit_move_id) + return partials + + @api.model + def _update_matching_number(self, amls): + amls = amls._all_reconciled_lines() + self.env['account.partial.reconcile'].flush_model() + amls.flush_recordset(['full_reconcile_id']) + self.env.cr.execute(""" + WITH RECURSIVE partials (line_id, current_id) AS ( + SELECT id, id + FROM account_move_line + WHERE id = ANY(%s) + AND full_reconcile_id IS NULL + + UNION + + SELECT p.line_id, + CASE WHEN partial.debit_move_id = p.current_id THEN partial.credit_move_id + ELSE partial.debit_move_id + END + FROM partials p + JOIN account_partial_reconcile partial ON p.current_id = partial.debit_move_id + OR p.current_id = partial.credit_move_id + ) + SELECT line_id, 'P' || MIN(partial.id) AS matching_number + FROM partials + JOIN account_partial_reconcile partial ON current_id = partial.debit_move_id + OR current_id = partial.credit_move_id + GROUP BY line_id + """, [ + amls.ids, + ]) + + line_matching_number = dict(self.env.cr.fetchall()) + + for line in amls: + if line.full_reconcile_id: + line.matching_number = str(line.full_reconcile_id.id) + elif line.matched_debit_ids or line.matched_credit_ids: + line.matching_number = line_matching_number[line.id] + else: + line.matching_number = False + + # ------------------------------------------------------------------------- # RECONCILIATION METHODS # ------------------------------------------------------------------------- diff --git a/addons/account/tests/test_account_move_reconcile.py b/addons/account/tests/test_account_move_reconcile.py index f8a11433493..6cf0d32a25d 100644 --- a/addons/account/tests/test_account_move_reconcile.py +++ b/addons/account/tests/test_account_move_reconcile.py @@ -4000,6 +4000,104 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): {'debit': 0.0, 'credit': 0.01, 'tax_ids': [], 'tax_tag_ids': self.tax_tags[5].ids, 'account_id': self.tax_account_2.id}, ]) + def test_matching_number_full_reconcile(self): + currency = self.env.company.currency_id + line_a = self.create_line_for_reconciliation(1000, 1000, currency, '2016-01-01') + line_b = self.create_line_for_reconciliation(-1000, -1000, currency, '2016-01-01') + (line_a + line_b).reconcile() + self.assertFullReconcile(line_a.full_reconcile_id, (line_a + line_b)) + self.assertEqual(line_a.matching_number, str(line_a.full_reconcile_id.id)) + self.assertEqual(line_a.matching_number, line_b.matching_number) + + def test_matching_number_partial_single_reconcile(self): + currency = self.env.company.currency_id + line_a = self.create_line_for_reconciliation(1000, 1000, currency, '2016-01-01') + line_b = self.create_line_for_reconciliation(-500, -500, currency, '2016-01-01') + (line_a + line_b).reconcile() + self.assertEqual(line_a.matching_number, f'P{line_a.matched_credit_ids.id}') + self.assertEqual(line_a.matching_number, line_b.matching_number) + + def test_matching_number_partial_multi_reconcile(self): + currency = self.env.company.currency_id + line_a = self.create_line_for_reconciliation(1000, 1000, currency, '2016-01-01') + line_b = self.create_line_for_reconciliation(-500, -500, currency, '2016-01-01') + line_c = self.create_line_for_reconciliation(-1000, -1000, currency, '2016-01-01') + line_d = self.create_line_for_reconciliation(1000, 1000, currency, '2016-01-01') + (line_a + line_b).reconcile() + (line_a + line_c).reconcile() + (line_c + line_d).reconcile() + self.assertEqual(line_a.matching_number, f'P{line_a.matched_credit_ids.ids[0]}') + self.assertEqual(line_b.matching_number, line_a.matching_number) + self.assertEqual(line_c.matching_number, line_a.matching_number) + self.assertEqual(line_d.matching_number, line_a.matching_number) + + line_b.remove_move_reconcile() + self.assertEqual(line_a.matching_number, f'P{line_a.matched_credit_ids.ids[0]}') + self.assertEqual(line_b.matching_number, False) + self.assertEqual(line_c.matching_number, f'P{line_c.matched_debit_ids.ids[0]}') + self.assertEqual(line_d.matching_number, line_c.matching_number) + + (line_a + line_b).reconcile() # everything should be matched again + self.assertEqual(line_a.matching_number, line_c.matching_number) + self.assertEqual(line_b.matching_number, line_c.matching_number) + self.assertEqual(line_c.matching_number, f'P{line_c.matched_debit_ids.ids[0]}') + self.assertEqual(line_d.matching_number, line_c.matching_number) + + def test_matching_number_partial_multi_separate_reconcile(self): + currency = self.env.company.currency_id + line_a = self.create_line_for_reconciliation(1000, 1000, currency, '2016-01-01') + line_b = self.create_line_for_reconciliation(-500, -500, currency, '2016-01-01') + (line_a + line_b).reconcile() + self.assertEqual(line_a.matching_number, f'P{line_a.matched_credit_ids.id}') + self.assertEqual(line_a.matching_number, line_b.matching_number) + + line_c = self.create_line_for_reconciliation(-300, -300, currency, '2016-01-01') + (line_a + line_c).reconcile() + self.assertEqual(line_a.matching_number, f'P{line_a.matched_credit_ids.ids[0]}') + self.assertEqual(line_a.matching_number, line_b.matching_number) + self.assertEqual(line_a.matching_number, line_c.matching_number) + + def test_matching_number_unreconcile_single(self): + currency = self.env.company.currency_id + full_line_a = self.create_line_for_reconciliation(200, 200, currency, '2016-01-01') + full_line_b = self.create_line_for_reconciliation(-200, -200, currency, '2016-01-01') + partial_line_a = self.create_line_for_reconciliation(1000, 1000, currency, '2016-01-01') + partial_line_b = self.create_line_for_reconciliation(-500, -500, currency, '2016-01-01') + (full_line_a + full_line_b).reconcile() + (partial_line_a + partial_line_b).reconcile() + (full_line_a + full_line_b + partial_line_a + partial_line_b).remove_move_reconcile() + self.assertFalse(full_line_a.matching_number) + self.assertFalse(full_line_b.matching_number) + self.assertFalse(partial_line_a.matching_number) + self.assertFalse(partial_line_b.matching_number) + + def test_matching_number_unreconcile_multi(self): + currency = self.env.company.currency_id + line_a = self.create_line_for_reconciliation(-500, -500, currency, '2016-01-01') + line_b = self.create_line_for_reconciliation(1000, 1000, currency, '2016-01-01') + line_c = self.create_line_for_reconciliation(-1000, -1000, currency, '2016-01-01') + line_d = self.create_line_for_reconciliation(300, 300, currency, '2016-01-01') + (line_a + line_b).reconcile() + (line_b + line_c).reconcile() + (line_c + line_d).reconcile() + + previous_matching_number = line_a.matching_number + line_a.remove_move_reconcile() + self.assertFalse(line_a.matching_number) + self.assertNotEqual(previous_matching_number, line_b.matching_number) + self.assertEqual(line_b.matching_number, line_c.matching_number) + self.assertEqual(line_b.matching_number, line_d.matching_number) + + previous_matching_number = line_b.matching_number + line_b.remove_move_reconcile() + self.assertFalse(line_b.matching_number) + self.assertNotEqual(previous_matching_number, line_c.matching_number) + self.assertEqual(line_c.matching_number, line_d.matching_number) + + line_c.remove_move_reconcile() + self.assertFalse(line_c.matching_number) + self.assertFalse(line_d.matching_number) + def test_caba_mix_reconciliation(self): """ Test the reconciliation of tax lines (when using a reconcilable tax account) for cases mixing taxes exigible on payment and on invoices. diff --git a/addons/account/views/account_full_reconcile_views.xml b/addons/account/views/account_full_reconcile_views.xml index d96c83359b9..f1760e5d244 100644 --- a/addons/account/views/account_full_reconcile_views.xml +++ b/addons/account/views/account_full_reconcile_views.xml @@ -9,7 +9,7 @@
-

+

diff --git a/addons/account/views/account_move_views.xml b/addons/account/views/account_move_views.xml index e2400c3cf88..565d4f336e5 100644 --- a/addons/account/views/account_move_views.xml +++ b/addons/account/views/account_move_views.xml @@ -360,7 +360,7 @@ - +