[12.0][IMP] - Add strat/stop wizard to contract line

[12.0][IMP] - Add pause button to contract line

[IMP] - Add state filed in contract line form

[FIX] - stop don't change date_end for finished contract line

[IMP] - Change contract line buttons visibility

Add renewal process with termination notice

[FIX] - don't consider stop_date If it is after the contract line end_date

[IMP] - consider more cases in stop_plan_successor

[IMP] - cancel upcoming line on stop

[IMP] - Chnage next invoice date on un-cancel

[IMP] - Post message in contract on contract line actions

[IMP] - check contract line overlap
This commit is contained in:
sbejaoui
2018-11-13 11:29:46 +01:00
parent b979a4a342
commit 73c08d0f2f
14 changed files with 1727 additions and 122 deletions

View File

@@ -86,8 +86,28 @@ class AccountAbstractAnalyticContractLine(models.AbstractModel):
pricelist_id = fields.Many2one(
comodel_name='product.pricelist', string='Pricelist'
)
recurring_next_date = fields.Date(
copy=False, string='Date of Next Invoice'
recurring_next_date = fields.Date(string='Date of Next Invoice')
is_canceled = fields.Boolean(string="Canceled", default=False)
is_auto_renew = fields.Boolean(string="Auto Renew", default=False)
auto_renew_interval = fields.Integer(
default=1,
string='Renew Every',
help="Renew every (Days/Week/Month/Year)",
)
auto_renew_rule_type = fields.Selection(
[('monthly', 'Month(s)'), ('yearly', 'Year(s)')],
default='yearly',
string='Renewal type',
help="Specify Interval for automatic renewal.",
)
termination_notice_interval = fields.Integer(
default=1, string='Termination Notice Before'
)
termination_notice_rule_type = fields.Selection(
[('daily', 'Day(s)'), ('weekly', 'Week(s)'), ('monthly', 'Month(s)')],
default='monthly',
string='Termination Notice type',
)
@api.depends(

View File

@@ -6,6 +6,8 @@ from dateutil.relativedelta import relativedelta
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
from ..data.contract_line_constraints import get_allowed
class AccountAnalyticInvoiceLine(models.Model):
_name = 'account.analytic.invoice.line'
@@ -20,9 +22,7 @@ class AccountAnalyticInvoiceLine(models.Model):
)
date_start = fields.Date(string='Date Start', default=fields.Date.today())
date_end = fields.Date(string='Date End', index=True)
recurring_next_date = fields.Date(
copy=False, string='Date of Next Invoice'
)
recurring_next_date = fields.Date(string='Date of Next Invoice')
create_invoice_visibility = fields.Boolean(
compute='_compute_create_invoice_visibility'
)
@@ -40,6 +40,139 @@ class AccountAnalyticInvoiceLine(models.Model):
store=True,
readonly=True,
)
successor_contract_line_id = fields.Many2one(
comodel_name='account.analytic.invoice.line',
string="Successor Contract Line",
required=False,
readonly=True,
copy=False,
help="Contract Line created by this one.",
)
predecessor_contract_line_id = fields.Many2one(
comodel_name='account.analytic.invoice.line',
string="Predecessor Contract Line",
required=False,
readonly=True,
copy=False,
help="Contract Line origin of this one.",
)
is_plan_successor_allowed = fields.Boolean(
string="Plan successor allowed?", compute='_compute_allowed'
)
is_stop_plan_successor_allowed = fields.Boolean(
string="Stop/Plan successor allowed?", compute='_compute_allowed'
)
is_stop_allowed = fields.Boolean(
string="Stop allowed?", compute='_compute_allowed'
)
is_cancel_allowed = fields.Boolean(
string="Cancel allowed?", compute='_compute_allowed'
)
is_un_cancel_allowed = fields.Boolean(
string="Un-Cancel allowed?", compute='_compute_allowed'
)
state = fields.Selection(
string="State",
selection=[
('upcoming', 'Upcoming'),
('in-progress', 'In-progress'),
('upcoming-close', 'Upcoming Close'),
('closed', 'Closed'),
('canceled', 'Canceled'),
],
compute="_compute_state",
)
@api.multi
def _compute_state(self):
today = fields.Date.today()
for rec in self:
if rec.is_canceled:
rec.state = 'canceled'
elif today < rec.date_start:
rec.state = 'upcoming'
elif not rec.date_end or (
today <= rec.date_end and rec.is_auto_renew
):
rec.state = 'in-progress'
elif today <= rec.date_end and not rec.is_auto_renew:
rec.state = 'upcoming-close'
else:
rec.state = 'closed'
@api.depends(
'date_start',
'date_end',
'is_auto_renew',
'successor_contract_line_id',
'is_canceled',
)
def _compute_allowed(self):
for rec in self:
allowed = get_allowed(
rec.date_start,
rec.date_end,
rec.is_auto_renew,
rec.successor_contract_line_id,
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.UN_CANCEL
@api.constrains('is_auto_renew', 'successor_contract_line_id', 'date_end')
def _check_allowed(self):
"""
logical impossible combination:
* a line with is_auto_renew True should have date_end and
couldn't have successor_contract_line_id
* a line without date_end can't have successor_contract_line_id
"""
for rec in self:
if rec.is_auto_renew:
if rec.successor_contract_line_id:
raise ValidationError(
_(
"A contract line with a successor "
"can't be set to auto-renew"
)
)
if not rec.date_end:
raise ValidationError(
_("An auto-renew line should have a " "date end ")
)
else:
if not rec.date_end and rec.successor_contract_line_id:
raise ValidationError(
_(
"A contract line with a successor "
"should have date end"
)
)
@api.constrains('successor_contract_line_id', 'date_end')
def _check_overlap_successor(self):
for rec in self:
if rec.date_end and rec.successor_contract_line_id:
if rec.date_end > rec.successor_contract_line_id.date_start:
raise ValidationError(
_("Contract line and its successor overlapped")
)
@api.constrains('predecessor_contract_line_id', 'date_start')
def _check_overlap_predecessor(self):
for rec in self:
if rec.predecessor_contract_line_id:
if rec.date_start < rec.predecessor_contract_line_id.date_end:
raise ValidationError(
_("Contract line and its predecessor overlapped")
)
@api.model
def _compute_first_recurring_next_date(
@@ -59,6 +192,17 @@ class AccountAnalyticInvoiceLine(models.Model):
recurring_rule_type, recurring_interval
)
@api.onchange(
'is_auto_renew', 'auto_renew_rule_type', 'auto_renew_interval'
)
def _onchange_is_auto_renew(self):
"""Date end should be auto-computed if a contract line is set to
auto_renew"""
for rec in self.filtered('is_auto_renew'):
rec.date_end = self.date_start + self.get_relative_delta(
rec.auto_renew_rule_type, rec.auto_renew_interval
)
@api.onchange(
'date_start',
'recurring_invoicing_type',
@@ -143,6 +287,7 @@ class AccountAnalyticInvoiceLine(models.Model):
[
('contract_id.recurring_invoices', '=', True),
('recurring_next_date', '<=', date_ref),
('is_canceled', '=', False),
'|',
('date_end', '=', False),
('date_end', '>=', date_ref),
@@ -248,3 +393,386 @@ class AccountAnalyticInvoiceLine(models.Model):
return relativedelta(months=interval, day=31)
else:
return relativedelta(years=interval)
@api.multi
def delay(self, delay_delta):
"""
Delay a contract line
:param delay_delta: delay relative delta
:return: delayed contract line
"""
for rec in self:
old_date_start = rec.date_start
old_date_end = rec.date_end
new_date_start = rec.date_start + delay_delta
rec.recurring_next_date = self._compute_first_recurring_next_date(
new_date_start,
rec.recurring_invoicing_type,
rec.recurring_rule_type,
rec.recurring_interval,
)
rec.date_end = (
rec.date_end
if not rec.date_end
else rec.date_end + delay_delta
)
rec.date_start = new_date_start
msg = _(
"""Contract line for <strong>{product}</strong>
delayed: <br/>
- <strong>Start</strong>: {old_date_start} -- {new_date_start}
<br/>
- <strong>End</strong>: {old_date_end} -- {new_date_end}
""".format(
product=rec.name,
old_date_start=old_date_start,
new_date_start=rec.date_start,
old_date_end=old_date_end,
new_date_end=rec.date_end,
)
)
rec.contract_id.message_post(body=msg)
@api.multi
def stop(self, date_end):
"""
Put date_end on contract line
We don't consider contract lines that end's before the new end date
:param date_end: new date end for contract line
:return: True
"""
if not all(self.mapped('is_stop_allowed')):
raise ValidationError(_('Stop not allowed for this line'))
for rec in self:
if date_end < rec.date_start:
rec.cancel()
else:
old_date_end = rec.date_end
date_end = (
rec.date_end
if rec.date_end and rec.date_end < date_end
else date_end
)
rec.write({'date_end': date_end, 'is_auto_renew': False})
msg = _(
"""Contract line for <strong>{product}</strong>
stopped: <br/>
- <strong>End</strong>: {old_date_end} -- {new_date_end}
""".format(
product=rec.name,
old_date_end=old_date_end,
new_date_end=rec.date_end,
)
)
rec.contract_id.message_post(body=msg)
return True
@api.multi
def _prepare_value_for_plan_successor(
self, date_start, date_end, is_auto_renew, recurring_next_date=False
):
self.ensure_one()
if not recurring_next_date:
recurring_next_date = self._compute_first_recurring_next_date(
date_start,
self.recurring_invoicing_type,
self.recurring_rule_type,
self.recurring_interval,
)
new_vals = self.read()[0]
new_vals.pop("id", None)
values = self._convert_to_write(new_vals)
values['date_start'] = date_start
values['date_end'] = date_end
values['recurring_next_date'] = recurring_next_date
values['is_auto_renew'] = is_auto_renew
values['predecessor_contract_line_id'] = self.id
return values
@api.multi
def plan_successor(
self, date_start, date_end, is_auto_renew, recurring_next_date=False
):
"""
Create a copy of a contract line in a new interval
:param date_start: date_start for the successor_contract_line
:param date_end: date_end for the successor_contract_line
:param is_auto_renew: is_auto_renew option for successor_contract_line
:param recurring_next_date: recurring_next_date for the
successor_contract_line
:return: successor_contract_line
"""
contract_line = self.env['account.analytic.invoice.line']
for rec in self:
if not rec.is_plan_successor_allowed:
raise ValidationError(
_('Plan successor not allowed for this line')
)
rec.is_auto_renew = False
new_line = self.create(
rec._prepare_value_for_plan_successor(
date_start, date_end, is_auto_renew, recurring_next_date
)
)
rec.successor_contract_line_id = new_line
contract_line |= new_line
msg = _(
"""Contract line for <strong>{product}</strong>
planned a successor: <br/>
- <strong>Start</strong>: {new_date_start}
<br/>
- <strong>End</strong>: {new_date_end}
""".format(
product=rec.name,
new_date_start=new_line.date_start,
new_date_end=new_line.date_end,
)
)
rec.contract_id.message_post(body=msg)
return contract_line
@api.multi
def stop_plan_successor(self, date_start, date_end, is_auto_renew):
"""
Stop a contract line for a defined period and start it later
Cases to consider:
* contract line end's before the suspension period:
-> apply stop
* contract line start before the suspension period and end in it
-> apply stop at suspension start date
-> apply plan successor:
- date_start: suspension.date_end
- date_end: date_end + (contract_line.date_end
- suspension.date_start)
* contract line start before the suspension period and end after it
-> apply stop at suspension start date
-> apply plan successor:
- date_start: suspension.date_end
- date_end: date_end + (suspension.date_end
- suspension.date_start)
* contract line start and end's in the suspension period
-> apply delay
- delay: suspension.date_end - contract_line.end_date
* contract line start in the suspension period and end after it
-> apply delay
- delay: suspension.date_end - contract_line.date_start
* contract line start and end after the suspension period
-> apply delay
- delay: suspension.date_end - suspension.start_date
:param date_start: suspension start date
:param date_end: suspension end date
:param is_auto_renew: is the new line is set to auto_renew
:return: created contract line
"""
if not all(self.mapped('is_stop_plan_successor_allowed')):
raise ValidationError(
_('Stop/Plan successor not allowed for this line')
)
contract_line = self.env['account.analytic.invoice.line']
for rec in self:
if rec.date_start >= date_start:
if rec.date_end and rec.date_end <= date_end:
delay = date_end - rec.date_end
elif (
rec.date_end
and rec.date_end > date_end
or not rec.date_end
) and rec.date_start <= date_end:
delay = date_end - rec.date_start
else:
delay = date_end - date_start
rec.delay(delay)
contract_line |= rec
else:
if rec.date_end and rec.date_end < date_start:
rec.stop(date_start)
elif (
rec.date_end
and rec.date_end > date_start
and rec.date_end < date_end
):
new_date_start = date_end
new_date_end = date_end + (rec.date_end - date_start)
rec.stop(date_start)
contract_line |= rec.plan_successor(
new_date_start, new_date_end, is_auto_renew
)
else:
new_date_start = date_end
new_date_end = (
rec.date_end
if not rec.date_end
else rec.date_end + (date_end - date_start)
)
rec.stop(date_start)
contract_line |= rec.plan_successor(
new_date_start, new_date_end, is_auto_renew
)
return contract_line
@api.multi
def cancel(self):
if not all(self.mapped('is_cancel_allowed')):
raise ValidationError(_('Cancel not allowed for this line'))
for contract in self.mapped('contract_id'):
lines = self.filtered(lambda l, c=contract: l.contract_id == c)
msg = _(
"""Contract line canceled: %s"""
% "<br/>- ".join(
[
"<strong>%s</strong>" % name
for name in lines.mapped('name')
]
)
)
contract.message_post(body=msg)
return self.write({'is_canceled': True})
@api.multi
def uncancel(self, recurring_next_date):
if not all(self.mapped('is_un_cancel_allowed')):
raise ValidationError(_('Un-cancel not allowed for this line'))
for contract in self.mapped('contract_id'):
lines = self.filtered(lambda l, c=contract: l.contract_id == c)
msg = _(
"""Contract line Un-canceled: %s"""
% "<br/>- ".join(
[
"<strong>%s</strong>" % name
for name in lines.mapped('name')
]
)
)
contract.message_post(body=msg)
return self.write(
{'is_canceled': False, 'recurring_next_date': recurring_next_date}
)
@api.multi
def action_uncancel(self):
self.ensure_one()
context = {
'default_contract_line_id': self.id,
'default_recurring_next_date': fields.Date.today(),
}
context.update(self.env.context)
view_id = self.env.ref(
'contract.contract_line_wizard_uncancel_form_view'
).id
return {
'type': 'ir.actions.act_window',
'name': 'Un-Cancel Contract Line',
'res_model': 'account.analytic.invoice.line.wizard',
'view_type': 'form',
'view_mode': 'form',
'views': [(view_id, 'form')],
'target': 'new',
'context': context,
}
@api.multi
def action_plan_successor(self):
self.ensure_one()
context = {
'default_contract_line_id': self.id,
'default_is_auto_renew': self.is_auto_renew,
}
context.update(self.env.context)
view_id = self.env.ref(
'contract.contract_line_wizard_plan_successor_form_view'
).id
return {
'type': 'ir.actions.act_window',
'name': 'Plan contract line successor',
'res_model': 'account.analytic.invoice.line.wizard',
'view_type': 'form',
'view_mode': 'form',
'views': [(view_id, 'form')],
'target': 'new',
'context': context,
}
@api.multi
def action_stop(self):
self.ensure_one()
context = {
'default_contract_line_id': self.id,
'default_date_end': self.date_end,
}
context.update(self.env.context)
view_id = self.env.ref(
'contract.contract_line_wizard_stop_form_view'
).id
return {
'type': 'ir.actions.act_window',
'name': 'Resiliate contract line',
'res_model': 'account.analytic.invoice.line.wizard',
'view_type': 'form',
'view_mode': 'form',
'views': [(view_id, 'form')],
'target': 'new',
'context': context,
}
@api.multi
def action_stop_plan_successor(self):
self.ensure_one()
context = {
'default_contract_line_id': self.id,
'default_is_auto_renew': self.is_auto_renew,
}
context.update(self.env.context)
view_id = self.env.ref(
'contract.contract_line_wizard_stop_plan_successor_form_view'
).id
return {
'type': 'ir.actions.act_window',
'name': 'Suspend contract line',
'res_model': 'account.analytic.invoice.line.wizard',
'view_type': 'form',
'view_mode': 'form',
'views': [(view_id, 'form')],
'target': 'new',
'context': context,
}
@api.multi
def _get_renewal_dates(self):
self.ensure_one()
date_start = self.date_end
date_end = date_start + self.get_relative_delta(
self.auto_renew_rule_type, self.auto_renew_interval
)
return date_start, date_end
@api.multi
def renew(self):
res = self.env['account.analytic.invoice.line']
for rec in self:
is_auto_renew = rec.is_auto_renew
rec.stop(rec.date_end)
date_start, date_end = rec._get_renewal_dates()
new_line = rec.plan_successor(date_start, date_end, is_auto_renew)
new_line._onchange_date_start()
res |= new_line
return res
@api.model
def _contract_line_to_renew_domain(self):
date_ref = fields.datetime.today() + self.get_relative_delta(
self.termination_notice_rule_type, self.termination_notice_interval
)
return [
('is_auto_renew', '=', True),
('date_end', '<=', date_ref),
('is_canceled', '=', False),
]
@api.model
def cron_renew_contract_line(self):
domain = self._contract_line_to_renew_domain()
to_renew = self.search(domain)
to_renew.renew()