diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 7ef39fd7be7..ad7eec8c651 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -788,7 +788,9 @@ class AccountMoveLine(models.Model): total_amount_currency = 0 maxdate = date.min to_balance = {} + cash_basis_partial = self.env['account.partial.reconcile'] for aml in amls: + cash_basis_partial |= aml.move_id.tax_cash_basis_rec_id total_debit += aml.debit total_credit += aml.credit maxdate = max(aml.date, maxdate) @@ -803,15 +805,28 @@ class AccountMoveLine(models.Model): to_balance[aml.currency_id] = [self.env['account.move.line'], 0] to_balance[aml.currency_id][0] += aml to_balance[aml.currency_id][1] += aml.amount_residual != 0 and aml.amount_residual or aml.amount_residual_currency + # Check if reconciliation is total # To check if reconciliation is total we have 3 differents use case: # 1) There are multiple currency different than company currency, in that case we check using debit-credit # 2) We only have one currency which is different than company currency, in that case we check using amount_currency # 3) We have only one currency and some entries that don't have a secundary currency, in that case we check debit-credit # or amount_currency. + # 4) Cash basis full reconciliation + # - either none of the moves are cash basis reconciled, and we proceed + # - or some moves are cash basis reconciled and we make sure they are all fully reconciled + digits_rounding_precision = amls[0].company_id.currency_id.rounding - if (currency and float_is_zero(total_amount_currency, precision_rounding=currency.rounding)) or \ - (multiple_currency and float_compare(total_debit, total_credit, precision_rounding=digits_rounding_precision) == 0): + if ( + ( + not cash_basis_partial or (cash_basis_partial and all([p >= 1.0 for p in amls._get_matched_percentage().values()])) + ) and + ( + currency and float_is_zero(total_amount_currency, precision_rounding=currency.rounding) or + multiple_currency and float_compare(total_debit, total_credit, precision_rounding=digits_rounding_precision) == 0 + ) + ): + exchange_move_id = False # Eventually create a journal entry to book the difference due to foreign currency's exchange rate that fluctuates if to_balance and any([not float_is_zero(residual, precision_rounding=digits_rounding_precision) for aml, residual in to_balance.values()]): @@ -905,7 +920,8 @@ class AccountMoveLine(models.Model): for after_rec_dict in cash_basis_subjected: new_rec = part_rec.create(after_rec_dict) - if cash_basis: + # if the pair belongs to move being reverted, do not create CABA entry + if cash_basis and not (new_rec.debit_move_id + new_rec.credit_move_id).mapped('move_id').mapped('reverse_entry_id'): new_rec.create_tax_cash_basis_entry(cash_basis_percentage_before_rec) self.recompute() diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py index 49a9be865e2..0fc4c2bc7dd 100644 --- a/addons/account/tests/test_reconciliation.py +++ b/addons/account/tests/test_reconciliation.py @@ -6,7 +6,8 @@ import time import unittest -@tagged('post_install', '-at_install') +# TODO in master +# The name of this class should be TestReconciliationHelpers class TestReconciliation(AccountingTestCase): """Tests for reconciliation (account.tax) @@ -167,6 +168,10 @@ class TestReconciliation(AccountingTestCase): supplier_move_lines = bank_stmt.move_line_ids return customer_move_lines, supplier_move_lines + +@tagged('post_install', '-at_install') +class TestReconciliationExec(TestReconciliation): + def test_statement_usd_invoice_eur_transaction_eur(self): customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_euro_id, 30, self.bank_journal_usd, 42, 30, self.currency_euro_id) self.assertRecordValues(customer_move_lines, [ @@ -1911,3 +1916,121 @@ class TestReconciliation(AccountingTestCase): (move_lines - base_amount_tax_lines) .filtered(lambda l: l.account_id == self.tax_final_account) .debit, 17094.66) + + def test_reconciliation_cash_basis_revert(self): + company = self.env.ref('base.main_company') + company.tax_cash_basis_journal_id = self.cash_basis_journal + tax_cash_basis10percent = self.tax_cash_basis.copy({'amount': 10}) + self.tax_waiting_account.reconcile = True + tax_waiting_account10 = self.tax_waiting_account.copy({ + 'name': 'TAX WAIT 10', + 'code': 'TWAIT1', + }) + + AccountMoveLine = self.env['account.move.line'].with_context(check_move_validity=False) + + # Purchase + purchase_move = self.env['account.move'].create({ + 'name': 'invoice', + 'journal_id': self.purchase_journal.id, + }) + + purchase_payable_line0 = AccountMoveLine.create({ + 'account_id': self.account_rsa.id, + 'credit': 175, + 'move_id': purchase_move.id, + }) + + AccountMoveLine.create({ + 'name': 'expenseTaxed 10%', + 'account_id': self.expense_account.id, + 'debit': 50, + 'move_id': purchase_move.id, + 'tax_ids': [(4, tax_cash_basis10percent.id, False)], + }) + tax_line0 = AccountMoveLine.create({ + 'name': 'TaxLine0', + 'account_id': tax_waiting_account10.id, + 'debit': 5, + 'move_id': purchase_move.id, + 'tax_line_id': tax_cash_basis10percent.id, + }) + AccountMoveLine.create({ + 'name': 'expenseTaxed 20%', + 'account_id': self.expense_account.id, + 'debit': 100, + 'move_id': purchase_move.id, + 'tax_ids': [(4, self.tax_cash_basis.id, False)], + }) + tax_line1 = AccountMoveLine.create({ + 'name': 'TaxLine1', + 'account_id': self.tax_waiting_account.id, + 'debit': 20, + 'move_id': purchase_move.id, + 'tax_line_id': self.tax_cash_basis.id, + }) + purchase_move.post() + + reverted = self.env['account.move'].browse(purchase_move.reverse_moves()) + self.assertTrue(reverted.exists()) + + for inv_line in [purchase_payable_line0, tax_line0, tax_line1]: + self.assertTrue(inv_line.full_reconcile_id.exists()) + reverted_expected = reverted.line_ids.filtered(lambda l: l.account_id == inv_line.account_id) + self.assertEqual(len(reverted_expected), 1) + self.assertEqual(reverted_expected.full_reconcile_id, inv_line.full_reconcile_id) + + def test_reconciliation_cash_basis_foreign_currency_low_values(self): + journal = self.env['account.journal'].create({ + 'name': 'Bank', 'type': 'bank', 'code': 'THE', + 'currency_id': self.currency_usd_id, + }) + usd = self.env['res.currency'].browse(self.currency_usd_id) + usd.rate_ids.unlink() + self.env['res.currency.rate'].create({ + 'name': time.strftime('%Y-01-01'), + 'rate': 1/17.0, + 'currency_id': self.currency_usd_id, + 'company_id': self.env.ref('base.main_company').id, + }) + invoice = self.create_invoice( + type='out_invoice', invoice_amount=50, + currency_id=self.currency_usd_id) + invoice.journal_id.update_posted = True + invoice.action_cancel() + invoice.state = 'draft' + invoice.invoice_line_ids.write({ + 'invoice_line_tax_ids': [(6, 0, [self.tax_cash_basis.id])]}) + invoice.compute_taxes() + invoice.action_invoice_open() + + self.assertTrue(invoice.currency_id != self.env.user.company_id.currency_id) + + # First Payment + payment0 = self.make_payment(invoice, journal, invoice.amount_total - 0.01) + self.assertEqual(invoice.residual, 0.01) + + tax_waiting_line = invoice.move_id.line_ids.filtered(lambda l: l.account_id == self.tax_waiting_account) + self.assertFalse(tax_waiting_line.reconciled) + + move_caba0 = tax_waiting_line.matched_debit_ids.debit_move_id.move_id + self.assertTrue(move_caba0.exists()) + self.assertEqual(move_caba0.journal_id, self.env.user.company_id.tax_cash_basis_journal_id) + + pay_receivable_line0 = payment0.move_line_ids.filtered(lambda l: l.account_id == self.account_rcv) + self.assertTrue(pay_receivable_line0.reconciled) + self.assertEqual(pay_receivable_line0.matched_debit_ids, move_caba0.tax_cash_basis_rec_id) + + # Second Payment + payment1 = self.make_payment(invoice, journal, 0.01) + self.assertEqual(invoice.residual, 0) + self.assertEqual(invoice.state, 'paid') + + self.assertTrue(tax_waiting_line.reconciled) + move_caba1 = tax_waiting_line.matched_debit_ids.mapped('debit_move_id').mapped('move_id').filtered(lambda m: m != move_caba0) + self.assertEqual(len(move_caba1.exists()), 1) + self.assertEqual(move_caba1.journal_id, self.env.user.company_id.tax_cash_basis_journal_id) + + pay_receivable_line1 = payment1.move_line_ids.filtered(lambda l: l.account_id == self.account_rcv) + self.assertTrue(pay_receivable_line1.reconciled) + self.assertEqual(pay_receivable_line1.matched_debit_ids, move_caba1.tax_cash_basis_rec_id) diff --git a/addons/mail/models/ir_attachment.py b/addons/mail/models/ir_attachment.py index 527b22409d6..ee51ed8382e 100644 --- a/addons/mail/models/ir_attachment.py +++ b/addons/mail/models/ir_attachment.py @@ -15,19 +15,6 @@ class IrAttachment(models.Model): for record in self: record.register_as_main_attachment(force=False) - @api.multi - def unlink(self): - self.remove_as_main_attachment() - super(IrAttachment, self).unlink() - - @api.multi - def remove_as_main_attachment(self): - for attachment in self: - related_record = self.env[attachment.res_model].browse(attachment.res_id) - if related_record and hasattr(related_record, 'message_main_attachment_id'): - if related_record.message_main_attachment_id == attachment: - related_record.message_main_attachment_id = False - def register_as_main_attachment(self, force=True): """ Registers this attachment as the main one of the model it is attached to.