diff --git a/contract/README.rst b/contract/README.rst index 320ce0888..c2592f9d3 100644 --- a/contract/README.rst +++ b/contract/README.rst @@ -74,6 +74,7 @@ Known issues / Roadmap ====================== * Recover states and others functional fields in Contracts. +* Add recurrence flag at template level. Bug Tracker =========== diff --git a/contract/models/__init__.py b/contract/models/__init__.py index a8107a94f..82e9c3b11 100644 --- a/contract/models/__init__.py +++ b/contract/models/__init__.py @@ -1,5 +1,6 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import contract_recurrency_mixin # should be first from . import abstract_contract from . import abstract_contract_line from . import contract_template diff --git a/contract/models/abstract_contract.py b/contract/models/abstract_contract.py index 18753c3bb..377f32f14 100644 --- a/contract/models/abstract_contract.py +++ b/contract/models/abstract_contract.py @@ -10,6 +10,7 @@ from odoo import api, fields, models class ContractAbstractContract(models.AbstractModel): + _inherit = "contract.recurrency.basic.mixin" _name = "contract.abstract.contract" _description = "Abstract Recurring Contract" @@ -27,12 +28,13 @@ class ContractAbstractContract(models.AbstractModel): default="sale", index=True, ) - journal_id = fields.Many2one( - "account.journal", + comodel_name="account.journal", string="Journal", - default=lambda s: s._default_journal(), domain="[('type', '=', contract_type)," "('company_id', '=', company_id)]", + compute="_compute_journal_id", + store=True, + readonly=False, index=True, ) company_id = fields.Many2one( @@ -41,6 +43,11 @@ class ContractAbstractContract(models.AbstractModel): required=True, default=lambda self: self.env.company.id, ) + line_recurrence = fields.Boolean( + string="Recurrence at line level?", + help="Mark this check if you want to control recurrrence at line level instead" + " of all together for the whole contract.", + ) @api.onchange("contract_type") def _onchange_contract_type(self): @@ -48,19 +55,15 @@ class ContractAbstractContract(models.AbstractModel): self.contract_line_ids.filtered("automatic_price").update( {"automatic_price": False} ) - self.journal_id = self.env["account.journal"].search( - [ - ("type", "=", self.contract_type), - ("company_id", "=", self.company_id.id), - ], - limit=1, - ) - @api.model - def _default_journal(self): - company_id = self.env.context.get("company_id", self.env.user.company_id.id) - domain = [ - ("type", "=", self.contract_type), - ("company_id", "=", company_id), - ] - return self.env["account.journal"].search(domain, limit=1) + @api.depends("contract_type", "company_id") + def _compute_journal_id(self): + AccountJournal = self.env["account.journal"] + for contract in self: + domain = [ + ("type", "=", contract.contract_type), + ("company_id", "=", contract.company_id.id), + ] + journal = AccountJournal.search(domain, limit=1) + if journal: + contract.journal_id = journal.id diff --git a/contract/models/abstract_contract_line.py b/contract/models/abstract_contract_line.py index fb3b80ac9..f20870556 100644 --- a/contract/models/abstract_contract_line.py +++ b/contract/models/abstract_contract_line.py @@ -12,6 +12,7 @@ from odoo.tools.translate import _ class ContractAbstractContractLine(models.AbstractModel): + _inherit = "contract.recurrency.basic.mixin" _name = "contract.abstract.contract.line" _description = "Abstract Recurring Contract Line" @@ -47,46 +48,29 @@ class ContractAbstractContractLine(models.AbstractModel): help="Sequence of the contract line when displaying contracts", ) recurring_rule_type = fields.Selection( - [ - ("daily", "Day(s)"), - ("weekly", "Week(s)"), - ("monthly", "Month(s)"), - ("monthlylastday", "Month(s) last day"), - ("quarterly", "Quarter(s)"), - ("semesterly", "Semester(s)"), - ("yearly", "Year(s)"), - ], - default="monthly", - string="Recurrence", - help="Specify Interval for automatic invoice generation.", + compute="_compute_recurring_rule_type", + store=True, + readonly=False, required=True, + copy=True, ) recurring_invoicing_type = fields.Selection( - [("pre-paid", "Pre-paid"), ("post-paid", "Post-paid")], - default="pre-paid", - string="Invoicing type", - help=( - "Specify if the invoice must be generated at the beginning " - "(pre-paid) or end (post-paid) of the period." - ), + compute="_compute_recurring_invoicing_type", + store=True, + readonly=False, required=True, - ) - recurring_invoicing_offset = fields.Integer( - compute="_compute_recurring_invoicing_offset", - string="Invoicing offset", - help=( - "Number of days to offset the invoice from the period end " - "date (in post-paid mode) or start date (in pre-paid mode)." - ), + copy=True, ) recurring_interval = fields.Integer( - default=1, - string="Invoice Every", - help="Invoice every (Days/Week/Month/Year)", + compute="_compute_recurring_interval", + store=True, + readonly=False, required=True, + copy=True, + ) + date_start = fields.Date( + compute="_compute_date_start", store=True, readonly=False, copy=True, ) - date_start = fields.Date(string="Date Start") - recurring_next_date = fields.Date(string="Date of Next Invoice") last_date_invoiced = fields.Date(string="Last Date Invoiced") is_canceled = fields.Boolean(string="Canceled", default=False) is_auto_renew = fields.Boolean(string="Auto Renew", default=False) @@ -138,17 +122,38 @@ class ContractAbstractContractLine(models.AbstractModel): is_recurring_note = fields.Boolean(compute="_compute_is_recurring_note") company_id = fields.Many2one(related="contract_id.company_id", store=True) - @api.model - def _get_default_recurring_invoicing_offset( - self, recurring_invoicing_type, recurring_rule_type - ): - if ( - recurring_invoicing_type == "pre-paid" - or recurring_rule_type == "monthlylastday" - ): - return 0 - else: - return 1 + def _set_recurrence_field(self, field): + """Helper method for computed methods that gets the equivalent field + in the header. + + We need to re-assign the original value for avoiding a missing error. + """ + for record in self: + if record.contract_id.line_recurrence: + record[field] = record[field] + else: + record[field] = record.contract_id[field] + + @api.depends("contract_id.recurring_rule_type", "contract_id.line_recurrence") + def _compute_recurring_rule_type(self): + self._set_recurrence_field("recurring_rule_type") + + @api.depends("contract_id.recurring_invoicing_type", "contract_id.line_recurrence") + def _compute_recurring_invoicing_type(self): + self._set_recurrence_field("recurring_invoicing_type") + + @api.depends("contract_id.recurring_interval", "contract_id.line_recurrence") + def _compute_recurring_interval(self): + self._set_recurrence_field("recurring_interval") + + @api.depends("contract_id.date_start", "contract_id.line_recurrence") + def _compute_date_start(self): + self._set_recurrence_field("date_start") + + @api.depends("contract_id.recurring_next_date", "contract_id.line_recurrence") + def _compute_recurring_next_date(self): + super()._compute_recurring_next_date() + self._set_recurrence_field("recurring_next_date") @api.depends("display_type", "note_invoicing_mode") def _compute_is_recurring_note(self): @@ -158,14 +163,6 @@ class ContractAbstractContractLine(models.AbstractModel): and record.note_invoicing_mode == "custom" ) - @api.depends("recurring_invoicing_type", "recurring_rule_type") - def _compute_recurring_invoicing_offset(self): - for rec in self: - method = self._get_default_recurring_invoicing_offset - rec.recurring_invoicing_offset = method( - rec.recurring_invoicing_type, rec.recurring_rule_type - ) - @api.depends( "automatic_price", "specific_price", diff --git a/contract/models/account_move.py b/contract/models/account_move.py index ba2528cb8..6e957f89b 100644 --- a/contract/models/account_move.py +++ b/contract/models/account_move.py @@ -6,7 +6,7 @@ from odoo import fields, models -class AccountInvoice(models.Model): +class AccountMove(models.Model): _inherit = "account.move" # We keep this field for migration purpose diff --git a/contract/models/contract.py b/contract/models/contract.py index b381d5dc5..06955ebb3 100644 --- a/contract/models/contract.py +++ b/contract/models/contract.py @@ -20,6 +20,7 @@ class ContractContract(models.Model): "mail.thread", "mail.activity.mixin", "contract.abstract.contract", + "contract.recurrency.mixin", ] active = fields.Boolean(default=True,) @@ -43,6 +44,16 @@ class ContractContract(models.Model): inverse_name="contract_id", copy=True, ) + # Trick for being able to have 2 different views for the same o2m + # We need this as one2many widget doesn't allow to define in the view + # the same field 2 times with different views. 2 views are needed because + # one of them must be editable inline and the other not, which can't be + # parametrized through attrs. + contract_line_fixed_ids = fields.One2many( + string="Contract lines (fixed)", + comodel_name="contract.line", + inverse_name="contract_id", + ) user_id = fields.Many2one( comodel_name="res.users", @@ -53,12 +64,7 @@ class ContractContract(models.Model): create_invoice_visibility = fields.Boolean( compute="_compute_create_invoice_visibility" ) - recurring_next_date = fields.Date( - compute="_compute_recurring_next_date", - string="Date of Next Invoice", - store=True, - ) - date_end = fields.Date(compute="_compute_date_end", string="Date End", store=True) + date_end = fields.Date(compute="_compute_date_end", store=True, readonly=False) payment_term_id = fields.Many2one( comodel_name="account.payment.term", string="Payment Terms", index=True ) @@ -122,6 +128,8 @@ class ContractContract(models.Model): .search([("contract_line_id", "in", self.contract_line_ids.ids,)]) .mapped("move_id") ) + # we are forced to always search for this for not losing possible <=v11 + # generated invoices invoices |= self.env["account.move"].search([("old_contract_id", "=", self.id)]) return invoices @@ -198,10 +206,15 @@ class ContractContract(models.Model): and (not l.display_type or l.is_recurring_note) ) ).mapped("recurring_next_date") - if recurring_next_date: - contract.recurring_next_date = min(recurring_next_date) + # we give priority to computation from date_start if modified + if ( + contract._origin + and contract._origin.date_start != contract.date_start + or not recurring_next_date + ): + super(ContractContract, contract)._compute_recurring_next_date() else: - contract.recurring_next_date = False + contract.recurring_next_date = min(recurring_next_date) @api.depends("contract_line_ids.create_invoice_visibility") def _compute_create_invoice_visibility(self): @@ -276,7 +289,6 @@ class ContractContract(models.Model): 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_date_start() new_lines._onchange_is_auto_renew() return new_lines diff --git a/contract/models/contract_line.py b/contract/models/contract_line.py index 3be185e92..e3122b101 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -16,7 +16,10 @@ from .contract_line_constraints import get_allowed class ContractLine(models.Model): _name = "contract.line" _description = "Contract Line" - _inherit = "contract.abstract.contract.line" + _inherit = [ + "contract.abstract.contract.line", + "contract.recurrency.mixin", + ] _order = "sequence,id" sequence = fields.Integer(string="Sequence",) @@ -34,22 +37,8 @@ class ContractLine(models.Model): analytic_tag_ids = fields.Many2many( comodel_name="account.analytic.tag", string="Analytic Tags", ) - date_start = fields.Date( - string="Date Start", - required=True, - default=lambda self: fields.Date.context_today(self), - ) - date_end = fields.Date(string="Date End", index=True) - recurring_next_date = fields.Date(string="Date of Next Invoice") - last_date_invoiced = fields.Date( - string="Last Date Invoiced", readonly=True, copy=False - ) - next_period_date_start = fields.Date( - string="Next Period Start", compute="_compute_next_period_date_start", - ) - next_period_date_end = fields.Date( - string="Next Period End", compute="_compute_next_period_date_end", - ) + date_start = fields.Date(required=True) + date_end = fields.Date(compute="_compute_date_end", store=True, readonly=False) termination_notice_date = fields.Date( string="Termination notice date", compute="_compute_termination_notice_date", @@ -119,6 +108,29 @@ class ContractLine(models.Model): default=True, ) + @api.depends( + "last_date_invoiced", "date_start", "date_end", "contract_id.last_date_invoiced" + ) + def _compute_next_period_date_start(self): + """Rectify next period date start if another line in the contract has been + already invoiced previously. + """ + for rec in self: + lines = rec.contract_id.contract_line_ids + if not rec.last_date_invoiced and any(lines.mapped("last_date_invoiced")): + next_period_date_start = max( + lines.filtered("last_date_invoiced").mapped("last_date_invoiced") + ) + relativedelta(days=1) + if rec.date_end and next_period_date_start > rec.date_end: + next_period_date_start = False + rec.next_period_date_start = next_period_date_start + else: + super(ContractLine, rec)._compute_next_period_date_start() + + @api.depends("contract_id.date_end", "contract_id.line_recurrence") + def _compute_date_end(self): + self._set_recurrence_field("date_end") + @api.depends( "date_end", "termination_notice_rule_type", "termination_notice_interval", ) @@ -389,115 +401,6 @@ class ContractLine(models.Model): max_date_end=False, ) - @api.model - def get_next_invoice_date( - self, - next_period_date_start, - recurring_invoicing_type, - recurring_invoicing_offset, - recurring_rule_type, - recurring_interval, - max_date_end, - ): - next_period_date_end = self.get_next_period_date_end( - next_period_date_start, - recurring_rule_type, - recurring_interval, - max_date_end=max_date_end, - ) - if not next_period_date_end: - return False - if recurring_invoicing_type == "pre-paid": - recurring_next_date = next_period_date_start + relativedelta( - days=recurring_invoicing_offset - ) - else: # post-paid - recurring_next_date = next_period_date_end + relativedelta( - days=recurring_invoicing_offset - ) - return recurring_next_date - - @api.model - def get_next_period_date_end( - self, - next_period_date_start, - recurring_rule_type, - recurring_interval, - max_date_end, - next_invoice_date=False, - recurring_invoicing_type=False, - recurring_invoicing_offset=False, - ): - """Compute the end date for the next period. - - The next period normally depends on recurrence options only. - It is however possible to provide it a next invoice date, in - which case this method can adjust the next period based on that - too. In that scenario it required the invoicing type and offset - arguments. - """ - if not next_period_date_start: - return False - if max_date_end and next_period_date_start > max_date_end: - # start is past max date end: there is no next period - return False - if not next_invoice_date: - # regular algorithm - next_period_date_end = ( - next_period_date_start - + self.get_relative_delta(recurring_rule_type, recurring_interval) - - relativedelta(days=1) - ) - else: - # special algorithm when the next invoice date is forced - if recurring_invoicing_type == "pre-paid": - next_period_date_end = ( - next_invoice_date - - relativedelta(days=recurring_invoicing_offset) - + self.get_relative_delta(recurring_rule_type, recurring_interval) - - relativedelta(days=1) - ) - else: # post-paid - next_period_date_end = next_invoice_date - relativedelta( - days=recurring_invoicing_offset - ) - if max_date_end and next_period_date_end > max_date_end: - # end date is past max_date_end: trim it - next_period_date_end = max_date_end - return next_period_date_end - - @api.depends("last_date_invoiced", "date_start", "date_end") - def _compute_next_period_date_start(self): - for rec in self: - if rec.last_date_invoiced: - next_period_date_start = rec.last_date_invoiced + relativedelta(days=1) - else: - next_period_date_start = rec.date_start - if rec.date_end and next_period_date_start > rec.date_end: - next_period_date_start = False - rec.next_period_date_start = next_period_date_start - - @api.depends( - "next_period_date_start", - "recurring_invoicing_type", - "recurring_invoicing_offset", - "recurring_rule_type", - "recurring_interval", - "date_end", - "recurring_next_date", - ) - def _compute_next_period_date_end(self): - for rec in self: - rec.next_period_date_end = self.get_next_period_date_end( - rec.next_period_date_start, - rec.recurring_rule_type, - rec.recurring_interval, - max_date_end=rec.date_end, - next_invoice_date=rec.recurring_next_date, - recurring_invoicing_type=rec.recurring_invoicing_type, - recurring_invoicing_offset=rec.recurring_invoicing_offset, - ) - @api.model def _get_first_date_end( self, date_start, auto_renew_rule_type, auto_renew_interval @@ -520,24 +423,6 @@ class ContractLine(models.Model): rec.date_start, rec.auto_renew_rule_type, rec.auto_renew_interval, ) - @api.onchange( - "date_start", - "date_end", - "recurring_invoicing_type", - "recurring_rule_type", - "recurring_interval", - ) - def _onchange_date_start(self): - for rec in self.filtered("date_start"): - rec.recurring_next_date = self.get_next_invoice_date( - rec.next_period_date_start, - rec.recurring_invoicing_type, - rec.recurring_invoicing_offset, - rec.recurring_rule_type, - rec.recurring_interval, - max_date_end=rec.date_end, - ) - @api.constrains("is_canceled", "is_auto_renew") def _check_auto_renew_canceled_lines(self): for rec in self: @@ -548,7 +433,9 @@ class ContractLine(models.Model): @api.constrains("recurring_next_date", "date_start") def _check_recurring_next_date_start_date(self): - for line in self.filtered("recurring_next_date"): + for line in self: + if line.display_type == "line_section" or not line.recurring_next_date: + continue if line.date_start and line.recurring_next_date: if line.date_start > line.recurring_next_date: raise ValidationError( @@ -564,14 +451,6 @@ class ContractLine(models.Model): ) def _check_last_date_invoiced(self): for rec in self.filtered("last_date_invoiced"): - if rec.date_start and rec.date_start > rec.last_date_invoiced: - raise ValidationError( - _( - "You can't have the start date after the date of last " - "invoice for the contract line '%s'" - ) - % rec.name - ) if rec.date_end and rec.date_end < rec.last_date_invoiced: raise ValidationError( _( @@ -580,6 +459,16 @@ class ContractLine(models.Model): ) % rec.name ) + if not rec.contract_id.line_recurrence: + continue + if rec.date_start and rec.date_start > rec.last_date_invoiced: + raise ValidationError( + _( + "You can't have the start date after the date of last " + "invoice for the contract line '%s'" + ) + % rec.name + ) if ( rec.recurring_next_date and rec.recurring_next_date <= rec.last_date_invoiced @@ -719,51 +608,6 @@ class ContractLine(models.Model): } ) - def _init_last_date_invoiced(self): - """Used to init last_date_invoiced for migration purpose""" - for rec in self: - last_date_invoiced = rec.recurring_next_date - relativedelta(days=1) - if rec.recurring_rule_type == "monthlylastday": - last_date_invoiced = ( - rec.recurring_next_date - - self.get_relative_delta( - rec.recurring_rule_type, rec.recurring_interval - 1 - ) - - relativedelta(days=1) - ) - elif rec.recurring_invoicing_type == "post-paid": - last_date_invoiced = ( - rec.recurring_next_date - - self.get_relative_delta( - rec.recurring_rule_type, rec.recurring_interval - ) - - relativedelta(days=1) - ) - if last_date_invoiced > rec.date_start: - rec.last_date_invoiced = last_date_invoiced - - @api.model - def get_relative_delta(self, recurring_rule_type, interval): - """Return a relativedelta for one period. - - When added to the first day of the period, - it gives the first day of the next period. - """ - if recurring_rule_type == "daily": - return relativedelta(days=interval) - elif recurring_rule_type == "weekly": - return relativedelta(weeks=interval) - elif recurring_rule_type == "monthly": - return relativedelta(months=interval) - elif recurring_rule_type == "monthlylastday": - return relativedelta(months=interval, day=1) - elif recurring_rule_type == "quarterly": - return relativedelta(months=3 * interval) - elif recurring_rule_type == "semesterly": - return relativedelta(months=6 * interval) - else: - return relativedelta(years=interval) - def _delay(self, delay_delta): """ Delay a contract line @@ -1145,7 +989,6 @@ class ContractLine(models.Model): new_line = self.plan_successor( date_start, date_end, is_auto_renew, post_message=False ) - new_line._onchange_date_start() return new_line def _renew_extend_line(self, date_end): diff --git a/contract/models/contract_recurrency_mixin.py b/contract/models/contract_recurrency_mixin.py new file mode 100644 index 000000000..cbbcd5a11 --- /dev/null +++ b/contract/models/contract_recurrency_mixin.py @@ -0,0 +1,233 @@ +# Copyright 2018 ACSONE SA/NV. +# Copyright 2020 Tecnativa - Pedro M. Baeza +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models + + +class ContractRecurrencyBasicMixin(models.AbstractModel): + _name = "contract.recurrency.basic.mixin" + _description = "Basic recurrency mixin for abstract contract models" + + recurring_rule_type = fields.Selection( + [ + ("daily", "Day(s)"), + ("weekly", "Week(s)"), + ("monthly", "Month(s)"), + ("monthlylastday", "Month(s) last day"), + ("quarterly", "Quarter(s)"), + ("semesterly", "Semester(s)"), + ("yearly", "Year(s)"), + ], + default="monthly", + string="Recurrence", + help="Specify Interval for automatic invoice generation.", + ) + recurring_invoicing_type = fields.Selection( + [("pre-paid", "Pre-paid"), ("post-paid", "Post-paid")], + default="pre-paid", + string="Invoicing type", + help=( + "Specify if the invoice must be generated at the beginning " + "(pre-paid) or end (post-paid) of the period." + ), + ) + recurring_invoicing_offset = fields.Integer( + compute="_compute_recurring_invoicing_offset", + string="Invoicing offset", + help=( + "Number of days to offset the invoice from the period end " + "date (in post-paid mode) or start date (in pre-paid mode)." + ), + ) + recurring_interval = fields.Integer( + default=1, string="Invoice Every", help="Invoice every (Days/Week/Month/Year)", + ) + date_start = fields.Date(string="Date Start") + recurring_next_date = fields.Date(string="Date of Next Invoice") + + @api.depends("recurring_invoicing_type", "recurring_rule_type") + def _compute_recurring_invoicing_offset(self): + for rec in self: + method = self._get_default_recurring_invoicing_offset + rec.recurring_invoicing_offset = method( + rec.recurring_invoicing_type, rec.recurring_rule_type + ) + + @api.model + def _get_default_recurring_invoicing_offset( + self, recurring_invoicing_type, recurring_rule_type + ): + if ( + recurring_invoicing_type == "pre-paid" + or recurring_rule_type == "monthlylastday" + ): + return 0 + else: + return 1 + + +class ContractRecurrencyMixin(models.AbstractModel): + _inherit = "contract.recurrency.basic.mixin" + _name = "contract.recurrency.mixin" + _description = "Recurrency mixin for contract models" + + date_start = fields.Date(default=lambda self: fields.Date.context_today(self)) + recurring_next_date = fields.Date( + compute="_compute_recurring_next_date", store=True, readonly=False, copy=True + ) + date_end = fields.Date(string="Date End", index=True) + next_period_date_start = fields.Date( + string="Next Period Start", compute="_compute_next_period_date_start", + ) + next_period_date_end = fields.Date( + string="Next Period End", compute="_compute_next_period_date_end", + ) + last_date_invoiced = fields.Date( + string="Last Date Invoiced", readonly=True, copy=False + ) + + @api.depends("next_period_date_start") + def _compute_recurring_next_date(self): + for rec in self.filtered("next_period_date_start"): + rec.recurring_next_date = self.get_next_invoice_date( + rec.next_period_date_start, + rec.recurring_invoicing_type, + rec.recurring_invoicing_offset, + rec.recurring_rule_type, + rec.recurring_interval, + max_date_end=rec.date_end, + ) + + @api.depends("last_date_invoiced", "date_start", "date_end") + def _compute_next_period_date_start(self): + for rec in self: + if rec.last_date_invoiced: + next_period_date_start = rec.last_date_invoiced + relativedelta(days=1) + else: + next_period_date_start = rec.date_start + if rec.date_end and next_period_date_start > rec.date_end: + next_period_date_start = False + rec.next_period_date_start = next_period_date_start + + @api.depends( + "next_period_date_start", + "recurring_invoicing_type", + "recurring_invoicing_offset", + "recurring_rule_type", + "recurring_interval", + "date_end", + "recurring_next_date", + ) + def _compute_next_period_date_end(self): + for rec in self: + rec.next_period_date_end = self.get_next_period_date_end( + rec.next_period_date_start, + rec.recurring_rule_type, + rec.recurring_interval, + max_date_end=rec.date_end, + next_invoice_date=rec.recurring_next_date, + recurring_invoicing_type=rec.recurring_invoicing_type, + recurring_invoicing_offset=rec.recurring_invoicing_offset, + ) + + @api.model + def get_relative_delta(self, recurring_rule_type, interval): + """Return a relativedelta for one period. + + When added to the first day of the period, + it gives the first day of the next period. + """ + if recurring_rule_type == "daily": + return relativedelta(days=interval) + elif recurring_rule_type == "weekly": + return relativedelta(weeks=interval) + elif recurring_rule_type == "monthly": + return relativedelta(months=interval) + elif recurring_rule_type == "monthlylastday": + return relativedelta(months=interval, day=1) + elif recurring_rule_type == "quarterly": + return relativedelta(months=3 * interval) + elif recurring_rule_type == "semesterly": + return relativedelta(months=6 * interval) + else: + return relativedelta(years=interval) + + @api.model + def get_next_period_date_end( + self, + next_period_date_start, + recurring_rule_type, + recurring_interval, + max_date_end, + next_invoice_date=False, + recurring_invoicing_type=False, + recurring_invoicing_offset=False, + ): + """Compute the end date for the next period. + + The next period normally depends on recurrence options only. + It is however possible to provide it a next invoice date, in + which case this method can adjust the next period based on that + too. In that scenario it required the invoicing type and offset + arguments. + """ + if not next_period_date_start: + return False + if max_date_end and next_period_date_start > max_date_end: + # start is past max date end: there is no next period + return False + if not next_invoice_date: + # regular algorithm + next_period_date_end = ( + next_period_date_start + + self.get_relative_delta(recurring_rule_type, recurring_interval) + - relativedelta(days=1) + ) + else: + # special algorithm when the next invoice date is forced + if recurring_invoicing_type == "pre-paid": + next_period_date_end = ( + next_invoice_date + - relativedelta(days=recurring_invoicing_offset) + + self.get_relative_delta(recurring_rule_type, recurring_interval) + - relativedelta(days=1) + ) + else: # post-paid + next_period_date_end = next_invoice_date - relativedelta( + days=recurring_invoicing_offset + ) + if max_date_end and next_period_date_end > max_date_end: + # end date is past max_date_end: trim it + next_period_date_end = max_date_end + return next_period_date_end + + @api.model + def get_next_invoice_date( + self, + next_period_date_start, + recurring_invoicing_type, + recurring_invoicing_offset, + recurring_rule_type, + recurring_interval, + max_date_end, + ): + next_period_date_end = self.get_next_period_date_end( + next_period_date_start, + recurring_rule_type, + recurring_interval, + max_date_end=max_date_end, + ) + if not next_period_date_end: + return False + if recurring_invoicing_type == "pre-paid": + recurring_next_date = next_period_date_start + relativedelta( + days=recurring_invoicing_offset + ) + else: # post-paid + recurring_next_date = next_period_date_end + relativedelta( + days=recurring_invoicing_offset + ) + return recurring_next_date diff --git a/contract/readme/ROADMAP.rst b/contract/readme/ROADMAP.rst index 9fcacdf41..0e42fcb53 100644 --- a/contract/readme/ROADMAP.rst +++ b/contract/readme/ROADMAP.rst @@ -1 +1,2 @@ * Recover states and others functional fields in Contracts. +* Add recurrence flag at template level. diff --git a/contract/static/description/index.html b/contract/static/description/index.html index 97cc2b423..a2c439178 100644 --- a/contract/static/description/index.html +++ b/contract/static/description/index.html @@ -425,6 +425,7 @@ To use it, just select the template on the contract and fields will be filled au

