From 5ee13d4cbdd49930ded75e1f6a78274e6a4f0948 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= Date: Fri, 6 Dec 2019 09:19:18 +0100 Subject: [PATCH 01/18] [REF] contract: rename misnamed methods --- contract/models/contract_line.py | 14 +++++++------- contract/tests/test_contract.py | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/contract/models/contract_line.py b/contract/models/contract_line.py index 3178f24094..344ebebd3f 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -356,7 +356,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, @@ -374,7 +374,7 @@ def _compute_first_recurring_next_date( ) @api.model - def compute_first_date_end( + def _get_first_date_end( self, date_start, auto_renew_rule_type, auto_renew_interval ): return ( @@ -396,7 +396,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_first_date_end( rec.date_start, rec.auto_renew_rule_type, rec.auto_renew_interval, @@ -410,7 +410,7 @@ def _onchange_is_auto_renew(self): ) 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, @@ -651,7 +651,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, @@ -712,7 +712,7 @@ 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, @@ -1023,7 +1023,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_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 76ea414492..42a10761b0 100644 --- a/contract/tests/test_contract.py +++ b/contract/tests/test_contract.py @@ -537,7 +537,7 @@ def test_act_show_contract(self): 'There was an error and the view couldn\'t be opened.', ) - def test_compute_first_recurring_next_date(self): + def test_get_recurring_next_date(self): """Test different combination to compute recurring_next_date Combination format { @@ -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), From 5bc895980e4347b7971b89bd73c83883160ac403 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= Date: Fri, 6 Dec 2019 10:19:55 +0100 Subject: [PATCH 02/18] [REF] contract: clarify _get_recurring_next_date First compute the next period end date, then derive the next invoice date from the next period stard and end date. --- contract/models/contract_line.py | 47 ++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/contract/models/contract_line.py b/contract/models/contract_line.py index 344ebebd3f..c16186e736 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -358,20 +358,51 @@ def _check_overlap_predecessor(self): @api.model def _get_recurring_next_date( self, - date_start, + next_period_date_start, recurring_invoicing_type, 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=False, # TODO + ) + if recurring_rule_type == 'monthlylastday': + return next_period_date_end + elif recurring_invoicing_type == 'pre-paid': + return next_period_date_start + else: # post-paid + return next_period_date_end + relativedelta(days=1) + + @api.model + def _get_next_period_date_end( + self, + next_period_date_start, + recurring_rule_type, + recurring_interval, + max_date_end, + ): + """Compute the end date for the next period""" if recurring_rule_type == 'monthlylastday': - return date_start + self.get_relative_delta( - recurring_rule_type, recurring_interval - 1 + next_period_date_end = ( + next_period_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 - ) + else: + next_period_date_end = ( + next_period_date_start + + 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 + return next_period_date_end @api.model def _get_first_date_end( From e6b9ea9ce70c31f9a6dd6a51b4a53e9982788939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= Date: Fri, 6 Dec 2019 10:46:40 +0100 Subject: [PATCH 03/18] [REF] contract: handle max_date_end in _get_recurring_next_date This concentrates all next date calculation logic in one place, and will allow further simplifications. --- contract/models/contract_line.py | 34 ++++++++++++++++----- contract/tests/test_contract.py | 52 ++++++++++++++++++++++++++------ 2 files changed, 68 insertions(+), 18 deletions(-) diff --git a/contract/models/contract_line.py b/contract/models/contract_line.py index c16186e736..7b4565a686 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -362,19 +362,23 @@ def _get_recurring_next_date( recurring_invoicing_type, 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=False, # TODO + max_date_end=max_date_end, ) + if not next_period_date_end: + return False if recurring_rule_type == 'monthlylastday': - return next_period_date_end + recurring_next_date = next_period_date_end elif recurring_invoicing_type == 'pre-paid': - return next_period_date_start + recurring_next_date = next_period_date_start else: # post-paid - return next_period_date_end + relativedelta(days=1) + recurring_next_date = next_period_date_end + relativedelta(days=1) + return recurring_next_date @api.model def _get_next_period_date_end( @@ -385,6 +389,9 @@ def _get_next_period_date_end( max_date_end, ): """Compute the end date for the next period""" + 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 recurring_rule_type == 'monthlylastday': next_period_date_end = ( next_period_date_start @@ -401,6 +408,7 @@ def _get_next_period_date_end( - relativedelta(days=1) ) 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 @@ -435,6 +443,7 @@ def _onchange_is_auto_renew(self): @api.onchange( 'date_start', + 'date_end', 'recurring_invoicing_type', 'recurring_rule_type', 'recurring_interval', @@ -446,6 +455,7 @@ def _onchange_date_start(self): rec.recurring_invoicing_type, rec.recurring_rule_type, rec.recurring_interval, + max_date_end=rec.date_end, ) @api.constrains('is_canceled', 'is_auto_renew') @@ -682,15 +692,22 @@ def _delay(self, delay_delta): ) ) new_date_start = rec.date_start + delay_delta - rec.recurring_next_date = self._get_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_recurring_next_date( new_date_start, rec.recurring_invoicing_type, 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): @@ -748,6 +765,7 @@ def _prepare_value_for_plan_successor( self.recurring_invoicing_type, self.recurring_rule_type, self.recurring_interval, + max_date_end=date_end, ) new_vals = self.read()[0] new_vals.pop("id", None) diff --git a/contract/tests/test_contract.py b/contract/tests/test_contract.py index 42a10761b0..5a73c63e44 100644 --- a/contract/tests/test_contract.py +++ b/contract/tests/test_contract.py @@ -547,6 +547,7 @@ def test_get_recurring_next_date(self): recurring_rule_type, # ('daily', 'weekly', 'monthly', # 'monthlylastday', 'yearly'), recurring_interval, # integer + max_date_end, # date ), } """ @@ -556,50 +557,81 @@ def error_message( recurring_invoicing_type, recurring_rule_type, recurring_interval, + max_date_end, ): - return "Error in %s every %d %s case, start with %s " % ( + return "Error in %s every %d %s case, start with %s (max_date_end=%s)" % ( recurring_invoicing_type, 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', 'monthly', 1, + False), ), ( to_date('2018-01-01'), - (to_date('2018-01-01'), 'pre-paid', 'monthly', 2), + (to_date('2018-01-01'), 'pre-paid', 'monthly', 1, + to_date('2018-01-15')), + ), + ( + False, + (to_date('2018-01-16'), 'pre-paid', 'monthly', 1, + to_date('2018-01-15')), + ), + ( + to_date('2018-01-01'), + (to_date('2018-01-01'), 'pre-paid', 'monthly', 2, + False), ), ( to_date('2018-02-01'), - (to_date('2018-01-01'), 'post-paid', 'monthly', 1), + (to_date('2018-01-01'), 'post-paid', 'monthly', 1, + False), + ), + ( + to_date('2018-01-16'), + (to_date('2018-01-01'), 'post-paid', 'monthly', 1, + to_date('2018-01-15')), + ), + ( + False, + (to_date('2018-01-16'), 'post-paid', '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', 'monthly', 2, + False), ), ( to_date('2018-01-31'), - (to_date('2018-01-05'), 'post-paid', 'monthlylastday', 1), + (to_date('2018-01-05'), 'post-paid', 'monthlylastday', 1, + False), ), ( to_date('2018-01-31'), - (to_date('2018-01-06'), 'pre-paid', 'monthlylastday', 1), + (to_date('2018-01-06'), 'pre-paid', 'monthlylastday', 1, + False), ), ( to_date('2018-02-28'), - (to_date('2018-01-05'), 'pre-paid', 'monthlylastday', 2), + (to_date('2018-01-05'), 'pre-paid', 'monthlylastday', 2, + False), ), ( to_date('2018-01-05'), - (to_date('2018-01-05'), 'pre-paid', 'yearly', 1), + (to_date('2018-01-05'), 'pre-paid', 'yearly', 1, + False), ), ( to_date('2019-01-05'), - (to_date('2018-01-05'), 'post-paid', 'yearly', 1), + (to_date('2018-01-05'), 'post-paid', 'yearly', 1, + False), ), ] contract_line_env = self.env['contract.line'] From edf6833becbce04d51974c115462f97ac2b46c8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= Date: Fri, 6 Dec 2019 11:09:32 +0100 Subject: [PATCH 04/18] [IMP] contract: add next period start/end fields Add two computed field showing the next period start and end date. This improve the UX and will enable further simplifications in the code. --- contract/models/contract_line.py | 38 ++++++++++++++++++++++++++++++++ contract/views/contract_line.xml | 2 ++ 2 files changed, 40 insertions(+) diff --git a/contract/models/contract_line.py b/contract/models/contract_line.py index 7b4565a686..afbc23f21f 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", @@ -389,6 +397,8 @@ def _get_next_period_date_end( max_date_end, ): """Compute the end date for the next period""" + 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 @@ -412,6 +422,34 @@ def _get_next_period_date_end( 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_rule_type', + 'recurring_interval', + 'date_end', + ) + 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, + ) + @api.model def _get_first_date_end( self, date_start, auto_renew_rule_type, auto_renew_interval diff --git a/contract/views/contract_line.xml b/contract/views/contract_line.xml index 2d14a81f8d..85f96db4b1 100644 --- a/contract/views/contract_line.xml +++ b/contract/views/contract_line.xml @@ -15,11 +15,13 @@ + + From 83cb7c1d2189228b549f6e99309f9c3e70089d58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= Date: Fri, 6 Dec 2019 12:44:23 +0100 Subject: [PATCH 05/18] [REF] contract: refactor _get_period_to_invoice Move the part of the logic that compute the next period depending on the chosen next invoice date to _get_next_period_date_end. --- contract/models/contract_line.py | 76 +++++++++++++++++++------------- 1 file changed, 46 insertions(+), 30 deletions(-) diff --git a/contract/models/contract_line.py b/contract/models/contract_line.py index afbc23f21f..b1fa391376 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -374,6 +374,7 @@ def _get_recurring_next_date( ): next_period_date_end = self._get_next_period_date_end( next_period_date_start, + recurring_invoicing_type, recurring_rule_type, recurring_interval, max_date_end=max_date_end, @@ -392,9 +393,11 @@ def _get_recurring_next_date( def _get_next_period_date_end( self, next_period_date_start, + recurring_invoicing_type, recurring_rule_type, recurring_interval, max_date_end, + next_invoice_date=False, ): """Compute the end date for the next period""" if not next_period_date_start: @@ -402,21 +405,39 @@ def _get_next_period_date_end( 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 recurring_rule_type == 'monthlylastday': - next_period_date_end = ( - next_period_date_start - + self.get_relative_delta( - recurring_rule_type, recurring_interval - 1 + if not next_invoice_date: + # regular algorithm + if recurring_rule_type == 'monthlylastday': + next_period_date_end = ( + next_period_date_start + + self.get_relative_delta( + recurring_rule_type, recurring_interval - 1 + ) + ) + else: + next_period_date_end = ( + next_period_date_start + + self.get_relative_delta( + recurring_rule_type, recurring_interval + ) + - relativedelta(days=1) ) - ) else: - next_period_date_end = ( - next_period_date_start - + self.get_relative_delta( - recurring_rule_type, recurring_interval + # special algorithm when the next invoice date is forced + if recurring_rule_type == 'monthlylastday': + next_period_date_end = next_invoice_date + elif recurring_invoicing_type == 'pre-paid': + next_period_date_end = ( + next_invoice_date + + self.get_relative_delta( + recurring_rule_type, recurring_interval + ) + - relativedelta(days=1) + ) + else: # post-paid + next_period_date_end = next_invoice_date - relativedelta( + days=1 ) - - relativedelta(days=1) - ) 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 @@ -437,6 +458,7 @@ def _compute_next_period_date_start(self): @api.depends( 'next_period_date_start', + 'recurring_invoicing_type', 'recurring_rule_type', 'recurring_interval', 'date_end', @@ -445,9 +467,11 @@ 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_invoicing_type, rec.recurring_rule_type, rec.recurring_interval, max_date_end=rec.date_end, + next_invoice_date=rec.recurring_next_date, ) @api.model @@ -612,6 +636,8 @@ def _prepare_invoice_line(self, invoice_id=False): 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: @@ -621,24 +647,14 @@ 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 - ) - 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_invoicing_type, + 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, + ) return first_date_invoiced, last_date_invoiced, recurring_next_date @api.multi From aff3781c92b144b4fc8aa5dbcaed8b8bda15ea11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= Date: Fri, 6 Dec 2019 14:12:18 +0100 Subject: [PATCH 06/18] [REF] contract: refactor _update_recurring_next_date Reuse the logic that is now fully located in _get_recurring_next_date. --- contract/models/contract_line.py | 27 +++++++++++---------------- contract/tests/test_contract.py | 4 ++-- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/contract/models/contract_line.py b/contract/models/contract_line.py index b1fa391376..8124786ea4 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -675,23 +675,18 @@ 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 + last_date_invoiced = rec.next_period_date_end + recurring_next_date = rec._get_recurring_next_date( + last_date_invoiced + relativedelta(days=1), + rec.recurring_invoicing_type, + 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): diff --git a/contract/tests/test_contract.py b/contract/tests/test_contract.py index 5a73c63e44..925ab2cee9 100644 --- a/contract/tests/test_contract.py +++ b/contract/tests/test_contract.py @@ -247,7 +247,7 @@ def test_contract_yearly_pre_paid(self): 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 +279,7 @@ def test_last_invoice_post_paid(self): ) 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') From 7c3b9bcfa72569a81610b441bda27d8cf7d5f112 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= Date: Fri, 6 Dec 2019 14:15:47 +0100 Subject: [PATCH 07/18] [REF] contract: re-add _compute_first_recurring_next_date For backward compatibility --- contract/models/contract_line.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/contract/models/contract_line.py b/contract/models/contract_line.py index 8124786ea4..0b711a02fb 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -363,6 +363,23 @@ def _check_overlap_predecessor(self): _("Contract line and its predecessor overlapped") ) + @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_recurring_next_date( + date_start, + recurring_invoicing_type, + recurring_rule_type, + recurring_interval, + max_date_end=False, + ) + @api.model def _get_recurring_next_date( self, From dd3f3a6c7725943ca974b314aa8f3fa4f3c87af9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= Date: Fri, 6 Dec 2019 15:33:26 +0100 Subject: [PATCH 08/18] [FIX] contract: add missing dependency in computed field --- contract/models/contract_line.py | 1 + 1 file changed, 1 insertion(+) diff --git a/contract/models/contract_line.py b/contract/models/contract_line.py index 0b711a02fb..714c3bc146 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -479,6 +479,7 @@ def _compute_next_period_date_start(self): 'recurring_rule_type', 'recurring_interval', 'date_end', + 'recurring_next_date', ) def _compute_next_period_date_end(self): for rec in self: From d53f01d66f9e9a60ee17233411d940d685be369b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= Date: Sun, 8 Dec 2019 12:06:57 +0100 Subject: [PATCH 09/18] [REF] contract: remove one monthlylastday special case get_relative_delta now works the same for all recurring rules. Move the special case handling to _init_last_date_invoiced which is used only for migration. --- contract/models/contract_line.py | 33 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/contract/models/contract_line.py b/contract/models/contract_line.py index 714c3bc146..93711c5d38 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -424,21 +424,13 @@ def _get_next_period_date_end( return False if not next_invoice_date: # regular algorithm - if recurring_rule_type == 'monthlylastday': - next_period_date_end = ( - next_period_date_start - + self.get_relative_delta( - recurring_rule_type, recurring_interval - 1 - ) - ) - else: - next_period_date_end = ( - next_period_date_start - + self.get_relative_delta( - recurring_rule_type, recurring_interval - ) - - relativedelta(days=1) + 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_rule_type == 'monthlylastday': @@ -717,8 +709,9 @@ def _init_last_date_invoiced(self): 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 = ( @@ -726,12 +719,18 @@ def _init_last_date_invoiced(self): - 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': @@ -739,7 +738,7 @@ def get_relative_delta(self, recurring_rule_type, interval): 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) From 557097be2d710e724dcc52f4be37ffba1e5ce2b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= Date: Sun, 8 Dec 2019 12:31:04 +0100 Subject: [PATCH 10/18] [IMP] contract: support pre-paid for monthlylastday monthlylastday is (almost) not a special case anymore \o/. montlylastday is simply a montly period where the periods are aligned on month boundaries. The last bit of special casing is that postpaid generates invoice the day after the last dasy of the period, except for monthlylastday where the invoice is generated on the last day of the period. This last exception will disappear when we put the offset under user control. This is a breaking change because the post-paid/pre-paid mode becomes relevant for monthlylastday invoicing. The field becomes visible in the UI. Code that generate monthlylastday contract lines must now correctly set the pre-paid/post-paid mode too. Some tests have had to be adapted to reflect that. --- .../migrations/12.0.5.0.0/pre-migration.py | 10 +++++ contract/models/contract_line.py | 35 +++++++++++----- contract/tests/test_contract.py | 40 ++++++++++++++++++- contract/views/abstract_contract_line.xml | 3 +- .../tests/test_contract_sale_mandate.py | 1 + product_contract/tests/test_sale_order.py | 1 + product_contract/views/product_template.xml | 3 +- product_contract/views/sale_order.xml | 3 +- 8 files changed, 77 insertions(+), 19 deletions(-) create mode 100644 contract/migrations/12.0.5.0.0/pre-migration.py 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 0000000000..ca208e9ddc --- /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/contract_line.py b/contract/models/contract_line.py index 93711c5d38..1a5fa3d08d 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -380,6 +380,22 @@ def _compute_first_recurring_next_date( max_date_end=False, ) + @api.model + def _get_offset(self, recurring_invoicing_type, recurring_rule_type): + """Return a relativedelta to offset the invoice date compared + to the period start or end date. + + This method will disappear when the offset becomes user controlled. + """ + if ( + recurring_invoicing_type == 'pre-paid' + or recurring_rule_type == 'monthlylastday' + ): + offset = 0 + else: + offset = 1 + return relativedelta(days=offset) + @api.model def _get_recurring_next_date( self, @@ -398,12 +414,11 @@ def _get_recurring_next_date( ) if not next_period_date_end: return False - if recurring_rule_type == 'monthlylastday': - recurring_next_date = next_period_date_end - elif recurring_invoicing_type == 'pre-paid': - recurring_next_date = next_period_date_start + offset = self._get_offset(recurring_invoicing_type, recurring_rule_type) + if recurring_invoicing_type == 'pre-paid': + recurring_next_date = next_period_date_start + offset else: # post-paid - recurring_next_date = next_period_date_end + relativedelta(days=1) + recurring_next_date = next_period_date_end + offset return recurring_next_date @api.model @@ -433,20 +448,18 @@ def _get_next_period_date_end( ) else: # special algorithm when the next invoice date is forced - if recurring_rule_type == 'monthlylastday': - next_period_date_end = next_invoice_date - elif recurring_invoicing_type == 'pre-paid': + offset = self._get_offset(recurring_invoicing_type, recurring_rule_type) + if recurring_invoicing_type == 'pre-paid': next_period_date_end = ( next_invoice_date + - 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=1 - ) + next_period_date_end = next_invoice_date - 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 diff --git a/contract/tests/test_contract.py b/contract/tests/test_contract.py index 925ab2cee9..e6bf09ecfb 100644 --- a/contract/tests/test_contract.py +++ b/contract/tests/test_contract.py @@ -614,12 +614,17 @@ def error_message( False), ), ( - to_date('2018-01-31'), + to_date('2018-01-06'), (to_date('2018-01-06'), 'pre-paid', 'monthlylastday', 1, False), ), ( to_date('2018-02-28'), + (to_date('2018-01-05'), 'post-paid', 'monthlylastday', 2, + False), + ), + ( + to_date('2018-01-05'), (to_date('2018-01-05'), 'pre-paid', 'monthlylastday', 2, False), ), @@ -1363,7 +1368,7 @@ def test_cron_recurring_create_invoice(self): 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' @@ -1394,6 +1399,37 @@ def test_get_period_to_invoice_monthlylastday(self): 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.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.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.acct_line.manual_renew_needed = True + 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 b66077eeb4..351ded65f6 100644 --- a/contract/views/abstract_contract_line.xml +++ b/contract/views/abstract_contract_line.xml @@ -59,8 +59,7 @@ - + diff --git a/contract_sale_mandate/tests/test_contract_sale_mandate.py b/contract_sale_mandate/tests/test_contract_sale_mandate.py index eb36adcde3..0451513353 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 @@ def setUpClass(cls): '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/tests/test_sale_order.py b/product_contract/tests/test_sale_order.py index 7a630cbbd8..307cf90841 100644 --- a/product_contract/tests/test_sale_order.py +++ b/product_contract/tests/test_sale_order.py @@ -43,6 +43,7 @@ def setUp(self): '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 400afa9903..057ec95bca 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 e5da25febd..4fd1b90b99 100644 --- a/product_contract/views/sale_order.xml +++ b/product_contract/views/sale_order.xml @@ -58,8 +58,7 @@ - + Date: Mon, 9 Dec 2019 10:27:02 +0100 Subject: [PATCH 11/18] [FIX] contract: Improve unit tests --- contract/models/contract_line.py | 2 +- contract/tests/test_contract.py | 32 +++++++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/contract/models/contract_line.py b/contract/models/contract_line.py index 1a5fa3d08d..aacb2c1d3b 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -664,7 +664,7 @@ def _get_period_to_invoice( self.ensure_one() first_date_invoiced = False if not recurring_next_date: - return first_date_invoiced, last_date_invoiced, recurring_next_date + return first_date_invoiced, False, recurring_next_date first_date_invoiced = ( last_date_invoiced + relativedelta(days=1) if last_date_invoiced diff --git a/contract/tests/test_contract.py b/contract/tests/test_contract.py index e6bf09ecfb..b9ed4eddfc 100644 --- a/contract/tests/test_contract.py +++ b/contract/tests/test_contract.py @@ -1412,6 +1412,10 @@ def test_get_period_to_invoice_monthlylastday_prepaid(self): ) 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( @@ -1420,6 +1424,13 @@ def test_get_period_to_invoice_monthlylastday_prepaid(self): ) 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( @@ -1428,7 +1439,26 @@ def test_get_period_to_invoice_monthlylastday_prepaid(self): ) self.assertEqual(first, to_date('2018-03-01')) self.assertEqual(last, to_date('2018-03-15')) - self.acct_line.manual_renew_needed = True + 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' From 8b30ed0a3e3e34a4c011674ab20cf0464fcb0509 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= Date: Mon, 9 Dec 2019 10:21:20 +0100 Subject: [PATCH 12/18] [REF] contract: make recurring_invoicing_offset a computed field In preparation to making it user modifiable. --- contract/__manifest__.py | 2 +- contract/models/abstract_contract_line.py | 34 ++++++++++- contract/models/contract_line.py | 48 ++++++++------- contract/tests/test_contract.py | 72 ++++++++++++++++------- 4 files changed, 112 insertions(+), 44 deletions(-) diff --git a/contract/__manifest__.py b/contract/__manifest__.py index 274ab73260..f698f180cd 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/models/abstract_contract_line.py b/contract/models/abstract_contract_line.py index c1320208d7..d4d9469142 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 beginning 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 aacb2c1d3b..6054b303bf 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -375,32 +375,20 @@ def _compute_first_recurring_next_date( return self._get_recurring_next_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_offset(self, recurring_invoicing_type, recurring_rule_type): - """Return a relativedelta to offset the invoice date compared - to the period start or end date. - - This method will disappear when the offset becomes user controlled. - """ - if ( - recurring_invoicing_type == 'pre-paid' - or recurring_rule_type == 'monthlylastday' - ): - offset = 0 - else: - offset = 1 - return relativedelta(days=offset) - @api.model def _get_recurring_next_date( self, next_period_date_start, recurring_invoicing_type, + recurring_invoicing_offset, recurring_rule_type, recurring_interval, max_date_end, @@ -408,17 +396,23 @@ def _get_recurring_next_date( next_period_date_end = self._get_next_period_date_end( next_period_date_start, recurring_invoicing_type, + recurring_invoicing_offset, recurring_rule_type, recurring_interval, max_date_end=max_date_end, ) if not next_period_date_end: return False - offset = self._get_offset(recurring_invoicing_type, recurring_rule_type) if recurring_invoicing_type == 'pre-paid': - recurring_next_date = next_period_date_start + offset + recurring_next_date = ( + next_period_date_start + + relativedelta(days=recurring_invoicing_offset) + ) else: # post-paid - recurring_next_date = next_period_date_end + offset + recurring_next_date = ( + next_period_date_end + + relativedelta(days=recurring_invoicing_offset) + ) return recurring_next_date @api.model @@ -426,6 +420,7 @@ def _get_next_period_date_end( self, next_period_date_start, recurring_invoicing_type, + recurring_invoicing_offset, recurring_rule_type, recurring_interval, max_date_end, @@ -448,18 +443,20 @@ def _get_next_period_date_end( ) else: # special algorithm when the next invoice date is forced - offset = self._get_offset(recurring_invoicing_type, recurring_rule_type) if recurring_invoicing_type == 'pre-paid': next_period_date_end = ( next_invoice_date - - offset + - 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 - offset + 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 @@ -481,6 +478,7 @@ def _compute_next_period_date_start(self): @api.depends( 'next_period_date_start', 'recurring_invoicing_type', + 'recurring_invoicing_offset', 'recurring_rule_type', 'recurring_interval', 'date_end', @@ -491,6 +489,7 @@ def _compute_next_period_date_end(self): rec.next_period_date_end = self._get_next_period_date_end( 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, @@ -538,6 +537,7 @@ def _onchange_date_start(self): rec.recurring_next_date = self._get_recurring_next_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, @@ -673,6 +673,7 @@ def _get_period_to_invoice( last_date_invoiced = self._get_next_period_date_end( first_date_invoiced, self.recurring_invoicing_type, + self.recurring_invoicing_offset, self.recurring_rule_type, self.recurring_interval, max_date_end=(self.date_end if stop_at_date_end else False), @@ -702,6 +703,7 @@ def _update_recurring_next_date(self): recurring_next_date = rec._get_recurring_next_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, @@ -778,6 +780,7 @@ def _delay(self, delay_delta): new_recurring_next_date = self._get_recurring_next_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 @@ -842,6 +845,7 @@ def _prepare_value_for_plan_successor( recurring_next_date = self._get_recurring_next_date( date_start, self.recurring_invoicing_type, + self.recurring_invoicing_offset, self.recurring_rule_type, self.recurring_interval, max_date_end=date_end, diff --git a/contract/tests/test_contract.py b/contract/tests/test_contract.py index b9ed4eddfc..e8c49b6f11 100644 --- a/contract/tests/test_contract.py +++ b/contract/tests/test_contract.py @@ -537,6 +537,33 @@ def test_act_show_contract(self): 'There was an error and the view couldn\'t be opened.', ) + 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_recurring_next_date(self): """Test different combination to compute recurring_next_date Combination format @@ -555,87 +582,92 @@ def test_get_recurring_next_date(self): 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 (max_date_end=%s)" % ( - recurring_invoicing_type, - recurring_interval, - recurring_rule_type, - date_start, - max_date_end, + 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', 1, + (to_date('2018-01-01'), 'pre-paid', 0, 'monthly', 1, to_date('2018-01-15')), ), ( False, - (to_date('2018-01-16'), 'pre-paid', 'monthly', 1, + (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', 'monthly', 2, + (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', 'monthly', 1, + (to_date('2018-01-01'), 'post-paid', 1, 'monthly', 1, to_date('2018-01-15')), ), ( False, - (to_date('2018-01-16'), 'post-paid', 'monthly', 1, + (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-06'), - (to_date('2018-01-06'), 'pre-paid', 'monthlylastday', 1, + (to_date('2018-01-06'), 'pre-paid', 0, 'monthlylastday', 1, False), ), ( to_date('2018-02-28'), - (to_date('2018-01-05'), 'post-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', 'monthlylastday', 2, + (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), ), ] From 5286520795ce3931c8756df18b2a0adde23a56ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= Date: Mon, 9 Dec 2019 10:29:16 +0100 Subject: [PATCH 13/18] [REF] contract: make get_next_period_date_end public Make it public because it is the core logic of the module. Also, clarify that recurring_invoicing_type and recurring_invoicing_offset are needed only when we want the next period to be computed from a user chosen next invoice date. --- contract/models/contract_line.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/contract/models/contract_line.py b/contract/models/contract_line.py index 6054b303bf..cd321c40ce 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -393,10 +393,8 @@ def _get_recurring_next_date( recurring_interval, max_date_end, ): - next_period_date_end = self._get_next_period_date_end( + next_period_date_end = self.get_next_period_date_end( next_period_date_start, - recurring_invoicing_type, - recurring_invoicing_offset, recurring_rule_type, recurring_interval, max_date_end=max_date_end, @@ -416,17 +414,24 @@ def _get_recurring_next_date( return recurring_next_date @api.model - def _get_next_period_date_end( + def get_next_period_date_end( self, next_period_date_start, - recurring_invoicing_type, - recurring_invoicing_offset, 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""" + """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: @@ -486,14 +491,14 @@ def _compute_next_period_date_start(self): ) 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_end = self.get_next_period_date_end( 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, next_invoice_date=rec.recurring_next_date, + recurring_invoicing_type=rec.recurring_invoicing_type, + recurring_invoicing_offset=rec.recurring_invoicing_offset, ) @api.model @@ -670,14 +675,14 @@ def _get_period_to_invoice( if last_date_invoiced else self.date_start ) - last_date_invoiced = self._get_next_period_date_end( + last_date_invoiced = self.get_next_period_date_end( first_date_invoiced, - self.recurring_invoicing_type, - self.recurring_invoicing_offset, 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 From 8ba9033903509ae77738317f6d8a3008cfd5c15f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= Date: Mon, 9 Dec 2019 10:31:17 +0100 Subject: [PATCH 14/18] [REF] contract: rename _get_recurring_next_date as get_next_invoice_date It is easier to understand. Also make it public. --- contract/models/contract_line.py | 12 ++++++------ contract/tests/test_contract.py | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/contract/models/contract_line.py b/contract/models/contract_line.py index cd321c40ce..e8b79bfa5f 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -372,7 +372,7 @@ def _compute_first_recurring_next_date( recurring_interval ): # deprecated method for backward compatibility - return self._get_recurring_next_date( + return self.get_next_invoice_date( date_start, recurring_invoicing_type, self._get_default_recurring_invoicing_offset( @@ -384,7 +384,7 @@ def _compute_first_recurring_next_date( ) @api.model - def _get_recurring_next_date( + def get_next_invoice_date( self, next_period_date_start, recurring_invoicing_type, @@ -539,7 +539,7 @@ def _onchange_is_auto_renew(self): ) def _onchange_date_start(self): for rec in self.filtered('date_start'): - rec.recurring_next_date = self._get_recurring_next_date( + rec.recurring_next_date = self.get_next_invoice_date( rec.date_start, rec.recurring_invoicing_type, rec.recurring_invoicing_offset, @@ -705,7 +705,7 @@ def _insert_markers(self, first_date_invoiced, last_date_invoiced): def _update_recurring_next_date(self): for rec in self: last_date_invoiced = rec.next_period_date_end - recurring_next_date = rec._get_recurring_next_date( + recurring_next_date = rec.get_next_invoice_date( last_date_invoiced + relativedelta(days=1), rec.recurring_invoicing_type, rec.recurring_invoicing_offset, @@ -782,7 +782,7 @@ def _delay(self, delay_delta): new_date_end = rec.date_end + delay_delta else: new_date_end = False - new_recurring_next_date = self._get_recurring_next_date( + new_recurring_next_date = self.get_next_invoice_date( new_date_start, rec.recurring_invoicing_type, rec.recurring_invoicing_offset, @@ -847,7 +847,7 @@ def _prepare_value_for_plan_successor( ): self.ensure_one() if not recurring_next_date: - recurring_next_date = self._get_recurring_next_date( + recurring_next_date = self.get_next_invoice_date( date_start, self.recurring_invoicing_type, self.recurring_invoicing_offset, diff --git a/contract/tests/test_contract.py b/contract/tests/test_contract.py index e8c49b6f11..fd669ea403 100644 --- a/contract/tests/test_contract.py +++ b/contract/tests/test_contract.py @@ -564,7 +564,7 @@ def test_get_default_recurring_invoicing_offset(self): 0 ) - def test_get_recurring_next_date(self): + def test_get_next_invoice_date(self): """Test different combination to compute recurring_next_date Combination format { @@ -675,7 +675,7 @@ def error_message( for recurring_next_date, combination in combinations: self.assertEqual( recurring_next_date, - contract_line_env._get_recurring_next_date( + contract_line_env.get_next_invoice_date( *combination ), error_message(*combination), From 5a46e9752765a1ef081a427da2213ffedfe8ccbf Mon Sep 17 00:00:00 2001 From: sbejaoui Date: Mon, 9 Dec 2019 11:59:35 +0100 Subject: [PATCH 15/18] [IMP] contract: add unit test for different combinations for next invoicing period --- contract/tests/test_contract.py | 311 ++++++++++++++++++++++++++++++++ 1 file changed, 311 insertions(+) diff --git a/contract/tests/test_contract.py b/contract/tests/test_contract.py index fd669ea403..e406a45e3b 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 @@ -681,6 +682,316 @@ def error_message( 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() From 7212fc4a13237192f7bd09b20a6ae01f451e42fe Mon Sep 17 00:00:00 2001 From: Bejaoui Souheil Date: Mon, 9 Dec 2019 12:28:57 +0100 Subject: [PATCH 16/18] [REF] contract: simplify _get_period_to_invoice --- contract/models/contract_line.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contract/models/contract_line.py b/contract/models/contract_line.py index e8b79bfa5f..c9d475bde4 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -667,9 +667,8 @@ def _get_period_to_invoice( # 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, False, recurring_next_date + return False, False, False first_date_invoiced = ( last_date_invoiced + relativedelta(days=1) if last_date_invoiced From d9bd82241ee1f60da3d2d8e5d0400690793f5c2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= Date: Mon, 9 Dec 2019 13:12:26 +0100 Subject: [PATCH 17/18] [IMP] contract: display invoicing offset --- contract/models/abstract_contract_line.py | 2 +- contract/views/abstract_contract_line.xml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/contract/models/abstract_contract_line.py b/contract/models/abstract_contract_line.py index d4d9469142..99892d82f1 100644 --- a/contract/models/abstract_contract_line.py +++ b/contract/models/abstract_contract_line.py @@ -81,7 +81,7 @@ class ContractAbstractContractLine(models.AbstractModel): string="Invoicing offset", help=( "Number of days to offset the invoice from the period end " - "date (in post-paid mode) or beginning date (in pre-paid mode)." + "date (in post-paid mode) or start date (in pre-paid mode)." ) ) recurring_interval = fields.Integer( diff --git a/contract/views/abstract_contract_line.xml b/contract/views/abstract_contract_line.xml index 351ded65f6..288511304a 100644 --- a/contract/views/abstract_contract_line.xml +++ b/contract/views/abstract_contract_line.xml @@ -60,6 +60,7 @@ + From ab4ed31e393779c7ed35416778bb9b1d296d7c46 Mon Sep 17 00:00:00 2001 From: sbejaoui Date: Mon, 9 Dec 2019 16:57:56 +0100 Subject: [PATCH 18/18] [IMP] - Set recurring_invoicing_type for existing products set to monthlylastday --- .../migrations/12.0.3.0.0/pre-migration.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 product_contract/migrations/12.0.3.0.0/pre-migration.py 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 0000000000..31b3f1794e --- /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' + """ + )