diff --git a/addons/account/models/__init__.py b/addons/account/models/__init__.py index a25c1272e76..6f178fd2665 100644 --- a/addons/account/models/__init__.py +++ b/addons/account/models/__init__.py @@ -19,3 +19,4 @@ from . import account_cash_rounding from . import reconciliation_widget from . import account_incoterms from . import digest +from . import res_users diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 103d1421e20..f2a5e0d785b 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -103,6 +103,11 @@ class AccountInvoice(models.Model): if invoice_type == 'out_invoice' and self.env['ir.config_parameter'].sudo().get_param('account.use_invoice_terms'): return self.env.user.company_id.invoice_terms + def _get_aml_for_amount_residual(self): + """ Get the aml to consider to compute the amount residual of invoices """ + self.ensure_one() + return self.sudo().move_id.line_ids.filtered(lambda l: l.account_id == self.account_id) + @api.one @api.depends( 'state', 'currency_id', 'invoice_line_ids.price_subtotal', @@ -112,14 +117,13 @@ class AccountInvoice(models.Model): residual = 0.0 residual_company_signed = 0.0 sign = self.type in ['in_refund', 'out_refund'] and -1 or 1 - for line in self.sudo().move_id.line_ids: - if line.account_id == self.account_id: - residual_company_signed += line.amount_residual - if line.currency_id == self.currency_id: - residual += line.amount_residual_currency if line.currency_id else line.amount_residual - else: - from_currency = line.currency_id or line.company_id.currency_id - residual += from_currency._convert(line.amount_residual, self.currency_id, line.company_id, line.date or fields.Date.today()) + for line in self._get_aml_for_amount_residual(): + residual_company_signed += line.amount_residual + if line.currency_id == self.currency_id: + residual += line.amount_residual_currency if line.currency_id else line.amount_residual + else: + from_currency = line.currency_id or line.company_id.currency_id + residual += from_currency._convert(line.amount_residual, self.currency_id, line.company_id, line.date or fields.Date.today()) self.residual_company_signed = abs(residual_company_signed) * sign self.residual_signed = abs(residual) * sign self.residual = abs(residual) @@ -1114,12 +1118,18 @@ class AccountInvoice(models.Model): tax_grouped[key][field] += round_curr(val.get(field) or 0) return tax_grouped + @api.multi + def _get_aml_for_register_payment(self): + """ Get the aml to consider to reconcile in register payment """ + self.ensure_one() + return self.move_id.line_ids.filtered(lambda r: not r.reconciled and r.account_id.internal_type in ('payable', 'receivable')) + @api.multi def register_payment(self, payment_line, writeoff_acc_id=False, writeoff_journal_id=False): """ Reconcile payable/receivable lines from the invoice with payment_line """ line_to_reconcile = self.env['account.move.line'] for inv in self: - line_to_reconcile += inv.move_id.line_ids.filtered(lambda r: not r.reconciled and r.account_id.internal_type in ('payable', 'receivable')) + line_to_reconcile += inv._get_aml_for_register_payment() return (line_to_reconcile + payment_line).reconcile(writeoff_acc_id, writeoff_journal_id) @api.multi diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 7c9884fc69f..d634186ffde 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -817,7 +817,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) @@ -832,15 +834,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()]): @@ -934,7 +949,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/models/res_users.py b/addons/account/models/res_users.py new file mode 100644 index 00000000000..3e6fd163482 --- /dev/null +++ b/addons/account/models/res_users.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, models, _ +from odoo.exceptions import ValidationError + + +class Users(models.Model): + _inherit = "res.users" + + @api.multi + @api.constrains('groups_id') + def _check_one_user_type(self): + super(Users, self)._check_one_user_type() + for user in self: + if (user.user_has_groups('account.group_show_line_subtotals_tax_included') and + user.user_has_groups('account.group_show_line_subtotals_tax_excluded')): + raise ValidationError(_('A user cannot have both Tax B2B and Tax B2C')) diff --git a/addons/account/security/account_security.xml b/addons/account/security/account_security.xml index 5e5a63f120f..bad495b9290 100644 --- a/addons/account/security/account_security.xml +++ b/addons/account/security/account_security.xml @@ -25,18 +25,6 @@ - - - - - - - - - - - - Show Full Accounting Features @@ -80,6 +68,16 @@ + + + + + + + + + + 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/account/views/account_move_views.xml b/addons/account/views/account_move_views.xml index 641596bd7b6..69956a6f349 100644 --- a/addons/account/views/account_move_views.xml +++ b/addons/account/views/account_move_views.xml @@ -36,6 +36,7 @@ + @@ -43,7 +44,7 @@ - +