Known issues / Roadmap

diff --git a/contract/tests/test_contract.py b/contract/tests/test_contract.py index 22476ddee..8b6b566f1 100644 --- a/contract/tests/test_contract.py +++ b/contract/tests/test_contract.py @@ -9,7 +9,7 @@ from dateutil.relativedelta import relativedelta from odoo import fields from odoo.exceptions import UserError, ValidationError -from odoo.tests import common +from odoo.tests import Form, common def to_date(date): @@ -72,6 +72,7 @@ class TestContractBase(common.SavepointCase): "name": "Test Contract", "partner_id": cls.partner.id, "pricelist_id": cls.partner.property_product_pricelist.id, + "line_recurrence": True, } ) cls.contract2 = cls.env["contract.contract"].create( @@ -79,6 +80,7 @@ class TestContractBase(common.SavepointCase): "name": "Test Contract 2", "partner_id": cls.partner.id, "pricelist_id": cls.partner.property_product_pricelist.id, + "line_recurrence": True, "contract_type": "purchase", "contract_line_ids": [ ( @@ -152,7 +154,7 @@ class TestContract(TestContractBase): self.assertEqual(self.acct_line.price_unit, 10) def test_contract(self): - recurring_next_date = to_date("2018-02-15") + self.assertEqual(self.contract.recurring_next_date, to_date("2018-01-15")) self.assertAlmostEqual(self.acct_line.price_subtotal, 50.0) res = self.acct_line._onchange_product_id() self.assertIn("uom_id", res["domain"]) @@ -161,22 +163,12 @@ class TestContract(TestContractBase): self.contract.recurring_create_invoice() self.invoice_monthly = self.contract._get_related_invoices() self.assertTrue(self.invoice_monthly) - self.assertEqual(self.acct_line.recurring_next_date, recurring_next_date) + self.assertEqual(self.acct_line.recurring_next_date, to_date("2018-02-15")) self.inv_line = self.invoice_monthly.invoice_line_ids[0] self.assertTrue(self.inv_line.tax_ids) self.assertAlmostEqual(self.inv_line.price_subtotal, 50.0) self.assertEqual(self.contract.user_id, self.invoice_monthly.user_id) - def test_contract_recurring_next_date(self): - recurring_next_date = to_date("2018-01-15") - self.assertEqual(self.contract.recurring_next_date, recurring_next_date) - contract_line = self.acct_line.copy({"recurring_next_date": "2018-01-14"}) - recurring_next_date = to_date("2018-01-14") - self.assertEqual(self.contract.recurring_next_date, recurring_next_date) - contract_line.cancel() - recurring_next_date = to_date("2018-01-15") - self.assertEqual(self.contract.recurring_next_date, recurring_next_date) - def test_contract_daily(self): recurring_next_date = to_date("2018-02-23") last_date_invoiced = to_date("2018-02-22") @@ -306,7 +298,6 @@ class TestContract(TestContractBase): self.acct_line.date_start = "2018-01-01" self.acct_line.recurring_invoicing_type = "post-paid" self.acct_line.date_end = "2018-03-15" - self.acct_line._onchange_date_start() self.assertTrue(self.acct_line.create_invoice_visibility) self.assertEqual(self.acct_line.recurring_next_date, to_date("2018-02-01")) self.assertFalse(self.acct_line.last_date_invoiced) @@ -333,7 +324,6 @@ class TestContract(TestContractBase): self.acct_line.date_start = "2018-01-01" self.acct_line.recurring_invoicing_type = "pre-paid" self.acct_line.date_end = "2018-03-15" - self.acct_line._onchange_date_start() self.assertTrue(self.acct_line.create_invoice_visibility) self.assertEqual(self.acct_line.recurring_next_date, to_date("2018-01-01")) self.assertFalse(self.acct_line.last_date_invoiced) @@ -363,12 +353,6 @@ class TestContract(TestContractBase): self.contract.partner_id.property_product_pricelist, ) - def test_onchange_date_start(self): - recurring_next_date = to_date("2018-01-01") - self.acct_line.date_start = recurring_next_date - self.acct_line._onchange_date_start() - self.assertEqual(self.acct_line.recurring_next_date, recurring_next_date) - def test_uom(self): uom_litre = self.env.ref("uom.product_uom_litre") self.acct_line.uom_id = uom_litre.id @@ -1626,7 +1610,6 @@ class TestContract(TestContractBase): self.acct_line.date_start = "2018-01-01" self.acct_line.recurring_invoicing_type = "post-paid" self.acct_line.date_end = "2018-03-15" - self.acct_line._onchange_date_start() contracts = self.contract2 for _i in range(10): contracts |= self.contract.copy() @@ -1642,7 +1625,6 @@ class TestContract(TestContractBase): self.acct_line.date_start = "2018-01-01" self.acct_line.recurring_invoicing_type = "post-paid" self.acct_line.date_end = "2018-03-15" - self.acct_line._onchange_date_start() self.contract2.unlink() contracts = self.contract for _i in range(10): @@ -1674,7 +1656,6 @@ class TestContract(TestContractBase): self.acct_line.recurring_invoicing_type = "post-paid" self.acct_line.recurring_rule_type = "monthlylastday" self.acct_line.date_end = "2018-03-15" - self.acct_line._onchange_date_start() first, last, recurring_next_date = self.acct_line._get_period_to_invoice( self.acct_line.last_date_invoiced, self.acct_line.recurring_next_date, ) @@ -1699,7 +1680,6 @@ class TestContract(TestContractBase): self.acct_line.recurring_invoicing_type = "pre-paid" self.acct_line.recurring_rule_type = "monthlylastday" self.acct_line.date_end = "2018-03-15" - self.acct_line._onchange_date_start() first, last, recurring_next_date = self.acct_line._get_period_to_invoice( self.acct_line.last_date_invoiced, self.acct_line.recurring_next_date, ) @@ -1740,7 +1720,6 @@ class TestContract(TestContractBase): self.acct_line.recurring_invoicing_type = "pre-paid" self.acct_line.recurring_rule_type = "monthly" self.acct_line.date_end = "2018-08-15" - self.acct_line._onchange_date_start() self.contract.recurring_create_invoice() first, last, recurring_next_date = self.acct_line._get_period_to_invoice( self.acct_line.last_date_invoiced, self.acct_line.recurring_next_date, @@ -1759,7 +1738,6 @@ class TestContract(TestContractBase): self.acct_line.recurring_invoicing_type = "post-paid" self.acct_line.recurring_rule_type = "monthly" self.acct_line.date_end = "2018-08-15" - self.acct_line._onchange_date_start() self.contract.recurring_create_invoice() first, last, recurring_next_date = self.acct_line._get_period_to_invoice( self.acct_line.last_date_invoiced, self.acct_line.recurring_next_date, @@ -1778,7 +1756,6 @@ class TestContract(TestContractBase): self.acct_line.recurring_invoicing_type = "post-paid" self.acct_line.recurring_rule_type = "monthly" self.acct_line.date_end = "2018-03-15" - self.acct_line._onchange_date_start() first, last, recurring_next_date = self.acct_line._get_period_to_invoice( self.acct_line.last_date_invoiced, self.acct_line.recurring_next_date, ) @@ -1802,7 +1779,6 @@ class TestContract(TestContractBase): self.acct_line.recurring_invoicing_type = "pre-paid" self.acct_line.recurring_rule_type = "monthly" self.acct_line.date_end = "2018-03-15" - self.acct_line._onchange_date_start() first, last, recurring_next_date = self.acct_line._get_period_to_invoice( self.acct_line.last_date_invoiced, self.acct_line.recurring_next_date, ) @@ -1826,7 +1802,6 @@ class TestContract(TestContractBase): self.acct_line.recurring_invoicing_type = "post-paid" self.acct_line.recurring_rule_type = "yearly" self.acct_line.date_end = "2020-03-15" - self.acct_line._onchange_date_start() first, last, recurring_next_date = self.acct_line._get_period_to_invoice( self.acct_line.last_date_invoiced, self.acct_line.recurring_next_date, ) @@ -1850,7 +1825,6 @@ class TestContract(TestContractBase): self.acct_line.recurring_invoicing_type = "pre-paid" self.acct_line.recurring_rule_type = "yearly" self.acct_line.date_end = "2020-03-15" - self.acct_line._onchange_date_start() first, last, recurring_next_date = self.acct_line._get_period_to_invoice( self.acct_line.last_date_invoiced, self.acct_line.recurring_next_date, ) @@ -2024,30 +1998,6 @@ class TestContract(TestContractBase): {"last_date_invoiced": self.acct_line.date_end + relativedelta(days=1)} ) - def test_init_last_date_invoiced(self): - self.acct_line.write( - {"date_start": "2019-01-01", "recurring_next_date": "2019-03-01"} - ) - line_monthlylastday = self.acct_line.copy( - { - "recurring_rule_type": "monthlylastday", - "recurring_next_date": "2019-03-31", - } - ) - line_prepaid = self.acct_line.copy( - {"recurring_invoicing_type": "pre-paid", "recurring_rule_type": "monthly"} - ) - line_postpaid = self.acct_line.copy( - {"recurring_invoicing_type": "post-paid", "recurring_rule_type": "monthly"} - ) - lines = line_monthlylastday | line_prepaid | line_postpaid - lines.write({"last_date_invoiced": False}) - self.assertFalse(any(lines.mapped("last_date_invoiced"))) - lines._init_last_date_invoiced() - self.assertEqual(line_monthlylastday.last_date_invoiced, to_date("2019-02-28")) - self.assertEqual(line_prepaid.last_date_invoiced, to_date("2019-02-28")) - self.assertEqual(line_postpaid.last_date_invoiced, to_date("2019-01-31")) - def test_delay_invoiced_contract_line(self): self.acct_line.write( {"last_date_invoiced": self.acct_line.date_start + relativedelta(days=1)} @@ -2236,6 +2186,31 @@ class TestContract(TestContractBase): self.terminate_reason, "terminate_comment", to_date("2018-02-13"), ) + def test_recurrency_propagation(self): + # Existing contract + vals = { + "recurring_rule_type": "yearly", + "recurring_interval": 2, + "date_start": to_date("2020-01-01"), + } + vals2 = vals.copy() + vals2["line_recurrence"] = False + self.contract.write(vals2) + for field in vals: + self.assertEqual(vals[field], self.acct_line[field]) + # New contract + contract_form = Form(self.env["contract.contract"]) + contract_form.partner_id = self.partner + contract_form.name = "Test new contract" + contract_form.line_recurrence = False + for field in vals: + setattr(contract_form, field, vals[field]) + with contract_form.contract_line_fixed_ids.new() as line_form: + line_form.product_id = self.product_1 + contract2 = contract_form.save() + for field in vals: + self.assertEqual(vals[field], contract2.contract_line_ids[field]) + def test_currency(self): currency_eur = self.env.ref("base.EUR") currency_cad = self.env.ref("base.CAD") diff --git a/contract/views/contract.xml b/contract/views/contract.xml index a677e3713..927a4a08f 100644 --- a/contract/views/contract.xml +++ b/contract/views/contract.xml @@ -40,7 +40,7 @@