diff --git a/contract/__manifest__.py b/contract/__manifest__.py
index 274ab73260..1d1bc4769b 100644
--- a/contract/__manifest__.py
+++ b/contract/__manifest__.py
@@ -9,7 +9,7 @@
{
'name': 'Recurring - Contracts Management',
- 'version': '12.0.4.2.5',
+ 'version': '12.0.5.0.0',
'category': 'Contract Management',
'license': 'AGPL-3',
'author': "OpenERP SA, "
diff --git a/contract/migrations/12.0.5.0.0/post-migration.py b/contract/migrations/12.0.5.0.0/post-migration.py
new file mode 100644
index 0000000000..7d27ccad72
--- /dev/null
+++ b/contract/migrations/12.0.5.0.0/post-migration.py
@@ -0,0 +1,29 @@
+# Copyright 2019 ACSONE SA/NV
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+import logging
+from dateutil.relativedelta import relativedelta
+from odoo import api, SUPERUSER_ID
+
+_logger = logging.getLogger(__name__)
+
+
+def migrate(cr, version):
+ _logger.info("Init next_period_end_date field")
+ with api.Environment.manage():
+ env = api.Environment(cr, SUPERUSER_ID, {})
+ cl_model = env['contract.line']
+ contract_lines = cl_model.search([])
+ for cl in contract_lines:
+ if cl.date_end and cl.last_date_invoiced == cl.date_end:
+ continue # Finished lines
+ next_period_start_date = (
+ cl.last_date_invoiced + relativedelta(days=1)
+ if cl.last_date_invoiced
+ else cl.date_start
+ )
+ cl.next_period_end_date = cl_model._get_next_period_end_date(
+ next_period_start_date,
+ cl.recurring_rule_type,
+ cl.recurring_interval,
+ )
diff --git a/contract/models/contract_line.py b/contract/models/contract_line.py
index 3178f24094..e60c687752 100644
--- a/contract/models/contract_line.py
+++ b/contract/models/contract_line.py
@@ -37,10 +37,22 @@ class ContractLine(models.Model):
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')
+ recurring_next_date = fields.Date(
+ string='Date of Next Invoice',
+ compute='_compute_recurring_next_date',
+ inverse='_inverse_recurring_next_date',
+ store=True,
+ )
last_date_invoiced = fields.Date(
string='Last Date Invoiced', readonly=True, copy=False
)
+ next_period_start_date = fields.Date(
+ string='Next Period Start Date',
+ compute='_compute_next_period_start_date',
+ )
+ next_period_end_date = fields.Date(
+ string='Next Period End Date'
+ )
termination_notice_date = fields.Date(
string='Termination notice date',
compute="_compute_termination_notice_date",
@@ -356,7 +368,7 @@ def _check_overlap_predecessor(self):
)
@api.model
- def _compute_first_recurring_next_date(
+ def _get_recurring_next_date(
self,
date_start,
recurring_invoicing_type,
@@ -373,8 +385,51 @@ def _compute_first_recurring_next_date(
recurring_rule_type, recurring_interval
)
+ @api.depends('last_date_invoiced')
+ def _compute_next_period_start_date(self):
+ for rec in self:
+ if rec.last_date_invoiced:
+ rec.next_period_start_date = (
+ rec.last_date_invoiced + relativedelta(days=1)
+ )
+ else:
+ rec.next_period_start_date = rec.date_start
+
+ @api.depends(
+ 'recurring_invoicing_type',
+ 'recurring_rule_type',
+ 'next_period_end_date',
+ 'date_start',
+ 'date_end',
+ 'last_date_invoiced',
+ )
+ def _compute_recurring_next_date(self):
+ for rec in self:
+ if rec.next_period_end_date:
+ if rec.recurring_rule_type == 'monthlylastday':
+ rec.recurring_next_date = rec.next_period_end_date
+ else:
+ if rec.recurring_invoicing_type == 'pre-paid':
+ rec.recurring_next_date = rec.next_period_start_date
+ else:
+ rec.recurring_next_date = rec.next_period_end_date + relativedelta(days=1)
+ if rec.date_end and rec.recurring_next_date > rec.date_end:
+ rec.recurring_next_date = False
+
+ @api.multi
+ def _inverse_recurring_next_date(self):
+ for rec in self:
+ # TODO: monthlylastday
+ if rec.recurring_next_date:
+ if rec.recurring_invoicing_type == 'pre-paid':
+ rec.next_period_end_date = rec.recurring_next_date + self.get_relative_delta(
+ rec.recurring_rule_type, rec.recurring_interval
+ ) - relativedelta(days=1)
+ else:
+ rec.next_period_end_date = rec.recurring_next_date - relativedelta(days=1)
+
@api.model
- def compute_first_date_end(
+ def _get_date_end(
self, date_start, auto_renew_rule_type, auto_renew_interval
):
return (
@@ -385,6 +440,20 @@ def compute_first_date_end(
- relativedelta(days=1)
)
+ @api.model
+ def _get_next_period_end_date(
+ self, next_period_start_date, recurring_rule_type, recurring_interval
+ ):
+ if recurring_rule_type == 'monthlylastday':
+ return next_period_start_date + self.get_relative_delta(
+ recurring_rule_type, recurring_interval - 1
+ )
+ return (
+ next_period_start_date
+ + self.get_relative_delta(recurring_rule_type, recurring_interval)
+ - relativedelta(days=1)
+ )
+
@api.onchange(
'date_start',
'is_auto_renew',
@@ -396,7 +465,7 @@ def _onchange_is_auto_renew(self):
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_date_end(
rec.date_start,
rec.auto_renew_rule_type,
rec.auto_renew_interval,
@@ -407,15 +476,19 @@ def _onchange_is_auto_renew(self):
'recurring_invoicing_type',
'recurring_rule_type',
'recurring_interval',
+ 'last_date_invoiced',
)
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_recurring_next_date(
rec.date_start,
rec.recurring_invoicing_type,
rec.recurring_rule_type,
rec.recurring_interval,
)
+ rec.next_period_end_date = self._get_next_period_end_date(
+ rec.date_start, rec.recurring_rule_type, rec.recurring_interval
+ )
@api.constrains('is_canceled', 'is_auto_renew')
def _check_auto_renew_canceled_lines(self):
@@ -487,6 +560,20 @@ def _check_start_end_dates(self):
% line.name
)
+ @api.constrains('next_period_end_date', 'last_date_invoiced', 'date_end', 'date_start')
+ def _check_next_period_end_date(self):
+ for line in self.filtered('next_period_end_date'):
+ if line.next_period_end_date < line.date_start or (
+ line.date_end and line.next_period_end_date > line.date_end
+ ):
+ raise ValidationError(
+ _(
+ "Next period end date must be between the "
+ "start and the end of the contract line: %s"
+ )
+ % line.name
+ )
+
@api.depends('recurring_next_date', 'date_start', 'date_end')
def _compute_create_invoice_visibility(self):
today = fields.Date.context_today(self)
@@ -542,23 +629,9 @@ def _get_period_to_invoice(
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
- )
+ last_date_invoiced = self.next_period_end_date
if stop_at_date_end:
- if self.date_end and self.date_end < last_date_invoiced:
+ if not self.next_period_end_date and self.date_end:
last_date_invoiced = self.date_end
return first_date_invoiced, last_date_invoiced, recurring_next_date
@@ -580,23 +653,32 @@ def _insert_markers(self, first_date_invoiced, last_date_invoiced):
@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
- )
- 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
+ if rec.next_period_end_date:
+ last_date_invoiced = rec.next_period_end_date
+ next_period_start_date = last_date_invoiced + relativedelta(
+ days=1
+ )
+ next_period_end_date = self._get_next_period_end_date(
+ next_period_start_date,
+ rec.recurring_rule_type,
+ rec.recurring_interval,
+ )
+ recurring_next_date = self._get_recurring_next_date(
+ next_period_start_date,
+ rec.recurring_invoicing_type,
+ rec.recurring_rule_type,
+ rec.recurring_interval,
+ )
+ if rec.date_end and next_period_end_date > rec.date_end:
+ next_period_end_date = rec.date_end
+ if rec.date_end and last_date_invoiced >= rec.date_end:
+ rec.last_date_invoiced = rec.date_end
+ rec.recurring_next_date = False
+ rec.next_period_end_date = False
+ else:
+ rec.last_date_invoiced = last_date_invoiced
+ rec.recurring_next_date = recurring_next_date
+ rec.next_period_end_date = next_period_end_date
@api.multi
def _init_last_date_invoiced(self):
@@ -651,7 +733,7 @@ def _delay(self, delay_delta):
)
)
new_date_start = rec.date_start + delay_delta
- rec.recurring_next_date = self._compute_first_recurring_next_date(
+ rec.recurring_next_date = self._get_recurring_next_date(
new_date_start,
rec.recurring_invoicing_type,
rec.recurring_rule_type,
@@ -684,6 +766,7 @@ def stop(self, date_end, manual_renew_needed=False, post_message=True):
}
if rec.last_date_invoiced == date_end:
values['recurring_next_date'] = False
+ values['next_period_end_date'] = False
rec.write(values)
if post_message:
msg = _(
@@ -712,12 +795,15 @@ def _prepare_value_for_plan_successor(
):
self.ensure_one()
if not recurring_next_date:
- recurring_next_date = self._compute_first_recurring_next_date(
+ recurring_next_date = self._get_recurring_next_date(
date_start,
self.recurring_invoicing_type,
self.recurring_rule_type,
self.recurring_interval,
)
+ next_period_end_date = self._get_next_period_end_date(
+ date_start, self.recurring_rule_type, self.recurring_interval
+ )
new_vals = self.read()[0]
new_vals.pop("id", None)
new_vals.pop("last_date_invoiced", None)
@@ -725,6 +811,7 @@ def _prepare_value_for_plan_successor(
values['date_start'] = date_start
values['date_end'] = date_end
values['recurring_next_date'] = recurring_next_date
+ values['next_period_end_date'] = next_period_end_date
values['is_auto_renew'] = is_auto_renew
values['predecessor_contract_line_id'] = self.id
return values
@@ -1023,7 +1110,7 @@ def action_stop_plan_successor(self):
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_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 74adae5176..9148648b0a 100644
--- a/contract/tests/test_contract.py
+++ b/contract/tests/test_contract.py
@@ -234,9 +234,9 @@ def test_contract_yearly_post_paid(self):
def test_contract_yearly_pre_paid(self):
recurring_next_date = to_date('2019-02-22')
last_date_invoiced = to_date('2019-02-21')
+ self.acct_line.recurring_rule_type = 'yearly'
self.acct_line.date_end = '2020-02-22'
self.acct_line.recurring_next_date = '2018-02-22'
- self.acct_line.recurring_rule_type = 'yearly'
self.acct_line.recurring_invoicing_type = 'pre-paid'
self.contract.recurring_create_invoice()
invoices_weekly = self.contract._get_related_invoices()
@@ -606,7 +606,7 @@ def error_message(
for recurring_next_date, combination in combinations:
self.assertEqual(
recurring_next_date,
- contract_line_env._compute_first_recurring_next_date(
+ contract_line_env._get_recurring_next_date(
*combination
),
error_message(*combination),
@@ -1534,6 +1534,12 @@ def test_unlink(self):
def test_contract_line_state(self):
lines = self.env['contract.line']
+ new_line = self.acct_line.copy()
+ new_line.write({
+ 'date_start': self.today + relativedelta(months=3),
+ 'recurring_next_date': self.today + relativedelta(months=3),
+ 'date_end': self.today + relativedelta(months=5),
+ })
# upcoming
lines |= self.acct_line.copy(
{
@@ -1839,3 +1845,65 @@ def test_stop_at_last_date_invoiced(self):
self.assertTrue(self.acct_line.recurring_next_date)
self.acct_line.stop(self.acct_line.last_date_invoiced)
self.assertFalse(self.acct_line.recurring_next_date)
+
+ def test_contract_monthly_pre_paid_custom_first_period(self):
+ recurring_next_date = to_date('2019-04-01')
+ last_date_invoiced = to_date('2019-03-31')
+ self.acct_line.recurring_next_date = '2019-03-22'
+ self.acct_line.date_start = '2019-03-22'
+ self.acct_line.next_period_end_date = '2019-03-31'
+ self.acct_line.recurring_rule_type = 'monthly'
+ self.acct_line.recurring_invoicing_type = 'pre-paid'
+ self.contract.recurring_create_invoice()
+ invoices = self.contract._get_related_invoices()
+ self.assertTrue(invoices)
+ self.assertEqual(
+ self.acct_line.recurring_next_date, recurring_next_date
+ )
+ self.assertEqual(self.acct_line.last_date_invoiced, last_date_invoiced)
+
+ def test_contract_monthly_post_paid_custom_first_period(self):
+ recurring_next_date = to_date('2019-05-01')
+ last_date_invoiced = to_date('2019-03-31')
+ self.acct_line.recurring_next_date = '2019-04-22'
+ self.acct_line.date_start = '2019-03-22'
+ self.acct_line.next_period_end_date = '2019-03-31'
+ self.acct_line.recurring_rule_type = 'monthly'
+ self.acct_line.recurring_invoicing_type = 'post-paid'
+ self.contract.recurring_create_invoice()
+ invoices = self.contract._get_related_invoices()
+ self.assertTrue(invoices)
+ self.assertEqual(
+ self.acct_line.recurring_next_date, recurring_next_date
+ )
+ self.assertEqual(self.acct_line.last_date_invoiced, last_date_invoiced)
+
+ def test_contract_monthlylastday_custom_first_period(self):
+ recurring_next_date = to_date('2019-03-31')
+ last_date_invoiced = to_date('2019-03-28')
+ self.acct_line.recurring_next_date = '2019-04-22'
+ self.acct_line.date_start = '2019-03-22'
+ self.acct_line.next_period_end_date = '2019-03-28'
+ self.acct_line.recurring_rule_type = 'monthlylastday'
+ self.acct_line.recurring_invoicing_type = 'post-paid'
+ self.contract.recurring_create_invoice()
+ invoices = self.contract._get_related_invoices()
+ self.assertTrue(invoices)
+ self.assertEqual(
+ self.acct_line.recurring_next_date, recurring_next_date
+ )
+ self.assertEqual(self.acct_line.last_date_invoiced, last_date_invoiced)
+
+ def test_check_next_period_end_date(self):
+ self.acct_line.recurring_next_date = '2019-04-22'
+ self.acct_line.date_start = '2019-03-22'
+ self.acct_line.date_end = '2019-05-22'
+ with self.assertRaises(ValidationError):
+ self.acct_line.next_period_end_date = '2019-03-21'
+ with self.assertRaises(ValidationError):
+ self.acct_line.next_period_end_date = '2019-05-23'
+ with self.assertRaises(ValidationError):
+ self.acct_line.last_date_invoiced = '2019-04-22'
+ with self.assertRaises(ValidationError):
+ self.acct_line.next_period_end_date = False
+ self.acct_line.last_date_invoiced = '2019-04-22'
diff --git a/contract/views/contract_line.xml b/contract/views/contract_line.xml
index 2d14a81f8d..703f028040 100644
--- a/contract/views/contract_line.xml
+++ b/contract/views/contract_line.xml
@@ -15,14 +15,15 @@
+
+
-
diff --git a/product_contract/models/sale_order_line.py b/product_contract/models/sale_order_line.py
index 6f55f5c4b6..f3adb99fad 100644
--- a/product_contract/models/sale_order_line.py
+++ b/product_contract/models/sale_order_line.py
@@ -108,7 +108,7 @@ def _prepare_contract_line_values(
self.ensure_one()
recurring_next_date = self.env[
'contract.line'
- ]._compute_first_recurring_next_date(
+ ]._get_recurring_next_date(
self.date_start or fields.Date.today(),
self.recurring_invoicing_type,
self.recurring_rule_type,