diff --git a/contract/__manifest__.py b/contract/__manifest__.py
index 274ab732..f698f180 100644
--- a/contract/__manifest__.py
+++ b/contract/__manifest__.py
@@ -4,7 +4,7 @@
# Copyright 2016-2018 Tecnativa - Carlos Dauden
# Copyright 2017 Tecnativa - Vicent Cubells
# Copyright 2016-2017 LasLabs Inc.
-# Copyright 2018 ACSONE SA/NV
+# Copyright 2018-2019 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
diff --git a/contract/migrations/12.0.5.0.0/pre-migration.py b/contract/migrations/12.0.5.0.0/pre-migration.py
new file mode 100644
index 00000000..ca208e9d
--- /dev/null
+++ b/contract/migrations/12.0.5.0.0/pre-migration.py
@@ -0,0 +1,10 @@
+def migrate(cr, version):
+ # pre-paid/post-paid becomes significant for monthlylastday too,
+ # make sure it has the value that was implied for previous versions.
+ cr.execute(
+ """\
+ UPDATE contract_line
+ SET recurring_invoicing_type = 'post-paid'
+ WHERE recurring_rule_type = 'monthlylastday'
+ """
+ )
diff --git a/contract/models/abstract_contract_line.py b/contract/models/abstract_contract_line.py
index c1320208..99892d82 100644
--- a/contract/models/abstract_contract_line.py
+++ b/contract/models/abstract_contract_line.py
@@ -70,9 +70,20 @@ class ContractAbstractContractLine(models.AbstractModel):
[('pre-paid', 'Pre-paid'), ('post-paid', 'Post-paid')],
default='pre-paid',
string='Invoicing type',
- help="Specify if process date is 'from' or 'to' invoicing date",
+ help=(
+ "Specify if the invoice must be generated at the beginning "
+ "(pre-paid) or end (post-paid) of the period."
+ ),
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)."
+ )
+ )
recurring_interval = fields.Integer(
default=1,
string='Invoice Every',
@@ -115,6 +126,27 @@ class ContractAbstractContractLine(models.AbstractModel):
ondelete='cascade',
)
+ @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
+
+ @api.depends('recurring_invoicing_type', 'recurring_rule_type')
+ def _compute_recurring_invoicing_offset(self):
+ for rec in self:
+ rec.recurring_invoicing_offset = (
+ self._get_default_recurring_invoicing_offset(
+ rec.recurring_invoicing_type, rec.recurring_rule_type
+ )
+ )
+
@api.depends(
'automatic_price',
'specific_price',
diff --git a/contract/models/contract_line.py b/contract/models/contract_line.py
index 3178f240..c9d475bd 100644
--- a/contract/models/contract_line.py
+++ b/contract/models/contract_line.py
@@ -41,6 +41,14 @@ class ContractLine(models.Model):
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',
+ )
termination_notice_date = fields.Date(
string='Termination notice date',
compute="_compute_termination_notice_date",
@@ -361,20 +369,140 @@ class ContractLine(models.Model):
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,
+ )
+
+ @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,
):
- if recurring_rule_type == 'monthlylastday':
- return date_start + self.get_relative_delta(
- recurring_rule_type, recurring_interval - 1
- )
- if recurring_invoicing_type == 'pre-paid':
- return date_start
- return date_start + self.get_relative_delta(
- recurring_rule_type, recurring_interval
+ 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 compute_first_date_end(
+ 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
):
return (
@@ -396,7 +524,7 @@ class ContractLine(models.Model):
auto_renew"""
for rec in self.filtered('is_auto_renew'):
if rec.date_start:
- rec.date_end = self.compute_first_date_end(
+ rec.date_end = self._get_first_date_end(
rec.date_start,
rec.auto_renew_rule_type,
rec.auto_renew_interval,
@@ -404,17 +532,20 @@ class ContractLine(models.Model):
@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._compute_first_recurring_next_date(
+ rec.recurring_next_date = self.get_next_invoice_date(
rec.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')
@@ -533,33 +664,25 @@ class ContractLine(models.Model):
def _get_period_to_invoice(
self, last_date_invoiced, recurring_next_date, stop_at_date_end=True
):
+ # TODO this method can now be removed, since
+ # TODO self.next_period_date_start/end have the same values
self.ensure_one()
- first_date_invoiced = False
if not recurring_next_date:
- return first_date_invoiced, last_date_invoiced, recurring_next_date
+ return False, False, False
first_date_invoiced = (
last_date_invoiced + relativedelta(days=1)
if last_date_invoiced
else self.date_start
)
- if self.recurring_rule_type == 'monthlylastday':
- last_date_invoiced = recurring_next_date
- else:
- if self.recurring_invoicing_type == 'pre-paid':
- last_date_invoiced = (
- recurring_next_date
- + self.get_relative_delta(
- self.recurring_rule_type, self.recurring_interval
- )
- - relativedelta(days=1)
- )
- else:
- last_date_invoiced = recurring_next_date - relativedelta(
- days=1
- )
- if stop_at_date_end:
- if self.date_end and self.date_end < last_date_invoiced:
- last_date_invoiced = self.date_end
+ last_date_invoiced = self.get_next_period_date_end(
+ first_date_invoiced,
+ self.recurring_rule_type,
+ self.recurring_interval,
+ max_date_end=(self.date_end if stop_at_date_end else False),
+ next_invoice_date=recurring_next_date,
+ recurring_invoicing_type=self.recurring_invoicing_type,
+ recurring_invoicing_offset=self.recurring_invoicing_offset,
+ )
return first_date_invoiced, last_date_invoiced, recurring_next_date
@api.multi
@@ -580,23 +703,19 @@ class ContractLine(models.Model):
@api.multi
def _update_recurring_next_date(self):
for rec in self:
- old_date = rec.recurring_next_date
- new_date = old_date + self.get_relative_delta(
- rec.recurring_rule_type, rec.recurring_interval
+ last_date_invoiced = rec.next_period_date_end
+ recurring_next_date = rec.get_next_invoice_date(
+ last_date_invoiced + relativedelta(days=1),
+ rec.recurring_invoicing_type,
+ rec.recurring_invoicing_offset,
+ rec.recurring_rule_type,
+ rec.recurring_interval,
+ max_date_end=rec.date_end,
)
- if rec.recurring_rule_type == 'monthlylastday':
- last_date_invoiced = old_date
- elif rec.recurring_invoicing_type == 'post-paid':
- last_date_invoiced = old_date - relativedelta(days=1)
- elif rec.recurring_invoicing_type == 'pre-paid':
- last_date_invoiced = new_date - relativedelta(days=1)
-
- if rec.date_end and last_date_invoiced >= rec.date_end:
- rec.last_date_invoiced = rec.date_end
- rec.recurring_next_date = False
- else:
- rec.last_date_invoiced = last_date_invoiced
- rec.recurring_next_date = new_date
+ rec.write({
+ "recurring_next_date": recurring_next_date,
+ "last_date_invoiced": last_date_invoiced,
+ })
@api.multi
def _init_last_date_invoiced(self):
@@ -609,8 +728,9 @@ class ContractLine(models.Model):
last_date_invoiced = (
rec.recurring_next_date
- self.get_relative_delta(
- rec.recurring_rule_type, rec.recurring_interval
+ rec.recurring_rule_type, rec.recurring_interval - 1
)
+ - relativedelta(days=1)
)
elif rec.recurring_invoicing_type == 'post-paid':
last_date_invoiced = (
@@ -618,12 +738,18 @@ class ContractLine(models.Model):
- self.get_relative_delta(
rec.recurring_rule_type, rec.recurring_interval
)
- ) - relativedelta(days=1)
+ - 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':
@@ -631,7 +757,7 @@ class ContractLine(models.Model):
elif recurring_rule_type == 'monthly':
return relativedelta(months=interval)
elif recurring_rule_type == 'monthlylastday':
- return relativedelta(months=interval, day=31)
+ return relativedelta(months=interval, day=1)
else:
return relativedelta(years=interval)
@@ -651,15 +777,23 @@ class ContractLine(models.Model):
)
)
new_date_start = rec.date_start + delay_delta
- rec.recurring_next_date = self._compute_first_recurring_next_date(
+ if rec.date_end:
+ new_date_end = rec.date_end + delay_delta
+ else:
+ new_date_end = False
+ new_recurring_next_date = self.get_next_invoice_date(
new_date_start,
rec.recurring_invoicing_type,
+ rec.recurring_invoicing_offset,
rec.recurring_rule_type,
rec.recurring_interval,
+ max_date_end=new_date_end
)
- if rec.date_end:
- rec.date_end += delay_delta
- rec.date_start = new_date_start
+ rec.write({
+ "date_start": new_date_start,
+ "date_end": new_date_end,
+ "recurring_next_date": new_recurring_next_date,
+ })
@api.multi
def stop(self, date_end, manual_renew_needed=False, post_message=True):
@@ -712,11 +846,13 @@ class ContractLine(models.Model):
):
self.ensure_one()
if not recurring_next_date:
- recurring_next_date = self._compute_first_recurring_next_date(
+ recurring_next_date = self.get_next_invoice_date(
date_start,
self.recurring_invoicing_type,
+ self.recurring_invoicing_offset,
self.recurring_rule_type,
self.recurring_interval,
+ max_date_end=date_end,
)
new_vals = self.read()[0]
new_vals.pop("id", None)
@@ -1023,7 +1159,7 @@ class ContractLine(models.Model):
def _get_renewal_dates(self):
self.ensure_one()
date_start = self.date_end + relativedelta(days=1)
- date_end = self.compute_first_date_end(
+ date_end = self._get_first_date_end(
date_start, self.auto_renew_rule_type, self.auto_renew_interval
)
return date_start, date_end
diff --git a/contract/tests/test_contract.py b/contract/tests/test_contract.py
index 76ea4144..e406a45e 100644
--- a/contract/tests/test_contract.py
+++ b/contract/tests/test_contract.py
@@ -2,6 +2,7 @@
# Copyright 2018 Tecnativa - Pedro M. Baeza
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+from collections import namedtuple
from datetime import timedelta
from dateutil.relativedelta import relativedelta
from odoo import fields
@@ -247,7 +248,7 @@ class TestContract(TestContractBase):
self.assertEqual(self.acct_line.last_date_invoiced, last_date_invoiced)
def test_contract_monthly_lastday(self):
- recurring_next_date = to_date('2018-03-31')
+ recurring_next_date = to_date('2018-02-28')
last_date_invoiced = to_date('2018-02-22')
self.acct_line.recurring_next_date = '2018-02-22'
self.acct_line.recurring_invoicing_type = 'post-paid'
@@ -279,7 +280,7 @@ class TestContract(TestContractBase):
)
self.contract.recurring_create_invoice()
self.assertEqual(
- self.acct_line.recurring_next_date, to_date('2018-04-01')
+ self.acct_line.recurring_next_date, to_date('2018-3-16')
)
self.assertEqual(
self.acct_line.last_date_invoiced, to_date('2018-02-28')
@@ -537,7 +538,34 @@ class TestContract(TestContractBase):
'There was an error and the view couldn\'t be opened.',
)
- def test_compute_first_recurring_next_date(self):
+ def test_get_default_recurring_invoicing_offset(self):
+ clm = self.env['contract.line']
+ self.assertEqual(
+ clm._get_default_recurring_invoicing_offset(
+ "pre-paid", "monthly"
+ ),
+ 0
+ )
+ self.assertEqual(
+ clm._get_default_recurring_invoicing_offset(
+ "post-paid", "monthly"
+ ),
+ 1
+ )
+ self.assertEqual(
+ clm._get_default_recurring_invoicing_offset(
+ "pre-paid", "monthlylastday"
+ ),
+ 0
+ )
+ self.assertEqual(
+ clm._get_default_recurring_invoicing_offset(
+ "post-paid", "monthlylastday"
+ ),
+ 0
+ )
+
+ def test_get_next_invoice_date(self):
"""Test different combination to compute recurring_next_date
Combination format
{
@@ -547,6 +575,7 @@ class TestContract(TestContractBase):
recurring_rule_type, # ('daily', 'weekly', 'monthly',
# 'monthlylastday', 'yearly'),
recurring_interval, # integer
+ max_date_end, # date
),
}
"""
@@ -554,64 +583,415 @@ class TestContract(TestContractBase):
def error_message(
date_start,
recurring_invoicing_type,
+ recurring_invoicing_offset,
recurring_rule_type,
recurring_interval,
+ max_date_end,
):
- return "Error in %s every %d %s case, start with %s " % (
- recurring_invoicing_type,
- recurring_interval,
- recurring_rule_type,
- date_start,
+ return (
+ "Error in %s-%d every %d %s case, "
+ "start with %s (max_date_end=%s)" % (
+ recurring_invoicing_type,
+ recurring_invoicing_offset,
+ recurring_interval,
+ recurring_rule_type,
+ date_start,
+ max_date_end,
+ )
)
combinations = [
(
to_date('2018-01-01'),
- (to_date('2018-01-01'), 'pre-paid', 'monthly', 1),
+ (to_date('2018-01-01'), 'pre-paid', 0, 'monthly', 1,
+ False),
),
(
to_date('2018-01-01'),
- (to_date('2018-01-01'), 'pre-paid', 'monthly', 2),
+ (to_date('2018-01-01'), 'pre-paid', 0, 'monthly', 1,
+ to_date('2018-01-15')),
+ ),
+ (
+ False,
+ (to_date('2018-01-16'), 'pre-paid', 0, 'monthly', 1,
+ to_date('2018-01-15')),
+ ),
+ (
+ to_date('2018-01-01'),
+ (to_date('2018-01-01'), 'pre-paid', 0, 'monthly', 2,
+ False),
),
(
to_date('2018-02-01'),
- (to_date('2018-01-01'), 'post-paid', 'monthly', 1),
+ (to_date('2018-01-01'), 'post-paid', 1, 'monthly', 1,
+ False),
+ ),
+ (
+ to_date('2018-01-16'),
+ (to_date('2018-01-01'), 'post-paid', 1, 'monthly', 1,
+ to_date('2018-01-15')),
+ ),
+ (
+ False,
+ (to_date('2018-01-16'), 'post-paid', 1, 'monthly', 1,
+ to_date('2018-01-15')),
),
(
to_date('2018-03-01'),
- (to_date('2018-01-01'), 'post-paid', 'monthly', 2),
+ (to_date('2018-01-01'), 'post-paid', 1, 'monthly', 2,
+ False),
),
(
to_date('2018-01-31'),
- (to_date('2018-01-05'), 'post-paid', 'monthlylastday', 1),
+ (to_date('2018-01-05'), 'post-paid', 0, 'monthlylastday', 1,
+ False),
),
(
- to_date('2018-01-31'),
- (to_date('2018-01-06'), 'pre-paid', 'monthlylastday', 1),
+ to_date('2018-01-06'),
+ (to_date('2018-01-06'), 'pre-paid', 0, 'monthlylastday', 1,
+ False),
),
(
to_date('2018-02-28'),
- (to_date('2018-01-05'), 'pre-paid', 'monthlylastday', 2),
+ (to_date('2018-01-05'), 'post-paid', 0, 'monthlylastday', 2,
+ False),
+ ),
+ (
+ to_date('2018-01-05'),
+ (to_date('2018-01-05'), 'pre-paid', 0, 'monthlylastday', 2,
+ False),
),
(
to_date('2018-01-05'),
- (to_date('2018-01-05'), 'pre-paid', 'yearly', 1),
+ (to_date('2018-01-05'), 'pre-paid', 0, 'yearly', 1,
+ False),
),
(
to_date('2019-01-05'),
- (to_date('2018-01-05'), 'post-paid', 'yearly', 1),
+ (to_date('2018-01-05'), 'post-paid', 1, 'yearly', 1,
+ False),
),
]
contract_line_env = self.env['contract.line']
for recurring_next_date, combination in combinations:
self.assertEqual(
recurring_next_date,
- contract_line_env._compute_first_recurring_next_date(
+ contract_line_env.get_next_invoice_date(
*combination
),
error_message(*combination),
)
+ def test_next_invoicing_period(self):
+ """Test different combination for next invoicing period
+ {
+ (
+ 'recurring_next_date', # date
+ 'next_period_date_start', # date
+ 'next_period_date_end' # date
+ ): (
+ date_start, # date
+ date_end, # date
+ last_date_invoiced, # date
+ recurring_next_date, # date
+ recurring_invoicing_type, # ('pre-paid','post-paid',)
+ recurring_rule_type, # ('daily', 'weekly', 'monthly',
+ # 'monthlylastday', 'yearly'),
+ recurring_interval, # integer
+ max_date_end, # date
+ ),
+ }
+ """
+
+ def _update_contract_line(
+ case,
+ date_start,
+ date_end,
+ last_date_invoiced,
+ recurring_next_date,
+ recurring_invoicing_type,
+ recurring_rule_type,
+ recurring_interval,
+ max_date_end,
+ ):
+ self.acct_line.write(
+ {
+ 'date_start': date_start,
+ 'date_end': date_end,
+ 'last_date_invoiced': last_date_invoiced,
+ 'recurring_next_date': recurring_next_date,
+ 'recurring_invoicing_type': recurring_invoicing_type,
+ 'recurring_rule_type': recurring_rule_type,
+ 'recurring_interval': recurring_interval,
+ 'max_date_end': max_date_end,
+ }
+ )
+
+ def _get_result():
+ return Result(
+ recurring_next_date=self.acct_line.recurring_next_date,
+ next_period_date_start=self.acct_line.next_period_date_start,
+ next_period_date_end=self.acct_line.next_period_date_end,
+ )
+
+ def _error_message(
+ case,
+ date_start,
+ date_end,
+ last_date_invoiced,
+ recurring_next_date,
+ recurring_invoicing_type,
+ recurring_rule_type,
+ recurring_interval,
+ max_date_end,
+ ):
+ return (
+ "Error in case %s:"
+ "date_start: %s, "
+ "date_end: %s, "
+ "last_date_invoiced: %s, "
+ "recurring_next_date: %s, "
+ "recurring_invoicing_type: %s, "
+ "recurring_rule_type: %s, "
+ "recurring_interval: %s, "
+ "max_date_end: %s, "
+ ) % (
+ case,
+ date_start,
+ date_end,
+ last_date_invoiced,
+ recurring_next_date,
+ recurring_invoicing_type,
+ recurring_rule_type,
+ recurring_interval,
+ max_date_end,
+ )
+
+ Result = namedtuple(
+ 'Result',
+ [
+ 'recurring_next_date',
+ 'next_period_date_start',
+ 'next_period_date_end',
+ ],
+ )
+ Combination = namedtuple(
+ 'Combination',
+ [
+ 'case',
+ 'date_start',
+ 'date_end',
+ 'last_date_invoiced',
+ 'recurring_next_date',
+ 'recurring_invoicing_type',
+ 'recurring_rule_type',
+ 'recurring_interval',
+ 'max_date_end',
+ ],
+ )
+ combinations = {
+ Result(
+ recurring_next_date=to_date('2019-01-01'),
+ next_period_date_start=to_date('2019-01-01'),
+ next_period_date_end=to_date('2019-01-31'),
+ ): Combination(
+ case="1",
+ date_start='2019-01-01',
+ date_end=False,
+ last_date_invoiced=False,
+ recurring_next_date='2019-01-01',
+ recurring_invoicing_type='pre-paid',
+ recurring_rule_type='monthly',
+ recurring_interval=1,
+ max_date_end=False,
+ ),
+ Result(
+ recurring_next_date=to_date('2019-01-01'),
+ next_period_date_start=to_date('2019-01-01'),
+ next_period_date_end=to_date('2019-01-15'),
+ ): Combination(
+ case="2",
+ date_start='2019-01-01',
+ date_end='2019-01-15',
+ last_date_invoiced=False,
+ recurring_next_date='2019-01-01',
+ recurring_invoicing_type='pre-paid',
+ recurring_rule_type='monthly',
+ recurring_interval=1,
+ max_date_end=False,
+ ),
+ Result(
+ recurring_next_date=to_date('2019-01-05'),
+ next_period_date_start=to_date('2019-01-05'),
+ next_period_date_end=to_date('2019-01-15'),
+ ): Combination(
+ case="3",
+ date_start='2019-01-05',
+ date_end='2019-01-15',
+ last_date_invoiced=False,
+ recurring_next_date='2019-01-05',
+ recurring_invoicing_type='pre-paid',
+ recurring_rule_type='monthly',
+ recurring_interval=1,
+ max_date_end=False,
+ ),
+ Result(
+ recurring_next_date=to_date('2019-01-05'),
+ next_period_date_start=to_date('2019-01-01'),
+ next_period_date_end=to_date('2019-01-15'),
+ ): Combination(
+ case="4",
+ date_start='2019-01-01',
+ date_end='2019-01-15',
+ last_date_invoiced=False,
+ recurring_next_date='2019-01-05',
+ recurring_invoicing_type='pre-paid',
+ recurring_rule_type='monthly',
+ recurring_interval=1,
+ max_date_end=False,
+ ),
+ Result(
+ recurring_next_date=to_date('2019-02-01'),
+ next_period_date_start=to_date('2019-01-01'),
+ next_period_date_end=to_date('2019-01-31'),
+ ): Combination(
+ case="5",
+ date_start='2019-01-01',
+ date_end=False,
+ last_date_invoiced=False,
+ recurring_next_date='2019-02-01',
+ recurring_invoicing_type='post-paid',
+ recurring_rule_type='monthly',
+ recurring_interval=1,
+ max_date_end=False,
+ ),
+ Result(
+ recurring_next_date=to_date('2019-02-01'),
+ next_period_date_start=to_date('2019-01-01'),
+ next_period_date_end=to_date('2019-01-15'),
+ ): Combination(
+ case="6",
+ date_start='2019-01-01',
+ date_end='2019-01-15',
+ last_date_invoiced=False,
+ recurring_next_date='2019-02-01',
+ recurring_invoicing_type='post-paid',
+ recurring_rule_type='monthly',
+ recurring_interval=1,
+ max_date_end=False,
+ ),
+ Result(
+ recurring_next_date=to_date('2019-02-01'),
+ next_period_date_start=to_date('2019-01-05'),
+ next_period_date_end=to_date('2019-01-31'),
+ ): Combination(
+ case="7",
+ date_start='2019-01-05',
+ date_end=False,
+ last_date_invoiced=False,
+ recurring_next_date='2019-02-01',
+ recurring_invoicing_type='post-paid',
+ recurring_rule_type='monthly',
+ recurring_interval=1,
+ max_date_end=False,
+ ),
+ Result(
+ recurring_next_date=to_date('2019-01-05'),
+ next_period_date_start=to_date('2019-01-01'),
+ next_period_date_end=to_date('2019-01-15'),
+ ): Combination(
+ case="8",
+ date_start='2019-01-01',
+ date_end='2019-01-15',
+ last_date_invoiced=False,
+ recurring_next_date='2019-01-05',
+ recurring_invoicing_type='pre-paid',
+ recurring_rule_type='monthly',
+ recurring_interval=1,
+ max_date_end=False,
+ ),
+ Result(
+ recurring_next_date=to_date('2019-01-01'),
+ next_period_date_start=to_date('2018-12-16'),
+ next_period_date_end=to_date('2019-01-31'),
+ ): Combination(
+ case="9",
+ date_start='2018-01-01',
+ date_end='2020-01-15',
+ last_date_invoiced='2018-12-15',
+ recurring_next_date='2019-01-01',
+ recurring_invoicing_type='pre-paid',
+ recurring_rule_type='monthly',
+ recurring_interval=1,
+ max_date_end=False,
+ ),
+ Result(
+ recurring_next_date=to_date('2019-01-01'),
+ next_period_date_start=to_date('2018-12-16'),
+ next_period_date_end=to_date('2018-12-31'),
+ ): Combination(
+ case="10",
+ date_start='2018-01-01',
+ date_end='2020-01-15',
+ last_date_invoiced='2018-12-15',
+ recurring_next_date='2019-01-01',
+ recurring_invoicing_type='post-paid',
+ recurring_rule_type='monthly',
+ recurring_interval=1,
+ max_date_end=False,
+ ),
+ Result(
+ recurring_next_date=to_date('2018-12-31'),
+ next_period_date_start=to_date('2018-12-16'),
+ next_period_date_end=to_date('2018-12-31'),
+ ): Combination(
+ case="11",
+ date_start='2018-01-01',
+ date_end='2020-01-15',
+ last_date_invoiced='2018-12-15',
+ recurring_next_date='2018-12-31',
+ recurring_invoicing_type='post-paid',
+ recurring_rule_type='monthlylastday',
+ recurring_interval=1,
+ max_date_end=False,
+ ),
+ Result(
+ recurring_next_date=to_date('2018-12-16'),
+ next_period_date_start=to_date('2018-12-16'),
+ next_period_date_end=to_date('2018-12-31'),
+ ): Combination(
+ case="12",
+ date_start='2018-01-01',
+ date_end='2020-01-15',
+ last_date_invoiced='2018-12-15',
+ recurring_next_date='2018-12-16',
+ recurring_invoicing_type='pre-paid',
+ recurring_rule_type='monthlylastday',
+ recurring_interval=1,
+ max_date_end=False,
+ ),
+ Result(
+ recurring_next_date=to_date('2018-01-05'),
+ next_period_date_start=to_date('2018-01-05'),
+ next_period_date_end=to_date('2018-03-31'),
+ ): Combination(
+ case="12",
+ date_start='2018-01-05',
+ date_end='2020-01-15',
+ last_date_invoiced=False,
+ recurring_next_date='2018-01-05',
+ recurring_invoicing_type='pre-paid',
+ recurring_rule_type='monthlylastday',
+ recurring_interval=3,
+ max_date_end=False,
+ ),
+ }
+ for result, combination in combinations.items():
+ _update_contract_line(*combination)
+ self.assertEqual(
+ result, _get_result(), _error_message(*combination)
+ )
+
def test_recurring_next_date(self):
"""recurring next date for a contract is the min for all lines"""
self.contract.recurring_create_invoice()
@@ -1331,7 +1711,7 @@ class TestContract(TestContractBase):
len(invoice_lines),
)
- def test_get_period_to_invoice_monthlylastday(self):
+ def test_get_period_to_invoice_monthlylastday_postpaid(self):
self.acct_line.date_start = '2018-01-05'
self.acct_line.recurring_invoicing_type = 'post-paid'
self.acct_line.recurring_rule_type = 'monthlylastday'
@@ -1362,6 +1742,67 @@ class TestContract(TestContractBase):
self.assertEqual(last, to_date('2018-03-15'))
self.acct_line.manual_renew_needed = True
+ def test_get_period_to_invoice_monthlylastday_prepaid(self):
+ self.acct_line.date_start = '2018-01-05'
+ 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,
+ )
+ self.assertEqual(first, to_date('2018-01-05'))
+ self.assertEqual(last, to_date('2018-01-31'))
+ self.assertEqual(recurring_next_date, to_date('2018-01-05'))
+ self.assertEqual(
+ self.acct_line.recurring_next_date, to_date('2018-01-05')
+ )
+ 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,
+ )
+ self.assertEqual(first, to_date('2018-02-01'))
+ self.assertEqual(last, to_date('2018-02-28'))
+ self.assertEqual(recurring_next_date, to_date('2018-02-01'))
+ self.assertEqual(
+ self.acct_line.recurring_next_date, to_date('2018-02-01')
+ )
+ self.assertEqual(
+ self.acct_line.last_date_invoiced, to_date('2018-01-31')
+ )
+ 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,
+ )
+ self.assertEqual(first, to_date('2018-03-01'))
+ self.assertEqual(last, to_date('2018-03-15'))
+ self.assertEqual(recurring_next_date, to_date('2018-03-01'))
+ self.assertEqual(
+ self.acct_line.recurring_next_date, to_date('2018-03-01')
+ )
+ self.assertEqual(
+ self.acct_line.last_date_invoiced, to_date('2018-02-28')
+ )
+ 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,
+ )
+ self.assertFalse(first)
+ self.assertFalse(last)
+ self.assertFalse(recurring_next_date)
+ self.assertFalse(self.acct_line.recurring_next_date)
+ self.assertEqual(
+ self.acct_line.last_date_invoiced, to_date('2018-03-15')
+ )
+
def test_get_period_to_invoice_monthly_pre_paid_2(self):
self.acct_line.date_start = '2018-01-05'
self.acct_line.recurring_invoicing_type = 'pre-paid'
diff --git a/contract/views/abstract_contract_line.xml b/contract/views/abstract_contract_line.xml
index b66077ee..28851130 100644
--- a/contract/views/abstract_contract_line.xml
+++ b/contract/views/abstract_contract_line.xml
@@ -59,8 +59,8 @@
-
+
+
diff --git a/contract/views/contract_line.xml b/contract/views/contract_line.xml
index 2d14a81f..85f96db4 100644
--- a/contract/views/contract_line.xml
+++ b/contract/views/contract_line.xml
@@ -15,11 +15,13 @@
+
+
diff --git a/contract_sale_mandate/tests/test_contract_sale_mandate.py b/contract_sale_mandate/tests/test_contract_sale_mandate.py
index eb36adcd..04515133 100644
--- a/contract_sale_mandate/tests/test_contract_sale_mandate.py
+++ b/contract_sale_mandate/tests/test_contract_sale_mandate.py
@@ -26,6 +26,7 @@ class TestContractSaleMandate(TestContractBase):
'is_contract': True,
'default_qty': 12,
'recurring_rule_type': "monthlylastday",
+ 'recurring_invoicing_type': "post-paid",
'contract_template_id': cls.contract_template1.id,
}
)
diff --git a/product_contract/migrations/12.0.3.0.0/pre-migration.py b/product_contract/migrations/12.0.3.0.0/pre-migration.py
new file mode 100644
index 00000000..31b3f179
--- /dev/null
+++ b/product_contract/migrations/12.0.3.0.0/pre-migration.py
@@ -0,0 +1,10 @@
+def migrate(cr, version):
+ # pre-paid/post-paid becomes significant for monthlylastday too,
+ # make sure it has the value that was implied for previous versions.
+ cr.execute(
+ """\
+ UPDATE product_template
+ SET recurring_invoicing_type = 'post-paid'
+ WHERE recurring_rule_type = 'monthlylastday'
+ """
+ )
diff --git a/product_contract/tests/test_sale_order.py b/product_contract/tests/test_sale_order.py
index 7a630cbb..307cf908 100644
--- a/product_contract/tests/test_sale_order.py
+++ b/product_contract/tests/test_sale_order.py
@@ -43,6 +43,7 @@ class TestSaleOrder(TransactionCase):
'is_contract': True,
'default_qty': 12,
'recurring_rule_type': "monthlylastday",
+ 'recurring_invoicing_type': "post-paid",
'contract_template_id': self.contract_template1.id,
}
)
diff --git a/product_contract/views/product_template.xml b/product_contract/views/product_template.xml
index 400afa99..057ec95b 100644
--- a/product_contract/views/product_template.xml
+++ b/product_contract/views/product_template.xml
@@ -33,8 +33,7 @@
-
+
diff --git a/product_contract/views/sale_order.xml b/product_contract/views/sale_order.xml
index e5da25fe..4fd1b90b 100644
--- a/product_contract/views/sale_order.xml
+++ b/product_contract/views/sale_order.xml
@@ -58,8 +58,7 @@
-
+