diff --git a/contract/README.rst b/contract/README.rst index 0630d36c50..5749aa8aac 100644 --- a/contract/README.rst +++ b/contract/README.rst @@ -7,7 +7,7 @@ Recurring - Contracts Management !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:d03061fa09dd38d53cbaf6f7ca79de5ff114d100162c7f7646d6a6f301ad3941 + !! source digest: sha256:ee6d7ce08f892d005e2127c6e81e5b0acde4662a40e24db17e55eea58a21f12c !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png @@ -153,6 +153,12 @@ Contributors - Antoni Marroig +- `APSL `__: + + - Manuel Regidor + - Valentín Vinagre + - Harald Panten + Maintainers ----------- diff --git a/contract/__manifest__.py b/contract/__manifest__.py index 28b4634a38..8365c46b0c 100644 --- a/contract/__manifest__.py +++ b/contract/__manifest__.py @@ -11,7 +11,7 @@ { "name": "Recurring - Contracts Management", - "version": "17.0.1.1.1", + "version": "17.0.1.1.0", "category": "Contract Management", "license": "AGPL-3", "author": "Tecnativa, ACSONE SA/NV, Odoo Community Association (OCA)", @@ -27,7 +27,6 @@ "report/report_contract.xml", "report/contract_views.xml", "data/contract_cron.xml", - "data/contract_renew_cron.xml", "data/mail_template.xml", "data/template_mail_notification.xml", "data/mail_message_subtype.xml", @@ -42,7 +41,6 @@ "views/contract_template.xml", "views/contract_template_line.xml", "views/res_partner_view.xml", - "views/res_config_settings.xml", "views/contract_terminate_reason.xml", "views/contract_portal_templates.xml", ], diff --git a/contract/models/__init__.py b/contract/models/__init__.py index d8669b19f4..bb679330ac 100644 --- a/contract/models/__init__.py +++ b/contract/models/__init__.py @@ -11,6 +11,4 @@ from . import account_move from . import res_partner from . import contract_tag -from . import res_company -from . import res_config_settings from . import contract_terminate_reason diff --git a/contract/models/abstract_contract.py b/contract/models/abstract_contract.py index 70210cec12..77a9daf39f 100644 --- a/contract/models/abstract_contract.py +++ b/contract/models/abstract_contract.py @@ -60,23 +60,16 @@ class ContractAbstractContract(models.AbstractModel): def _default_generation_type(self): return "invoice" - @api.onchange("contract_type") - def _onchange_contract_type(self): - if self.contract_type == "purchase": - self.contract_line_ids.filtered("automatic_price").update( - {"automatic_price": False} - ) - @api.depends("contract_type", "company_id") def _compute_journal_id(self): AccountJournal = self.env["account.journal"] for contract in self: + journal_id = False domain = [ ("type", "=", contract.contract_type), ("company_id", "=", contract.company_id.id), ] journal = AccountJournal.search(domain, limit=1) if journal: - contract.journal_id = journal.id - else: - contract.journal_id = None + journal_id = journal.id + contract.journal_id = journal_id diff --git a/contract/models/abstract_contract_line.py b/contract/models/abstract_contract_line.py index 2df6d97510..253624fda8 100644 --- a/contract/models/abstract_contract_line.py +++ b/contract/models/abstract_contract_line.py @@ -19,7 +19,9 @@ class ContractAbstractContractLine(models.AbstractModel): product_id = fields.Many2one("product.product", string="Product") name = fields.Text(string="Description", required=True) - quantity = fields.Float(default=1.0, required=True) + quantity = fields.Float( + default=1.0, digits="Product Unit of Measure", required=True + ) product_uom_category_id = fields.Many2one( # Used for domain of field uom_id comodel_name="uom.category", related="product_id.uom_id.category_id", @@ -43,10 +45,10 @@ class ContractAbstractContractLine(models.AbstractModel): string="Unit Price", compute="_compute_price_unit", inverse="_inverse_price_unit", + digits="Product Price", ) price_subtotal = fields.Monetary( - compute="_compute_price_subtotal", - string="Sub Total", + compute="_compute_price_subtotal", string="Sub Total", digits="Product Price" ) discount = fields.Float( string="Discount (%)", @@ -87,23 +89,6 @@ class ContractAbstractContractLine(models.AbstractModel): ) last_date_invoiced = fields.Date() 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( - [ - ("daily", "Day(s)"), - ("weekly", "Week(s)"), - ("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" ) @@ -136,6 +121,7 @@ class ContractAbstractContractLine(models.AbstractModel): "- Custom: Depending on the recurrence to be define.", ) is_recurring_note = fields.Boolean(compute="_compute_is_recurring_note") + company_id = fields.Many2one(related="contract_id.company_id", store=True) def _set_recurrence_field(self, field): diff --git a/contract/models/contract.py b/contract/models/contract.py index 7208e276b4..ba8fd86cfa 100644 --- a/contract/models/contract.py +++ b/contract/models/contract.py @@ -8,8 +8,6 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import logging -from markupsafe import Markup - from odoo import Command, api, fields, models from odoo.exceptions import UserError, ValidationError from odoo.osv import expression @@ -101,7 +99,6 @@ class ContractContract(models.Model): partner_id = fields.Many2one( comodel_name="res.partner", inverse="_inverse_partner_id", required=True ) - commercial_partner_id = fields.Many2one( "res.partner", compute_sudo=True, @@ -345,6 +342,13 @@ def _compute_create_invoice_visibility(self): contract.contract_line_ids.mapped("create_invoice_visibility") ) + @api.onchange("contract_type") + def _onchange_contract_type(self): + if self.contract_type == "purchase": + self.contract_line_ids.filtered("automatic_price").update( + {"automatic_price": False} + ) + @api.onchange("contract_template_id") def _onchange_contract_template_id(self): """Update the contract fields with that of the template. @@ -402,7 +406,6 @@ def _convert_contract_lines(self, contract): vals["date_start"] = fields.Date.context_today(contract_line) vals["recurring_next_date"] = fields.Date.context_today(contract_line) new_lines += contract_line_model.new(vals) - new_lines._onchange_is_auto_renew() return new_lines def _prepare_invoice(self, date_invoice, journal=None): @@ -426,13 +429,12 @@ def _prepare_invoice(self, date_invoice, journal=None): if not journal: raise ValidationError( _( - "Please define a %(contract_type)s journal " - "for the company '%(company)s'." + "Please define a {contract_type} journal " + "for the company {company}." + ).format( + contract_type=self.contract_type, + company=self.company_id.name or "", ) - % { - "contract_type": self.contract_type, - "company": self.company_id.name or "", - } ) invoice_type = ( "in_invoice" if self.contract_type == "purchase" else "out_invoice" @@ -589,10 +591,19 @@ def recurring_create_invoice(self): """ invoices = self._recurring_create_invoice() for invoice in invoices: - body = Markup(_("Contract manually invoiced: %(invoice_link)s")) % { - "invoice_link": invoice._get_html_link(title=invoice.name) - } - self.message_post(body=body) + self.message_post( + body=_( + "Contract manually invoiced: " + "Invoice" + "" + ).format( + model_name=invoice._name, + rec_id=invoice.id, + ) + ) return invoices @api.model @@ -613,11 +624,20 @@ def _invoice_followers(self, invoices): def _add_contract_origin(self, invoices): for item in self: for move in invoices & item._get_related_invoices(): - body = Markup(_("%(msg)s by contract: %(contract_link)s")) % { - "msg": move._creation_message(), - "contract_link": move._get_html_link(title=item.display_name), - } - move.message_post(body=body) + move.message_post( + body=( + _( + ( + "%(msg)s by contract %(contract)s." + ), + msg=move._creation_message(), + contract_id=item.id, + contract=item.display_name, + ) + ) + ) def _recurring_create_invoice(self, date_ref=False): invoices_values = self._prepare_recurring_invoices_values(date_ref) diff --git a/contract/models/contract_line.py b/contract/models/contract_line.py index 40768a5c2b..a6cfd866c2 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -3,14 +3,12 @@ # Copyright 2020 Tecnativa - Pedro M. Baeza # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from datetime import timedelta - from dateutil.relativedelta import relativedelta +from markupsafe import Markup from odoo import _, api, fields, models from odoo.exceptions import ValidationError - -from .contract_line_constraints import get_allowed +from odoo.fields import Date class ContractLine(models.Model): @@ -43,49 +41,19 @@ class ContractLine(models.Model): create_invoice_visibility = fields.Boolean( compute="_compute_create_invoice_visibility" ) - successor_contract_line_id = fields.Many2one( - comodel_name="contract.line", - string="Successor Contract Line", - required=False, - readonly=True, - index=True, - copy=False, - help="In case of restart after suspension, this field contain the new " - "contract line created.", - ) - predecessor_contract_line_id = fields.Many2one( - comodel_name="contract.line", - string="Predecessor Contract Line", - required=False, - readonly=True, - index=True, - copy=False, - help="Contract Line origin of this one.", - ) - manual_renew_needed = fields.Boolean( - default=False, - help="This flag is used to make a difference between a definitive stop" - "and temporary one for which a user is not able to plan a" - "successor in advance", - ) - is_plan_successor_allowed = fields.Boolean( - string="Plan successor allowed?", compute="_compute_allowed" + is_stop_allowed = fields.Boolean( + string="Stop allowed?", compute="_compute_is_stop_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" + string="Cancel allowed?", compute="_compute_is_cancel_allowed" ) is_un_cancel_allowed = fields.Boolean( - string="Un-Cancel allowed?", compute="_compute_allowed" + string="Un-Cancel allowed?", compute="_compute_is_un_cancel_allowed" ) state = fields.Selection( selection=[ ("upcoming", "Upcoming"), ("in-progress", "In-progress"), - ("to-renew", "To renew"), ("upcoming-close", "Upcoming Close"), ("closed", "Closed"), ("canceled", "Canceled"), @@ -107,7 +75,6 @@ class ContractLine(models.Model): "contract_id.last_date_invoiced", "contract_id.contract_line_ids.last_date_invoiced", ) - # pylint: disable=missing-return def _compute_next_period_date_start(self): """Rectify next period date start if another line in the contract has been already invoiced previously when the recurrence is by contract. @@ -124,7 +91,7 @@ def _compute_next_period_date_start(self): rec.next_period_date_start = next_period_date_start else: rest |= rec - super(ContractLine, rest)._compute_next_period_date_start() + return super(ContractLine, rest)._compute_next_period_date_start() @api.depends("contract_id.date_end", "contract_id.line_recurrence") def _compute_date_end(self): @@ -149,10 +116,7 @@ def _compute_termination_notice_date(self): "is_canceled", "date_start", "date_end", - "is_auto_renew", - "manual_renew_needed", "termination_notice_date", - "successor_contract_line_id", ) def _compute_state(self): today = fields.Date.context_today(self) @@ -174,26 +138,14 @@ def _compute_state(self): and (not rec.date_end or rec.date_end >= today) ): # In period - if ( - rec.termination_notice_date - and rec.termination_notice_date < today - and not rec.is_auto_renew - and not rec.manual_renew_needed - ): + if rec.termination_notice_date and rec.termination_notice_date < today: rec.state = "upcoming-close" else: rec.state = "in-progress" continue if rec.date_end and rec.date_end < today: # After - if ( - rec.manual_renew_needed - and not rec.successor_contract_line_id - or rec.is_auto_renew - ): - rec.state = "to-renew" - else: - rec.state = "closed" + rec.state = "closed" @api.model def _get_state_domain(self, state): @@ -214,51 +166,23 @@ def _get_state_domain(self, state): "|", ("date_end", ">=", today), ("date_end", "=", False), - "|", - ("is_auto_renew", "=", True), - "&", - ("is_auto_renew", "=", False), ("termination_notice_date", ">", today), ] - if state == "to-renew": - return [ - "&", - "&", - ("is_canceled", "=", False), - ("date_end", "<", today), - "|", - "&", - ("manual_renew_needed", "=", True), - ("successor_contract_line_id", "=", False), - ("is_auto_renew", "=", True), - ] if state == "upcoming-close": return [ - "&", - "&", "&", "&", "&", ("date_start", "<=", today), - ("is_auto_renew", "=", False), - ("manual_renew_needed", "=", False), ("is_canceled", "=", False), ("termination_notice_date", "<", today), ("date_end", ">=", today), ] if state == "closed": return [ - "&", - "&", "&", ("is_canceled", "=", False), ("date_end", "<", today), - ("is_auto_renew", "=", False), - "|", - "&", - ("manual_renew_needed", "=", True), - ("successor_contract_line_id", "!=", False), - ("manual_renew_needed", "=", False), ] if state == "canceled": return [("is_canceled", "=", True)] @@ -270,7 +194,6 @@ def _search_state(self, operator, value): states = [ "upcoming", "in-progress", - "to-renew", "upcoming-close", "closed", "canceled", @@ -301,98 +224,76 @@ def _search_state(self, operator, value): "in", [state for state in states if state not in value] ) + def _get_allowed_when(self): + self.ensure_one() + today = Date.today() + if self.date_start and today < self.date_start: + return "BEFORE" + if self.date_end and today > self.date_end: + return "AFTER" + return "IN" + @api.depends( "date_start", "date_end", "last_date_invoiced", - "is_auto_renew", - "successor_contract_line_id", - "predecessor_contract_line_id", "is_canceled", "contract_id.is_terminated", ) - def _compute_allowed(self): - for rec in self: - rec.update( - { - "is_plan_successor_allowed": False, - "is_stop_plan_successor_allowed": False, - "is_stop_allowed": False, - "is_cancel_allowed": False, - "is_un_cancel_allowed": False, - } - ) - if rec.contract_id.is_terminated: - continue - if rec.date_start: - allowed = get_allowed( - rec.date_start, - rec.date_end, - rec.last_date_invoiced, - rec.is_auto_renew, - rec.successor_contract_line_id, - rec.predecessor_contract_line_id, - rec.is_canceled, - ) - if allowed: - rec.update( - { - "is_plan_successor_allowed": allowed.plan_successor, - "is_stop_plan_successor_allowed": ( - allowed.stop_plan_successor - ), - "is_stop_allowed": allowed.stop, - "is_cancel_allowed": allowed.cancel, - "is_un_cancel_allowed": allowed.uncancel, - } - ) - - @api.constrains("is_auto_renew", "successor_contract_line_id", "date_end") - def _check_allowed(self): - """ - 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 must have a end date")) - else: - if not rec.date_end and rec.successor_contract_line_id: - raise ValidationError( - _("A contract line with a successor " "must have a end date") - ) + def _compute_is_stop_allowed(self): + for line in self: + is_stop_allowed = False + if not line.contract_id.is_terminated: + when = line._get_allowed_when() + if when == "BEFORE" and not ( + line.last_date_invoiced and line.is_canceled + ): + is_stop_allowed = True + elif when == "AFTER": + is_stop_allowed = True + elif when == "IN" and not line.is_canceled: + is_stop_allowed = True + line.is_stop_allowed = is_stop_allowed - @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.depends( + "date_start", + "date_end", + "last_date_invoiced", + "is_canceled", + "contract_id.is_terminated", + ) + def _compute_is_cancel_allowed(self): + for line in self: + is_cancel_allowed = False + if not ( + line.contract_id.is_terminated + or line.is_canceled + or line.last_date_invoiced + ): + when = line._get_allowed_when() + if when in ["BEFORE", "IN"]: + is_cancel_allowed = True + line.is_cancel_allowed = is_cancel_allowed - @api.constrains("predecessor_contract_line_id", "date_start") - def _check_overlap_predecessor(self): - for rec in self: + @api.depends( + "is_stop_allowed", + "is_cancel_allowed", + "is_canceled", + "contract_id.is_terminated", + ) + def _compute_is_un_cancel_allowed(self): + for line in self: + is_un_cancel_allowed = False if ( - rec.predecessor_contract_line_id - and rec.predecessor_contract_line_id.date_end + not ( + line.contract_id.is_terminated + or line.is_stop_allowed + or line.is_cancel_allowed + ) + and line.is_canceled ): - if rec.date_start <= rec.predecessor_contract_line_id.date_end: - raise ValidationError( - _("Contract line and its predecessor overlapped") - ) + is_un_cancel_allowed = True + line.is_un_cancel_allowed = is_un_cancel_allowed @api.model def _compute_first_recurring_next_date( @@ -414,41 +315,6 @@ def _compute_first_recurring_next_date( max_date_end=False, ) - @api.model - def _get_first_date_end( - self, date_start, auto_renew_rule_type, auto_renew_interval - ): - return ( - date_start - + self.get_relative_delta(auto_renew_rule_type, auto_renew_interval) - - relativedelta(days=1) - ) - - @api.onchange( - "date_start", - "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"): - if rec.date_start: - rec.date_end = self._get_first_date_end( - rec.date_start, - rec.auto_renew_rule_type, - rec.auto_renew_interval, - ) - - @api.constrains("is_canceled", "is_auto_renew") - def _check_auto_renew_canceled_lines(self): - for rec in self: - if rec.is_canceled and rec.is_auto_renew: - raise ValidationError( - _("A canceled contract line can't be set to auto-renew") - ) - @api.constrains("recurring_next_date", "date_start") def _check_recurring_next_date_start_date(self): for line in self: @@ -645,12 +511,10 @@ def _delay(self, delay_delta): } ) - def _prepare_value_for_stop(self, date_end, manual_renew_needed): + def _prepare_value_for_stop(self, date_end, **kwargs): self.ensure_one() return { "date_end": date_end, - "is_auto_renew": False, - "manual_renew_needed": manual_renew_needed, "recurring_next_date": self.get_next_invoice_date( self.next_period_date_start, self.recurring_invoicing_type, @@ -661,7 +525,7 @@ def _prepare_value_for_stop(self, date_end, manual_renew_needed): ), } - def stop(self, date_end, manual_renew_needed=False, post_message=True): + def stop(self, date_end, post_message=True, **kwargs): """ Put date_end on contract line We don't consider contract lines that end's before the new end date @@ -676,202 +540,23 @@ def stop(self, date_end, manual_renew_needed=False, post_message=True): else: if not rec.date_end or rec.date_end > date_end: old_date_end = rec.date_end - rec.write( - rec._prepare_value_for_stop(date_end, manual_renew_needed) - ) + rec.write(rec._prepare_value_for_stop(date_end, **kwargs)) if post_message: - msg = _( - """Contract line for %(product)s + msg = Markup( + _( + """Contract line for %(product)s stopped:
- End: %(old_end)s -- %(new_end)s """ + ) ) % { "product": rec.name, "old_end": old_date_end, "new_end": rec.date_end, } rec.contract_id.message_post(body=msg) - else: - rec.write( - { - "is_auto_renew": False, - "manual_renew_needed": manual_renew_needed, - } - ) return True - 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.get_next_invoice_date( - date_start, - self.recurring_invoicing_type, - self.recurring_invoicing_offset, - self.recurring_rule_type, - self.recurring_interval, - max_date_end=date_end, - ) - new_vals = self.read()[0] - new_vals.pop("id", None) - new_vals.pop("last_date_invoiced", 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 - - def plan_successor( - self, - date_start, - date_end, - is_auto_renew, - recurring_next_date=False, - post_message=True, - ): - """ - 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["contract.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 - if post_message: - msg = _( - """Contract line for %(product)s - planned a successor:
- - Start: %(new_date_start)s -
- - End: %(new_date_end)s - """ - ) % { - "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 - - 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.date_start - * 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["contract.line"] - for rec in self: - if rec.date_start >= date_start: - if rec.date_start < date_end: - delay = (date_end - rec.date_start) + timedelta(days=1) - else: - delay = (date_end - date_start) + timedelta(days=1) - rec._delay(delay) - contract_line |= rec - else: - if rec.date_end and rec.date_end < date_start: - rec.stop(date_start, post_message=False) - elif ( - rec.date_end - and rec.date_end > date_start - and rec.date_end < date_end - ): - new_date_start = date_end + relativedelta(days=1) - new_date_end = ( - date_end + (rec.date_end - date_start) + relativedelta(days=1) - ) - rec.stop( - date_start - relativedelta(days=1), - manual_renew_needed=True, - post_message=False, - ) - contract_line |= rec.plan_successor( - new_date_start, - new_date_end, - is_auto_renew, - post_message=False, - ) - else: - new_date_start = date_end + relativedelta(days=1) - if rec.date_end: - new_date_end = ( - rec.date_end - + (date_end - date_start) - + relativedelta(days=1) - ) - else: - new_date_end = rec.date_end - - rec.stop( - date_start - relativedelta(days=1), - manual_renew_needed=True, - post_message=False, - ) - contract_line |= rec.plan_successor( - new_date_start, - new_date_end, - is_auto_renew, - post_message=False, - ) - msg = _( - """Contract line for %(product)s - suspended:
- - Suspension Start: %(new_date_start)s -
- - Suspension End: %(new_date_end)s - """ - ) % { - "product": rec.name, - "new_date_start": date_start, - "new_date_end": date_end, - } - rec.contract_id.message_post(body=msg) - return contract_line - def cancel(self): if not all(self.mapped("is_cancel_allowed")): raise ValidationError(_("Cancel not allowed for this line")) @@ -884,10 +569,7 @@ def cancel(self): ), ) contract.message_post(body=msg) - self.mapped("predecessor_contract_line_id").write( - {"successor_contract_line_id": False} - ) - return self.write({"is_canceled": True, "is_auto_renew": False}) + return self.write({"is_canceled": True}) def uncancel(self, recurring_next_date): if not all(self.mapped("is_un_cancel_allowed")): @@ -902,10 +584,6 @@ def uncancel(self, recurring_next_date): ) contract.message_post(body=msg) for rec in self: - if rec.predecessor_contract_line_id: - predecessor_contract_line = rec.predecessor_contract_line_id - assert not predecessor_contract_line.successor_contract_line_id - predecessor_contract_line.successor_contract_line_id = rec rec.is_canceled = False rec.recurring_next_date = recurring_next_date return True @@ -928,26 +606,6 @@ def action_uncancel(self): "context": context, } - 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": "contract.line.wizard", - "view_mode": "form", - "views": [(view_id, "form")], - "target": "new", - "context": context, - } - def action_stop(self): self.ensure_one() context = { @@ -966,90 +624,6 @@ def action_stop(self): "context": context, } - 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": "contract.line.wizard", - "view_mode": "form", - "views": [(view_id, "form")], - "target": "new", - "context": context, - } - - def _get_renewal_new_date_end(self): - self.ensure_one() - date_start = self.date_end + relativedelta(days=1) - date_end = self._get_first_date_end( - date_start, self.auto_renew_rule_type, self.auto_renew_interval - ) - return date_end - - def _renew_create_line(self, date_end): - self.ensure_one() - date_start = self.date_end + relativedelta(days=1) - is_auto_renew = self.is_auto_renew - self.stop(self.date_end, post_message=False) - new_line = self.plan_successor( - date_start, date_end, is_auto_renew, post_message=False - ) - return new_line - - def _renew_extend_line(self, date_end): - self.ensure_one() - self.date_end = date_end - return self - - def renew(self): - res = self.env["contract.line"] - for rec in self: - company = rec.contract_id.company_id - date_end = rec._get_renewal_new_date_end() - date_start = rec.date_end + relativedelta(days=1) - if company.create_new_line_at_contract_line_renew: - new_line = rec._renew_create_line(date_end) - else: - new_line = rec._renew_extend_line(date_end) - res |= new_line - msg = _( - """Contract line for %(product)s - renewed:
- - Start: %(new_date_start)s -
- - End: %(new_date_end)s - """ - ) % { - "product": rec.name, - "new_date_start": date_start, - "new_date_end": date_end, - } - rec.contract_id.message_post(body=msg) - return res - - @api.model - def _contract_line_to_renew_domain(self): - return [ - ("contract_id.is_terminated", "=", False), - ("is_auto_renew", "=", True), - ("is_canceled", "=", False), - ("termination_notice_date", "<=", fields.Date.context_today(self)), - ] - - @api.model - def cron_renew_contract_line(self): - domain = self._contract_line_to_renew_domain() - to_renew = self.search(domain) - to_renew.renew() - @api.model def get_view(self, view_id=None, view_type="form", **options): default_contract_type = self.env.context.get("default_contract_type") diff --git a/contract/models/contract_line_constraints.py b/contract/models/contract_line_constraints.py deleted file mode 100644 index eeaba307bb..0000000000 --- a/contract/models/contract_line_constraints.py +++ /dev/null @@ -1,429 +0,0 @@ -# Copyright 2018 ACSONE SA/NV. -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -import itertools -from collections import namedtuple - -from odoo.fields import Date - -Criteria = namedtuple( - "Criteria", - [ - "when", # Contract line relatively to today (BEFORE, IN, AFTER) - "has_date_end", # Is date_end set on contract line (bool) - "has_last_date_invoiced", # Is last_date_invoiced set on contract line - "is_auto_renew", # Is is_auto_renew set on contract line (bool) - "has_successor", # Is contract line has_successor (bool) - "predecessor_has_successor", - # Is contract line predecessor has successor (bool) - # In almost of the cases - # contract_line.predecessor.successor == contract_line - # But at cancel action, - # contract_line.predecessor.successor == False - # This is to permit plan_successor on predecessor - # If contract_line.predecessor.successor != False - # and contract_line is canceled, we don't allow uncancel - # else we re-link contract_line and its predecessor - "canceled", # Is contract line canceled (bool) - ], -) -Allowed = namedtuple( - "Allowed", - ["plan_successor", "stop_plan_successor", "stop", "cancel", "uncancel"], -) - - -def _expand_none(criteria): - variations = [] - for attribute, value in criteria._asdict().items(): - if value is None: - if attribute == "when": - variations.append(["BEFORE", "IN", "AFTER"]) - else: - variations.append([True, False]) - else: - variations.append([value]) - return itertools.product(*variations) - - -def _add(matrix, criteria, allowed): - """Expand None values to True/False combination""" - for c in _expand_none(criteria): - matrix[c] = allowed - - -CRITERIA_ALLOWED_DICT = { - Criteria( - when="BEFORE", - has_date_end=True, - has_last_date_invoiced=False, - is_auto_renew=True, - has_successor=False, - predecessor_has_successor=None, - canceled=False, - ): Allowed( - plan_successor=False, - stop_plan_successor=True, - stop=True, - cancel=True, - uncancel=False, - ), - Criteria( - when="BEFORE", - has_date_end=True, - has_last_date_invoiced=False, - is_auto_renew=False, - has_successor=True, - predecessor_has_successor=None, - canceled=False, - ): Allowed( - plan_successor=False, - stop_plan_successor=False, - stop=True, - cancel=True, - uncancel=False, - ), - Criteria( - when="BEFORE", - has_date_end=True, - has_last_date_invoiced=False, - is_auto_renew=False, - has_successor=False, - predecessor_has_successor=None, - canceled=False, - ): Allowed( - plan_successor=True, - stop_plan_successor=True, - stop=True, - cancel=True, - uncancel=False, - ), - Criteria( - when="BEFORE", - has_date_end=False, - has_last_date_invoiced=False, - is_auto_renew=False, - has_successor=False, - predecessor_has_successor=None, - canceled=False, - ): Allowed( - plan_successor=False, - stop_plan_successor=True, - stop=True, - cancel=True, - uncancel=False, - ), - Criteria( - when="IN", - has_date_end=True, - has_last_date_invoiced=False, - is_auto_renew=True, - has_successor=False, - predecessor_has_successor=None, - canceled=False, - ): Allowed( - plan_successor=False, - stop_plan_successor=True, - stop=True, - cancel=True, - uncancel=False, - ), - Criteria( - when="IN", - has_date_end=True, - has_last_date_invoiced=False, - is_auto_renew=False, - has_successor=True, - predecessor_has_successor=None, - canceled=False, - ): Allowed( - plan_successor=False, - stop_plan_successor=False, - stop=True, - cancel=True, - uncancel=False, - ), - Criteria( - when="IN", - has_date_end=True, - has_last_date_invoiced=False, - is_auto_renew=False, - has_successor=False, - predecessor_has_successor=None, - canceled=False, - ): Allowed( - plan_successor=True, - stop_plan_successor=True, - stop=True, - cancel=True, - uncancel=False, - ), - Criteria( - when="IN", - has_date_end=False, - has_last_date_invoiced=False, - is_auto_renew=False, - has_successor=False, - predecessor_has_successor=None, - canceled=False, - ): Allowed( - plan_successor=False, - stop_plan_successor=True, - stop=True, - cancel=True, - uncancel=False, - ), - Criteria( - when="BEFORE", - has_date_end=True, - has_last_date_invoiced=True, - is_auto_renew=True, - has_successor=False, - predecessor_has_successor=None, - canceled=False, - ): Allowed( - plan_successor=False, - stop_plan_successor=True, - stop=True, - cancel=False, - uncancel=False, - ), - Criteria( - when="BEFORE", - has_date_end=True, - has_last_date_invoiced=True, - is_auto_renew=False, - has_successor=True, - predecessor_has_successor=None, - canceled=False, - ): Allowed( - plan_successor=False, - stop_plan_successor=False, - stop=True, - cancel=False, - uncancel=False, - ), - Criteria( - when="BEFORE", - has_date_end=True, - has_last_date_invoiced=True, - is_auto_renew=False, - has_successor=False, - predecessor_has_successor=None, - canceled=False, - ): Allowed( - plan_successor=True, - stop_plan_successor=True, - stop=True, - cancel=False, - uncancel=False, - ), - Criteria( - when="BEFORE", - has_date_end=False, - has_last_date_invoiced=True, - is_auto_renew=False, - has_successor=False, - predecessor_has_successor=None, - canceled=False, - ): Allowed( - plan_successor=False, - stop_plan_successor=True, - stop=True, - cancel=False, - uncancel=False, - ), - Criteria( - when="IN", - has_date_end=True, - has_last_date_invoiced=True, - is_auto_renew=True, - has_successor=False, - predecessor_has_successor=None, - canceled=False, - ): Allowed( - plan_successor=False, - stop_plan_successor=True, - stop=True, - cancel=False, - uncancel=False, - ), - Criteria( - when="IN", - has_date_end=True, - has_last_date_invoiced=True, - is_auto_renew=False, - has_successor=True, - predecessor_has_successor=None, - canceled=False, - ): Allowed( - plan_successor=False, - stop_plan_successor=False, - stop=True, - cancel=False, - uncancel=False, - ), - Criteria( - when="IN", - has_date_end=True, - has_last_date_invoiced=True, - is_auto_renew=False, - has_successor=False, - predecessor_has_successor=None, - canceled=False, - ): Allowed( - plan_successor=True, - stop_plan_successor=True, - stop=True, - cancel=False, - uncancel=False, - ), - Criteria( - when="IN", - has_date_end=False, - has_last_date_invoiced=True, - is_auto_renew=False, - has_successor=False, - predecessor_has_successor=None, - canceled=False, - ): Allowed( - plan_successor=False, - stop_plan_successor=True, - stop=True, - cancel=False, - uncancel=False, - ), - Criteria( - when="AFTER", - has_date_end=True, - has_last_date_invoiced=None, - is_auto_renew=True, - has_successor=False, - predecessor_has_successor=None, - canceled=False, - ): Allowed( - plan_successor=False, - stop_plan_successor=False, - stop=True, - cancel=False, - uncancel=False, - ), - Criteria( - when="AFTER", - has_date_end=True, - has_last_date_invoiced=None, - is_auto_renew=False, - has_successor=True, - predecessor_has_successor=None, - canceled=False, - ): Allowed( - plan_successor=False, - stop_plan_successor=False, - stop=False, - cancel=False, - uncancel=False, - ), - Criteria( - when="AFTER", - has_date_end=True, - has_last_date_invoiced=None, - is_auto_renew=False, - has_successor=False, - predecessor_has_successor=None, - canceled=False, - ): Allowed( - plan_successor=True, - stop_plan_successor=False, - stop=True, - cancel=False, - uncancel=False, - ), - Criteria( - when=None, - has_date_end=None, - has_last_date_invoiced=None, - is_auto_renew=None, - has_successor=None, - predecessor_has_successor=False, - canceled=True, - ): Allowed( - plan_successor=False, - stop_plan_successor=False, - stop=False, - cancel=False, - uncancel=True, - ), - Criteria( - when=None, - has_date_end=None, - has_last_date_invoiced=None, - is_auto_renew=None, - has_successor=None, - predecessor_has_successor=True, - canceled=True, - ): Allowed( - plan_successor=False, - stop_plan_successor=False, - stop=False, - cancel=False, - uncancel=False, - ), -} -criteria_allowed_dict = {} - -for c in CRITERIA_ALLOWED_DICT: - _add(criteria_allowed_dict, c, CRITERIA_ALLOWED_DICT[c]) - - -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, - has_last_date_invoiced, - is_auto_renew, - successor_contract_line_id, - predecessor_contract_line_id, - is_canceled, -): - return Criteria( - when=compute_when(date_start, date_end), - has_date_end=bool(date_end), - has_last_date_invoiced=bool(has_last_date_invoiced), - is_auto_renew=is_auto_renew, - has_successor=bool(successor_contract_line_id), - predecessor_has_successor=bool( - predecessor_contract_line_id.successor_contract_line_id - ), - canceled=is_canceled, - ) - - -def get_allowed( - date_start, - date_end, - has_last_date_invoiced, - is_auto_renew, - successor_contract_line_id, - predecessor_contract_line_id, - is_canceled, -): - criteria = compute_criteria( - date_start, - date_end, - has_last_date_invoiced, - is_auto_renew, - successor_contract_line_id, - predecessor_contract_line_id, - is_canceled, - ) - if criteria in criteria_allowed_dict: - return criteria_allowed_dict[criteria] - return False diff --git a/contract/models/contract_recurrency_mixin.py b/contract/models/contract_recurrency_mixin.py index aebc76c3f2..823854f2de 100644 --- a/contract/models/contract_recurrency_mixin.py +++ b/contract/models/contract_recurrency_mixin.py @@ -89,6 +89,7 @@ class ContractRecurrencyMixin(models.AbstractModel): string="Next Period End", compute="_compute_next_period_date_end", ) + last_date_invoiced = fields.Date(readonly=True, copy=False) @api.depends("next_period_date_start") diff --git a/contract/models/contract_template.py b/contract/models/contract_template.py index ec2aec8261..d7f186da44 100644 --- a/contract/models/contract_template.py +++ b/contract/models/contract_template.py @@ -6,7 +6,7 @@ # Copyright 2018 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import fields, models +from odoo import api, fields, models class ContractTemplate(models.Model): @@ -20,3 +20,10 @@ class ContractTemplate(models.Model): copy=True, string="Contract template lines", ) + + @api.onchange("contract_type") + def _onchange_contract_type(self): + if self.contract_type == "purchase": + self.contract_line_ids.filtered("automatic_price").update( + {"automatic_price": False} + ) diff --git a/contract/readme/CONTRIBUTORS.md b/contract/readme/CONTRIBUTORS.md index 10c934c1c8..46650e4d31 100644 --- a/contract/readme/CONTRIBUTORS.md +++ b/contract/readme/CONTRIBUTORS.md @@ -25,3 +25,9 @@ - [APSL](https://www.apsl.tech): > - Antoni Marroig \<\> + +- [APSL](https://www.sygel.es): + + > - Manuel Regidor \<\> + > - Valentín Vinagre \<\> + > - Harald Panten \<\> diff --git a/contract/static/description/index.html b/contract/static/description/index.html index 31daf98b90..2e1ba7ac37 100644 --- a/contract/static/description/index.html +++ b/contract/static/description/index.html @@ -8,11 +8,10 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ +:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. -Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -275,7 +274,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: gray; } /* line numbers */ +pre.code .ln { color: grey; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -301,7 +300,7 @@ span.pre { white-space: pre } -span.problematic, pre.problematic { +span.problematic { color: red } span.section-subtitle { @@ -367,7 +366,7 @@

Recurring - Contracts Management

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:d03061fa09dd38d53cbaf6f7ca79de5ff114d100162c7f7646d6a6f301ad3941 +!! source digest: sha256:ee6d7ce08f892d005e2127c6e81e5b0acde4662a40e24db17e55eea58a21f12c !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Production/Stable License: AGPL-3 OCA/contract Translate me on Weblate Try me on Runboat

This module enables contracts management with recurring invoicing @@ -500,14 +499,21 @@

Contributors

+
  • APSL:

    +
    + +
    +
  • Maintainers

    This module is maintained by the OCA.

    - -Odoo Community Association - +Odoo Community Association

    OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.

    diff --git a/contract/tests/test_contract.py b/contract/tests/test_contract.py index 77e8f5c2ac..953e277a5a 100644 --- a/contract/tests/test_contract.py +++ b/contract/tests/test_contract.py @@ -5,7 +5,6 @@ import logging from collections import namedtuple -from datetime import timedelta from dateutil.relativedelta import relativedelta @@ -117,10 +116,8 @@ def setUpClass(cls): "recurring_interval": 1, "date_start": "2018-01-01", "recurring_next_date": "2018-01-15", - "is_auto_renew": False, } cls.acct_line = cls.env["contract.line"].create(cls.line_vals) - cls.contract.company_id.create_new_line_at_contract_line_renew = True cls.terminate_reason = cls.env["contract.terminate.reason"].create( {"name": "terminate_reason"} ) @@ -1099,7 +1096,7 @@ def test_date_end(self): """recurring next date for a contract is the min for all lines""" self.acct_line.date_end = "2018-01-01" self.acct_line.copy() - self.acct_line.write({"date_end": False, "is_auto_renew": False}) + self.acct_line.write({"date_end": False}) self.assertFalse(self.contract.date_end) def test_cancel_contract_line(self): @@ -1115,7 +1112,6 @@ def test_stop_contract_line(self): "date_start": self.today, "recurring_next_date": self.today, "date_end": self.today + relativedelta(months=7), - "is_auto_renew": True, } ) self.acct_line.stop(self.today + relativedelta(months=5)) @@ -1128,7 +1124,6 @@ def test_stop_upcoming_contract_line(self): "date_start": self.today + relativedelta(months=3), "recurring_next_date": self.today + relativedelta(months=3), "date_end": self.today + relativedelta(months=7), - "is_auto_renew": True, } ) self.acct_line.stop(self.today) @@ -1137,15 +1132,13 @@ def test_stop_upcoming_contract_line(self): def test_stop_past_contract_line(self): """Past contract line are ignored on stop""" - self.acct_line.write( - {"date_end": self.today + relativedelta(months=5), "is_auto_renew": True} - ) + self.acct_line.write({"date_end": self.today + relativedelta(months=5)}) self.acct_line.stop(self.today + relativedelta(months=7)) self.assertEqual(self.acct_line.date_end, self.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.write({"date_end": False}) self.acct_line.stop(self.today + relativedelta(months=7)) self.assertEqual(self.acct_line.date_end, self.today + relativedelta(months=7)) @@ -1155,7 +1148,6 @@ def test_stop_wizard(self): "date_start": self.today, "recurring_next_date": self.today, "date_end": self.today + relativedelta(months=5), - "is_auto_renew": True, } ) wizard = self.env["contract.line.wizard"].create( @@ -1166,419 +1158,11 @@ def test_stop_wizard(self): ) wizard.stop() self.assertEqual(self.acct_line.date_end, self.today + relativedelta(months=3)) - self.assertFalse(self.acct_line.is_auto_renew) - - def test_stop_plan_successor_contract_line_0(self): - successor_contract_line = self.acct_line.copy( - { - "date_start": self.today + relativedelta(months=5), - "recurring_next_date": self.today + relativedelta(months=5), - } - ) - self.acct_line.write( - { - "successor_contract_line_id": successor_contract_line.id, - "is_auto_renew": False, - "date_end": self.today, - } - ) - suspension_start = self.today + relativedelta(months=5) - suspension_end = self.today + relativedelta(months=6) - with self.assertRaises(ValidationError): - self.acct_line.stop_plan_successor(suspension_start, suspension_end, True) - - def test_stop_plan_successor_contract_line_1(self): - """ - * contract line end's before the suspension period: - -> apply stop - """ - suspension_start = self.today + relativedelta(months=5) - suspension_end = self.today + relativedelta(months=6) - start_date = self.today - end_date = self.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["contract.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 = self.today + relativedelta(months=3) - suspension_end = self.today + relativedelta(months=5) - start_date = self.today - end_date = self.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 - relativedelta(days=1) - ) - new_line = self.env["contract.line"].search( - [("predecessor_contract_line_id", "=", self.acct_line.id)] - ) - self.assertTrue(new_line) - new_date_end = ( - suspension_end + (end_date - suspension_start) + relativedelta(days=1) - ) - self.assertEqual(new_line.date_start, suspension_end + relativedelta(days=1)) - self.assertEqual(new_line.date_end, new_date_end) - self.assertTrue(self.acct_line.manual_renew_needed) - - 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 = self.today + relativedelta(months=3) - suspension_end = self.today + relativedelta(months=5) - start_date = self.today - end_date = self.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 - relativedelta(days=1) - ) - new_line = self.env["contract.line"].search( - [("predecessor_contract_line_id", "=", self.acct_line.id)] - ) - self.assertTrue(new_line) - new_date_end = ( - end_date + (suspension_end - suspension_start) + relativedelta(days=1) - ) - self.assertEqual(new_line.date_start, suspension_end + relativedelta(days=1)) - self.assertEqual(new_line.date_end, new_date_end) - self.assertTrue(self.acct_line.manual_renew_needed) - - 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 = self.today + relativedelta(months=3) - suspension_end = self.today + relativedelta(months=5) - start_date = self.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 - relativedelta(days=1) - ) - new_line = self.env["contract.line"].search( - [("predecessor_contract_line_id", "=", self.acct_line.id)] - ) - self.assertTrue(new_line) - self.assertEqual(new_line.date_start, suspension_end + relativedelta(days=1)) - self.assertFalse(new_line.date_end) - self.assertTrue(self.acct_line.manual_renew_needed) - - 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 = self.today + relativedelta(months=2) - suspension_end = self.today + relativedelta(months=5) - start_date = self.today + relativedelta(months=3) - end_date = self.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 - start_date) + timedelta(days=1), - ) - self.assertEqual( - self.acct_line.date_end, - end_date + (suspension_end - start_date) + timedelta(days=1), - ) - new_line = self.env["contract.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 = self.today + relativedelta(months=2) - suspension_end = self.today + relativedelta(months=5) - start_date = self.today + relativedelta(months=3) - end_date = self.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) + timedelta(days=1), - ) - self.assertEqual( - self.acct_line.date_end, - end_date + (suspension_end - start_date) + timedelta(days=1), - ) - new_line = self.env["contract.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 = self.today + relativedelta(months=2) - suspension_end = self.today + relativedelta(months=5) - start_date = self.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) + timedelta(days=1), - ) - self.assertFalse(self.acct_line.date_end) - new_line = self.env["contract.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 = self.today + relativedelta(months=2) - suspension_end = self.today + relativedelta(months=3) - start_date = self.today + relativedelta(months=4) - end_date = self.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) + timedelta(days=1), - ) - self.assertEqual( - self.acct_line.date_end, - end_date + (suspension_end - suspension_start) + timedelta(days=1), - ) - new_line = self.env["contract.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 = self.today + relativedelta(months=2) - suspension_end = self.today + relativedelta(months=3) - start_date = self.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) + timedelta(days=1), - ) - self.assertFalse(self.acct_line.date_end) - new_line = self.env["contract.line"].search( - [("predecessor_contract_line_id", "=", self.acct_line.id)] - ) - self.assertFalse(new_line) - - def test_stop_plan_successor_wizard(self): - suspension_start = self.today + relativedelta(months=2) - suspension_end = self.today + relativedelta(months=3) - start_date = self.today + relativedelta(months=4) - end_date = self.today + relativedelta(months=6) - self.acct_line.write( - { - "date_start": start_date, - "recurring_next_date": start_date, - "date_end": end_date, - } - ) - wizard = self.env["contract.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) + timedelta(days=1), - ) - self.assertEqual( - self.acct_line.date_end, - end_date + (suspension_end - suspension_start) + timedelta(days=1), - ) - new_line = self.env["contract.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": self.today, - "recurring_next_date": self.today, - "date_end": self.today + relativedelta(months=3), - "is_auto_renew": False, - } - ) - self.acct_line.plan_successor( - self.today + relativedelta(months=5), - self.today + relativedelta(months=7), - True, - ) - new_line = self.env["contract.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, self.today + relativedelta(months=5)) - self.assertEqual(new_line.date_end, self.today + relativedelta(months=7)) - - def test_overlap(self): - self.acct_line.write( - { - "date_start": self.today, - "recurring_next_date": self.today, - "date_end": self.today + relativedelta(months=3), - "is_auto_renew": False, - } - ) - self.acct_line.plan_successor( - self.today + relativedelta(months=5), - self.today + relativedelta(months=7), - True, - ) - new_line = self.env["contract.line"].search( - [("predecessor_contract_line_id", "=", self.acct_line.id)] - ) - with self.assertRaises(ValidationError): - new_line.date_start = self.today + relativedelta(months=2) - with self.assertRaises(ValidationError): - self.acct_line.date_end = self.today + relativedelta(months=6) - - def test_plan_successor_wizard(self): - self.acct_line.write( - { - "date_start": self.today, - "recurring_next_date": self.today, - "date_end": self.today + relativedelta(months=2), - "is_auto_renew": False, - } - ) - wizard = self.env["contract.line.wizard"].create( - { - "date_start": self.today + relativedelta(months=3), - "date_end": self.today + relativedelta(months=5), - "is_auto_renew": True, - "contract_line_id": self.acct_line.id, - } - ) - wizard.plan_successor() - new_line = self.env["contract.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, self.today + relativedelta(months=3)) - self.assertEqual(new_line.date_end, self.today + relativedelta(months=5)) def test_cancel(self): - self.acct_line.write( - {"date_end": self.today + relativedelta(months=5), "is_auto_renew": True} - ) + self.acct_line.write({"date_end": self.today + relativedelta(months=5)}) self.acct_line.cancel() self.assertTrue(self.acct_line.is_canceled) - self.assertFalse(self.acct_line.is_auto_renew) - with self.assertRaises(ValidationError): - self.acct_line.is_auto_renew = True self.acct_line.uncancel(self.today) self.assertFalse(self.acct_line.is_canceled) @@ -1591,133 +1175,6 @@ def test_uncancel_wizard(self): wizard.uncancel() self.assertFalse(self.acct_line.is_canceled) - def test_cancel_uncancel_with_predecessor(self): - suspension_start = self.today + relativedelta(months=3) - suspension_end = self.today + relativedelta(months=5) - start_date = self.today - end_date = self.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 - relativedelta(days=1) - ) - new_line = self.env["contract.line"].search( - [("predecessor_contract_line_id", "=", self.acct_line.id)] - ) - self.assertEqual(self.acct_line.successor_contract_line_id, new_line) - new_line.cancel() - self.assertTrue(new_line.is_canceled) - self.assertFalse(self.acct_line.successor_contract_line_id) - self.assertEqual(new_line.predecessor_contract_line_id, self.acct_line) - new_line.uncancel(suspension_end + relativedelta(days=1)) - self.assertFalse(new_line.is_canceled) - self.assertEqual(self.acct_line.successor_contract_line_id, new_line) - self.assertEqual( - new_line.recurring_next_date, - suspension_end + relativedelta(days=1), - ) - - def test_cancel_uncancel_with_predecessor_has_successor(self): - suspension_start = self.today + relativedelta(months=6) - suspension_end = self.today + relativedelta(months=7) - start_date = self.today - end_date = self.today + relativedelta(months=8) - 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) - new_line = self.env["contract.line"].search( - [("predecessor_contract_line_id", "=", self.acct_line.id)] - ) - new_line.cancel() - suspension_start = self.today + relativedelta(months=4) - suspension_end = self.today + relativedelta(months=5) - self.acct_line.stop_plan_successor(suspension_start, suspension_end, True) - with self.assertRaises(ValidationError): - new_line.uncancel(suspension_end) - - 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": self.today, "is_auto_renew": True}) - line_1 = self.acct_line.copy({"date_end": self.today + relativedelta(months=1)}) - line_2 = self.acct_line.copy({"date_end": self.today - relativedelta(months=1)}) - line_3 = self.acct_line.copy({"date_end": self.today - relativedelta(months=2)}) - line_4 = self.acct_line.copy({"date_end": self.today + relativedelta(months=2)}) - to_renew = self.acct_line.search( - self.acct_line._contract_line_to_renew_domain() - ) - self.assertEqual(set(to_renew), {self.acct_line, line_1, line_2, line_3}) - self.acct_line.cron_renew_contract_line() - self.assertTrue(self.acct_line.successor_contract_line_id) - self.assertTrue(line_1.successor_contract_line_id) - self.assertTrue(line_2.successor_contract_line_id) - self.assertTrue(line_3.successor_contract_line_id) - self.assertFalse(line_4.successor_contract_line_id) - - def test_renew_create_new_line(self): - date_start = self.today - relativedelta(months=9) - date_end = date_start + relativedelta(months=12) - relativedelta(days=1) - self.acct_line.write( - { - "is_auto_renew": True, - "date_start": date_start, - "recurring_next_date": date_start, - "date_end": self.today, - } - ) - self.acct_line._onchange_is_auto_renew() - self.assertEqual(self.acct_line.date_end, date_end) - 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, date_start + relativedelta(months=12)) - self.assertEqual(new_line.date_end, date_end + relativedelta(months=12)) - - def test_renew_extend_original_line(self): - self.contract.company_id.create_new_line_at_contract_line_renew = False - date_start = self.today - relativedelta(months=9) - date_end = date_start + relativedelta(months=12) - relativedelta(days=1) - self.acct_line.write( - { - "is_auto_renew": True, - "date_start": date_start, - "recurring_next_date": date_start, - "date_end": self.today, - } - ) - self.acct_line._onchange_is_auto_renew() - self.assertEqual(self.acct_line.date_end, date_end) - self.acct_line.renew() - self.assertTrue(self.acct_line.is_auto_renew) - self.assertEqual(self.acct_line.date_start, date_start) - self.assertEqual(self.acct_line.date_end, date_end + relativedelta(months=12)) - def test_cron_recurring_create_invoice(self): self.acct_line.date_start = "2018-01-01" self.acct_line.recurring_invoicing_type = "post-paid" @@ -1759,7 +1216,6 @@ def test_get_period_to_invoice_monthlylastday_postpaid(self): ) self.assertEqual(first, to_date("2018-03-01")) self.assertEqual(last, to_date("2018-03-15")) - self.acct_line.manual_renew_needed = True def test_get_period_to_invoice_monthlylastday_prepaid(self): self.acct_line.date_start = "2018-01-05" @@ -1971,31 +1427,12 @@ def test_contract_line_state(self): "date_end": self.today + relativedelta(months=5), } ) - # in-progress - lines |= self.acct_line.copy( - { - "date_start": self.today, - "recurring_next_date": self.today, - "date_end": self.today + relativedelta(months=5), - "manual_renew_needed": True, - } - ) - # to-renew - lines |= self.acct_line.copy( - { - "date_start": self.today - relativedelta(months=5), - "recurring_next_date": self.today - relativedelta(months=5), - "date_end": self.today - relativedelta(months=2), - "manual_renew_needed": True, - } - ) # upcoming-close lines |= self.acct_line.copy( { "date_start": self.today - relativedelta(months=5), "recurring_next_date": self.today - relativedelta(months=5), "date_end": self.today + relativedelta(days=20), - "is_auto_renew": False, } ) # closed @@ -2004,7 +1441,6 @@ def test_contract_line_state(self): "date_start": self.today - relativedelta(months=5), "recurring_next_date": self.today - relativedelta(months=5), "date_end": self.today - relativedelta(months=2), - "is_auto_renew": False, } ) # canceled @@ -2027,7 +1463,6 @@ def test_contract_line_state(self): states = [ "upcoming", "in-progress", - "to-renew", "upcoming-close", "closed", "canceled", @@ -2057,32 +1492,6 @@ def test_contract_line_state(self): lines = self.env["contract.line"].search([("state", "not in", state2)]) self.assertEqual(set(lines.mapped("state")), set(states) - set(state2)) - def test_check_auto_renew_contract_line_with_successor(self): - """ - A contract line with a successor can't be set to auto-renew - """ - successor_contract_line = self.acct_line.copy() - with self.assertRaises(ValidationError): - self.acct_line.write( - { - "is_auto_renew": True, - "successor_contract_line_id": successor_contract_line.id, - } - ) - - def test_check_no_date_end_contract_line_with_successor(self): - """ - A contract line with a successor must have a end date - """ - successor_contract_line = self.acct_line.copy() - with self.assertRaises(ValidationError): - self.acct_line.write( - { - "date_end": False, - "successor_contract_line_id": successor_contract_line.id, - } - ) - def test_check_last_date_invoiced_1(self): """ start end can't be before the date of last invoice @@ -2125,24 +1534,12 @@ def test_action_uncancel(self): action["context"]["default_contract_line_id"], self.acct_line.id ) - def test_action_plan_successor(self): - action = self.acct_line.action_plan_successor() - self.assertEqual( - action["context"]["default_contract_line_id"], self.acct_line.id - ) - def test_action_stop(self): action = self.acct_line.action_stop() self.assertEqual( action["context"]["default_contract_line_id"], self.acct_line.id ) - def test_action_stop_plan_successor(self): - action = self.acct_line.action_stop_plan_successor() - self.assertEqual( - action["context"]["default_contract_line_id"], self.acct_line.id - ) - def test_purchase_get_view(self): purchase_tree_view = self.env.ref("contract.contract_line_supplier_tree_view") purchase_form_view = self.env.ref("contract.contract_line_supplier_form_view") diff --git a/contract/tests/test_multicompany.py b/contract/tests/test_multicompany.py index 77a24a2f90..6401e7da40 100644 --- a/contract/tests/test_multicompany.py +++ b/contract/tests/test_multicompany.py @@ -63,7 +63,6 @@ def setUpClass(cls): "recurring_interval": 1, "date_start": "2018-01-01", "recurring_next_date": "2018-01-15", - "is_auto_renew": False, } cls.acct_line_mc = ( cls.env["contract.line"].with_company(cls.company_2).create(cls.line_vals) diff --git a/contract/views/abstract_contract_line.xml b/contract/views/abstract_contract_line.xml index abd0a857fc..8b05e8cc42 100644 --- a/contract/views/abstract_contract_line.xml +++ b/contract/views/abstract_contract_line.xml @@ -61,37 +61,19 @@ - - - - + diff --git a/contract/views/contract.xml b/contract/views/contract.xml index eae34394ee..62713e9c92 100644 --- a/contract/views/contract.xml +++ b/contract/views/contract.xml @@ -261,10 +261,6 @@ name="create_invoice_visibility" column_invisible="True" /> - - - - - + + + diff --git a/contract_renewal/views/contract_template_views.xml b/contract_renewal/views/contract_template_views.xml new file mode 100644 index 0000000000..9f69713c9a --- /dev/null +++ b/contract_renewal/views/contract_template_views.xml @@ -0,0 +1,20 @@ + + + + + contract.renewal.template.form.view + contract.template + + + + + + + parent.contract_type == 'purchase' and not is_auto_renew + + + + diff --git a/contract_renewal/views/contract_views.xml b/contract_renewal/views/contract_views.xml new file mode 100644 index 0000000000..9601f0c85f --- /dev/null +++ b/contract_renewal/views/contract_views.xml @@ -0,0 +1,29 @@ + + + + + contract.contract.renewal.form.view + contract.contract + + + + + + + + + diff --git a/contract_renewal/wizards/__init__.py b/contract_renewal/wizards/__init__.py new file mode 100644 index 0000000000..da65f56988 --- /dev/null +++ b/contract_renewal/wizards/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2024 Manuel Regidor +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import contract_line_wizard diff --git a/contract_renewal/wizards/contract_line_wizard.py b/contract_renewal/wizards/contract_line_wizard.py new file mode 100644 index 0000000000..bdcb7f9b73 --- /dev/null +++ b/contract_renewal/wizards/contract_line_wizard.py @@ -0,0 +1,20 @@ +# Copyright 2024 Manuel Regidor +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ContractLineWizard(models.TransientModel): + _inherit = "contract.line.wizard" + + manual_renew_needed = fields.Boolean( + default=False, + help="This flag is used to make a difference between a definitive stop" + "and temporary one for which a user is not able to plan a" + "successor in advance", + ) + + def _get_stop_extra_vals(self): + vals = super()._get_stop_extra_vals() + vals["manual_renew_needed"] = self.manual_renew_needed + return vals diff --git a/contract_renewal/wizards/contract_line_wizard_views.xml b/contract_renewal/wizards/contract_line_wizard_views.xml new file mode 100644 index 0000000000..4a2b8c6e97 --- /dev/null +++ b/contract_renewal/wizards/contract_line_wizard_views.xml @@ -0,0 +1,32 @@ + + + + + contract.line.renewal.wizard.stop.form.view + contract.line.wizard + + + + + + + + + +