diff --git a/account_permanent_lock_move/README.rst b/account_permanent_lock_move/README.rst new file mode 100644 index 000000000..ec51d4025 --- /dev/null +++ b/account_permanent_lock_move/README.rst @@ -0,0 +1,60 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +=================== +Permanent Lock Move +=================== + +This module extends the functionality of the basic "lock date" +functionality in invoicing, in order to add a more definitive +lock date (which cannot be empty or set in the past, and only +accepts unposted moves) + +Configuration +============= + +To configure this module, you need to: + +#. Go to Invoicing -> Settings +#. Set the Permanent Lock Date. + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/92/9.0 + +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. + +Credits +======= + +Images +------ + +* Odoo Community Association: `Icon `_. + +Contributors +------------ + +* Matthieu Dietrich + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +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. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/account_permanent_lock_move/__init__.py b/account_permanent_lock_move/__init__.py new file mode 100755 index 000000000..35e7c9600 --- /dev/null +++ b/account_permanent_lock_move/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import models +from . import wizard diff --git a/account_permanent_lock_move/__openerp__.py b/account_permanent_lock_move/__openerp__.py new file mode 100644 index 000000000..c18f0fc46 --- /dev/null +++ b/account_permanent_lock_move/__openerp__.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# © 2016 Camptocamp SA (Matthieu Dietrich) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +{ + "name": "Permanent Lock Move", + "version": "9.0.1.0.0", + "depends": ["account"], + "author": "Camptocamp,Odoo Community Association (OCA)", + "website": "http://www.camptocamp.com/", + "category": "Finance", + "data": [ + "views/res_config_view.xml", + "wizard/permanent_lock_date_wizard.xml", + ], + 'license': 'AGPL-3', + "auto_install": False, + 'installable': True, +} diff --git a/account_permanent_lock_move/models/__init__.py b/account_permanent_lock_move/models/__init__.py new file mode 100644 index 000000000..8993bef01 --- /dev/null +++ b/account_permanent_lock_move/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +from . import account_move +from . import company +from . import res_config diff --git a/account_permanent_lock_move/models/account_move.py b/account_permanent_lock_move/models/account_move.py new file mode 100644 index 000000000..5d9c9ba7c --- /dev/null +++ b/account_permanent_lock_move/models/account_move.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# © 2016 Camptocamp SA (Matthieu Dietrich) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from openerp import _, api, models +from openerp.exceptions import UserError + + +class AccountMove(models.Model): + _inherit = "account.move" + + @api.multi + def _check_lock_date(self): + for move in self: + if move.date <= move.company_id.permanent_lock_date: + raise UserError(_( + "You cannot add/modify entries prior to and inclusive " + "of the permanent lock date.")) + return super(AccountMove, self)._check_lock_date() + + @api.multi + def button_cancel(self): + # Add check for button_cancel, as it does not use ORM + self._check_lock_date() + return super(AccountMove, self).button_cancel() + + @api.model + def create(self, vals): + # Add _check_lock_date for create of account.move, + # as it is not done by default + result = super(AccountMove, self).create(vals) + result._check_lock_date() + return result + + @api.multi + def write(self, vals): + # Add _check_lock_date for write of account.move, + # as it is not done by default + self._check_lock_date() + result = super(AccountMove, self).write(vals) + self._check_lock_date() + return result diff --git a/account_permanent_lock_move/models/company.py b/account_permanent_lock_move/models/company.py new file mode 100644 index 000000000..5e4bfbb63 --- /dev/null +++ b/account_permanent_lock_move/models/company.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# © 2016 Camptocamp SA (Matthieu Dietrich) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from openerp import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + permanent_lock_date = fields.Date( + string="Permanent Lock Date", + help="Non-revertible closing of accounts prior to and inclusive of " + "this date. Use it for fiscal year locking instead of ""Lock Date"".") diff --git a/account_permanent_lock_move/models/res_config.py b/account_permanent_lock_move/models/res_config.py new file mode 100644 index 000000000..1e312fff8 --- /dev/null +++ b/account_permanent_lock_move/models/res_config.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# © 2016 Camptocamp SA (Matthieu Dietrich) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from openerp import api, fields, models + + +class AccountConfigSettings(models.TransientModel): + _inherit = 'account.config.settings' + + permanent_lock_date = fields.Date( + string="Permanent Lock Date", + related='company_id.permanent_lock_date', + help='Non-revertible closing of accounts prior to and inclusive of ' + 'this date. Use it for fiscal year locking instead of "Lock Date".') + + @api.multi + def change_permanent_lock_date(self): + wizard = self.env['permanent.lock.date.wizard'].create({ + 'company_id': self.company_id.id + }) + return { + 'type': 'ir.actions.act_window', + 'res_model': 'permanent.lock.date.wizard', + 'view_mode': 'form', + 'view_type': 'form', + 'res_id': wizard.id, + 'target': 'new', + } diff --git a/account_permanent_lock_move/tests/__init__.py b/account_permanent_lock_move/tests/__init__.py new file mode 100644 index 000000000..117d3ecd2 --- /dev/null +++ b/account_permanent_lock_move/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# © 2016 Camptocamp SA (Matthieu Dietrich) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import test_permanent_lock diff --git a/account_permanent_lock_move/tests/test_permanent_lock.py b/account_permanent_lock_move/tests/test_permanent_lock.py new file mode 100644 index 000000000..63c69c9c4 --- /dev/null +++ b/account_permanent_lock_move/tests/test_permanent_lock.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +# © 2016 Camptocamp SA (Matthieu Dietrich) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import datetime +from openerp import fields, tools +from openerp.modules import get_module_resource +from openerp.tests import common +from openerp.exceptions import UserError + + +class PermanentLock(common.TransactionCase): + + def setUp(self): + super(PermanentLock, self).setUp() + tools.convert_file(self.cr, 'account', + get_module_resource('account', 'test', + 'account_minimal_test.xml'), + {}, 'init', False, 'test') + self.account_move_obj = self.env["account.move"] + self.account_move_line_obj = \ + self.env["account.move.line"] + self.company_id = self.ref('base.main_company') + self.partner = self.browse_ref("base.res_partner_12") + self.account_id = self.ref("account.a_recv") + self.account_id2 = self.ref("account.a_expense") + self.journal_id = self.ref("account.bank_journal") + self.wizard_obj = self.env["permanent.lock.date.wizard"] + + def test_name_completion(self): + """Test complete partner_id from statement line label + Test the automatic completion of the partner_id based if the name of + the partner appears in the statement line label + """ + + # Create a move + self.move = self.account_move_obj.create({ + 'date': fields.Date.today(), + 'journal_id': self.journal_id, + 'line_ids': [(0, 0, { + 'account_id': self.account_id, + 'credit': 1000.0, + 'name': 'Credit line', + }), (0, 0, { + 'account_id': self.account_id2, + 'debit': 1000.0, + 'name': 'Debit line', + })] + }) + + # Call lock wizard on entry + raised_lock_error = False + try: + self.wizard = self.wizard_obj.create({ + 'company_id': self.company_id, + 'lock_date': fields.Date.today() + }) + self.wizard.confirm_date() + except UserError as ue: + if 'entries are still unposted' in ue.name: + raised_lock_error = True + + self.assertTrue(raised_lock_error, + "Permanent lock done even with unposted entry.") + + # Post entry and lock + self.move.post() + + moves = self.env['account.move'].search( + [('company_id', '=', self.company_id), + ('date', '<=', fields.Date.today()), + ('state', '=', 'draft')]) + moves.post() + + self.wizard = self.wizard_obj.create({ + 'company_id': self.company_id, + 'lock_date': fields.Date.today() + }) + self.wizard.confirm_date() + + # Try to lock the day before + raised_lock_error = False + try: + yesterday = fields.Date.to_string( + fields.Date.from_string( + fields.Date.today()) + + datetime.timedelta(days=-1)) + self.wizard = self.wizard_obj.create({ + 'company_id': self.company_id, + 'lock_date': yesterday + }) + self.wizard.confirm_date() + except UserError as ue: + if 'permanent lock date in the past' in ue.name: + raised_lock_error = True + + self.assertTrue(raised_lock_error, + "Permanent lock set the day before.") + + # Test that the move cannot be created, written, or cancelled + raised_create_error = False + raised_write_error = False + raised_cancel_error = False + try: + self.move2 = self.account_move_obj.create({ + 'date': fields.Date.today(), + 'journal_id': self.journal_id, + 'line_ids': [(0, 0, { + 'account_id': self.account_id, + 'credit': 1000.0, + 'name': 'Credit line', + }), (0, 0, { + 'account_id': self.account_id2, + 'debit': 1000.0, + 'name': 'Debit line', + })] + }) + except UserError as ue: + if 'permanent lock date' in ue.name: + raised_create_error = True + + self.assertTrue(raised_create_error, + "Journal Entry could be created after locking!") + + try: + self.move.write({'name': 'TEST'}) + except UserError as ue: + if 'permanent lock date' in ue.name: + raised_write_error = True + + self.assertTrue(raised_write_error, + "Journal Entry could be modified after locking!") + + try: + self.move.button_cancel() + except UserError as ue: + if 'permanent lock date' in ue.name: + raised_cancel_error = True + + self.assertTrue(raised_cancel_error, + "Journal Entry could be cancelled after locking!") diff --git a/account_permanent_lock_move/views/res_config_view.xml b/account_permanent_lock_move/views/res_config_view.xml new file mode 100644 index 000000000..acc77dd5f --- /dev/null +++ b/account_permanent_lock_move/views/res_config_view.xml @@ -0,0 +1,18 @@ + + + account settings permanent lock + account.config.settings + + + +
+
+
+
+
+
+
+
diff --git a/account_permanent_lock_move/wizard/__init__.py b/account_permanent_lock_move/wizard/__init__.py new file mode 100644 index 000000000..def3c56f1 --- /dev/null +++ b/account_permanent_lock_move/wizard/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import permanent_lock_date_wizard diff --git a/account_permanent_lock_move/wizard/permanent_lock_date_wizard.py b/account_permanent_lock_move/wizard/permanent_lock_date_wizard.py new file mode 100644 index 000000000..d9de74109 --- /dev/null +++ b/account_permanent_lock_move/wizard/permanent_lock_date_wizard.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# © 2016 Camptocamp SA (Matthieu Dietrich) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from openerp import _, api, fields, models +from openerp.exceptions import UserError + + +class PermanentLockDateWizard(models.TransientModel): + _name = 'permanent.lock.date.wizard' + + lock_date = fields.Date(string="Lock Date") + company_id = fields.Many2one(comodel_name='res.company', + string='Company') + + @api.multi + def confirm_date(self): + self.ensure_one() + company = self.company_id + if (company.permanent_lock_date and + self.lock_date < company.permanent_lock_date): + raise UserError( + _("You cannot set the permanent lock date in the past.") + ) + # Then check if unposted moves are present before the date + moves = self.env['account.move'].search( + [('company_id', '=', company.id), + ('date', '<=', self.lock_date), + ('state', '=', 'draft')]) + if moves: + raise UserError( + _("You cannot set the permanent lock date since entries are " + "still unposted before this date.") + ) + + company.permanent_lock_date = self.lock_date + return {'type': 'ir.actions.act_window_close'} diff --git a/account_permanent_lock_move/wizard/permanent_lock_date_wizard.xml b/account_permanent_lock_move/wizard/permanent_lock_date_wizard.xml new file mode 100644 index 000000000..0ffbe0046 --- /dev/null +++ b/account_permanent_lock_move/wizard/permanent_lock_date_wizard.xml @@ -0,0 +1,19 @@ + + + permanent.lock.date.wizard.form + permanent.lock.date.wizard + form + +
+
+
+