diff --git a/contract/__manifest__.py b/contract/__manifest__.py index 274ab7326..f698f180c 100644 --- a/contract/__manifest__.py +++ b/contract/__manifest__.py @@ -4,7 +4,7 @@ # Copyright 2016-2018 Tecnativa - Carlos Dauden # Copyright 2017 Tecnativa - Vicent Cubells # Copyright 2016-2017 LasLabs Inc. -# Copyright 2018 ACSONE SA/NV +# Copyright 2018-2019 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { diff --git a/contract/migrations/12.0.5.0.0/pre-migration.py b/contract/migrations/12.0.5.0.0/pre-migration.py new file mode 100644 index 000000000..ca208e9dd --- /dev/null +++ b/contract/migrations/12.0.5.0.0/pre-migration.py @@ -0,0 +1,10 @@ +def migrate(cr, version): + # pre-paid/post-paid becomes significant for monthlylastday too, + # make sure it has the value that was implied for previous versions. + cr.execute( + """\ + UPDATE contract_line + SET recurring_invoicing_type = 'post-paid' + WHERE recurring_rule_type = 'monthlylastday' + """ + ) diff --git a/contract/models/abstract_contract_line.py b/contract/models/abstract_contract_line.py index c1320208d..d4d946914 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 b1fa39137..c8aa46854 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -364,42 +364,74 @@ class ContractLine(models.Model): ) @api.model - def _get_recurring_next_date( + 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_next_invoice_date( + date_start, + recurring_invoicing_type, + self._get_default_recurring_invoicing_offset( + recurring_invoicing_type, recurring_rule_type + ), + recurring_rule_type, + recurring_interval, + max_date_end=False, + ) + + @api.model + def get_next_invoice_date( self, next_period_date_start, recurring_invoicing_type, + recurring_invoicing_offset, recurring_rule_type, recurring_interval, max_date_end, ): - 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_rule_type, recurring_interval, max_date_end=max_date_end, ) 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 + if recurring_invoicing_type == 'pre-paid': + recurring_next_date = ( + next_period_date_start + + relativedelta(days=recurring_invoicing_offset) + ) else: # post-paid - recurring_next_date = next_period_date_end + relativedelta(days=1) + recurring_next_date = ( + next_period_date_end + + relativedelta(days=recurring_invoicing_offset) + ) 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_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: @@ -407,36 +439,28 @@ 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': - next_period_date_end = next_invoice_date - elif recurring_invoicing_type == 'pre-paid': + if recurring_invoicing_type == 'pre-paid': next_period_date_end = ( next_invoice_date + - relativedelta(days=recurring_invoicing_offset) + self.get_relative_delta( recurring_rule_type, recurring_interval ) - relativedelta(days=1) ) else: # post-paid - next_period_date_end = next_invoice_date - relativedelta( - days=1 + 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 @@ -459,19 +483,22 @@ class ContractLine(models.Model): @api.depends( 'next_period_date_start', 'recurring_invoicing_type', + 'recurring_invoicing_offset', 'recurring_rule_type', 'recurring_interval', 'date_end', + 'recurring_next_date', ) def _compute_next_period_date_end(self): for rec in self: - rec.next_period_date_end = self._get_next_period_date_end( + rec.next_period_date_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, + recurring_invoicing_type=rec.recurring_invoicing_type, + recurring_invoicing_offset=rec.recurring_invoicing_offset, ) @api.model @@ -512,9 +539,10 @@ 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, rec.recurring_rule_type, rec.recurring_interval, max_date_end=rec.date_end, @@ -647,13 +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_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 @@ -675,23 +704,19 @@ class ContractLine(models.Model): @api.multi def _update_recurring_next_date(self): for rec in self: - old_date = rec.recurring_next_date - new_date = old_date + self.get_relative_delta( - rec.recurring_rule_type, rec.recurring_interval + last_date_invoiced = rec.next_period_date_end + recurring_next_date = rec.get_next_invoice_date( + last_date_invoiced + relativedelta(days=1), + rec.recurring_invoicing_type, + rec.recurring_invoicing_offset, + rec.recurring_rule_type, + rec.recurring_interval, + max_date_end=rec.date_end, ) - if rec.recurring_rule_type == 'monthlylastday': - last_date_invoiced = old_date - elif rec.recurring_invoicing_type == 'post-paid': - last_date_invoiced = old_date - relativedelta(days=1) - elif rec.recurring_invoicing_type == 'pre-paid': - last_date_invoiced = new_date - relativedelta(days=1) - - if rec.date_end and last_date_invoiced >= rec.date_end: - rec.last_date_invoiced = rec.date_end - rec.recurring_next_date = False - else: - rec.last_date_invoiced = last_date_invoiced - rec.recurring_next_date = new_date + rec.write({ + "recurring_next_date": recurring_next_date, + "last_date_invoiced": last_date_invoiced, + }) @api.multi def _init_last_date_invoiced(self): @@ -704,8 +729,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 = ( @@ -713,12 +739,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': @@ -726,7 +758,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) @@ -750,9 +782,10 @@ 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, rec.recurring_rule_type, rec.recurring_interval, max_date_end=new_date_end @@ -814,9 +847,10 @@ 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, 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 5a73c63e4..2e39d8375 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') @@ -537,7 +537,34 @@ class TestContract(TestContractBase): 'There was an error and the view couldn\'t be opened.', ) - def test_get_recurring_next_date(self): + def test_get_default_recurring_invoicing_offset(self): + clm = self.env['contract.line'] + self.assertEqual( + clm._get_default_recurring_invoicing_offset( + "pre-paid", "monthly" + ), + 0 + ) + self.assertEqual( + clm._get_default_recurring_invoicing_offset( + "post-paid", "monthly" + ), + 1 + ) + self.assertEqual( + clm._get_default_recurring_invoicing_offset( + "pre-paid", "monthlylastday" + ), + 0 + ) + self.assertEqual( + clm._get_default_recurring_invoicing_offset( + "post-paid", "monthlylastday" + ), + 0 + ) + + def test_get_next_invoice_date(self): """Test different combination to compute recurring_next_date Combination format { @@ -555,82 +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-31'), - (to_date('2018-01-06'), 'pre-paid', 'monthlylastday', 1, + to_date('2018-01-06'), + (to_date('2018-01-06'), 'pre-paid', 0, 'monthlylastday', 1, False), ), ( to_date('2018-02-28'), - (to_date('2018-01-05'), 'pre-paid', 'monthlylastday', 2, + (to_date('2018-01-05'), 'post-paid', 0, 'monthlylastday', 2, False), ), ( to_date('2018-01-05'), - (to_date('2018-01-05'), 'pre-paid', 'yearly', 1, + (to_date('2018-01-05'), 'pre-paid', 0, 'monthlylastday', 2, + False), + ), + ( + to_date('2018-01-05'), + (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), ), ] @@ -638,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), @@ -1363,7 +1400,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 +1431,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 b66077eeb..351ded65f 100644 --- a/contract/views/abstract_contract_line.xml +++ b/contract/views/abstract_contract_line.xml @@ -59,8 +59,7 @@ - +