Browse Source

[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
pull/207/head
sbejaoui 6 years ago
parent
commit
73c08d0f2f
  1. 1
      contract/__init__.py
  2. 2
      contract/__manifest__.py
  3. 1
      contract/data/__init__.py
  4. 230
      contract/data/contract_line_constraints.py
  5. 16
      contract/data/contract_renew_cron.xml
  6. 24
      contract/models/abstract_contract_line.py
  7. 534
      contract/models/contract_line.py
  8. 583
      contract/tests/test_contract.py
  9. 84
      contract/views/abstract_contract_line.xml
  10. 207
      contract/views/contract.xml
  11. 13
      contract/views/contract_line.xml
  12. 1
      contract/wizards/__init__.py
  13. 48
      contract/wizards/contract_line_wizard.py
  14. 99
      contract/wizards/contract_line_wizard.xml

1
contract/__init__.py

@ -1 +1,2 @@
from . import models
from . import wizards

2
contract/__manifest__.py

@ -20,11 +20,13 @@
'website': 'https://github.com/oca/contract',
'depends': ['base', 'account', 'analytic'],
'data': [
'wizards/contract_line_wizard.xml',
'security/ir.model.access.csv',
'security/contract_security.xml',
'report/report_contract.xml',
'report/contract_views.xml',
'data/contract_cron.xml',
'data/contract_renew_cron.xml',
'data/mail_template.xml',
'views/abstract_contract_line.xml',
'views/contract.xml',

1
contract/data/__init__.py

@ -0,0 +1 @@
from . import contract_line_constraints

230
contract/data/contract_line_constraints.py

@ -0,0 +1,230 @@
# Copyright 2018 ACSONE SA/NV.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from collections import namedtuple
from odoo.fields import Date
CRITERIA = namedtuple(
'CRITERIA',
['WHEN', 'HAS_DATE_END', 'IS_AUTO_RENEW', 'HAS_SUCCESSOR', 'CANCELED'],
)
ALLOWED = namedtuple(
'ALLOWED',
['PLAN_SUCCESSOR', 'STOP_PLAN_SUCCESSOR', 'STOP', 'CANCEL', 'UN_CANCEL'],
)
CRITERIA_ALLOWED_DICT = {
CRITERIA(
WHEN='BEFORE',
HAS_DATE_END=True,
IS_AUTO_RENEW=True,
HAS_SUCCESSOR=False,
CANCELED=False,
): ALLOWED(
PLAN_SUCCESSOR=False,
STOP_PLAN_SUCCESSOR=True,
STOP=True,
CANCEL=True,
UN_CANCEL=False,
),
CRITERIA(
WHEN='BEFORE',
HAS_DATE_END=True,
IS_AUTO_RENEW=False,
HAS_SUCCESSOR=True,
CANCELED=False,
): ALLOWED(
PLAN_SUCCESSOR=False,
STOP_PLAN_SUCCESSOR=False,
STOP=True,
CANCEL=True,
UN_CANCEL=False,
),
CRITERIA(
WHEN='BEFORE',
HAS_DATE_END=True,
IS_AUTO_RENEW=False,
HAS_SUCCESSOR=False,
CANCELED=False,
): ALLOWED(
PLAN_SUCCESSOR=True,
STOP_PLAN_SUCCESSOR=True,
STOP=True,
CANCEL=True,
UN_CANCEL=False,
),
CRITERIA(
WHEN='BEFORE',
HAS_DATE_END=False,
IS_AUTO_RENEW=False,
HAS_SUCCESSOR=False,
CANCELED=False,
): ALLOWED(
PLAN_SUCCESSOR=False,
STOP_PLAN_SUCCESSOR=True,
STOP=True,
CANCEL=True,
UN_CANCEL=False,
),
CRITERIA(
WHEN='IN',
HAS_DATE_END=True,
IS_AUTO_RENEW=True,
HAS_SUCCESSOR=False,
CANCELED=False,
): ALLOWED(
PLAN_SUCCESSOR=False,
STOP_PLAN_SUCCESSOR=True,
STOP=True,
CANCEL=True,
UN_CANCEL=False,
),
CRITERIA(
WHEN='IN',
HAS_DATE_END=True,
IS_AUTO_RENEW=False,
HAS_SUCCESSOR=True,
CANCELED=False,
): ALLOWED(
PLAN_SUCCESSOR=False,
STOP_PLAN_SUCCESSOR=False,
STOP=True,
CANCEL=True,
UN_CANCEL=False,
),
CRITERIA(
WHEN='IN',
HAS_DATE_END=True,
IS_AUTO_RENEW=False,
HAS_SUCCESSOR=False,
CANCELED=False,
): ALLOWED(
PLAN_SUCCESSOR=True,
STOP_PLAN_SUCCESSOR=True,
STOP=True,
CANCEL=True,
UN_CANCEL=False,
),
CRITERIA(
WHEN='IN',
HAS_DATE_END=False,
IS_AUTO_RENEW=False,
HAS_SUCCESSOR=False,
CANCELED=False,
): ALLOWED(
PLAN_SUCCESSOR=False,
STOP_PLAN_SUCCESSOR=True,
STOP=True,
CANCEL=True,
UN_CANCEL=False,
),
CRITERIA(
WHEN='AFTER',
HAS_DATE_END=True,
IS_AUTO_RENEW=True,
HAS_SUCCESSOR=False,
CANCELED=False,
): ALLOWED(
PLAN_SUCCESSOR=False,
STOP_PLAN_SUCCESSOR=False,
STOP=False,
CANCEL=False,
UN_CANCEL=False,
),
CRITERIA(
WHEN='AFTER',
HAS_DATE_END=True,
IS_AUTO_RENEW=False,
HAS_SUCCESSOR=True,
CANCELED=False,
): ALLOWED(
PLAN_SUCCESSOR=False,
STOP_PLAN_SUCCESSOR=False,
STOP=False,
CANCEL=False,
UN_CANCEL=False,
),
CRITERIA(
WHEN='AFTER',
HAS_DATE_END=True,
IS_AUTO_RENEW=False,
HAS_SUCCESSOR=False,
CANCELED=False,
): ALLOWED(
PLAN_SUCCESSOR=True,
STOP_PLAN_SUCCESSOR=False,
STOP=False,
CANCEL=False,
UN_CANCEL=False,
),
CRITERIA(
WHEN=None,
HAS_DATE_END=None,
IS_AUTO_RENEW=None,
HAS_SUCCESSOR=None,
CANCELED=True,
): ALLOWED(
PLAN_SUCCESSOR=False,
STOP_PLAN_SUCCESSOR=False,
STOP=False,
CANCEL=False,
UN_CANCEL=True,
),
}
def compute_when(date_start, date_end):
today = Date.today()
if today < date_start:
return 'BEFORE'
if date_end and today > date_end:
return 'AFTER'
return 'IN'
def compute_criteria(
date_start,
date_end,
is_auto_renew,
successor_contract_line_id,
is_canceled,
):
if is_canceled:
return CRITERIA(
WHEN=None,
HAS_DATE_END=None,
IS_AUTO_RENEW=None,
HAS_SUCCESSOR=None,
CANCELED=True,
)
when = compute_when(date_start, date_end)
has_date_end = date_end if not date_end else True
is_auto_renew = is_auto_renew
has_successor = True if successor_contract_line_id else False
canceled = is_canceled
return CRITERIA(
WHEN=when,
HAS_DATE_END=has_date_end,
IS_AUTO_RENEW=is_auto_renew,
HAS_SUCCESSOR=has_successor,
CANCELED=canceled,
)
def get_allowed(
date_start,
date_end,
is_auto_renew,
successor_contract_line_id,
is_canceled,
):
criteria = compute_criteria(
date_start,
date_end,
is_auto_renew,
successor_contract_line_id,
is_canceled,
)
if criteria in CRITERIA_ALLOWED_DICT:
return CRITERIA_ALLOWED_DICT[criteria]
return False

16
contract/data/contract_renew_cron.xml

@ -0,0 +1,16 @@
<?xml version="1.0" encoding='UTF-8'?>
<odoo noupdate="1">
<record model="ir.cron" id="contract_line_cron_for_renew">
<field name="name">Renew Contract lines</field>
<field name="model_id" ref="model_account_analytic_invoice_line"/>
<field name="state">code</field>
<field name="code">model.cron_renew_contract_line()</field>
<field name="user_id" ref="base.user_root" />
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field eval="False" name="doall" />
</record>
</odoo>

24
contract/models/abstract_contract_line.py

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

534
contract/models/contract_line.py

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

583
contract/tests/test_contract.py

@ -1,7 +1,8 @@
# Copyright 2016 Tecnativa - Carlos Dauden
# Copyright 2017 Tecnativa - Pedro M. Baeza
# Copyright 2018 Tecnativa - Carlos Dauden
# Copyright 2018 Tecnativa - Pedro M. Baeza
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from dateutil.relativedelta import relativedelta
from odoo import fields
from odoo.exceptions import ValidationError
from odoo.tests import common
@ -75,8 +76,8 @@ class TestContractBase(common.SavepointCase):
'discount': 50,
'recurring_rule_type': 'monthly',
'recurring_interval': 1,
'date_start': '2016-02-15',
'recurring_next_date': '2016-02-29',
'date_start': '2018-02-15',
'recurring_next_date': '2018-02-22',
},
)
],
@ -92,8 +93,10 @@ class TestContractBase(common.SavepointCase):
'discount': 50,
'recurring_rule_type': 'monthly',
'recurring_interval': 1,
'date_start': '2016-02-15',
'recurring_next_date': '2016-02-29',
'date_start': '2018-01-01',
'date_end': '2019-01-01',
'recurring_next_date': '2018-01-15',
'is_auto_renew': True,
}
cls.acct_line = cls.env['account.analytic.invoice.line'].create(
cls.line_vals
@ -107,6 +110,7 @@ class TestContract(TestContractBase):
vals = self.line_vals.copy()
del vals['contract_id']
del vals['date_start']
del vals['date_end']
vals['contract_template_id'] = self.template.id
vals.update(overrides)
return self.env['account.analytic.contract.line'].create(vals)
@ -130,7 +134,7 @@ class TestContract(TestContractBase):
self.assertEqual(self.acct_line.price_unit, 10)
def test_contract(self):
recurring_next_date = to_date('2016-03-29')
recurring_next_date = to_date('2018-02-15')
self.assertAlmostEqual(self.acct_line.price_subtotal, 50.0)
res = self.acct_line._onchange_product_id()
self.assertIn('uom_id', res['domain'])
@ -154,8 +158,8 @@ class TestContract(TestContractBase):
)
def test_contract_daily(self):
recurring_next_date = to_date('2016-03-01')
self.acct_line.recurring_next_date = '2016-02-29'
recurring_next_date = to_date('2018-02-23')
self.acct_line.recurring_next_date = '2018-02-22'
self.acct_line.recurring_rule_type = 'daily'
self.contract.pricelist_id = False
self.contract.recurring_create_invoice()
@ -168,8 +172,8 @@ class TestContract(TestContractBase):
)
def test_contract_weekly(self):
recurring_next_date = to_date('2016-03-07')
self.acct_line.recurring_next_date = '2016-02-29'
recurring_next_date = to_date('2018-03-01')
self.acct_line.recurring_next_date = '2018-02-22'
self.acct_line.recurring_rule_type = 'weekly'
self.acct_line.recurring_invoicing_type = 'post-paid'
self.contract.recurring_create_invoice()
@ -182,8 +186,8 @@ class TestContract(TestContractBase):
)
def test_contract_yearly(self):
recurring_next_date = to_date('2017-02-28')
self.acct_line.recurring_next_date = '2016-02-29'
recurring_next_date = to_date('2019-02-22')
self.acct_line.recurring_next_date = '2018-02-22'
self.acct_line.recurring_rule_type = 'yearly'
self.contract.recurring_create_invoice()
invoices_weekly = self.env['account.invoice'].search(
@ -195,8 +199,8 @@ class TestContract(TestContractBase):
)
def test_contract_monthly_lastday(self):
recurring_next_date = to_date('2016-03-31')
self.acct_line.recurring_next_date = '2016-02-29'
recurring_next_date = to_date('2018-03-31')
self.acct_line.recurring_next_date = '2018-02-22'
self.acct_line.recurring_invoicing_type = 'post-paid'
self.acct_line.recurring_rule_type = 'monthlylastday'
self.contract.recurring_create_invoice()
@ -216,7 +220,7 @@ class TestContract(TestContractBase):
)
def test_onchange_date_start(self):
recurring_next_date = to_date('2016-01-01')
recurring_next_date = to_date('2018-01-01')
self.acct_line.date_start = recurring_next_date
self.acct_line._onchange_date_start()
self.assertEqual(
@ -255,8 +259,8 @@ class TestContract(TestContractBase):
with self.assertRaises(ValidationError):
self.acct_line.write(
{
'date_start': '2017-01-01',
'recurring_next_date': '2016-01-01',
'date_start': '2018-01-01',
'recurring_next_date': '2017-01-01',
}
)
@ -403,16 +407,17 @@ class TestContract(TestContractBase):
def test_compute_create_invoice_visibility(self):
self.acct_line.write(
{
'recurring_next_date': '2017-01-01',
'date_start': '2016-01-01',
'recurring_next_date': '2018-01-15',
'date_start': '2018-01-01',
'is_auto_renew': False,
'date_end': False,
}
)
self.assertTrue(self.contract.create_invoice_visibility)
self.acct_line.date_end = '2017-01-01'
self.acct_line.date_end = '2018-02-01'
self.contract.refresh()
self.assertTrue(self.contract.create_invoice_visibility)
self.acct_line.date_end = '2016-01-01'
self.acct_line.date_end = '2018-01-01'
self.contract.refresh()
self.assertFalse(self.contract.create_invoice_visibility)
@ -521,12 +526,534 @@ class TestContract(TestContractBase):
def test_date_end(self):
"""recurring next date for a contract is the min for all lines"""
self.assertFalse(self.contract.date_end)
self.assertEqual(self.acct_line.date_end, to_date('2019-01-01'))
self.acct_line.date_end = '2018-01-01'
self.assertEqual(
self.contract.date_end,
max(self.contract.recurring_invoice_line_ids.mapped('date_end')),
)
self.assertEqual(self.acct_line.date_end, to_date('2018-01-01'))
self.acct_line.copy()
self.acct_line.date_end = False
self.acct_line.write({'date_end': False, 'is_auto_renew': False})
self.assertFalse(self.contract.date_end)
def test_stop_contract_line(self):
"""It should put end to the contract line"""
self.acct_line.write(
{
'date_start': fields.Date.today(),
'recurring_next_date': fields.Date.today(),
'date_end': fields.Date.today() + relativedelta(months=7),
'is_auto_renew': True,
}
)
self.acct_line.stop(fields.Date.today() + relativedelta(months=5))
self.assertEqual(
self.acct_line.date_end,
fields.Date.today() + relativedelta(months=5),
)
def test_stop_upcoming_contract_line(self):
"""It should put end to the contract line"""
self.acct_line.write(
{
'date_start': fields.Date.today() + relativedelta(months=3),
'recurring_next_date': fields.Date.today()
+ relativedelta(months=3),
'date_end': fields.Date.today() + relativedelta(months=7),
'is_auto_renew': True,
}
)
self.acct_line.stop(fields.Date.today())
self.assertEqual(
self.acct_line.date_end,
fields.Date.today() + relativedelta(months=7),
)
self.assertTrue(self.acct_line.is_canceled)
def test_stop_past_contract_line(self):
"""Past contract line are ignored on stop"""
self.acct_line.write(
{
'date_end': fields.Date.today() + relativedelta(months=5),
'is_auto_renew': True,
}
)
self.acct_line.stop(fields.Date.today() + relativedelta(months=7))
self.assertEqual(
self.acct_line.date_end,
fields.Date.today() + relativedelta(months=5),
)
def test_stop_contract_line_without_date_end(self):
"""Past contract line are ignored on stop"""
self.acct_line.write({'date_end': False, 'is_auto_renew': False})
self.acct_line.stop(fields.Date.today() + relativedelta(months=7))
self.assertEqual(
self.acct_line.date_end,
fields.Date.today() + relativedelta(months=7),
)
def test_stop_plan_successor_wizard(self):
self.acct_line.write(
{
'date_start': fields.Date.today(),
'recurring_next_date': fields.Date.today(),
'date_end': fields.Date.today() + relativedelta(months=5),
'is_auto_renew': True,
}
)
wizard = self.env['account.analytic.invoice.line.wizard'].create(
{
'date_end': fields.Date.today() + relativedelta(months=7),
'contract_line_id': self.acct_line.id,
}
)
wizard.stop()
self.assertEqual(
self.acct_line.date_end,
fields.Date.today() + relativedelta(months=7),
)
self.assertFalse(self.acct_line.is_auto_renew)
def test_stop_plan_successor_contract_line_1(self):
"""
* contract line end's before the suspension period:
-> apply stop
"""
suspension_start = fields.Date.today() + relativedelta(months=5)
suspension_end = fields.Date.today() + relativedelta(months=6)
start_date = fields.Date.today()
end_date = fields.Date.today() + relativedelta(months=4)
self.acct_line.write(
{
'date_start': start_date,
'recurring_next_date': start_date,
'date_end': end_date,
}
)
self.acct_line.stop_plan_successor(
suspension_start, suspension_end, True
)
self.assertEqual(self.acct_line.date_end, end_date)
new_line = self.env['account.analytic.invoice.line'].search(
[('predecessor_contract_line_id', '=', self.acct_line.id)]
)
self.assertFalse(new_line)
def test_stop_plan_successor_contract_line_2(self):
"""
* 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: suspension.date_end + (contract_line.date_end
- suspension.date_start)
"""
suspension_start = fields.Date.today() + relativedelta(months=3)
suspension_end = fields.Date.today() + relativedelta(months=5)
start_date = fields.Date.today()
end_date = fields.Date.today() + relativedelta(months=4)
self.acct_line.write(
{
'date_start': start_date,
'recurring_next_date': start_date,
'date_end': end_date,
}
)
self.acct_line.stop_plan_successor(
suspension_start, suspension_end, True
)
self.assertEqual(self.acct_line.date_end, suspension_start)
new_line = self.env['account.analytic.invoice.line'].search(
[('predecessor_contract_line_id', '=', self.acct_line.id)]
)
self.assertTrue(new_line)
new_date_end = suspension_end + (end_date - suspension_start)
self.assertEqual(new_line.date_start, suspension_end)
self.assertEqual(new_line.date_end, new_date_end)
def test_stop_plan_successor_contract_line_3(self):
"""
* 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: suspension.date_end + (suspension.date_end
- suspension.date_start)
"""
suspension_start = fields.Date.today() + relativedelta(months=3)
suspension_end = fields.Date.today() + relativedelta(months=5)
start_date = fields.Date.today()
end_date = fields.Date.today() + relativedelta(months=6)
self.acct_line.write(
{
'date_start': start_date,
'recurring_next_date': start_date,
'date_end': end_date,
}
)
self.acct_line.stop_plan_successor(
suspension_start, suspension_end, True
)
self.assertEqual(self.acct_line.date_end, suspension_start)
new_line = self.env['account.analytic.invoice.line'].search(
[('predecessor_contract_line_id', '=', self.acct_line.id)]
)
self.assertTrue(new_line)
new_date_end = end_date + (suspension_end - suspension_start)
self.assertEqual(new_line.date_start, suspension_end)
self.assertEqual(new_line.date_end, new_date_end)
def test_stop_plan_successor_contract_line_3_without_end_date(self):
"""
* 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: suspension.date_end + (suspension.date_end
- suspension.date_start)
"""
suspension_start = fields.Date.today() + relativedelta(months=3)
suspension_end = fields.Date.today() + relativedelta(months=5)
start_date = fields.Date.today()
end_date = False
self.acct_line.write(
{
'date_start': start_date,
'recurring_next_date': start_date,
'date_end': end_date,
'is_auto_renew': False,
}
)
self.acct_line.stop_plan_successor(
suspension_start, suspension_end, False
)
self.assertEqual(self.acct_line.date_end, suspension_start)
new_line = self.env['account.analytic.invoice.line'].search(
[('predecessor_contract_line_id', '=', self.acct_line.id)]
)
self.assertTrue(new_line)
self.assertEqual(new_line.date_start, suspension_end)
self.assertFalse(new_line.date_end)
def test_stop_plan_successor_contract_line_4(self):
"""
* contract line start and end's in the suspension period
-> apply delay
- delay: suspension.date_end - contract_line.end_date
"""
suspension_start = fields.Date.today() + relativedelta(months=2)
suspension_end = fields.Date.today() + relativedelta(months=5)
start_date = fields.Date.today() + relativedelta(months=3)
end_date = fields.Date.today() + relativedelta(months=4)
self.acct_line.write(
{
'date_start': start_date,
'recurring_next_date': start_date,
'date_end': end_date,
}
)
self.acct_line.stop_plan_successor(
suspension_start, suspension_end, True
)
self.assertEqual(
self.acct_line.date_start, start_date + (suspension_end - end_date)
)
self.assertEqual(
self.acct_line.date_end, end_date + (suspension_end - end_date)
)
new_line = self.env['account.analytic.invoice.line'].search(
[('predecessor_contract_line_id', '=', self.acct_line.id)]
)
self.assertFalse(new_line)
def test_stop_plan_successor_contract_line_5(self):
"""
* contract line start in the suspension period and end after it
-> apply delay
- delay: suspension.date_end - contract_line.date_start
"""
suspension_start = fields.Date.today() + relativedelta(months=2)
suspension_end = fields.Date.today() + relativedelta(months=5)
start_date = fields.Date.today() + relativedelta(months=3)
end_date = fields.Date.today() + relativedelta(months=6)
self.acct_line.write(
{
'date_start': start_date,
'recurring_next_date': start_date,
'date_end': end_date,
}
)
self.acct_line.stop_plan_successor(
suspension_start, suspension_end, True
)
self.assertEqual(
self.acct_line.date_start,
start_date + (suspension_end - start_date),
)
self.assertEqual(
self.acct_line.date_end, end_date + (suspension_end - start_date)
)
new_line = self.env['account.analytic.invoice.line'].search(
[('predecessor_contract_line_id', '=', self.acct_line.id)]
)
self.assertFalse(new_line)
def test_stop_plan_successor_contract_line_5_without_date_end(self):
"""
* contract line start in the suspension period and end after it
-> apply delay
- delay: suspension.date_end - contract_line.date_start
"""
suspension_start = fields.Date.today() + relativedelta(months=2)
suspension_end = fields.Date.today() + relativedelta(months=5)
start_date = fields.Date.today() + relativedelta(months=3)
end_date = False
self.acct_line.write(
{
'date_start': start_date,
'recurring_next_date': start_date,
'date_end': end_date,
'is_auto_renew': False,
}
)
self.acct_line.stop_plan_successor(
suspension_start, suspension_end, True
)
self.assertEqual(
self.acct_line.date_start,
start_date + (suspension_end - start_date),
)
self.assertFalse(self.acct_line.date_end)
new_line = self.env['account.analytic.invoice.line'].search(
[('predecessor_contract_line_id', '=', self.acct_line.id)]
)
self.assertFalse(new_line)
def test_stop_plan_successor_contract_line_6(self):
"""
* contract line start and end after the suspension period
-> apply delay
- delay: suspension.date_end - suspension.start_date
"""
suspension_start = fields.Date.today() + relativedelta(months=2)
suspension_end = fields.Date.today() + relativedelta(months=3)
start_date = fields.Date.today() + relativedelta(months=4)
end_date = fields.Date.today() + relativedelta(months=6)
self.acct_line.write(
{
'date_start': start_date,
'recurring_next_date': start_date,
'date_end': end_date,
}
)
self.acct_line.stop_plan_successor(
suspension_start, suspension_end, True
)
self.assertEqual(
self.acct_line.date_start,
start_date + (suspension_end - suspension_start),
)
self.assertEqual(
self.acct_line.date_end,
end_date + (suspension_end - suspension_start),
)
new_line = self.env['account.analytic.invoice.line'].search(
[('predecessor_contract_line_id', '=', self.acct_line.id)]
)
self.assertFalse(new_line)
def test_stop_plan_successor_contract_line_6_without_date_end(self):
"""
* contract line start and end after the suspension period
-> apply delay
- delay: suspension.date_end - suspension.start_date
"""
suspension_start = fields.Date.today() + relativedelta(months=2)
suspension_end = fields.Date.today() + relativedelta(months=3)
start_date = fields.Date.today() + relativedelta(months=4)
end_date = False
self.acct_line.write(
{
'date_start': start_date,
'recurring_next_date': start_date,
'date_end': end_date,
'is_auto_renew': False,
}
)
self.acct_line.stop_plan_successor(
suspension_start, suspension_end, True
)
self.assertEqual(
self.acct_line.date_start,
start_date + (suspension_end - suspension_start),
)
self.assertFalse(self.acct_line.date_end)
new_line = self.env['account.analytic.invoice.line'].search(
[('predecessor_contract_line_id', '=', self.acct_line.id)]
)
self.assertFalse(new_line)
def test_stop_plan_successor_wizard(self):
suspension_start = fields.Date.today() + relativedelta(months=2)
suspension_end = fields.Date.today() + relativedelta(months=3)
start_date = fields.Date.today() + relativedelta(months=4)
end_date = fields.Date.today() + relativedelta(months=6)
self.acct_line.write(
{
'date_start': start_date,
'recurring_next_date': start_date,
'date_end': end_date,
}
)
wizard = self.env['account.analytic.invoice.line.wizard'].create(
{
'date_start': suspension_start,
'date_end': suspension_end,
'is_auto_renew': False,
'contract_line_id': self.acct_line.id,
}
)
wizard.stop_plan_successor()
self.assertEqual(
self.acct_line.date_start,
start_date + (suspension_end - suspension_start),
)
self.assertEqual(
self.acct_line.date_end,
end_date + (suspension_end - suspension_start),
)
new_line = self.env['account.analytic.invoice.line'].search(
[('predecessor_contract_line_id', '=', self.acct_line.id)]
)
self.assertFalse(new_line)
def test_plan_successor_contract_line(self):
self.acct_line.write(
{
'date_start': fields.Date.today(),
'recurring_next_date': fields.Date.today(),
'date_end': fields.Date.today() + relativedelta(months=3),
'is_auto_renew': False,
}
)
self.acct_line.plan_successor(
fields.Date.today() + relativedelta(months=5),
fields.Date.today() + relativedelta(months=7),
True,
)
new_line = self.env['account.analytic.invoice.line'].search(
[('predecessor_contract_line_id', '=', self.acct_line.id)]
)
self.assertFalse(self.acct_line.is_auto_renew)
self.assertTrue(new_line.is_auto_renew)
self.assertTrue(new_line, "should create a new contract line")
self.assertEqual(
new_line.date_start, fields.Date.today() + relativedelta(months=5)
)
self.assertEqual(
new_line.date_end, fields.Date.today() + relativedelta(months=7)
)
def test_overlap(self):
self.acct_line.write(
{
'date_start': fields.Date.today(),
'recurring_next_date': fields.Date.today(),
'date_end': fields.Date.today() + relativedelta(months=3),
'is_auto_renew': False,
}
)
self.acct_line.plan_successor(
fields.Date.today() + relativedelta(months=5),
fields.Date.today() + relativedelta(months=7),
True,
)
new_line = self.env['account.analytic.invoice.line'].search(
[('predecessor_contract_line_id', '=', self.acct_line.id)]
)
with self.assertRaises(ValidationError):
new_line.date_start = fields.Date.today() + relativedelta(months=2)
with self.assertRaises(ValidationError):
self.acct_line.date_end = fields.Date.today() + relativedelta(
months=6
)
def test_plan_successor_wizard(self):
self.acct_line.write(
{
'date_start': fields.Date.today(),
'recurring_next_date': fields.Date.today(),
'date_end': fields.Date.today() + relativedelta(months=2),
'is_auto_renew': False,
}
)
wizard = self.env['account.analytic.invoice.line.wizard'].create(
{
'date_start': fields.Date.today() + relativedelta(months=3),
'date_end': fields.Date.today() + relativedelta(months=5),
'is_auto_renew': True,
'contract_line_id': self.acct_line.id,
}
)
wizard.plan_successor()
new_line = self.env['account.analytic.invoice.line'].search(
[('predecessor_contract_line_id', '=', self.acct_line.id)]
)
self.assertFalse(self.acct_line.is_auto_renew)
self.assertTrue(new_line.is_auto_renew)
self.assertTrue(new_line, "should create a new contract line")
self.assertEqual(
new_line.date_start, fields.Date.today() + relativedelta(months=3)
)
self.assertEqual(
new_line.date_end, fields.Date.today() + relativedelta(months=5)
)
def test_cancel(self):
self.acct_line.cancel()
self.assertTrue(self.acct_line.is_canceled)
self.acct_line.uncancel(fields.Date.today())
self.assertFalse(self.acct_line.is_canceled)
def test_check_has_not_date_end_has_successor(self):
self.acct_line.write({'date_end': False, 'is_auto_renew': False})
with self.assertRaises(ValidationError):
self.acct_line.plan_successor(
to_date('2016-03-01'), to_date('2016-09-01'), False
)
def test_check_has_not_date_end_is_auto_renew(self):
with self.assertRaises(ValidationError):
self.acct_line.write({'date_end': False, 'is_auto_renew': True})
def test_check_has_successor_is_auto_renew(self):
with self.assertRaises(ValidationError):
self.acct_line.plan_successor(
to_date('2016-03-01'), to_date('2018-09-01'), False
)
def test_search_contract_line_to_renew(self):
self.acct_line.write({'date_end': fields.Date.today()})
line_1 = self.acct_line.copy(
{'date_end': fields.Date.today() + relativedelta(months=1)}
)
line_2 = self.acct_line.copy(
{'date_end': fields.Date.today() - relativedelta(months=1)}
)
line_3 = self.acct_line.copy(
{'date_end': fields.Date.today() - relativedelta(months=2)}
)
self.acct_line.copy(
{'date_end': fields.Date.today() + relativedelta(months=2)}
)
to_renew = self.acct_line.search(
self.acct_line._contract_line_to_renew_domain()
)
self.assertEqual(
set(to_renew), set((self.acct_line, line_1, line_2, line_3))
)
def test_renew(self):
new_line = self.acct_line.renew()
self.assertFalse(self.acct_line.is_auto_renew)
self.assertTrue(new_line.is_auto_renew)
self.assertEqual(new_line.date_start, to_date('2019-01-01'))
self.assertEqual(new_line.date_end, to_date('2020-01-01'))

84
contract/views/abstract_contract_line.xml

@ -1,37 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="account_abstract_analytic_contract_line_view_form" model="ir.ui.view">
<field name="name">Account Abstract Analytic Contract Line Form View</field>
<record id="account_abstract_analytic_contract_line_view_form"
model="ir.ui.view">
<field name="name">Account Abstract Analytic Contract Line Form View
</field>
<field name="model">account.abstract.analytic.contract.line</field>
<field name="arch" type="xml">
<form>
<group>
<field name="product_id"/>
<field name="name"/>
<field name="quantity" colspan="2"/>
<field name="uom_id" colspan="2"/>
<field name="automatic_price"/>
<field name="specific_price" invisible="1"/>
<field name="price_unit"
attrs="{'readonly': [('automatic_price', '=', True)]}"
colspan="2"/>
<field name="discount" colspan="2"/>
</group>
<group name="recurrence_info">
<group>
<field name="recurring_invoicing_type"/>
<sheet>
<group col="4">
<field colspan="4" name="product_id"/>
<field colspan="4" name="name"/>
<field colspan="2" name="quantity"/>
<field colspan="2" name="uom_id"/>
<field colspan="2" name="automatic_price"/>
<field name="specific_price" invisible="1"/>
<field colspan="2" name="price_unit"
attrs="{'readonly': [('automatic_price', '=', True)]}"/>
<field colspan="2" name="discount"/>
</group>
<group col="4">
<field colspan="2" name="is_auto_renew"/>
<field colspan="2" name="is_canceled" invisible="1"/>
</group>
<group>
<label for="recurring_interval"/>
<div>
<field name="recurring_interval"
class="oe_inline"/>
<field name="recurring_rule_type"
class="oe_inline"/>
</div>
<group attrs="{'invisible':[('is_auto_renew', '=', False)]}">
<group>
<label for="auto_renew_interval"/>
<div>
<field name="auto_renew_interval"
class="oe_inline" nolabel="1"
attrs="{'required':[('is_auto_renew', '=', True)]}"/>
<field name="auto_renew_rule_type"
class="oe_inline" nolabel="1"
attrs="{'required':[('is_auto_renew', '=', True)]}"/>
</div>
</group>
<group>
<label for="termination_notice_interval"/>
<div>
<field name="termination_notice_interval"
class="oe_inline" nolabel="1"
attrs="{'required':[('is_auto_renew', '=', True)]}"/>
<field name="termination_notice_rule_type"
class="oe_inline" nolabel="1"
attrs="{'required':[('is_auto_renew', '=', True)]}"/>
</div>
</group>
</group>
<group name="recurrence_info">
<group>
<field name="recurring_invoicing_type"/>
</group>
<group>
<label for="recurring_interval"/>
<div>
<field name="recurring_interval"
class="oe_inline" nolabel="1"/>
<field name="recurring_rule_type"
class="oe_inline" nolabel="1"/>
</div>
</group>
</group>
</group>
</sheet>
</form>
</field>
</record>

207
contract/views/contract.xml

@ -1,19 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="account_analytic_account_recurring_form_form" model="ir.ui.view">
<record id="account_analytic_account_recurring_form_form"
model="ir.ui.view">
<field name="name">Contract form</field>
<field name="model">account.analytic.account</field>
<field name="inherit_id" ref="analytic.view_account_analytic_account_form"/>
<field name="inherit_id"
ref="analytic.view_account_analytic_account_form"/>
<field name="mode">primary</field>
<field name="priority" eval="9999"/>
<field name="arch" type="xml">
<field name="partner_id" position="attributes">
<attribute name="attrs">{'required': [('recurring_invoices', '=', True)]}</attribute>
<attribute name="attrs">{'required': [('recurring_invoices',
'=', True)]}
</attribute>
</field>
<xpath expr="//div[@name='button_box']/.." position="before">
<header>
<button name="action_contract_send" type="object" string="Send by Email" groups="base.group_user"/>
<button name="action_contract_send" type="object"
string="Send by Email" groups="base.group_user"/>
</header>
</xpath>
<xpath expr='//field[@name="code"]' position='before'>
@ -22,27 +27,30 @@
<group name="main" position="after">
<separator string="Recurring Invoices"
attrs="{'invisible': [('recurring_invoices','!=',True)]}"
/>
/>
<div>
<field name="recurring_invoices" class="oe_inline"/>
<field name="create_invoice_visibility" invisible="1"/>
<label for="recurring_invoices" />
<label for="recurring_invoices"/>
<button name="recurring_create_invoice"
type="object"
attrs="{'invisible': ['|', ('recurring_invoices', '!=', True), ('create_invoice_visibility', '=', False)]}"
string="Create invoices"
class="oe_link"
groups="base.group_no_one"
/>
<button name="button_show_recurring_invoices"
type="object"
/>
<button name="contract.act_recurring_invoices"
type="action"
attrs="{'invisible': [('recurring_invoices','!=',True)]}"
string="⇒ Show recurring invoices"
class="oe_link"
/>
/>
</div>
<group col="4" attrs="{'invisible': [('recurring_invoices','!=',True)]}">
<field name="contract_template_id" colspan="4" domain="['|', ('contract_type', '=', contract_type), ('contract_type', '=', False)]" context="{'default_contract_type': contract_type}"/>
<group col="4"
attrs="{'invisible': [('recurring_invoices','!=',True)]}">
<field name="contract_template_id" colspan="4"
domain="['|', ('contract_type', '=', contract_type), ('contract_type', '=', False)]"
context="{'default_contract_type': contract_type}"/>
<field name="journal_id"
domain="[('type', '=', contract_type),('company_id', '=', company_id)]"
attrs="{'required': [('recurring_invoices', '=', True)]}"
@ -50,37 +58,85 @@
<field name="pricelist_id"/>
<field name="recurring_next_date"/>
<field name="date_end"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="company_id"
groups="base.group_multi_company"/>
</group>
<label for="recurring_invoice_line_ids"
attrs="{'invisible': [('recurring_invoices','=',False)]}"
/>
/>
<div attrs="{'invisible': [('recurring_invoices','=',False)]}">
<field name="recurring_invoice_line_ids">
<tree string="Account Analytic Lines">
<field name="sequence" widget="handle" />
<tree decoration-muted="is_canceled">
<field name="sequence" widget="handle"/>
<field name="product_id"/>
<field name="name"/>
<field name="quantity"/>
<field name="uom_id"/>
<field name="automatic_price"/>
<field name="price_unit" attrs="{'readonly': [('automatic_price', '=', True)]}"/>
<field name="price_unit"
attrs="{'readonly': [('automatic_price', '=', True)]}"/>
<field name="specific_price" invisible="1"/>
<field name="discount" groups="base.group_no_one" />
<field name="discount" groups="base.group_no_one"/>
<field name="price_subtotal"/>
<field name="recurring_interval" invisible="1"/>
<field name="recurring_rule_type" invisible="1"/>
<field name="recurring_invoicing_type" invisible="1"/>
<field name="recurring_invoicing_type"
invisible="1"/>
<field name="date_start" required="1"/>
<field name="date_end"/>
<field name="recurring_next_date" required="1"/>
<field name="create_invoice_visibility"
invisible="1"/>
<field name="is_plan_successor_allowed" invisible="1"/>
<field name="is_stop_plan_successor_allowed" invisible="1"/>
<field name="is_stop_allowed" invisible="1"/>
<field name="is_cancel_allowed" invisible="1"/>
<field name="is_un_cancel_allowed" invisible="1"/>
<field name="is_auto_renew" invisible="1"/>
<field name="is_canceled" invisible="1"/>
<button name="action_plan_successor"
string="Plan Start"
type="object"
icon="fa-calendar text-success"
attrs="{'invisible': [('is_plan_successor_allowed', '=', False)]}"/>
<button name="action_stop_plan_successor"
string="Stop Plan Successor"
type="object"
icon="fa-pause text-muted"
attrs="{'invisible': [('is_stop_plan_successor_allowed', '=', False)]}"/>
<button name="action_stop"
string="Stop"
type="object"
icon="fa-stop text-danger"
attrs="{'invisible': [('is_stop_allowed', '=', False)]}"/>
<button name="cancel"
string="Cancel"
type="object"
icon="fa-ban text-danger"
attrs="{'invisible': [('is_cancel_allowed', '=', False)]}"/>
<button name="action_uncancel"
string="Un-cancel"
type="object"
icon="fa-ban text-success"
attrs="{'invisible': [('is_un_cancel_allowed', '=', False)]}"/>
<button name="renew"
string="Renew"
type="object"
icon="fa-fast-forward text-success"
groups="base.group_no_one"
attrs="{'invisible': [('is_auto_renew', '=', False)]}"/>
</tree>
</field>
</div>
<group string="Legend (for the markers inside invoice lines description)"
name="group_legend" attrs="{'invisible': [('recurring_invoices','!=',True)]}">
<p colspan="2"> <strong>#START#</strong>: Start date of the invoiced period</p>
<p colspan="2"> <strong>#END#</strong>: End date of the invoiced period</p>
name="group_legend"
attrs="{'invisible': [('recurring_invoices','!=',True)]}">
<p colspan="2"><strong>#START#</strong>: Start date of the
invoiced period
</p>
<p colspan="2"><strong>#END#</strong>: End date of the
invoiced period
</p>
</group>
</group>
</field>
@ -89,14 +145,17 @@
<record id="account_analytic_account_sale_form" model="ir.ui.view">
<field name="name">account.analytic.account.sale.form</field>
<field name="model">account.analytic.account</field>
<field name="inherit_id" ref="account_analytic_account_recurring_form_form"/>
<field name="inherit_id"
ref="account_analytic_account_recurring_form_form"/>
<field name="mode">primary</field>
<field name="priority" eval="20"/>
<field name="arch" type="xml">
<field name="partner_id" position="attributes">
<attribute name="string">Customer</attribute>
<attribute name="domain">[('customer', '=', True)]</attribute>
<attribute name="context">{'default_customer': True, 'default_supplier': False}</attribute>
<attribute name="context">{'default_customer': True,
'default_supplier': False}
</attribute>
</field>
<field name="journal_id" position="attributes">
<attribute name="domain">[('type', '=', 'sale'),('company_id', '=', company_id)]</attribute>
@ -110,22 +169,27 @@
<record id="account_analytic_account_purchase_form" model="ir.ui.view">
<field name="name">account.analytic.account.purchase.form</field>
<field name="model">account.analytic.account</field>
<field name="inherit_id" ref="account_analytic_account_recurring_form_form"/>
<field name="inherit_id"
ref="account_analytic_account_recurring_form_form"/>
<field name="mode">primary</field>
<field name="priority" eval="20"/>
<field name="arch" type="xml">
<field name="partner_id" position="attributes">
<attribute name="string">Supplier</attribute>
<attribute name="domain">[('supplier', '=', True)]</attribute>
<attribute name="context">{'default_customer': False, 'default_supplier': True}</attribute>
<attribute name="context">{'default_customer': False,
'default_supplier': True}
</attribute>
</field>
<field name="journal_id" position="attributes">
<attribute name="domain">[('type', '=', 'purchase'),('company_id', '=', company_id)]</attribute>
</field>
<field name="product_id" position="attributes">
<attribute name="domain">[('purchase_ok', '=', True)]</attribute>
<attribute name="domain">[('purchase_ok', '=', True)]
</attribute>
</field>
<xpath expr="//field[@name='recurring_invoice_line_ids']/tree/field[@name='automatic_price']" position="attributes">
<xpath expr="//field[@name='recurring_invoice_line_ids']/tree/field[@name='automatic_price']"
position="attributes">
<attribute name="invisible">True</attribute>
</xpath>
</field>
@ -135,7 +199,8 @@
<record id="view_account_analytic_account_journal_tree" model="ir.ui.view">
<field name="name">Contract list</field>
<field name="model">account.analytic.account</field>
<field name="inherit_id" ref="analytic.view_account_analytic_account_list" />
<field name="inherit_id"
ref="analytic.view_account_analytic_account_list"/>
<field name="mode">primary</field>
<field name="priority" eval="9999"/>
<field name="arch" type="xml">
@ -146,13 +211,16 @@
</record>
<!-- Analytic Account search view for contract -->
<record id="view_account_analytic_account_contract_search" model="ir.ui.view">
<record id="view_account_analytic_account_contract_search"
model="ir.ui.view">
<field name="name">Contract search</field>
<field name="model">account.analytic.account</field>
<field name="inherit_id" ref="analytic.view_account_analytic_account_search"/>
<field name="inherit_id"
ref="analytic.view_account_analytic_account_search"/>
<field name="arch" type="xml">
<field name="partner_id" position="after">
<field name="partner_id" filter_domain="[('partner_id', 'child_of', self)]"
<field name="partner_id"
filter_domain="[('partner_id', 'child_of', self)]"
string="Partner and dependents"/>
</field>
<field name="name" position="after">
@ -162,41 +230,47 @@
<filter name="recurring_invoices"
string="Recurring Invoices"
domain="[('recurring_invoices','=',True)]"
/>
/>
<separator/>
<filter name="not_finished"
string="Valid"
domain="['|', ('date_end', '=', False), ('date_end', '&gt;=', time.strftime('%Y-%m-%d'))]"
/>
/>
<filter name="finished"
string="Finished"
domain="[('date_end', '&lt;', time.strftime('%Y-%m-%d'))]"
/>
/>
<group expand="0" string="Group By...">
<filter name="next_invoice"
string="Next Invoice"
domain="[]"
context="{'group_by':'recurring_next_date'}"
/>
/>
<filter name="date_end"
string="Date End"
domain="[]"
context="{'group_by':'date_end'}"
/>
/>
</group>
</field>
</field>
</record>
<!-- Action Sales/Sales/Contracts -->
<record id="action_account_analytic_sale_overdue_all" model="ir.actions.act_window">
<record id="action_account_analytic_sale_overdue_all"
model="ir.actions.act_window">
<field name="name">Customer Contracts</field>
<field name="res_model">account.analytic.account</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="domain">[('contract_type', '=', 'sale')]</field>
<field name="context">{'is_contract':1, 'search_default_not_finished':1, 'search_default_recurring_invoices':1, 'default_recurring_invoices': 1, 'default_contract_type': 'sale'}</field>
<field name="search_view_id" ref="view_account_analytic_account_contract_search"/>
<field name="context">{'is_contract':1,
'search_default_not_finished':1,
'search_default_recurring_invoices':1,
'default_recurring_invoices': 1, 'default_contract_type': 'sale'}
</field>
<field name="search_view_id"
ref="view_account_analytic_account_contract_search"/>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a new contract.
@ -204,60 +278,77 @@
</field>
</record>
<record id="action_account_analytic_sale_overdue_all_tree" model="ir.actions.act_window.view">
<record id="action_account_analytic_sale_overdue_all_tree"
model="ir.actions.act_window.view">
<field name="sequence" eval="1"/>
<field name="view_mode">tree</field>
<field name="view_id" ref="view_account_analytic_account_journal_tree"/>
<field name="act_window_id" ref="action_account_analytic_sale_overdue_all"/>
<field name="view_id"
ref="view_account_analytic_account_journal_tree"/>
<field name="act_window_id"
ref="action_account_analytic_sale_overdue_all"/>
</record>
<record id="action_account_analytic_sale_overdue_all_form" model="ir.actions.act_window.view">
<record id="action_account_analytic_sale_overdue_all_form"
model="ir.actions.act_window.view">
<field name="sequence" eval="2"/>
<field name="view_mode">form</field>
<field name="view_id" ref="account_analytic_account_sale_form"/>
<field name="act_window_id" ref="action_account_analytic_sale_overdue_all"/>
<field name="act_window_id"
ref="action_account_analytic_sale_overdue_all"/>
</record>
<menuitem id="menu_action_account_analytic_sale_overdue_all"
parent="account.menu_finance_receivables"
action="action_account_analytic_sale_overdue_all"
sequence="99"
/>
/>
<!-- Action Purchases/Purchases/Contracts -->
<record id="action_account_analytic_purchase_overdue_all" model="ir.actions.act_window">
<record id="action_account_analytic_purchase_overdue_all"
model="ir.actions.act_window">
<field name="name">Supplier Contracts</field>
<field name="res_model">account.analytic.account</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="domain">[('contract_type', '=', 'purchase')]</field>
<field name="context">{'is_contract':1, 'search_default_not_finished':1, 'search_default_recurring_invoices':1, 'default_recurring_invoices': 1, 'default_contract_type': 'purchase'}</field>
<field name="search_view_id" ref="view_account_analytic_account_contract_search"/>
<field name="context">{'is_contract':1,
'search_default_not_finished':1,
'search_default_recurring_invoices':1,
'default_recurring_invoices': 1, 'default_contract_type':
'purchase'}
</field>
<field name="search_view_id"
ref="view_account_analytic_account_contract_search"/>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a new contract.
</p>
</field>
</record>
<record id="action_account_analytic_purchase_overdue_all_tree" model="ir.actions.act_window.view">
<record id="action_account_analytic_purchase_overdue_all_tree"
model="ir.actions.act_window.view">
<field name="sequence" eval="1"/>
<field name="view_mode">tree</field>
<field name="view_id" ref="view_account_analytic_account_journal_tree"/>
<field name="act_window_id" ref="action_account_analytic_purchase_overdue_all"/>
<field name="view_id"
ref="view_account_analytic_account_journal_tree"/>
<field name="act_window_id"
ref="action_account_analytic_purchase_overdue_all"/>
</record>
<record id="action_account_analytic_purchase_overdue_all_form" model="ir.actions.act_window.view">
<record id="action_account_analytic_purchase_overdue_all_form"
model="ir.actions.act_window.view">
<field name="sequence" eval="2"/>
<field name="view_mode">form</field>
<field name="view_id" ref="account_analytic_account_purchase_form"/>
<field name="act_window_id" ref="action_account_analytic_purchase_overdue_all"/>
<field name="act_window_id"
ref="action_account_analytic_purchase_overdue_all"/>
</record>
<menuitem id="menu_action_account_analytic_purchase_overdue_all"
parent="account.menu_finance_payables"
action="action_account_analytic_purchase_overdue_all"
sequence="99"
/>
/>
</odoo>

13
contract/views/contract_line.xml

@ -8,18 +8,27 @@
ref="account_abstract_analytic_contract_line_view_form"/>
<field name="mode">primary</field>
<field name="arch" type="xml">
<xpath expr="//sheet" position="before">
<header>
<field name="state" widget="statusbar"/>
</header>
</xpath>
<xpath expr="//form" position="attributes">
<attribute name="string">Contract Line</attribute>
</xpath>
<xpath expr="//group[@name='recurrence_info']" position="inside">
<group>
<field name="date_start" required="1"/>
<field name="recurring_next_date"/>
</group>
<group>
<field name="date_end"/>
<field name="date_end" attrs="{'required': [('is_auto_renew', '=', True)]}"/>
</group>
<group>
<field name="recurring_next_date"/>
<field name="predecessor_contract_line_id"/>
</group>
<group>
<field name="successor_contract_line_id"/>
</group>
</xpath>
</field>

1
contract/wizards/__init__.py

@ -0,0 +1 @@
from . import contract_line_wizard

48
contract/wizards/contract_line_wizard.py

@ -0,0 +1,48 @@
# Copyright 2018 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class AccountAnalyticInvoiceLineWizard(models.TransientModel):
_name = 'account.analytic.invoice.line.wizard'
_description = 'Contract Line Wizard'
date_start = fields.Date(string='Date Start')
date_end = fields.Date(string='Date End')
recurring_next_date = fields.Date(string='Next Invoice Date')
is_auto_renew = fields.Boolean(string="Auto Renew", default=False)
contract_line_id = fields.Many2one(
comodel_name="account.analytic.invoice.line",
string="Contract Line",
required=True,
)
@api.multi
def stop(self):
for wizard in self:
wizard.contract_line_id.stop(wizard.date_end)
return True
@api.multi
def plan_successor(self):
for wizard in self:
wizard.contract_line_id.plan_successor(
wizard.date_start, wizard.date_end, wizard.is_auto_renew
)
return True
@api.multi
def stop_plan_successor(self):
for wizard in self:
wizard.contract_line_id.stop_plan_successor(
wizard.date_start, wizard.date_end, wizard.is_auto_renew
)
return True
@api.multi
def uncancel(self):
for wizard in self:
wizard.contract_line_id.uncancel(wizard.recurring_next_date)
return True

99
contract/wizards/contract_line_wizard.xml

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2018 ACSONE SA/NV
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record model="ir.ui.view" id="contract_line_wizard_stop_form_view">
<field name="name">contract.line.stop.wizard.form (in contract)</field>
<field name="model">account.analytic.invoice.line.wizard</field>
<field name="arch" type="xml">
<form>
<group>
<field name="contract_line_id" invisible="True"/>
<field string="Stop Date" name="date_end" required="True"/>
</group>
<footer>
<button name="stop"
string="Validate"
class="btn-primary"
type="object"/>
<button string="Cancel"
class="btn-default"
special="cancel"/>
</footer>
</form>
</field>
</record>
<record model="ir.ui.view" id="contract_line_wizard_plan_successor_form_view">
<field name="name">contract.line.plan_successor.wizard.form (in contract)</field>
<field name="model">account.analytic.invoice.line.wizard</field>
<field name="arch" type="xml">
<form>
<group>
<field name="contract_line_id" invisible="True"/>
<field name="date_start" required="True"/>
<field name="date_end" attrs="{'required': [('is_auto_renew', '=', True)]}"/>
<field name="is_auto_renew"/>
</group>
<footer>
<button name="plan_successor"
string="Validate"
class="btn-primary"
type="object"/>
<button string="Cancel"
class="btn-default"
special="cancel"/>
</footer>
</form>
</field>
</record>
<record model="ir.ui.view" id="contract_line_wizard_stop_plan_successor_form_view">
<field name="name">contract.line.stop_plan_successor.wizard.form (in contract)</field>
<field name="model">account.analytic.invoice.line.wizard</field>
<field name="arch" type="xml">
<form>
<group>
<field name="contract_line_id" invisible="True"/>
<field string="Suspension Start Date" name="date_start" required="True"/>
<field string="Suspension End Date" name="date_end" required="True"/>
<field name="is_auto_renew" invisible="1"/>
</group>
<footer>
<button name="stop_plan_successor"
string="Validate"
class="btn-primary"
type="object"/>
<button string="Cancel"
class="btn-default"
special="cancel"/>
</footer>
</form>
</field>
</record>
<record model="ir.ui.view" id="contract_line_wizard_uncancel_form_view">
<field name="name">contract.line.stop_plan_successor.wizard.form (in contract)</field>
<field name="model">account.analytic.invoice.line.wizard</field>
<field name="arch" type="xml">
<form>
<group>
<field name="contract_line_id" invisible="True"/>
<field name="recurring_next_date" required="True"/>
</group>
<footer>
<button name="uncancel"
string="Validate"
class="btn-primary"
type="object"/>
<button string="Cancel"
class="btn-default"
special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>
Loading…
Cancel
Save