[IMP] - contract resiliation

This commit is contained in:
sbejaoui
2020-01-29 17:06:31 +01:00
parent 70525f0e23
commit 58b38490f6
13 changed files with 320 additions and 19 deletions

View File

@@ -20,9 +20,11 @@
'depends': ['base', 'account', 'product'], 'depends': ['base', 'account', 'product'],
"external_dependencies": {"python": ["dateutil"]}, "external_dependencies": {"python": ["dateutil"]},
'data': [ 'data': [
'security/groups.xml',
'security/contract_tag.xml', 'security/contract_tag.xml',
'security/ir.model.access.csv', 'security/ir.model.access.csv',
'security/contract_security.xml', 'security/contract_security.xml',
'security/contract_resiliate_reason.xml',
'report/report_contract.xml', 'report/report_contract.xml',
'report/contract_views.xml', 'report/contract_views.xml',
'data/contract_cron.xml', 'data/contract_cron.xml',
@@ -30,6 +32,7 @@
'data/mail_template.xml', 'data/mail_template.xml',
'wizards/contract_line_wizard.xml', 'wizards/contract_line_wizard.xml',
'wizards/contract_manually_create_invoice.xml', 'wizards/contract_manually_create_invoice.xml',
'wizards/contract_contract_resiliate.xml',
'views/abstract_contract_line.xml', 'views/abstract_contract_line.xml',
'views/contract.xml', 'views/contract.xml',
'views/contract_line.xml', 'views/contract_line.xml',
@@ -37,6 +40,7 @@
'views/contract_template_line.xml', 'views/contract_template_line.xml',
'views/res_partner_view.xml', 'views/res_partner_view.xml',
'views/res_config_settings.xml', 'views/res_config_settings.xml',
'views/contract_resiliate_reason.xml',
], ],
'installable': True, 'installable': True,
} }

View File

@@ -12,3 +12,4 @@ from . import res_partner
from . import contract_tag from . import contract_tag
from . import res_company from . import res_company
from . import res_config_settings from . import res_config_settings
from . import contract_resiliate_reason

View File

@@ -7,7 +7,7 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models from odoo import api, fields, models
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError, UserError
from odoo.tools.translate import _ from odoo.tools.translate import _
@@ -93,6 +93,15 @@ class ContractContract(models.Model):
) )
tag_ids = fields.Many2many(comodel_name="contract.tag", string="Tags") tag_ids = fields.Many2many(comodel_name="contract.tag", string="Tags")
note = fields.Text(string="Notes") 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 @api.multi
def _inverse_partner_id(self): def _inverse_partner_id(self):
@@ -458,3 +467,43 @@ class ContractContract(models.Model):
domain = self._get_contracts_to_invoice_domain(date_ref) domain = self._get_contracts_to_invoice_domain(date_ref)
contracts_to_invoice = self.search(domain) contracts_to_invoice = self.search(domain)
return contracts_to_invoice._recurring_create_invoice(date_ref) 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,
})

View File

