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/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/abstract_contract_line.py b/contract/models/abstract_contract_line.py index c1320208..99892d82 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 start 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 3178f240..c9d475bd 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", @@ -361,20 +369,140 @@ class ContractLine(models.Model): 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, ): - if recurring_rule_type == 'monthlylastday': - return 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 + next_period_date_end = self.get_next_period_date_end( + next_period_date_start, + recurring_rule_type, + recurring_interval, + max_date_end=max_date_end, ) + if not next_period_date_end: + return False + 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=recurring_invoicing_offset) + ) + return recurring_next_date @api.model - def compute_first_date_end( + def get_next_period_date_end( + self, + next_period_date_start, + 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. + + 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: + # start is past max date end: there is no next period + return False + if not next_invoice_date: + # regular algorithm + 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_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=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 + 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_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_start, + 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 + def _get_first_date_end( self, date_start, auto_renew_rule_type, auto_renew_interval ): return ( @@ -396,7 +524,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, @@ -404,17 +532,20 @@ class ContractLine(models.Model): @api.onchange( 'date_start', + 'date_end', 'recurring_invoicing_type', 'recurring_rule_type', 'recurring_interval', ) 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_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, ) @api.constrains('is_canceled', 'is_auto_renew') @@ -533,33 +664,25 @@ 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: - return first_date_invoiced, last_date_invoiced, recurring_next_date + return False, False, False first_date_invoiced = ( last_date_invoiced + relativedelta(days=1) 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_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 @api.multi @@ -580,23 +703,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): @@ -609,8 +728,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 = ( @@ -618,12 +738,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': @@ -631,7 +757,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) @@ -651,15 +777,23 @@ class ContractLine(models.Model): ) ) new_date_start = rec.date_start + delay_delta - rec.recurring_next_date = self._compute_first_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_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 ) - 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): @@ -712,11 +846,13 @@ 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_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, ) new_vals = self.read()[0] new_vals.pop("id", None) @@ -1023,7 +1159,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..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 @@ -247,7 +248,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 +280,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 +538,34 @@ 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_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 { @@ -547,6 +575,7 @@ class TestContract(TestContractBase): recurring_rule_type, # ('daily', 'weekly', 'monthly', # 'monthlylastday', 'yearly'), recurring_interval, # integer + max_date_end, # date ), } """ @@ -554,64 +583,415 @@ 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 " % ( - recurring_invoicing_type, - recurring_interval, - recurring_rule_type, - date_start, + 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', 2), + (to_date('2018-01-01'), 'pre-paid', 0, 'monthly', 1, + to_date('2018-01-15')), + ), + ( + False, + (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', 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', 1, 'monthly', 1, + to_date('2018-01-15')), + ), + ( + False, + (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', 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), ), ] contract_line_env = self.env['contract.line'] for recurring_next_date, combination in combinations: self.assertEqual( recurring_next_date, - contract_line_env._compute_first_recurring_next_date( + contract_line_env.get_next_invoice_date( *combination ), 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() @@ -1331,7 +1711,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' @@ -1362,6 +1742,67 @@ 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.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( + 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.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( + 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.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' 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..28851130 100644 --- a/contract/views/abstract_contract_line.xml +++ b/contract/views/abstract_contract_line.xml @@ -59,8 +59,8 @@ - + + 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 @@ + + 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/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' + """ + ) 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 @@ - +