Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions contract/models/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ def _compute_invoice_count(self):
"next_period_date_start",
"recurring_invoicing_type",
"recurring_invoicing_offset",
"recurring_invoicing_offset_type",
"recurring_rule_type",
"recurring_interval",
"date_end",
Expand All @@ -195,6 +196,7 @@ def _compute_recurring_next_date(self):
contract.next_period_date_start,
contract.recurring_invoicing_type,
contract.recurring_invoicing_offset,
contract.recurring_invoicing_offset_type,
contract.recurring_rule_type,
contract.recurring_interval,
max_date_end=contract.date_end,
Expand Down
45 changes: 19 additions & 26 deletions contract/models/contract_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,26 +60,6 @@ def _compute_display_name(self):
for rec in self:
rec.display_name = f"{rec.date_start} - {rec.name}"

@api.model
def _compute_first_recurring_next_date(
self,
date_start,
recurring_invoicing_type,
recurring_rule_type,
recurring_interval,
):
# deprecated method for backward compatibility
return self.get_next_invoice_date(
date_start,
recurring_invoicing_type,
self._get_default_recurring_invoicing_offset(
recurring_invoicing_type, recurring_rule_type
),
recurring_rule_type,
recurring_interval,
max_date_end=False,
)

def _get_analytic_distribution_arguments(self):
self.ensure_one()
partner_categ_ids = self.contract_id.partner_id.category_id.ids
Expand All @@ -100,7 +80,12 @@ def _compute_analytic_distribution(self):
]._get_distribution(line._get_analytic_distribution_arguments())
line.analytic_distribution = distribution or line.analytic_distribution

@api.constrains("recurring_next_date", "date_start")
@api.constrains(
"recurring_next_date",
"date_start",
"recurring_invoicing_offset",
"recurring_invoicing_offset_type",
)
def _check_recurring_next_date_start_date(self):
for line in self:
if (
Expand All @@ -109,7 +94,16 @@ def _check_recurring_next_date_start_date(self):
):
continue
if line.date_start and line.recurring_next_date:
if line.date_start > line.recurring_next_date:
if (
line.date_start
+ self._get_invoicing_offset_relative_delta(
line.recurring_invoicing_type,
line.recurring_rule_type,
line.recurring_invoicing_offset,
line.recurring_invoicing_offset_type,
)
> line.recurring_next_date
):
raise ValidationError(
self.env._(
"You can't have a date of next invoice anterior "
Expand Down Expand Up @@ -163,8 +157,7 @@ def _check_recurring_next_date_recurring_invoices(self):
):
raise ValidationError(
self.env._(
"You must supply a date of next invoice for contract "
"line '%s'"
"You must supply a date of next invoice for contract line '%s'"
)
% rec.name
)
Expand All @@ -176,8 +169,7 @@ def _check_start_end_dates(self):
if line.date_start > line.date_end:
raise ValidationError(
self.env._(
"Contract line '%s' start date can't be later than"
" end date"
"Contract line '%s' start date can't be later than end date"
)
% line.name
)
Expand Down Expand Up @@ -243,6 +235,7 @@ def _get_period_to_invoice(
next_invoice_date=recurring_next_date,
recurring_invoicing_type=self.recurring_invoicing_type,
recurring_invoicing_offset=self.recurring_invoicing_offset,
recurring_invoicing_offset_type=self.recurring_invoicing_offset_type,
)
return first_date_invoiced, last_date_invoiced, recurring_next_date

Expand Down
84 changes: 55 additions & 29 deletions contract/models/contract_recurring_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,23 @@ class ContractRecurringMixin(models.AbstractModel):
),
)
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_invoicing_offset_type = fields.Selection(
selection=[
("daily", "Day(s)"),
("weekly", "Week(s)"),
("monthly", "Month(s)"),
("quarterly", "Quarter(s)"),
("semesterly", "Semester(s)"),
("yearly", "Year(s)"),
],
string="Invoicing offset type",
)
# === Invoicing Configuration Fields ===
# Define when and how invoices should be issued within the recurrence.

Expand Down Expand Up @@ -109,6 +119,7 @@ def _compute_next_period_date_start(self):
"next_period_date_start",
"recurring_invoicing_type",
"recurring_invoicing_offset",
"recurring_invoicing_offset_type",
"recurring_rule_type",
"recurring_interval",
"date_end",
Expand All @@ -125,21 +136,14 @@ def _compute_next_period_date_end(self):
next_invoice_date=rec.recurring_next_date,
recurring_invoicing_type=rec.recurring_invoicing_type,
recurring_invoicing_offset=rec.recurring_invoicing_offset,
)

@api.depends("recurring_invoicing_type", "recurring_rule_type")
def _compute_recurring_invoicing_offset(self):
"""Compute the invoicing offset based on type and rule."""
for rec in self:
method = self._get_default_recurring_invoicing_offset
rec.recurring_invoicing_offset = method(
rec.recurring_invoicing_type, rec.recurring_rule_type
recurring_invoicing_offset_type=rec.recurring_invoicing_offset_type,
)

