mirror of
https://github.com/OCA/contract.git
synced 2025-02-13 17:57:24 +02:00
[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:
@@ -74,6 +74,7 @@ Known issues / Roadmap
|
||||
======================
|
||||
|
||||
* Recover states and others functional fields in Contracts.
|
||||
* Add recurrence flag at template level.
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@api.depends("contract_type", "company_id")
|
||||
def _compute_journal_id(self):
|
||||
AccountJournal = self.env["account.journal"]
|
||||
for contract in self:
|
||||
domain = [
|
||||
("type", "=", self.contract_type),
|
||||
("company_id", "=", company_id),
|
||||
("type", "=", contract.contract_type),
|
||||
("company_id", "=", contract.company_id.id),
|
||||
]
|
||||
return self.env["account.journal"].search(domain, limit=1)
|
||||
journal = AccountJournal.search(domain, limit=1)
|
||||
if journal:
|
||||
contract.journal_id = journal.id
|
||||
|
||||
@@ -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
|
||||
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:
|
||||
return 1
|
||||
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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
233
contract/models/contract_recurrency_mixin.py
Normal file
233
contract/models/contract_recurrency_mixin.py
Normal 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
|
||||
@@ -1 +1,2 @@
|
||||
* Recover states and others functional fields in Contracts.
|
||||
* Add recurrence flag at template level.
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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="pricelist_id"
|
||||
attrs="{'readonly': [('is_terminated','=',True)]}"
|
||||
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="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}"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user