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 3178f240..344ebebd 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -356,7 +356,7 @@ class ContractLine(models.Model): ) @api.model - def _compute_first_recurring_next_date( + def _get_recurring_next_date( self, date_start, recurring_invoicing_type, @@ -374,7 +374,7 @@ class ContractLine(models.Model): ) @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 @@ class ContractLine(models.Model): auto_renew""" for rec in self.filtered('is_auto_renew'): if rec.date_start: - rec.date_end = self.compute_first_date_end( + rec.date_end = self._get_first_date_end( rec.date_start, rec.auto_renew_rule_type, rec.auto_renew_interval, @@ -410,7 +410,7 @@ class ContractLine(models.Model): ) 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 @@ class ContractLine(models.Model): ) ) 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 @@ class ContractLine(models.Model): ): self.ensure_one() if not recurring_next_date: - recurring_next_date = self._compute_first_recurring_next_date( + recurring_next_date = self._get_recurring_next_date( date_start, self.recurring_invoicing_type, self.recurring_rule_type, @@ -1023,7 +1023,7 @@ class ContractLine(models.Model): def _get_renewal_dates(self): self.ensure_one() date_start = self.date_end + relativedelta(days=1) - date_end = self.compute_first_date_end( + date_end = self._get_first_date_end( date_start, self.auto_renew_rule_type, self.auto_renew_interval ) return date_start, date_end diff --git a/contract/tests/test_contract.py b/contract/tests/test_contract.py index 76ea4144..42a10761 100644 --- a/contract/tests/test_contract.py +++ b/contract/tests/test_contract.py @@ -537,7 +537,7 @@ class TestContract(TestContractBase): 'There was an error and the view couldn\'t be opened.', ) - def test_compute_first_recurring_next_date(self): + def test_get_recurring_next_date(self): """Test different combination to compute recurring_next_date Combination format { @@ -606,7 +606,7 @@ class TestContract(TestContractBase): 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 344ebebd..c16186e7 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -358,20 +358,51 @@ class ContractLine(models.Model): @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 c16186e7..7b4565a6 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -362,19 +362,23 @@ class ContractLine(models.Model): 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 @@ class ContractLine(models.Model): 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 @@ class ContractLine(models.Model): - 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 @@ class ContractLine(models.Model): @api.onchange( 'date_start', + 'date_end', 'recurring_invoicing_type', 'recurring_rule_type', 'recurring_interval', @@ -446,6 +455,7 @@ class ContractLine(models.Model): 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 @@ class ContractLine(models.Model): ) ) 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 @@ class ContractLine(models.Model): 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 42a10761..5a73c63e 100644 --- a/contract/tests/test_contract.py +++ b/contract/tests/test_contract.py @@ -547,6 +547,7 @@ class TestContract(TestContractBase): recurring_rule_type, # ('daily', 'weekly', 'monthly', # 'monthlylastday', 'yearly'), recurring_interval, # integer + max_date_end, # date ), } """ @@ -556,50 +557,81 @@ class TestContract(TestContractBase): 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 7b4565a6..afbc23f2 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 @@ class ContractLine(models.Model): 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 @@ class ContractLine(models.Model): 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 2d14a81f..85f96db4 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 afbc23f2..b1fa3913 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -374,6 +374,7 @@ class ContractLine(models.Model): ): 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 @@ class ContractLine(models.Model): 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 @@ class ContractLine(models.Model): 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 @@ class ContractLine(models.Model): @api.depends( 'next_period_date_start', + 'recurring_invoicing_type', 'recurring_rule_type', 'recurring_interval', 'date_end', @@ -445,9 +467,11 @@ class ContractLine(models.Model): 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 @@ class ContractLine(models.Model): def _get_period_to_invoice( self, last_date_invoiced, recurring_next_date, stop_at_date_end=True ): + # TODO this method can now be removed, since + # TODO self.next_period_date_start/end have the same values self.ensure_one() first_date_invoiced = False if not recurring_next_date: @@ -621,24 +647,14 @@ class ContractLine(models.Model): 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 b1fa3913..8124786e 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -675,23 +675,18 @@ class ContractLine(models.Model): @api.multi def _update_recurring_next_date(self): for rec in self: - old_date = rec.recurring_next_date - new_date = old_date + self.get_relative_delta( - rec.recurring_rule_type, rec.recurring_interval + last_date_invoiced = rec.next_period_date_end + recurring_next_date = rec._get_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 5a73c63e..925ab2ce 100644 --- a/contract/tests/test_contract.py +++ b/contract/tests/test_contract.py @@ -247,7 +247,7 @@ class TestContract(TestContractBase): self.assertEqual(self.acct_line.last_date_invoiced, last_date_invoiced) def test_contract_monthly_lastday(self): - recurring_next_date = to_date('2018-03-31') + recurring_next_date = to_date('2018-02-28') last_date_invoiced = to_date('2018-02-22') self.acct_line.recurring_next_date = '2018-02-22' self.acct_line.recurring_invoicing_type = 'post-paid' @@ -279,7 +279,7 @@ class TestContract(TestContractBase): ) self.contract.recurring_create_invoice() self.assertEqual( - self.acct_line.recurring_next_date, to_date('2018-04-01') + self.acct_line.recurring_next_date, to_date('2018-3-16') ) self.assertEqual( self.acct_line.last_date_invoiced, to_date('2018-02-28') 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 8124786e..0b711a02 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -363,6 +363,23 @@ class ContractLine(models.Model): _("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 0b711a02..714c3bc1 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -479,6 +479,7 @@ class ContractLine(models.Model): '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 714c3bc1..93711c5d 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -424,21 +424,13 @@ class ContractLine(models.Model): 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 @@ class ContractLine(models.Model): last_date_invoiced = ( rec.recurring_next_date - self.get_relative_delta( - rec.recurring_rule_type, rec.recurring_interval + rec.recurring_rule_type, rec.recurring_interval - 1 ) + - relativedelta(days=1) ) elif rec.recurring_invoicing_type == 'post-paid': last_date_invoiced = ( @@ -726,12 +719,18 @@ class ContractLine(models.Model): - self.get_relative_delta( rec.recurring_rule_type, rec.recurring_interval ) - ) - relativedelta(days=1) + - relativedelta(days=1) + ) if last_date_invoiced > rec.date_start: rec.last_date_invoiced = last_date_invoiced @api.model def get_relative_delta(self, recurring_rule_type, interval): + """Return a relativedelta for one period. + + When added to the first day of the period, + it gives the first day of the next period. + """ if recurring_rule_type == 'daily': return relativedelta(days=interval) elif recurring_rule_type == 'weekly': @@ -739,7 +738,7 @@ class ContractLine(models.Model): elif recurring_rule_type == 'monthly': return relativedelta(months=interval) elif recurring_rule_type == 'monthlylastday': - return relativedelta(months=interval, day=31) + return relativedelta(months=interval, day=1) else: return relativedelta(years=interval) 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 00000000..ca208e9d --- /dev/null +++ b/contract/migrations/12.0.5.0.0/pre-migration.py @@ -0,0 +1,10 @@ +def migrate(cr, version): + # pre-paid/post-paid becomes significant for monthlylastday too, + # make sure it has the value that was implied for previous versions. + cr.execute( + """\ + UPDATE contract_line + SET recurring_invoicing_type = 'post-paid' + WHERE recurring_rule_type = 'monthlylastday' + """ + ) diff --git a/contract/models/contract_line.py b/contract/models/contract_line.py index 93711c5d..1a5fa3d0 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -380,6 +380,22 @@ class ContractLine(models.Model): 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 @@ class ContractLine(models.Model): ) 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 @@ class ContractLine(models.Model): ) 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 925ab2ce..e6bf09ec 100644 --- a/contract/tests/test_contract.py +++ b/contract/tests/test_contract.py @@ -614,12 +614,17 @@ class TestContract(TestContractBase): 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 @@ class TestContract(TestContractBase): len(invoice_lines), ) - def test_get_period_to_invoice_monthlylastday(self): + def test_get_period_to_invoice_monthlylastday_postpaid(self): self.acct_line.date_start = '2018-01-05' self.acct_line.recurring_invoicing_type = 'post-paid' self.acct_line.recurring_rule_type = 'monthlylastday' @@ -1394,6 +1399,37 @@ class TestContract(TestContractBase): self.assertEqual(last, to_date('2018-03-15')) self.acct_line.manual_renew_needed = True + def test_get_period_to_invoice_monthlylastday_prepaid(self): + self.acct_line.date_start = '2018-01-05' + self.acct_line.recurring_invoicing_type = 'pre-paid' + self.acct_line.recurring_rule_type = 'monthlylastday' + self.acct_line.date_end = '2018-03-15' + self.acct_line._onchange_date_start() + first, last, recurring_next_date = \ + self.acct_line._get_period_to_invoice( + self.acct_line.last_date_invoiced, + self.acct_line.recurring_next_date, + ) + self.assertEqual(first, to_date('2018-01-05')) + self.assertEqual(last, to_date('2018-01-31')) + self.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 b66077ee..351ded65 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 eb36adcd..04515133 100644 --- a/contract_sale_mandate/tests/test_contract_sale_mandate.py +++ b/contract_sale_mandate/tests/test_contract_sale_mandate.py @@ -26,6 +26,7 @@ class TestContractSaleMandate(TestContractBase): 'is_contract': True, 'default_qty': 12, 'recurring_rule_type': "monthlylastday", + 'recurring_invoicing_type': "post-paid", 'contract_template_id': cls.contract_template1.id, } ) diff --git a/product_contract/tests/test_sale_order.py b/product_contract/tests/test_sale_order.py index 7a630cbb..307cf908 100644 --- a/product_contract/tests/test_sale_order.py +++ b/product_contract/tests/test_sale_order.py @@ -43,6 +43,7 @@ class TestSaleOrder(TransactionCase): 'is_contract': True, 'default_qty': 12, 'recurring_rule_type': "monthlylastday", + 'recurring_invoicing_type': "post-paid", 'contract_template_id': self.contract_template1.id, } ) diff --git a/product_contract/views/product_template.xml b/product_contract/views/product_template.xml index 400afa99..057ec95b 100644 --- a/product_contract/views/product_template.xml +++ b/product_contract/views/product_template.xml @@ -33,8 +33,7 @@ - + diff --git a/product_contract/views/sale_order.xml b/product_contract/views/sale_order.xml index e5da25fe..4fd1b90b 100644 --- a/product_contract/views/sale_order.xml +++ b/product_contract/views/sale_order.xml @@ -58,8 +58,7 @@ - + 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 1a5fa3d0..aacb2c1d 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -664,7 +664,7 @@ class ContractLine(models.Model): 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 e6bf09ec..b9ed4edd 100644 --- a/contract/tests/test_contract.py +++ b/contract/tests/test_contract.py @@ -1412,6 +1412,10 @@ class TestContract(TestContractBase): ) 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 @@ class TestContract(TestContractBase): ) 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 @@ class TestContract(TestContractBase): ) 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 274ab732..f698f180 100644 --- a/contract/__manifest__.py +++ b/contract/__manifest__.py @@ -4,7 +4,7 @@ # Copyright 2016-2018 Tecnativa - Carlos Dauden # Copyright 2017 Tecnativa - Vicent Cubells # Copyright 2016-2017 LasLabs Inc. -# Copyright 2018 ACSONE SA/NV +# Copyright 2018-2019 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { diff --git a/contract/models/abstract_contract_line.py b/contract/models/abstract_contract_line.py index c1320208..d4d94691 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 aacb2c1d..6054b303 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -375,32 +375,20 @@ class ContractLine(models.Model): 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 @@ class ContractLine(models.Model): 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 @@ class ContractLine(models.Model): self, next_period_date_start, recurring_invoicing_type, + recurring_invoicing_offset, recurring_rule_type, recurring_interval, max_date_end, @@ -448,18 +443,20 @@ class ContractLine(models.Model): ) 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 @@ class ContractLine(models.Model): @api.depends( 'next_period_date_start', 'recurring_invoicing_type', + 'recurring_invoicing_offset', 'recurring_rule_type', 'recurring_interval', 'date_end', @@ -491,6 +489,7 @@ class ContractLine(models.Model): 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 @@ class ContractLine(models.Model): 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 @@ class ContractLine(models.Model): 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 @@ class ContractLine(models.Model): 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 @@ class ContractLine(models.Model): 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 @@ class ContractLine(models.Model): 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 b9ed4edd..e8c49b6f 100644 --- a/contract/tests/test_contract.py +++ b/contract/tests/test_contract.py @@ -537,6 +537,33 @@ class TestContract(TestContractBase): '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 @@ class TestContract(TestContractBase): def error_message( date_start, recurring_invoicing_type, + recurring_invoicing_offset, recurring_rule_type, recurring_interval, max_date_end, ): - return "Error in %s every %d %s case, start with %s (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 6054b303..cd321c40 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -393,10 +393,8 @@ class ContractLine(models.Model): 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 @@ class ContractLine(models.Model): 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 @@ class ContractLine(models.Model): ) 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 @@ class ContractLine(models.Model): 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 cd321c40..e8b79bfa 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -372,7 +372,7 @@ class ContractLine(models.Model): 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 @@ class ContractLine(models.Model): ) @api.model - def _get_recurring_next_date( + def get_next_invoice_date( self, next_period_date_start, recurring_invoicing_type, @@ -539,7 +539,7 @@ class ContractLine(models.Model): ) 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 @@ class ContractLine(models.Model): 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 @@ class ContractLine(models.Model): 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 @@ class ContractLine(models.Model): ): 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 e8c49b6f..fd669ea4 100644 --- a/contract/tests/test_contract.py +++ b/contract/tests/test_contract.py @@ -564,7 +564,7 @@ class TestContract(TestContractBase): 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 @@ class TestContract(TestContractBase): 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 fd669ea4..e406a45e 100644 --- a/contract/tests/test_contract.py +++ b/contract/tests/test_contract.py @@ -2,6 +2,7 @@ # Copyright 2018 Tecnativa - Pedro M. Baeza # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from collections import namedtuple from datetime import timedelta from dateutil.relativedelta import relativedelta from odoo import fields @@ -681,6 +682,316 @@ class TestContract(TestContractBase): 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 e8b79bfa..c9d475bd 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -667,9 +667,8 @@ class ContractLine(models.Model): # 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 d4d94691..99892d82 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 351ded65..28851130 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 00000000..31b3f179 --- /dev/null +++ b/product_contract/migrations/12.0.3.0.0/pre-migration.py @@ -0,0 +1,10 @@ +def migrate(cr, version): + # pre-paid/post-paid becomes significant for monthlylastday too, + # make sure it has the value that was implied for previous versions. + cr.execute( + """\ + UPDATE product_template + SET recurring_invoicing_type = 'post-paid' + WHERE recurring_rule_type = 'monthlylastday' + """ + )