@api.depends(
"next_period_date_start",
"recurring_invoicing_type",
"recurring_invoicing_offset",
"recurring_invoicing_offset_type",
"recurring_rule_type",
"recurring_interval",
"date_end",
Expand All @@ -151,6 +155,7 @@ def _compute_recurring_next_date(self):
rec.next_period_date_start,
rec.recurring_invoicing_type,
rec.recurring_invoicing_offset,
rec.recurring_invoicing_offset_type,
rec.recurring_rule_type,
rec.recurring_interval,
max_date_end=rec.date_end,
Expand Down Expand Up @@ -186,6 +191,7 @@ def get_next_period_date_end(
next_invoice_date=False,
recurring_invoicing_type=False,
recurring_invoicing_offset=False,
recurring_invoicing_offset_type=False,
):
"""Compute the end date for the next period."""
if not next_period_date_start or (
Expand All @@ -202,17 +208,20 @@ def get_next_period_date_end(
)
else:
# Forced invoice date: back-calculate period end
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
next_period_date_end = (
next_invoice_date
- self._get_invoicing_offset_relative_delta(
recurring_invoicing_type,
recurring_rule_type,
recurring_invoicing_offset,
recurring_invoicing_offset_type,
)
)

if recurring_invoicing_type == "pre-paid":
next_period_date_end += self.get_relative_delta(
recurring_rule_type, recurring_interval
) - relativedelta(days=1)

if max_date_end and next_period_date_end > max_date_end:
next_period_date_end = max_date_end
Expand All @@ -224,6 +233,7 @@ def get_next_invoice_date(
next_period_date_start,
recurring_invoicing_type,
recurring_invoicing_offset,
recurring_invoicing_offset_type,
recurring_rule_type,
recurring_interval,
max_date_end,
Expand All @@ -239,20 +249,36 @@ def get_next_invoice_date(
return False

if recurring_invoicing_type == "pre-paid":
return next_period_date_start + relativedelta(
days=recurring_invoicing_offset
)
next_invoice_date = next_period_date_start
else:
return next_period_date_end + relativedelta(days=recurring_invoicing_offset)
next_invoice_date = next_period_date_end

return next_invoice_date + self._get_invoicing_offset_relative_delta(
recurring_invoicing_type,
recurring_rule_type,
recurring_invoicing_offset,
recurring_invoicing_offset_type,
)

@api.model
def _get_default_recurring_invoicing_offset(
self, recurring_invoicing_type, recurring_rule_type
def _get_invoicing_offset_relative_delta(
self,
recurring_invoicing_type,
recurring_rule_type,
recurring_invoicing_offset,
recurring_invoicing_offset_type,
):
"""Return default offset in days based on invoicing type and rule."""
if (
recurring_invoicing_type == "pre-paid"
or recurring_rule_type == "monthlylastday"
):
return 0
return 1
invoice_offset = relativedelta()
else:
invoice_offset = relativedelta(days=1)

if recurring_invoicing_offset != 0 and recurring_invoicing_offset_type:
invoice_offset += self.get_relative_delta(
recurring_invoicing_offset_type, recurring_invoicing_offset
)

return invoice_offset
30 changes: 30 additions & 0 deletions contract/readme/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
period in the invoice line description
- pre-paid (invoice at period start) or post-paid (invoice at start
of next period)
- an invoicing offset, to move the invoice away from that date (see
below)
4. The "Generate Recurring Invoices from Contracts" cron runs daily to
generate the invoices. If you are in debug mode, you can click on
the invoice creation button.
Expand All @@ -36,3 +38,31 @@
![image](../static/src/screenshots/portal-list.png)

![image](../static/src/screenshots/portal-detail.png)

## Shifting the invoice date

*Invoicing type* says whether a period is billed at its start (pre-paid)
or after it has run (post-paid). *Invoicing offset* moves that date, in
the unit chosen next to it.

A positive offset delays the invoice. A negative one issues it earlier,
which is how you invoice up front: set `-1` with *Month(s)* on a pre-paid
monthly line and the invoice for March is raised on 1 February.

For a period running from 1 to 31 March, billed monthly:

| Invoicing type | Offset | Unit | Invoice issued |
|----------------|-------:|---------|----------------|
| Pre-paid | 0 | Day(s) | 1 March |
| Pre-paid | -1 | Month(s)| 1 February |
| Pre-paid | 15 | Day(s) | 16 March |
| Post-paid | 0 | Day(s) | 1 April |
| Post-paid | 2 | Week(s) | 15 April |
| Post-paid | -10 | Day(s) | 22 March |

Post-paid already includes the day after the period ends, which is why an
offset of 0 lands on 1 April rather than 31 March.

The quickest way to check a setting is to watch *Date of Next Invoice* on
the line. It recomputes as soon as the offset changes, so the effect is
visible before anything is invoiced.
Loading