@@ -296,9 +296,19 @@ class ContractLine(models.Model):
'successor_contract_line_id', 'successor_contract_line_id',
'predecessor_contract_line_id', 'predecessor_contract_line_id',
'is_canceled', 'is_canceled',
'contract_id.is_resiliated',
) )
def _compute_allowed(self): def _compute_allowed(self):
for rec in 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: if rec.date_start:
allowed = get_allowed( allowed = get_allowed(
rec.date_start, rec.date_start,
@@ -310,13 +320,14 @@ class ContractLine(models.Model):
rec.is_canceled, rec.is_canceled,
) )
if allowed: if allowed:
rec.is_plan_successor_allowed = allowed.plan_successor rec.update({
rec.is_stop_plan_successor_allowed = ( 'is_plan_successor_allowed': allowed.plan_successor,
allowed.stop_plan_successor 'is_stop_plan_successor_allowed':
) allowed.stop_plan_successor,
rec.is_stop_allowed = allowed.stop 'is_stop_allowed': allowed.stop,
rec.is_cancel_allowed = allowed.cancel 'is_cancel_allowed': allowed.cancel,
rec.is_un_cancel_allowed = allowed.uncancel 'is_un_cancel_allowed': allowed.uncancel,
})
@api.constrains('is_auto_renew', 'successor_contract_line_id', 'date_end') @api.constrains('is_auto_renew', 'successor_contract_line_id', 'date_end')
def _check_allowed(self): def _check_allowed(self):
@@ -1251,6 +1262,7 @@ class ContractLine(models.Model):
@api.model @api.model
def _contract_line_to_renew_domain(self): def _contract_line_to_renew_domain(self):
return [ return [
('contract_id.is_resiliated', '=', False),
('is_auto_renew', '=', True), ('is_auto_renew', '=', True),
('is_canceled', '=', False), ('is_canceled', '=', False),
('termination_notice_date', '<=', fields.Date.context_today(self)), ('termination_notice_date', '<=', fields.Date.context_today(self)),

View File

@@ -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)

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2020 ACSONE SA/NV
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record model="ir.model.access" id="contract_resiliate_reason_access_manager">
<field name="name">contract.resiliate.reason access manager</field>
<field name="model_id" ref="model_contract_resiliate_reason"/>
<field name="group_id" ref="account.group_account_manager"/>
<field name="perm_read" eval="1"/>
<field name="perm_create" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_unlink" eval="1"/>
</record>
<record model="ir.model.access" id="contract_resiliate_reason_access_user">
<field name="name">contract.resiliate.reason access user</field>
<field name="model_id" ref="model_contract_resiliate_reason"/>
<field name="group_id" ref="account.group_account_invoice"/>
<field name="perm_read" eval="1"/>
<field name="perm_create" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_unlink" eval="1"/>
</record>
</odoo>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2020 ACSONE SA/NV
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="can_resiliate_contract" model="res.groups">
<field name="name">Contract: Can Resiliate Contracts</field>
<field name="implied_ids" eval="[(4, ref('account.group_account_invoice'))]"/>
</record>
</odoo>

View File

@@ -6,7 +6,7 @@ from collections import namedtuple
from datetime import timedelta from datetime import timedelta
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from odoo import fields from odoo import fields
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError, UserError
from odoo.tests import common from odoo.tests import common
@@ -110,6 +110,9 @@ class TestContractBase(common.SavepointCase):
) )
cls.acct_line.product_id.is_auto_renew = True cls.acct_line.product_id.is_auto_renew = True
cls.contract.company_id.create_new_line_at_contract_line_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): class TestContract(TestContractBase):
@@ -2364,3 +2367,51 @@ class TestContract(TestContractBase):
self.assertEqual( self.assertEqual(
self.acct_line.recurring_next_date, to_date('2019-06-01') 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'),
)

View File

@@ -7,16 +7,32 @@
<field name="model">contract.contract</field> <field name="model">contract.contract</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form> <form>
<field name="is_resiliated" invisible="1"/>
<div class="alert alert-danger" role="alert" style="margin-bottom:0px;" attrs="{'invisible': [('is_resiliated','=',False)]}">
<p>This contract was resiliated for the reason <strong><field name="resiliate_reason_id" options="{'no_open':True}"/></strong> on <field name="resiliate_date"/>.</p>
<p><field name="resiliate_comment"/></p>
</div>
<header> <header>
<button name="action_contract_send" <button name="action_contract_send"
type="object" type="object"
string="Send by Email" string="Send by Email"
attrs="{'invisible': [('is_resiliated','=',True)]}"
groups="base.group_user"/> groups="base.group_user"/>
<button name="recurring_create_invoice" <button name="recurring_create_invoice"
type="object" type="object"
attrs="{'invisible': [('create_invoice_visibility', '=', False)]}" attrs="{'invisible': ['|', ('create_invoice_visibility', '=', False)]}"
string="Create invoices" string="Create invoices"
groups="base.group_no_one"/> groups="base.group_no_one"/>
<button name="action_resiliate_contract"
type="object"
string="Resiliate Contract"
attrs="{'invisible': [('is_resiliated','=',True)]}"
groups="contract.can_resiliate_contract"/>
<button name="action_cancel_contract_resiliation"
type="object"
string="Cancel Contract Resiliation"
attrs="{'invisible': [('is_resiliated','=',False)]}"
groups="contract.can_resiliate_contract"/>
</header> </header>
<sheet string="Contract"> <sheet string="Contract">
<div class="oe_button_box" name="button_box"> <div class="oe_button_box" name="button_box">
@@ -38,41 +54,43 @@
class="oe_edit_only"/> class="oe_edit_only"/>
<h3> <h3>
<field name="name" class="oe_inline" <field name="name" class="oe_inline"
attrs="{'readonly': [('is_resiliated','=',True)]}"
placeholder="e.g. Contract XYZ"/> placeholder="e.g. Contract XYZ"/>
</h3> </h3>
</div> </div>
<group name="main"> <group name="main">
<group> <group>
<field name="commercial_partner_id" invisible="1"/> <field name="commercial_partner_id" invisible="1"/>
<field name="partner_id" required="1"/> <field name="partner_id" required="1" attrs="{'readonly': [('is_resiliated','=',True)]}"/>
<field name="payment_term_id"/> <field name="payment_term_id" attrs="{'readonly': [('is_resiliated','=',True)]}"/>
<field name="user_id"/> <field name="user_id" attrs="{'readonly': [('is_resiliated','=',True)]}"/>
</group> </group>
<group> <group>
<field name="contract_template_id" <field name="contract_template_id"
attrs="{'readonly': [('is_resiliated','=',True)]}"
domain="['|', ('contract_type', '=', contract_type), ('contract_type', '=', False)]" domain="['|', ('contract_type', '=', contract_type), ('contract_type', '=', False)]"
context="{'default_contract_type': contract_type}"/> context="{'default_contract_type': contract_type}"/>
<field name="contract_type" invisible="1" <field name="contract_type" invisible="1"
required="1"/> required="1"/>
<field name="fiscal_position_id"/> <field name="fiscal_position_id" attrs="{'readonly': [('is_resiliated','=',True)]}"/>
<field name="tag_ids" widget="many2many_tags"/> <field name="tag_ids" widget="many2many_tags"/>
</group> </group>
</group> </group>
<group name="recurring_invoices"> <group name="recurring_invoices">
<group> <group>
<field name="journal_id" required="1"/> <field name="journal_id" required="1" attrs="{'readonly': [('is_resiliated','=',True)]}"/>
<field name="recurring_next_date"/> <field name="recurring_next_date"/>
</group> </group>
<group> <group>
<field name="pricelist_id"/> <field name="pricelist_id" attrs="{'readonly': [('is_resiliated','=',True)]}"/>
<field name="date_end"/> <field name="date_end"/>
</group> </group>
</group> </group>
<notebook> <notebook>
<page name="recurring_invoice_line" <page name="recurring_invoice_line"
string="Recurring Invoices"> string="Recurring Invoices">
<field name="contract_line_ids" <field name="contract_line_ids" attrs="{'readonly': [('is_resiliated','=',True)]}"
context="{'default_contract_type': contract_type}"/> context="{'default_contract_type': contract_type}"/>
<field name="note"/> <field name="note"/>
</page> </page>
@@ -80,15 +98,18 @@
<field name="create_invoice_visibility" <field name="create_invoice_visibility"
invisible="1"/> invisible="1"/>
<group> <group>
<field name="code"/> <field name="code" attrs="{'readonly': [('is_resiliated','=',True)]}"/>
<field name="group_id"/> <field name="group_id" attrs="{'readonly': [('is_resiliated','=',True)]}"/>
<field name="company_id" <field name="company_id"
attrs="{'readonly': [('is_resiliated','=',True)]}"
options="{'no_create': True}" options="{'no_create': True}"
groups="base.group_multi_company"/> groups="base.group_multi_company"/>
<field name="currency_id" <field name="currency_id"
attrs="{'readonly': [('is_resiliated','=',True)]}"
options="{'no_create': True}" options="{'no_create': True}"
groups="base.group_multi_currency"/> groups="base.group_multi_currency"/>
<field name="invoice_partner_id" <field name="invoice_partner_id"
attrs="{'readonly': [('is_resiliated','=',True)]}"
required="1"/> required="1"/>
</group> </group>
<group string="Legend (for the markers inside invoice lines description)" <group string="Legend (for the markers inside invoice lines description)"

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2020 ACSONE SA/NV
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record model="ir.ui.view" id="contract_resiliate_reason_form_view">
<field name="model">contract.resiliate.reason</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="name"/>
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="contract_resiliate_reason_tree_view">
<field name="model">contract.resiliate.reason</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
</tree>
</field>
</record>
<record model="ir.actions.act_window" id="contract_resiliate_reason_act_window">
<field name="name">Contract Resiliation Reason</field>
<field name="res_model">contract.resiliate.reason</field>
<field name="view_mode">tree,form</field>
</record>
<record model="ir.ui.menu" id="contract_resiliate_reason_menu">
<field name="name">Contract Resiliation Reason</field>
<field name="parent_id" ref="menu_config_contract"/>
<field name="action" ref="contract_resiliate_reason_act_window"/>
<field name="sequence" eval="16"/>
</record>
</odoo>

View File

@@ -1,2 +1,3 @@
from . import contract_line_wizard from . import contract_line_wizard
from . import contract_manually_create_invoice from . import contract_manually_create_invoice
from . import contract_contract_resiliate

View File

@@ -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

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2020 ACSONE SA/NV
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record model="ir.ui.view" id="contract_contract_resiliate_form_view">
<field name="model">contract.contract.resiliate</field>
<field name="arch" type="xml">
<form string="Contract Contract Resiliate">
<group>
<field name="contract_id" invisible="True"/>
<field name="resiliate_date"/>
<field name="resiliate_reason_id" widget="selection"/>
<field name="resiliate_comment"/>
</group>
<footer>
<button name="resiliate_contract"
string="Resiliate Contract"
class="btn-primary"
confirm="Are you sure you want to resiliate this contract?"
type="object"/>
<button string="Cancel"
class="btn-default"
special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>