diff --git a/account_document_reversal/README.rst b/account_document_reversal/README.rst new file mode 100644 index 000000000..34df80297 --- /dev/null +++ b/account_document_reversal/README.rst @@ -0,0 +1,104 @@ +========================= +Account Document Reversal +========================= + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge2| image:: https://img.shields.io/badge/github-OCA%2Faccount--financial--tools-lightgray.png?logo=github + :target: https://github.com/OCA/account-financial-tools/tree/12.0/account_document_reversal + :alt: OCA/account-financial-tools +.. |badge3| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/account-financial-tools-12-0/account-financial-tools-12-0-account_document_reversal + :alt: Translate me on Weblate +.. |badge4| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/92/12.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| + +By Odoo standard, when an account document is cancelled, its journal entry will be deleted completely. +This module enhance the process, instead of deletion, it will create new reversed journal entry. +This will help preserved the accounting history, which is strictly required by some country. + +Following are documented provide this feature, + +- Invoice (account.invoice) +- Payment (acccont.payment) +- Bank Statement (account.bank.statement.line) + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +To use document reversal, setup the document's journal as following, + +- Allow Cancelling = True +- Cancel method = Reversal (create reversed journal entries) + +Usage +===== + +After configure document journal to allow cancel with reversal, it is ready to use. + +- Cancel document as normally do, system will show new cancel wizard +- User can select cancel date and new journal (if different from the document) + which will be used for the reversed journal entry + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Ecosoft + +Contributors +~~~~~~~~~~~~ + +* Kitti Upariphutthiphong +* Jordi Ballester + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-kittiu| image:: https://github.com/kittiu.png?size=40px + :target: https://github.com/kittiu + :alt: kittiu + +Current `maintainer `__: + +|maintainer-kittiu| + +This module is part of the `OCA/account-financial-tools `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/account_document_reversal/__init__.py b/account_document_reversal/__init__.py new file mode 100644 index 000000000..d12862675 --- /dev/null +++ b/account_document_reversal/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) +from . import wizard +from . import models diff --git a/account_document_reversal/__manifest__.py b/account_document_reversal/__manifest__.py new file mode 100644 index 000000000..d1226cf7e --- /dev/null +++ b/account_document_reversal/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +{ + 'name': 'Account Document Reversal', + 'summary': 'Create reversed journal entries when cancel document', + 'version': '12.0.1.0.0', + 'author': 'Ecosoft,' + 'Odoo Community Association (OCA)', + 'website': 'https://github.com/OCA/account-financial-tools', + 'category': 'Accounting & Finance', + 'depends': ['account_cancel'], + 'data': [ + 'wizard/reverse_account_document_wizard.xml', + 'views/account_view.xml', + ], + 'license': 'AGPL-3', + 'installable': True, + 'application': False, + 'development_status': 'beta', + 'maintainers': ['kittiu'], +} diff --git a/account_document_reversal/models/__init__.py b/account_document_reversal/models/__init__.py new file mode 100644 index 000000000..397c86803 --- /dev/null +++ b/account_document_reversal/models/__init__.py @@ -0,0 +1,7 @@ +# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) +from . import account +from . import account_document_reversal +from . import account_invoice +from . import account_payment +from . import account_bank_statement diff --git a/account_document_reversal/models/account.py b/account_document_reversal/models/account.py new file mode 100644 index 000000000..fbd8a1813 --- /dev/null +++ b/account_document_reversal/models/account.py @@ -0,0 +1,24 @@ +# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) +from odoo import models, fields, api + + +class AccountJournal(models.Model): + _inherit = 'account.journal' + + cancel_method = fields.Selection( + [('normal', 'Normal (delete journal entries if exists)'), + ('reversal', 'Reversal (create reversed journal entries)')], + string='Cancel Method', + default='normal', + required=True) + is_cancel_reversal = fields.Boolean( + string='Use Cancel Reversal', + compute='_compute_is_cancel_reversal', + help="True, when journal allow cancel entries with method is reversal") + + @api.multi + def _compute_is_cancel_reversal(self): + for rec in self: + rec.is_cancel_reversal = \ + rec.update_posted and rec.cancel_method == 'reversal' diff --git a/account_document_reversal/models/account_bank_statement.py b/account_document_reversal/models/account_bank_statement.py new file mode 100644 index 000000000..caaff3a95 --- /dev/null +++ b/account_document_reversal/models/account_bank_statement.py @@ -0,0 +1,65 @@ +# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) +from odoo import api, models, _ +from odoo.exceptions import UserError + + +class AccountPayment(models.Model): + _name = 'account.bank.statement.line' + _inherit = ['account.bank.statement.line', 'account.document.reversal'] + + @api.multi + def button_cancel_reconciliation(self): + """ If cancel method is to reverse, use document reversal wizard """ + cancel_reversal = all(self.mapped('journal_entry_ids.move_id.' + 'journal_id.is_cancel_reversal')) + states = self.mapped('statement_id.state') + if cancel_reversal: + if not all(st == 'open' for st in states): + raise UserError( + _('Only new bank statement can be cancelled')) + return self.reverse_document_wizard() + return super().button_cancel_reconciliation() + + @api.multi + def action_document_reversal(self, date=None, journal_id=None): + """ Reverse all moves related to this statement + delete payment """ + # This part is from button_cancel_reconciliation() + aml_to_unbind = self.env['account.move.line'] + aml_to_cancel = self.env['account.move.line'] + payment_to_unreconcile = self.env['account.payment'] + payment_to_cancel = self.env['account.payment'] + for st_line in self: + aml_to_unbind |= st_line.journal_entry_ids + for line in st_line.journal_entry_ids: + payment_to_unreconcile |= line.payment_id + if st_line.move_name and \ + line.payment_id.payment_reference == st_line.move_name: + # there can be several moves linked to a statement line but + # maximum one created by the line itself + aml_to_cancel |= line + payment_to_cancel |= line.payment_id + aml_to_unbind = aml_to_unbind - aml_to_cancel + if aml_to_unbind: + aml_to_unbind.write({'statement_line_id': False}) + payment_to_unreconcile = payment_to_unreconcile - payment_to_cancel + if payment_to_unreconcile: + payment_to_unreconcile.unreconcile() + # -- + + # Set all moves to unreconciled + aml_to_cancel.filtered(lambda x: + x.account_id.reconcile).remove_move_reconcile() + moves = aml_to_cancel.mapped('move_id') + # Important to remove relation with move.line before reverse + aml_to_cancel.write({'payment_id': False, + 'statement_id': False, + 'statement_line_id': False}) + # Create reverse entries + moves.reverse_moves(date, journal_id) + # Delete related payments + if payment_to_cancel: + payment_to_cancel.unlink() + # Unlink from statement line + self.write({'move_name': False}) + return True diff --git a/account_document_reversal/models/account_document_reversal.py b/account_document_reversal/models/account_document_reversal.py new file mode 100644 index 000000000..4e80c7423 --- /dev/null +++ b/account_document_reversal/models/account_document_reversal.py @@ -0,0 +1,26 @@ +# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) +from odoo import models, api + + +class AccountDocumentReversal(models.AbstractModel): + _name = 'account.document.reversal' + _description = 'Abstract Module for Document Reversal' + + @api.model + def reverse_document_wizard(self): + """ Return Wizard to Cancel Document """ + action = self.env.ref('account_document_reversal.' + 'action_view_reverse_account_document') + vals = action.read()[0] + return vals + + @api.multi + def action_document_reversal(self, date=None, journal_id=None): + """ Reverse with following guildeline, + - Check existing document state / raise warning + - Find all related moves and unreconcile + - Create reversed moves + - Set state to cancel + """ + raise NotImplementedError() diff --git a/account_document_reversal/models/account_invoice.py b/account_document_reversal/models/account_invoice.py new file mode 100644 index 000000000..6e7e96341 --- /dev/null +++ b/account_document_reversal/models/account_invoice.py @@ -0,0 +1,52 @@ +# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) +from odoo import api, models, _ +from odoo.exceptions import ValidationError, UserError + + +class AccountInvoice(models.Model): + _name = 'account.invoice' + _inherit = ['account.invoice', 'account.document.reversal'] + + @api.multi + def action_invoice_cancel(self): + """ If cancel method is to reverse, use document reversal wizard + * Draft invoice, fall back to standard invoice cancel + * Non draft, must be fully open (not even partial reconciled) to cancel + """ + cancel_reversal = all(self.mapped('journal_id.is_cancel_reversal')) + states = self.mapped('state') + if cancel_reversal and 'draft' not in states: + if not all(st == 'open' for st in states) or \ + (self.mapped('move_id.line_ids.matched_debit_ids') | + self.mapped('move_id.line_ids.matched_credit_ids')): + raise UserError( + _('Only fully unpaid invoice can be cancelled.\n' + 'To cancel this invoice, make sure all payment(s) ' + 'are also cancelled.')) + return self.reverse_document_wizard() + return super().action_invoice_cancel() + + @api.multi + def action_document_reversal(self, date=None, journal_id=None): + """ Reverse all moves related to this invoice + set state to cancel """ + # Check document state + if 'cancel' in self.mapped('state'): + raise ValidationError( + _('You are trying to cancel the cancelled document')) + MoveLine = self.env['account.move.line'] + move_lines = MoveLine.search([('invoice_id', 'in', self.ids)]) + moves = move_lines.mapped('move_id') + # Set all moves to unreconciled + move_lines.filtered(lambda x: + x.account_id.reconcile).remove_move_reconcile() + # Important to remove relation with move.line before reverse + move_lines.write({'invoice_id': False}) + # Create reverse entries + moves.reverse_moves(date, journal_id) + # Set state cancelled and unlink with account.move + self.write({'move_id': False, + 'move_name': False, + 'reference': False, + 'state': 'cancel'}) + return True diff --git a/account_document_reversal/models/account_payment.py b/account_document_reversal/models/account_payment.py new file mode 100644 index 000000000..bc8d86a45 --- /dev/null +++ b/account_document_reversal/models/account_payment.py @@ -0,0 +1,40 @@ +# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) +from odoo import api, models, _ +from odoo.exceptions import ValidationError + + +class AccountPayment(models.Model): + _name = 'account.payment' + _inherit = ['account.payment', 'account.document.reversal'] + + @api.multi + def cancel(self): + """ If cancel method is to reverse, use document reversal wizard """ + cancel_reversal = all( + self.mapped('move_line_ids.move_id.journal_id.is_cancel_reversal')) + states = self.mapped('state') + if cancel_reversal and 'draft' not in states: + return self.reverse_document_wizard() + return super().cancel() + + @api.multi + def action_document_reversal(self, date=None, journal_id=None): + """ Reverse all moves related to this payment + set state to cancel """ + # Check document state + if 'cancelled' in self.mapped('state'): + raise ValidationError( + _('You are trying to cancel the cancelled document')) + move_lines = self.mapped('move_line_ids') + moves = move_lines.mapped('move_id') + # Set all moves to unreconciled + move_lines.filtered(lambda x: + x.account_id.reconcile).remove_move_reconcile() + # Important to remove relation with move.line before reverse + move_lines.write({'payment_id': False}) + # Create reverse entries + moves.reverse_moves(date, journal_id) + # Set state cancelled and unlink with account.move + self.write({'move_name': False, + 'state': 'cancelled'}) + return True diff --git a/account_document_reversal/readme/CONFIGURE.rst b/account_document_reversal/readme/CONFIGURE.rst new file mode 100644 index 000000000..fabba941f --- /dev/null +++ b/account_document_reversal/readme/CONFIGURE.rst @@ -0,0 +1,4 @@ +To use document reversal, setup the document's journal as following, + +- Allow Cancelling = True +- Cancel method = Reversal (create reversed journal entries) diff --git a/account_document_reversal/readme/CONTRIBUTORS.rst b/account_document_reversal/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..200e1b882 --- /dev/null +++ b/account_document_reversal/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Kitti Upariphutthiphong +* Jordi Ballester diff --git a/account_document_reversal/readme/DESCRIPTION.rst b/account_document_reversal/readme/DESCRIPTION.rst new file mode 100644 index 000000000..e2e724839 --- /dev/null +++ b/account_document_reversal/readme/DESCRIPTION.rst @@ -0,0 +1,9 @@ +By Odoo standard, when an account document is cancelled, its journal entry will be deleted completely. +This module enhance the process, instead of deletion, it will create new reversed journal entry. +This will help preserved the accounting history, which is strictly required by some country. + +Following are documented provide this feature, + +- Invoice (account.invoice) +- Payment (acccont.payment) +- Bank Statement (account.bank.statement.line) diff --git a/account_document_reversal/readme/USAGE.rst b/account_document_reversal/readme/USAGE.rst new file mode 100644 index 000000000..df83b3dee --- /dev/null +++ b/account_document_reversal/readme/USAGE.rst @@ -0,0 +1,5 @@ +After configure document journal to allow cancel with reversal, it is ready to use. + +- Cancel document as normally do, system will show new cancel wizard +- User can select cancel date and new journal (if different from the document) + which will be used for the reversed journal entry diff --git a/account_document_reversal/static/description/index.html b/account_document_reversal/static/description/index.html new file mode 100644 index 000000000..83cb2d34b --- /dev/null +++ b/account_document_reversal/static/description/index.html @@ -0,0 +1,449 @@ + + + + + + +Account Document Reversal + + + +
+

