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,