diff --git a/contract/__init__.py b/contract/__init__.py index 0650744f..aee8895e 100644 --- a/contract/__init__.py +++ b/contract/__init__.py @@ -1 +1,2 @@ from . import models +from . import wizards diff --git a/contract/__manifest__.py b/contract/__manifest__.py index 8ab77964..0a7549f1 100644 --- a/contract/__manifest__.py +++ b/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', diff --git a/contract/data/__init__.py b/contract/data/__init__.py new file mode 100644 index 00000000..8cbf3fc0 --- /dev/null +++ b/contract/data/__init__.py @@ -0,0 +1 @@ +from . import contract_line_constraints diff --git a/contract/data/contract_line_constraints.py b/contract/data/contract_line_constraints.py new file mode 100644 index 00000000..74e3eb2f --- /dev/null +++ b/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 diff --git a/contract/data/contract_renew_cron.xml b/contract/data/contract_renew_cron.xml new file mode 100644 index 00000000..dfc83d91 --- /dev/null +++ b/contract/data/contract_renew_cron.xml @@ -0,0 +1,16 @@ + + + + + Renew Contract lines + + code + model.cron_renew_contract_line() + + 1 + days + -1 + + + + diff --git a/contract/models/abstract_contract_line.py b/contract/models/abstract_contract_line.py index 8596389b..d3c6b2d4 100644 --- a/contract/models/abstract_contract_line.py +++ b/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( diff --git a/contract/models/contract_line.py b/contract/models/contract_line.py index 2a8429b8..22f2525a 100644 --- a/contract/models/contract_line.py +++ b/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 {product} + delayed:
+ - Start: {old_date_start} -- {new_date_start} +
+ - End: {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 {product} + stopped:
+ - End: {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 {product} + planned a successor:
+ - Start: {new_date_start} +
+ - End: {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""" + % "
- ".join( + [ + "%s" % 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""" + % "
- ".join( + [ + "%s" % 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() diff --git a/contract/tests/test_contract.py b/contract/tests/test_contract.py index 87588b3f..5c87c8f8 100644 --- a/contract/tests/test_contract.py +++ b/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')) diff --git a/contract/views/abstract_contract_line.xml b/contract/views/abstract_contract_line.xml index db67f768..7626e416 100644 --- a/contract/views/abstract_contract_line.xml +++ b/contract/views/abstract_contract_line.xml @@ -1,37 +1,69 @@ - - Account Abstract Analytic Contract Line Form View + + Account Abstract Analytic Contract Line Form View + account.abstract.analytic.contract.line
- - - - - - - - - - - - - + + + + + + + + + + + + + + - - +
diff --git a/contract/views/contract.xml b/contract/views/contract.xml index 48827084..dad3ecc3 100644 --- a/contract/views/contract.xml +++ b/contract/views/contract.xml @@ -1,19 +1,24 @@ - + Contract form account.analytic.account - + primary - {'required': [('recurring_invoices', '=', True)]} + {'required': [('recurring_invoices', + '=', True)]} +
-
@@ -22,27 +27,30 @@ + />
-
- - + + - +
@@ -89,14 +145,17 @@ account.analytic.account.sale.form account.analytic.account - + primary Customer [('customer', '=', True)] - {'default_customer': True, 'default_supplier': False} + {'default_customer': True, + 'default_supplier': False} + [('type', '=', 'sale'),('company_id', '=', company_id)] @@ -110,22 +169,27 @@ account.analytic.account.purchase.form account.analytic.account - + primary Supplier [('supplier', '=', True)] - {'default_customer': False, 'default_supplier': True} + {'default_customer': False, + 'default_supplier': True} + [('type', '=', 'purchase'),('company_id', '=', company_id)] - [('purchase_ok', '=', True)] + [('purchase_ok', '=', True)] + - + True @@ -135,7 +199,8 @@ Contract list account.analytic.account - + primary @@ -146,13 +211,16 @@ - + Contract search account.analytic.account - + - @@ -162,41 +230,47 @@ + /> + /> + /> + /> + /> - + Customer Contracts account.analytic.account form tree,form [('contract_type', '=', 'sale')] - {'is_contract':1, 'search_default_not_finished':1, 'search_default_recurring_invoices':1, 'default_recurring_invoices': 1, 'default_contract_type': 'sale'} - + {'is_contract':1, + 'search_default_not_finished':1, + 'search_default_recurring_invoices':1, + 'default_recurring_invoices': 1, 'default_contract_type': 'sale'} + +

Click to create a new contract. @@ -204,60 +278,77 @@ - + tree - - + + - + form - + - + /> + - + Supplier Contracts account.analytic.account form tree,form [('contract_type', '=', 'purchase')] - {'is_contract':1, 'search_default_not_finished':1, 'search_default_recurring_invoices':1, 'default_recurring_invoices': 1, 'default_contract_type': 'purchase'} - + {'is_contract':1, + 'search_default_not_finished':1, + 'search_default_recurring_invoices':1, + 'default_recurring_invoices': 1, 'default_contract_type': + 'purchase'} + +

Click to create a new contract.

- - + + tree - - + + - + form - + - + + />
diff --git a/contract/views/contract_line.xml b/contract/views/contract_line.xml index a39bb4f2..3bed1596 100644 --- a/contract/views/contract_line.xml +++ b/contract/views/contract_line.xml @@ -8,18 +8,27 @@ ref="account_abstract_analytic_contract_line_view_form"/> primary + +
+ +
+
Contract Line + - + - + + + +
diff --git a/contract/wizards/__init__.py b/contract/wizards/__init__.py new file mode 100644 index 00000000..146dcc66 --- /dev/null +++ b/contract/wizards/__init__.py @@ -0,0 +1 @@ +from . import contract_line_wizard diff --git a/contract/wizards/contract_line_wizard.py b/contract/wizards/contract_line_wizard.py new file mode 100644 index 00000000..df84644d --- /dev/null +++ b/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 diff --git a/contract/wizards/contract_line_wizard.xml b/contract/wizards/contract_line_wizard.xml new file mode 100644 index 00000000..d6b58d9d --- /dev/null +++ b/contract/wizards/contract_line_wizard.xml @@ -0,0 +1,99 @@ + + + + + + + contract.line.stop.wizard.form (in contract) + account.analytic.invoice.line.wizard + +
+ + + + +
+
+
+
+
+ + + contract.line.plan_successor.wizard.form (in contract) + account.analytic.invoice.line.wizard + +
+ + + + + + +
+
+
+
+
+ + + contract.line.stop_plan_successor.wizard.form (in contract) + account.analytic.invoice.line.wizard + +
+ + + + + + +
+
+
+
+
+ + + contract.line.stop_plan_successor.wizard.form (in contract) + account.analytic.invoice.line.wizard + +
+ + + + + +
+
+
+ +