[IMP+REF] contract: Allow to set recurrency at header level

Big refactoring for allowing to define recurrency at header level for simplifying
the use of the module for most of the cases where you don't need different
recurrency at line level.
This commit is contained in:
Pedro M. Baeza
2020-07-14 20:23:08 +02:00
committed by Francisco Ivan Anton Prieto
parent fc4eb98c74
commit 3374384101
12 changed files with 541 additions and 345 deletions

View File

@@ -74,6 +74,7 @@ Known issues / Roadmap
======================
* Recover states and others functional fields in Contracts.
* Add recurrence flag at template level.
Bug Tracker
===========

View File

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

View File

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

View File

@@ -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",

View File

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

View File

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

View File

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

View File

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

View File

@@ -1 +1,2 @@
* Recover states and others functional fields in Contracts.
* Add recurrence flag at template level.

View File

@@ -425,6 +425,7 @@ To use it, just select the template on the contract and fields will be filled au
<h1><a class="toc-backref" href="#id3">Known issues / Roadmap</a></h1>
<ul class="simple">
<li>Recover states and others functional fields in Contracts.</li>
<li>Add recurrence flag at template level.</li>
</ul>
</div>
<div class="section" id="bug-tracker">

View File

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

View File

@@ -40,7 +40,7 @@
<button
name="recurring_create_invoice"
type="object"
attrs="{'invisible': ['|', ('create_invoice_visibility', '=', False)]}"
attrs="{'invisible': [('create_invoice_visibility', '=', False)]}"
string="Create invoices"
groups="base.group_no_one"
/>
@@ -108,6 +108,10 @@
required="1"
attrs="{'readonly': [('is_terminated','=',True)]}"
/>
<field
name="pricelist_id"
attrs="{'readonly': [('is_terminated','=',True)]}"
/>
<field
name="payment_term_id"
attrs="{'readonly': [('is_terminated','=',True)]}"
@@ -129,31 +133,156 @@
name="fiscal_position_id"
attrs="{'readonly': [('is_terminated','=',True)]}"
/>
<field name="tag_ids" widget="many2many_tags" />
</group>
</group>
<group name="recurring_invoices">
<group>
<field
name="journal_id"
required="1"
attrs="{'readonly': [('is_terminated','=',True)]}"
/>
<field name="recurring_next_date" />
<field name="tag_ids" widget="many2many_tags" />
</group>
<group>
</group>
<group name="recurring_invoices">
<field name="line_recurrence" class="oe_inline" />
<label for="line_recurrence" />
<group attrs="{'invisible': [('line_recurrence', '=', True)]}">
<label for="recurring_interval" />
<div class="o_row">
<field
name="recurring_interval"
attrs="{'required': [('line_recurrence', '=', False)]}"
class="oe_inline"
nolabel="1"
/>
<field
name="recurring_rule_type"
attrs="{'required': [('line_recurrence', '=', False)]}"
class="oe_inline"
nolabel="1"
/>
</div>
<field
name="pricelist_id"
attrs="{'readonly': [('is_terminated','=',True)]}"
name="recurring_invoicing_type"
attrs="{'required': [('line_recurrence', '=', False)]}"
/>
</group>
<group attrs="{'invisible': [('line_recurrence', '=', True)]}">
<field
name="date_start"
attrs="{'required': [('line_recurrence', '=', False)]}"
/>
<field name="date_end" />
<field name="recurring_next_date" />
</group>
</group>
<notebook>
<page name="recurring_invoice_line" string="Recurring Invoices">
<field
name="contract_line_fixed_ids"
attrs="{'readonly': [('is_terminated','=',True)], 'invisible': [('line_recurrence', '=', True)]}"
widget="section_and_note_one2many"
context="{'default_contract_type': contract_type, 'default_recurring_rule_type': recurring_rule_type, 'default_recurring_invoicing_type': recurring_invoicing_type, 'default_recurring_interval': recurring_interval, 'default_date_start': date_start, 'default_recurring_next_date': recurring_next_date}"
>
<tree
decoration-muted="is_canceled"
decoration-info="create_invoice_visibility and not is_canceled"
editable="bottom"
>
<control>
<create string="Add a line" />
<create
string="Add a section"
context="{'default_display_type': 'line_section'}"
/>
<create
string="Add a note"
context="{'default_display_type': 'line_note'}"
/>
</control>
<field name="display_type" invisible="1" />
<field name="sequence" widget="handle" />
<field name="product_id" />
<field name="name" widget="section_and_note_text" />
<field
name="analytic_account_id"
groups="analytic.group_analytic_accounting"
/>
<field
name="analytic_tag_ids"
widget="many2many_tags"
groups="analytic.group_analytic_tags"
/>
<field name="quantity" />
<field name="uom_id" />
<field
name="automatic_price"
attrs="{'column_invisible': [('parent.contract_type', '=', 'purchase')]}"
/>
<field
name="price_unit"
attrs="{'readonly': [('automatic_price', '=', True)]}"
/>
<field name="specific_price" invisible="1" />
<field name="discount" groups="base.group_no_one" />
<field name="price_subtotal" />
<field name="recurring_interval" invisible="1" />
<field name="recurring_rule_type" invisible="1" />
<field
name="recurring_invoicing_type"
invisible="1"
/>
<field name="recurring_next_date" invisible="1" />
<field name="date_start" invisible="1" />
<field name="date_end" />
<field
name="last_date_invoiced"
groups="base.group_no_one"
/>
<field
name="create_invoice_visibility"
invisible="1"
/>
<field
name="is_plan_successor_allowed"
invisible="1"
/>
<field name="is_stop_allowed" invisible="1" />
<field name="is_cancel_allowed" invisible="1" />
<field name="is_un_cancel_allowed" invisible="1" />
<field name="is_canceled" invisible="1" />
<button
name="action_plan_successor"
string="Plan Start"
type="object"
icon="fa-calendar text-success"
attrs="{'invisible': [('is_plan_successor_allowed', '=', False)]}"
/>
<button
name="action_stop"
string="Stop"
type="object"
icon="fa-stop text-danger"
attrs="{'invisible': [('is_stop_allowed', '=', False)]}"
/>
<button
name="cancel"
string="Cancel"
type="object"
icon="fa-ban text-danger"
confirm="Are you sure you want to cancel this line"
attrs="{'invisible': [('is_cancel_allowed', '=', False)]}"
/>
<button
name="action_uncancel"
string="Un-cancel"
type="object"
icon="fa-ban text-success"
attrs="{'invisible': [('is_un_cancel_allowed', '=', False)]}"
/>
</tree>
</field>
<field
name="contract_line_ids"
attrs="{'readonly': [('is_terminated','=',True)]}"
attrs="{'readonly': [('is_terminated','=',True)], 'invisible': [('line_recurrence', '=', False)]}"
widget="section_and_note_one2many"
context="{'default_contract_type': contract_type}"
>