From ab02ba511fb02d74d9b8256bb8692d9823f288d5 Mon Sep 17 00:00:00 2001 From: sbejaoui Date: Wed, 29 Jan 2020 17:06:31 +0100 Subject: [PATCH 1/2] [IMP] - contract resiliation --- contract/__manifest__.py | 4 ++ contract/models/__init__.py | 1 + contract/models/contract.py | 51 +++++++++++++++++- contract/models/contract_line.py | 26 ++++++--- contract/models/contract_resiliate_reason.py | 12 +++++ .../security/contract_resiliate_reason.xml | 27 ++++++++++ contract/security/groups.xml | 12 +++++ contract/tests/test_contract.py | 53 ++++++++++++++++++- contract/views/contract.xml | 41 ++++++++++---- contract/views/contract_resiliate_reason.xml | 43 +++++++++++++++ contract/wizards/__init__.py | 1 + .../wizards/contract_contract_resiliate.py | 35 ++++++++++++ .../wizards/contract_contract_resiliate.xml | 33 ++++++++++++ 13 files changed, 320 insertions(+), 19 deletions(-) create mode 100644 contract/models/contract_resiliate_reason.py create mode 100644 contract/security/contract_resiliate_reason.xml create mode 100644 contract/security/groups.xml create mode 100644 contract/views/contract_resiliate_reason.xml create mode 100644 contract/wizards/contract_contract_resiliate.py create mode 100644 contract/wizards/contract_contract_resiliate.xml diff --git a/contract/__manifest__.py b/contract/__manifest__.py index 22967735648..cf10c11fbc7 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 6d46b4b6124..c09f4495ab0 100644 --- a/contract/models/__init__.py +++ b/contract/models/__init__.py @@ -12,3 +12,4 @@ 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 7b16dafd6fa..f0a4a2a9f98 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 _ @@ -92,6 +92,15 @@ class ContractContract(models.Model): index=True ) tag_ids = fields.Many2many(comodel_name="contract.tag", string="Tags") + 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): @@ -481,3 +490,43 @@ def cron_recurring_create_invoice(self, date_ref=None): 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 8969f881b90..29886714d9a 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -292,9 +292,19 @@ def _search_state(self, operator, value): '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, @@ -306,13 +316,14 @@ def _compute_allowed(self): 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): @@ -1241,6 +1252,7 @@ def renew(self): @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 00000000000..f33a0b2f64e --- /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 00000000000..d5fe5e4d2c8 --- /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 00000000000..638e0ab055b --- /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 9f613de8c44..df5b9c86d05 100644 --- a/contract/tests/test_contract.py +++ b/contract/tests/test_contract.py @@ -6,7 +6,7 @@ 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 @@ def setUpClass(cls): ) 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 @@ def test_stop_and_update_recurring_invoice_date(self): 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 670b4040750..bfd68125c55 100644 --- a/contract/views/contract.xml +++ b/contract/views/contract.xml @@ -7,16 +7,32 @@ contract.contract
+ +
@@ -38,56 +54,61 @@ class="oe_edit_only"/>

- - - + + + - + - + - + - - - + + + + + + + + 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 1fa21bcc961..bfba4f6307e 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 00000000000..4b3f19529bd --- /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 00000000000..9372c8378eb --- /dev/null +++ b/contract/wizards/contract_contract_resiliate.xml @@ -0,0 +1,33 @@ + + + + + + + contract.contract.resiliate + +
+ + + + + + +
+
+
+
+
+ + + +
From 60dafd3324c0e22b30871f9e74a7672632c82feb Mon Sep 17 00:00:00 2001 From: sbejaoui Date: Thu, 30 Jan 2020 13:24:25 +0100 Subject: [PATCH 2/2] [IMP] - can't upsell or downsell a resiliated contract --- product_contract/models/sale_order.py | 15 ++++++++++++++- product_contract/models/sale_order_line.py | 11 +++++++++++ product_contract/tests/test_sale_order.py | 15 +++++++++++++++ product_contract/views/sale_order.xml | 1 + 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/product_contract/models/sale_order.py b/product_contract/models/sale_order.py index 1a277a963d2..9ae467b7b2e 100644 --- a/product_contract/models/sale_order.py +++ b/product_contract/models/sale_order.py @@ -2,7 +2,8 @@ # Copyright 2018 ACSONE SA/NV. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo import fields, api, models +from odoo import fields, api, models, _ +from odoo.exceptions import ValidationError class SaleOrder(models.Model): @@ -16,6 +17,18 @@ class SaleOrder(models.Model): compute='_compute_need_contract_creation' ) + @api.constrains('state') + def check_contact_is_not_resiliated(self): + for rec in self: + if rec.state not in ( + 'sale', + 'done', + 'cancel', + ) and rec.order_line.filtered('contract_id.is_resiliated'): + raise ValidationError( + _("You can't upsell or downsell a resiliated contract") + ) + @api.depends('order_line.contract_id', 'state') def _compute_need_contract_creation(self): for rec in self: diff --git a/product_contract/models/sale_order_line.py b/product_contract/models/sale_order_line.py index 4f29da01c6a..b6f293709cc 100644 --- a/product_contract/models/sale_order_line.py +++ b/product_contract/models/sale_order_line.py @@ -51,6 +51,17 @@ class SaleOrderLine(models.Model): copy=False, ) + @api.constrains('contract_id') + def check_contact_is_not_resiliated(self): + for rec in self: + if ( + rec.order_id.state not in ('sale', 'done', 'cancel') + and rec.contract_id.is_resiliated + ): + raise ValidationError( + _("You can't upsell or downsell a resiliated contract") + ) + @api.multi def _get_auto_renew_rule_type(self): """monthly last day don't make sense for auto_renew_rule_type""" diff --git a/product_contract/tests/test_sale_order.py b/product_contract/tests/test_sale_order.py index 307cf908412..ae527487be8 100644 --- a/product_contract/tests/test_sale_order.py +++ b/product_contract/tests/test_sale_order.py @@ -327,3 +327,18 @@ def test_action_show_contracts(self): self.env['contract.contract'].search(action['domain']), self.sale.order_line.mapped('contract_id'), ) + + def test_check_contact_is_not_resiliated(self): + self.contract.is_resiliated = True + with self.assertRaises(ValidationError): + self.order_line1.contract_id = self.contract + + def test_check_contact_is_not_resiliated(self): + self.order_line1.contract_id = self.contract + self.sale.action_confirm() + self.contract.is_resiliated = True + self.sale.action_cancel() + with self.assertRaises(ValidationError): + self.sale.action_draft() + self.contract.is_resiliated = False + self.sale.action_draft() diff --git a/product_contract/views/sale_order.xml b/product_contract/views/sale_order.xml index 4fd1b90b992..2c462c99360 100644 --- a/product_contract/views/sale_order.xml +++ b/product_contract/views/sale_order.xml @@ -41,6 +41,7 @@ domain="['|',('contract_template_id','=',contract_template_id), ('contract_template_id','=',False), ('partner_id','=',parent.partner_id), + ('is_resiliated','=',False), ]"/>