Account Document Reversal

+ + +

License: AGPL-3 OCA/account-financial-tools Translate me on Weblate Try me on Runbot

+

By Odoo standard, when an account document is cancelled, its journal entry will be deleted completely. +This module enhance the process, instead of deletion, it will create new reversed journal entry. +This will help preserved the accounting history, which is strictly required by some country.

+

Following are documented provide this feature,

+
    +
  • Invoice (account.invoice)
  • +
  • Payment (acccont.payment)
  • +
  • Bank Statement (account.bank.statement.line)
  • +
+

Table of contents

+ +
+

Configuration

+

To use document reversal, setup the document’s journal as following,

+
    +
  • Allow Cancelling = True
  • +
  • Cancel method = Reversal (create reversed journal entries)
  • +
+
+
+

Usage

+

After configure document journal to allow cancel with reversal, it is ready to use.

+
    +
  • Cancel document as normally do, system will show new cancel wizard
  • +
  • User can select cancel date and new journal (if different from the document) +which will be used for the reversed journal entry
  • +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Ecosoft
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

kittiu

+

This module is part of the OCA/account-financial-tools project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/account_document_reversal/tests/__init__.py b/account_document_reversal/tests/__init__.py new file mode 100644 index 000000000..675709c36 --- /dev/null +++ b/account_document_reversal/tests/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) +from . import test_invoice_reversal +from . import test_payment_reversal diff --git a/account_document_reversal/tests/test_invoice_reversal.py b/account_document_reversal/tests/test_invoice_reversal.py new file mode 100644 index 000000000..4ae8c162c --- /dev/null +++ b/account_document_reversal/tests/test_invoice_reversal.py @@ -0,0 +1,78 @@ +# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) +from odoo.tests.common import SavepointCase, Form + + +class TestInvoiceReversal(SavepointCase): + + @classmethod + def setUpClass(cls): + super(TestInvoiceReversal, cls).setUpClass() + cls.partner = cls.env['res.partner'].create({'name': 'Test'}) + cls.account_type_receivable = cls.env['account.account.type'].create({ + 'name': 'Test Receivable', + 'type': 'receivable', + }) + cls.account_type_regular = cls.env['account.account.type'].create({ + 'name': 'Test Regular', + 'type': 'other', + }) + cls.account_receivable = cls.env['account.account'].create({ + 'name': 'Test Receivable', + 'code': 'TEST_AR', + 'user_type_id': cls.account_type_receivable.id, + 'reconcile': True, + }) + cls.account_income = cls.env['account.account'].create({ + 'name': 'Test Income', + 'code': 'TEST_IN', + 'user_type_id': cls.account_type_regular.id, + 'reconcile': False, + }) + cls.sale_journal = cls.env['account.journal'].\ + search([('type', '=', 'sale')])[0] + cls.invoice = cls.env['account.invoice'].create({ + 'name': "Test Customer Invoice", + 'journal_id': cls.sale_journal.id, + 'partner_id': cls.partner.id, + 'account_id': cls.account_receivable.id, + }) + cls.invoice_line = cls.env['account.invoice.line'] + cls.invoice_line1 = cls.invoice_line.create({ + 'invoice_id': cls.invoice.id, + 'name': 'Line 1', + 'price_unit': 200.0, + 'account_id': cls.account_income.id, + 'quantity': 1, + }) + + def test_journal_invoice_cancel_reversal(self): + """ Tests cancel with reversal, end result must follow, + - Reversal journal entry is created, and reconciled with original entry + - Status is changed to cancel + """ + # Test journal + self.assertFalse(self.sale_journal.is_cancel_reversal) + self.sale_journal.write({'update_posted': True, + 'cancel_method': 'reversal'}) + # Open invoice + self.invoice.action_invoice_open() + move = self.invoice.move_id + # Click Cancel will open reverse document wizard + res = self.invoice.action_invoice_cancel() + self.assertEqual(res['res_model'], 'reverse.account.document') + # Cancel invoice + ctx = {'active_model': 'account.invoice', + 'active_ids': [self.invoice.id]} + f = Form(self.env[res['res_model']].with_context(ctx)) + cancel_wizard = f.save() + cancel_wizard.action_cancel() + reversed_move = move.reverse_entry_id + move_reconcile = move.mapped('line_ids').mapped('full_reconcile_id') + reversed_move_reconcile = \ + reversed_move.mapped('line_ids').mapped('full_reconcile_id') + # Check + self.assertTrue(move_reconcile) + self.assertTrue(reversed_move_reconcile) + self.assertEqual(move_reconcile, reversed_move_reconcile) + self.assertEqual(self.invoice.state, 'cancel') diff --git a/account_document_reversal/tests/test_payment_reversal.py b/account_document_reversal/tests/test_payment_reversal.py new file mode 100644 index 000000000..f37dd9bba --- /dev/null +++ b/account_document_reversal/tests/test_payment_reversal.py @@ -0,0 +1,343 @@ +# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) +import time +from odoo.tests.common import SavepointCase, Form +from odoo.exceptions import UserError + + +class TestPaymentReversal(SavepointCase): + + @classmethod + def setUpClass(cls): + super(TestPaymentReversal, cls).setUpClass() + # Models + cls.acc_bank_stmt_model = cls.env['account.bank.statement'] + cls.acc_bank_stmt_line_model = cls.env['account.bank.statement.line'] + cls.partner = cls.env['res.partner'].create({'name': 'Test'}) + cls.account_account_type_model = cls.env['account.account.type'] + cls.account_account_model = cls.env['account.account'] + cls.account_journal_model = cls.env['account.journal'] + cls.account_invoice_model = cls.env['account.invoice'] + cls.account_move_line_model = cls.env['account.move.line'] + cls.invoice_line_model = cls.env['account.invoice.line'] + # Records + cls.account_type_bank = cls.account_account_type_model.create({ + 'name': 'Test Bank', + 'type': 'liquidity', + }) + cls.account_type_receivable = cls.account_account_type_model.create({ + 'name': 'Test Receivable', + 'type': 'receivable', + }) + cls.account_type_regular = cls.account_account_type_model.create({ + 'name': 'Test Regular', + 'type': 'other', + }) + cls.account_bank = cls.account_account_model.create({ + 'name': 'Test Bank', + 'code': 'TEST_BANK', + 'user_type_id': cls.account_type_bank.id, + 'reconcile': False, + }) + cls.account_receivable = cls.account_account_model.create({ + 'name': 'Test Receivable', + 'code': 'TEST_AR', + 'user_type_id': cls.account_type_receivable.id, + 'reconcile': True, + }) + cls.account_income = cls.account_account_model.create({ + 'name': 'Test Income', + 'code': 'TEST_IN', + 'user_type_id': cls.account_type_regular.id, + 'reconcile': False, + }) + cls.account_expense = cls.account_account_model.create({ + 'name': 'Test Expense', + 'code': 'TEST_EX', + 'user_type_id': cls.account_type_regular.id, + 'reconcile': False, + }) + cls.bank_journal = cls.account_journal_model.create({ + 'name': 'Test Bank', + 'code': 'TBK', + 'type': 'bank' + }) + cls.sale_journal = cls.account_journal_model.\ + search([('type', '=', 'sale')])[0] + cls.invoice = cls.account_invoice_model.create({ + 'name': "Test Customer Invoice", + 'journal_id': cls.sale_journal.id, + 'partner_id': cls.partner.id, + 'account_id': cls.account_receivable.id, + }) + + cls.invoice_line1 = cls.invoice_line_model.create({ + 'invoice_id': cls.invoice.id, + 'name': 'Line 1', + 'price_unit': 200.0, + 'account_id': cls.account_income.id, + 'quantity': 1, + }) + + def test_payment_cancel_normal(self): + """ Tests that, if I don't use cancel reversal, + I can create an invoice, pay it and then cancel as normal. I expect: + - account move are removed completely + """ + # Test journal with normal cancel + self.bank_journal.write({'update_posted': True, + 'cancel_method': 'normal'}) + # Open invoice + self.invoice.action_invoice_open() + # Pay invoice + self.invoice.pay_and_reconcile(self.bank_journal, 200.0) + payment = self.invoice.payment_ids[0] + payment.cancel() + move_lines = self.env['account.move.line'].\ + search([('payment_id', '=', payment.id)]) + # All account moves are removed completely + self.assertFalse(move_lines) + + def test_payment_cancel_reversal(self): + """ Tests that if I use cancel reversal, I can create an invoice, + pay it and then cancel the payment. I expect: + - Reversal journal entry is created, and reconciled with original entry + - Status of the payment is changed to cancel + - The invoice is not reconciled with the payment anymore + """ + # Test journal + self.bank_journal.write({'update_posted': True, + 'cancel_method': 'reversal'}) + # Open invoice + self.invoice.action_invoice_open() + # Pay invoice + self.invoice.pay_and_reconcile(self.bank_journal, 200.0) + payment = self.invoice.payment_ids[0] + move = self.env['account.move.line'].search( + [('payment_id', '=', payment.id)], limit=1).move_id + res = payment.cancel() + # Cancel payment + ctx = {'active_model': 'account.payment', + 'active_ids': [payment.id]} + f = Form(self.env[res['res_model']].with_context(ctx)) + self.assertEqual(res['res_model'], 'reverse.account.document') + cancel_wizard = f.save() + cancel_wizard.action_cancel() + payment_moves = self.env['account.move.line'].search( + [('payment_id', '=', payment.id)]) + self.assertFalse(payment_moves) + reversed_move = move.reverse_entry_id + move_reconcile = move.mapped('line_ids').mapped('full_reconcile_id') + reversed_move_reconcile = \ + reversed_move.mapped('line_ids').mapped('full_reconcile_id') + # Check + self.assertTrue(move_reconcile) + self.assertTrue(reversed_move_reconcile) + self.assertEqual(move_reconcile, reversed_move_reconcile) + self.assertEqual(payment.state, 'cancelled') + self.assertEqual(self.invoice.state, 'open') + + def test_bank_statement_cancel_normal(self): + """ Tests that, if I don't use cancel reversal, + I can create an invoice, pay it via a bank statement + line and then cancel the bank statement line as normal. I expect: + - account move are removed completely + """ + # Test journal with normal cancel + self.bank_journal.write({'update_posted': True, + 'cancel_method': 'normal'}) + # Open invoice + self.invoice.action_invoice_open() + bank_stmt = self.acc_bank_stmt_model.create({ + 'journal_id': self.bank_journal.id, + 'date': time.strftime('%Y') + '-07-15', + 'name': 'payment' + self.invoice.name + }) + bank_stmt_line = self.acc_bank_stmt_line_model.create( + {'name': 'payment', + 'statement_id': bank_stmt.id, + 'partner_id': self.partner.id, + 'amount': 200, + 'date': time.strftime('%Y') + '-07-15', }) + line_id = self.account_move_line_model + # reconcile the payment with the invoice + for l in self.invoice.move_id.line_ids: + if l.account_id.id == self.account_receivable.id: + line_id = l + break + bank_stmt_line.process_reconciliation(counterpart_aml_dicts=[{ + 'move_line': line_id, + 'account_id': self.account_income.id, + 'debit': 0.0, + 'credit': 200.0, + 'name': 'test_reconciliation', + }]) + self.assertTrue(bank_stmt_line.journal_entry_ids) + original_move_lines = bank_stmt_line.journal_entry_ids + self.assertTrue(original_move_lines.mapped('statement_id')) + # Cancel the statement line + bank_stmt_line.button_cancel_reconciliation() + move_lines = self.env['account.move.line'].\ + search([('statement_id', '=', bank_stmt.id)]) + # All account moves are removed completely + self.assertFalse(move_lines) + + def test_bank_statement_cancel_reversal_01(self): + """ Tests that I can create an invoice, pay it via a bank statement + line and then reverse the bank statement line. I expect: + - Reversal journal entry is created, and reconciled with original entry + - Payment is deleted + - The invoice is not reconciled with the payment anymore + - The line in the statement is ready to reconcile again + """ + # Test journal + self.bank_journal.write({'update_posted': True, + 'cancel_method': 'reversal'}) + # Open invoice + self.invoice.action_invoice_open() + bank_stmt = self.acc_bank_stmt_model.create({ + 'journal_id': self.bank_journal.id, + 'date': time.strftime('%Y') + '-07-15', + 'name': 'payment' + self.invoice.name + }) + + bank_stmt_line = self.acc_bank_stmt_line_model.create( + {'name': 'payment', + 'statement_id': bank_stmt.id, + 'partner_id': self.partner.id, + 'amount': 200, + 'date': time.strftime('%Y') + '-07-15', }) + line_id = self.account_move_line_model + # reconcile the payment with the invoice + for l in self.invoice.move_id.line_ids: + if l.account_id.id == self.account_receivable.id: + line_id = l + break + bank_stmt_line.process_reconciliation(counterpart_aml_dicts=[{ + 'move_line': line_id, + 'account_id': self.account_income.id, + 'debit': 0.0, + 'credit': 200.0, + 'name': 'test_reconciliation', + }]) + self.assertTrue(bank_stmt_line.journal_entry_ids) + original_move_lines = bank_stmt_line.journal_entry_ids + original_payment_id = original_move_lines.mapped('payment_id').id + self.assertTrue(original_move_lines.mapped('statement_id')) + # Cancel the statement line + res = bank_stmt_line.button_cancel_reconciliation() + ctx = {'active_model': 'account.bank.statement.line', + 'active_ids': [bank_stmt_line.id]} + f = Form(self.env[res['res_model']].with_context(ctx)) + self.assertEqual(res['res_model'], 'reverse.account.document') + cancel_wizard = f.save() + cancel_wizard.action_cancel() + self.assertFalse(bank_stmt_line.journal_entry_ids) + payment = self.env['account.payment'].search( + [('id', '=', original_payment_id)], + limit=1) + self.assertFalse(payment) + self.assertFalse(original_move_lines.mapped('statement_id')) + move = original_move_lines[0].move_id + reversed_move = move.reverse_entry_id + move_reconcile = move.mapped('line_ids').mapped('full_reconcile_id') + reversed_move_reconcile = \ + reversed_move.mapped('line_ids').mapped('full_reconcile_id') + # Check + self.assertTrue(move_reconcile) + self.assertTrue(reversed_move_reconcile) + self.assertEqual(move_reconcile, reversed_move_reconcile) + + def test_bank_statement_cancel_reversal_02(self): + """ Tests that I can create a bank statement line and reconcile it + to an expense account, and then reverse the reconciliation of the + statement line. I expect: + - Reversal journal entry is created, and reconciled with original entry + - Payment is deleted + - The line in the statement is ready to reconcile again + """ + # Test journal + self.bank_journal.write({'update_posted': True, + 'cancel_method': 'reversal'}) + # Create a bank statement + bank_stmt = self.acc_bank_stmt_model.create({ + 'journal_id': self.bank_journal.id, + 'date': time.strftime('%Y') + '-07-15', + 'name': 'payment' + self.invoice.name + }) + bank_stmt_line = self.acc_bank_stmt_line_model.create( + {'name': 'payment', + 'statement_id': bank_stmt.id, + 'partner_id': self.partner.id, + 'amount': 200, + 'date': time.strftime('%Y') + '-07-15', }) + line_id = self.account_move_line_model + + bank_stmt_line.process_reconciliation(new_aml_dicts=[{ + 'move_line': line_id, + 'account_id': self.account_expense.id, + 'debit': 200.0, + 'name': 'test_expense_reconciliation', + }]) + self.assertTrue(bank_stmt_line.journal_entry_ids) + original_move_lines = bank_stmt_line.journal_entry_ids + original_payment_id = original_move_lines.mapped('payment_id').id + self.assertTrue(original_move_lines.mapped('statement_id')) + # Cancel the statement line + res = bank_stmt_line.button_cancel_reconciliation() + ctx = {'active_model': 'account.bank.statement.line', + 'active_ids': [bank_stmt_line.id]} + f = Form(self.env[res['res_model']].with_context(ctx)) + self.assertEqual(res['res_model'], 'reverse.account.document') + cancel_wizard = f.save() + cancel_wizard.action_cancel() + self.assertFalse(bank_stmt_line.journal_entry_ids) + payment = self.env['account.payment'].search( + [('id', '=', original_payment_id)], + limit=1) + self.assertFalse(payment) + self.assertFalse(original_move_lines.mapped('statement_id')) + move = original_move_lines[0].move_id + reversed_move = move.reverse_entry_id + move_reconcile = move.mapped('line_ids').mapped('full_reconcile_id') + reversed_move_reconcile = \ + reversed_move.mapped('line_ids').mapped('full_reconcile_id') + # Check + self.assertTrue(move_reconcile) + self.assertTrue(reversed_move_reconcile) + self.assertEqual(move_reconcile, reversed_move_reconcile) + + def test_bank_statement_cancel_exception(self): + """ Tests on exception case, if statement is already validated, but + user cancel statement line. I expect: + - UserError will show + """ + # Test journal + self.bank_journal.write({'update_posted': True, + 'cancel_method': 'reversal'}) + # Create a bank statement + bank_stmt = self.acc_bank_stmt_model.create({ + 'journal_id': self.bank_journal.id, + 'date': time.strftime('%Y') + '-07-15', + 'name': 'payment' + self.invoice.name + }) + bank_stmt_line = self.acc_bank_stmt_line_model.create( + {'name': 'payment', + 'statement_id': bank_stmt.id, + 'partner_id': self.partner.id, + 'amount': 200, + 'date': time.strftime('%Y') + '-07-15', }) + line_id = self.account_move_line_model + + bank_stmt_line.process_reconciliation(new_aml_dicts=[{ + 'move_line': line_id, + 'account_id': self.account_expense.id, + 'debit': 200.0, + 'name': 'test_expense_reconciliation', + }]) + + bank_stmt.balance_end_real = 200.00 + bank_stmt.check_confirm_bank() + self.assertEqual(bank_stmt.state, 'confirm') + with self.assertRaises(UserError): + bank_stmt_line.button_cancel_reconciliation() diff --git a/account_document_reversal/views/account_view.xml b/account_document_reversal/views/account_view.xml new file mode 100644 index 000000000..3f2f7d6b1 --- /dev/null +++ b/account_document_reversal/views/account_view.xml @@ -0,0 +1,18 @@ + + + + + + account.journal.form + account.journal + + + + + + + + + + diff --git a/account_document_reversal/wizard/__init__.py b/account_document_reversal/wizard/__init__.py new file mode 100644 index 000000000..ffa532b7d --- /dev/null +++ b/account_document_reversal/wizard/__init__.py @@ -0,0 +1,3 @@ +# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) +from . import reverse_account_document diff --git a/account_document_reversal/wizard/reverse_account_document.py b/account_document_reversal/wizard/reverse_account_document.py new file mode 100644 index 000000000..85334374a --- /dev/null +++ b/account_document_reversal/wizard/reverse_account_document.py @@ -0,0 +1,28 @@ +# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) +from odoo import models, fields, api + + +class ReverseAccountDocument(models.TransientModel): + """ + Document reversal wizard, it cancel by reverse document journal entries + """ + _name = 'reverse.account.document' + _description = 'Account Document Reversal' + + date = fields.Date( + string='Reversal date', + default=fields.Date.context_today, + required=True) + journal_id = fields.Many2one( + 'account.journal', + string='Use Specific Journal', + help='If empty, uses the journal of the journal entry to be reversed.') + + @api.multi + def action_cancel(self): + model = self._context.get('active_model') + active_ids = self._context.get('active_ids') + documents = self.env[model].browse(active_ids) + documents.action_document_reversal(self.date, self.journal_id) + return {'type': 'ir.actions.act_window_close'} diff --git a/account_document_reversal/wizard/reverse_account_document_wizard.xml b/account_document_reversal/wizard/reverse_account_document_wizard.xml new file mode 100644 index 000000000..146140c27 --- /dev/null +++ b/account_document_reversal/wizard/reverse_account_document_wizard.xml @@ -0,0 +1,34 @@ + + + + + reverse.account.document.form + reverse.account.document + +
+ + + + + + + + +
+
+
+
+
+ + + Document Cancel + reverse.account.document + form + tree,form + + new + +
+