diff --git a/contract/__manifest__.py b/contract/__manifest__.py index 68f343c25..41400aacd 100644 --- a/contract/__manifest__.py +++ b/contract/__manifest__.py @@ -20,9 +20,11 @@ 'depends': ['base', 'account', 'product'], "external_dependencies": {"python": ["dateutil"]}, 'data': [ + 'security/groups.xml', 'security/contract_tag.xml', 'security/ir.model.access.csv', 'security/contract_security.xml', + 'security/contract_resiliate_reason.xml', 'report/report_contract.xml', 'report/contract_views.xml', 'data/contract_cron.xml', @@ -30,6 +32,7 @@ 'data/mail_template.xml', 'wizards/contract_line_wizard.xml', 'wizards/contract_manually_create_invoice.xml', + 'wizards/contract_contract_resiliate.xml', 'views/abstract_contract_line.xml', 'views/contract.xml', 'views/contract_line.xml', @@ -37,6 +40,7 @@ 'views/contract_template_line.xml', 'views/res_partner_view.xml', 'views/res_config_settings.xml', + 'views/contract_resiliate_reason.xml', ], 'installable': True, } diff --git a/contract/models/__init__.py b/contract/models/__init__.py index 6d46b4b61..c09f4495a 100644 --- a/contract/models/__init__.py +++ b/contract/models/__init__.py @@ -12,3 +12,4 @@ from . import res_partner from . import contract_tag from . import res_company from . import res_config_settings +from . import contract_resiliate_reason diff --git a/contract/models/contract.py b/contract/models/contract.py index bed38d0bb..8df4880da 100644 --- a/contract/models/contract.py +++ b/contract/models/contract.py @@ -7,7 +7,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import api, fields, models -from odoo.exceptions import ValidationError +from odoo.exceptions import ValidationError, UserError from odoo.tools.translate import _ @@ -93,6 +93,15 @@ class ContractContract(models.Model): ) tag_ids = fields.Many2many(comodel_name="contract.tag", string="Tags") note = fields.Text(string="Notes") + is_resiliated = fields.Boolean(string="Resiliated", readonly=True) + resiliate_reason_id = fields.Many2one( + comodel_name="contract.resiliate.reason", + string="Resiliate Reason", + ondelete="restrict", + readonly=True + ) + resiliate_comment = fields.Text(string="Resiliate Comment", readonly=True) + resiliate_date = fields.Date(string="Resiliate Date", readonly=True) @api.multi def _inverse_partner_id(self): @@ -458,3 +467,43 @@ class ContractContract(models.Model): domain = self._get_contracts_to_invoice_domain(date_ref) contracts_to_invoice = self.search(domain) return contracts_to_invoice._recurring_create_invoice(date_ref) + + @api.multi + def action_resiliate_contract(self): + self.ensure_one() + context = {"default_contract_id": self.id} + return { + 'type': 'ir.actions.act_window', + 'name': _('Resiliate Contract'), + 'res_model': 'contract.contract.resiliate', + 'view_type': 'form', + 'view_mode': 'form', + 'target': 'new', + 'context': context, + } + + @api.multi + def _resiliate_contract( + self, resiliate_reason_id, resiliate_comment, resiliate_date + ): + self.ensure_one() + if not self.env.user.has_group("contract.can_resiliate_contract"): + raise UserError(_('You are not allowed to resiliate contracts.')) + self.contract_line_ids.filtered('is_stop_allowed').stop(resiliate_date) + self.write({ + 'is_resiliated': True, + 'resiliate_reason_id': resiliate_reason_id.id, + 'resiliate_comment': resiliate_comment, + 'resiliate_date': resiliate_date, + }) + return True + + @api.multi + def action_cancel_contract_resiliation(self): + self.ensure_one() + self.write({ + 'is_resiliated': False, + 'resiliate_reason_id': False, + 'resiliate_comment': False, + 'resiliate_date': False, + }) diff --git a/contract/models/contract_line.py b/contract/models/contract_line.py index 81c09e05e..34c156f46 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -296,9 +296,19 @@ class ContractLine(models.Model): 'successor_contract_line_id', 'predecessor_contract_line_id', 'is_canceled', + 'contract_id.is_resiliated', ) def _compute_allowed(self): for rec in self: + if rec.contract_id.is_resiliated: + rec.update({ + 'is_plan_successor_allowed': False, + 'is_stop_plan_successor_allowed': False, + 'is_stop_allowed': False, + 'is_cancel_allowed': False, + 'is_un_cancel_allowed': False, + }) + continue if rec.date_start: allowed = get_allowed( rec.date_start, @@ -310,13 +320,14 @@ class ContractLine(models.Model): rec.is_canceled, ) if allowed: - rec.is_plan_successor_allowed = allowed.plan_successor - rec.is_stop_plan_successor_allowed = ( - allowed.stop_plan_successor - ) - rec.is_stop_allowed = allowed.stop - rec.is_cancel_allowed = allowed.cancel - rec.is_un_cancel_allowed = allowed.uncancel + rec.update({ + 'is_plan_successor_allowed': allowed.plan_successor, + 'is_stop_plan_successor_allowed': + allowed.stop_plan_successor, + 'is_stop_allowed': allowed.stop, + 'is_cancel_allowed': allowed.cancel, + 'is_un_cancel_allowed': allowed.uncancel, + }) @api.constrains('is_auto_renew', 'successor_contract_line_id', 'date_end') def _check_allowed(self): @@ -1251,6 +1262,7 @@ class ContractLine(models.Model): @api.model def _contract_line_to_renew_domain(self): return [ + ('contract_id.is_resiliated', '=', False), ('is_auto_renew', '=', True), ('is_canceled', '=', False), ('termination_notice_date', '<=', fields.Date.context_today(self)), diff --git a/contract/models/contract_resiliate_reason.py b/contract/models/contract_resiliate_reason.py new file mode 100644 index 000000000..f33a0b2f6 --- /dev/null +++ b/contract/models/contract_resiliate_reason.py @@ -0,0 +1,12 @@ +# Copyright 2020 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ContractResiliateReason(models.Model): + + _name = 'contract.resiliate.reason' + _description = 'Contract Resiliation Reason' + + name = fields.Char(required=True) diff --git a/contract/security/contract_resiliate_reason.xml b/contract/security/contract_resiliate_reason.xml new file mode 100644 index 000000000..d5fe5e4d2 --- /dev/null +++ b/contract/security/contract_resiliate_reason.xml @@ -0,0 +1,27 @@ + + + + + + + contract.resiliate.reason access manager + + + + + + + + + + contract.resiliate.reason access user + + + + + + + + + diff --git a/contract/security/groups.xml b/contract/security/groups.xml new file mode 100644 index 000000000..638e0ab05 --- /dev/null +++ b/contract/security/groups.xml @@ -0,0 +1,12 @@ + + + + + + + Contract: Can Resiliate Contracts + + + + diff --git a/contract/tests/test_contract.py b/contract/tests/test_contract.py index 9f613de8c..df5b9c86d 100644 --- a/contract/tests/test_contract.py +++ b/contract/tests/test_contract.py @@ -6,7 +6,7 @@ from collections import namedtuple from datetime import timedelta from dateutil.relativedelta import relativedelta from odoo import fields -from odoo.exceptions import ValidationError +from odoo.exceptions import ValidationError, UserError from odoo.tests import common @@ -110,6 +110,9 @@ class TestContractBase(common.SavepointCase): ) cls.acct_line.product_id.is_auto_renew = True cls.contract.company_id.create_new_line_at_contract_line_renew = True + cls.resiliate_reason = cls.env['contract.resiliate.reason'].create({ + 'name': 'resiliate_reason' + }) class TestContract(TestContractBase): @@ -2364,3 +2367,51 @@ class TestContract(TestContractBase): self.assertEqual( self.acct_line.recurring_next_date, to_date('2019-06-01') ) + + def test_action_resiliate_contract(self): + action = self.contract.action_resiliate_contract() + wizard = ( + self.env[action['res_model']] + .with_context(action['context']) + .create( + { + 'resiliate_date': '2018-03-01', + 'resiliate_reason_id': self.resiliate_reason.id, + 'resiliate_comment': 'resiliate_comment', + } + ) + ) + self.assertEqual(wizard.contract_id, self.contract) + with self.assertRaises(UserError): + wizard.resiliate_contract() + group_can_resiliate_contract = self.env.ref( + "contract.can_resiliate_contract" + ) + group_can_resiliate_contract.users |= self.env.user + wizard.resiliate_contract() + self.assertTrue(self.contract.is_resiliated) + self.assertEqual(self.contract.resiliate_date, to_date('2018-03-01')) + self.assertEqual( + self.contract.resiliate_reason_id.id, self.resiliate_reason.id + ) + self.assertEqual(self.contract.resiliate_comment, 'resiliate_comment') + self.contract.action_cancel_contract_resiliation() + self.assertFalse(self.contract.is_resiliated) + self.assertFalse(self.contract.resiliate_reason_id) + self.assertFalse(self.contract.resiliate_comment) + + def test_resiliate_date_before_last_date_invoiced(self): + self.contract.recurring_create_invoice() + self.assertEqual( + self.acct_line.last_date_invoiced, to_date('2018-02-14') + ) + group_can_resiliate_contract = self.env.ref( + "contract.can_resiliate_contract" + ) + group_can_resiliate_contract.users |= self.env.user + with self.assertRaises(ValidationError): + self.contract._resiliate_contract( + self.resiliate_reason, + 'resiliate_comment', + to_date('2018-02-13'), + ) diff --git a/contract/views/contract.xml b/contract/views/contract.xml index 33492798e..d3c79fca3 100644 --- a/contract/views/contract.xml +++ b/contract/views/contract.xml @@ -7,16 +7,32 @@ contract.contract + + + This contract was resiliated for the reason on . + + + + @@ -38,41 +54,43 @@ class="oe_edit_only"/> - - - + + + - + - + - + - @@ -80,15 +98,18 @@ - - + + + + + + + + contract.resiliate.reason + + + + + + + + + + + + + + contract.resiliate.reason + + + + + + + + + Contract Resiliation Reason + contract.resiliate.reason + tree,form + + + + Contract Resiliation Reason + + + + + + diff --git a/contract/wizards/__init__.py b/contract/wizards/__init__.py index 1fa21bcc9..bfba4f630 100644 --- a/contract/wizards/__init__.py +++ b/contract/wizards/__init__.py @@ -1,2 +1,3 @@ from . import contract_line_wizard from . import contract_manually_create_invoice +from . import contract_contract_resiliate diff --git a/contract/wizards/contract_contract_resiliate.py b/contract/wizards/contract_contract_resiliate.py new file mode 100644 index 000000000..4b3f19529 --- /dev/null +++ b/contract/wizards/contract_contract_resiliate.py @@ -0,0 +1,35 @@ +# Copyright 2020 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class ContractContractResiliate(models.TransientModel): + + _name = 'contract.contract.resiliate' + _description = "Resiliate Contract Wizard" + + contract_id = fields.Many2one( + comodel_name="contract.contract", + string="Contract", + required=True, + ondelete="cascade", + ) + resiliate_reason_id = fields.Many2one( + comodel_name="contract.resiliate.reason", + string="Resiliate Reason", + required=True, + ondelete="cascade", + ) + resiliate_comment = fields.Text(string="Resiliate Comment", required=True) + resiliate_date = fields.Date(string="Resiliate Date", required=True) + + @api.multi + def resiliate_contract(self): + for wizard in self: + wizard.contract_id._resiliate_contract( + wizard.resiliate_reason_id, + wizard.resiliate_comment, + wizard.resiliate_date, + ) + return True diff --git a/contract/wizards/contract_contract_resiliate.xml b/contract/wizards/contract_contract_resiliate.xml new file mode 100644 index 000000000..9372c8378 --- /dev/null +++ b/contract/wizards/contract_contract_resiliate.xml @@ -0,0 +1,33 @@ + + + + + + + contract.contract.resiliate + + + + + + + + + + + + + + + +
This contract was resiliated for the reason on .