diff --git a/contract/data/contract_line_constraints.py b/contract/data/contract_line_constraints.py index 74e3eb2f..94b5c8b9 100644 --- a/contract/data/contract_line_constraints.py +++ b/contract/data/contract_line_constraints.py @@ -6,7 +6,14 @@ from odoo.fields import Date CRITERIA = namedtuple( 'CRITERIA', - ['WHEN', 'HAS_DATE_END', 'IS_AUTO_RENEW', 'HAS_SUCCESSOR', 'CANCELED'], + [ + 'WHEN', + 'HAS_DATE_END', + 'IS_AUTO_RENEW', + 'HAS_SUCCESSOR', + 'PREDECESSOR_HAS_SUCCESSOR', + 'CANCELED', + ], ) ALLOWED = namedtuple( 'ALLOWED', @@ -19,6 +26,7 @@ CRITERIA_ALLOWED_DICT = { HAS_DATE_END=True, IS_AUTO_RENEW=True, HAS_SUCCESSOR=False, + PREDECESSOR_HAS_SUCCESSOR=None, CANCELED=False, ): ALLOWED( PLAN_SUCCESSOR=False, @@ -32,6 +40,7 @@ CRITERIA_ALLOWED_DICT = { HAS_DATE_END=True, IS_AUTO_RENEW=False, HAS_SUCCESSOR=True, + PREDECESSOR_HAS_SUCCESSOR=None, CANCELED=False, ): ALLOWED( PLAN_SUCCESSOR=False, @@ -45,6 +54,7 @@ CRITERIA_ALLOWED_DICT = { HAS_DATE_END=True, IS_AUTO_RENEW=False, HAS_SUCCESSOR=False, + PREDECESSOR_HAS_SUCCESSOR=None, CANCELED=False, ): ALLOWED( PLAN_SUCCESSOR=True, @@ -58,6 +68,7 @@ CRITERIA_ALLOWED_DICT = { HAS_DATE_END=False, IS_AUTO_RENEW=False, HAS_SUCCESSOR=False, + PREDECESSOR_HAS_SUCCESSOR=None, CANCELED=False, ): ALLOWED( PLAN_SUCCESSOR=False, @@ -71,6 +82,7 @@ CRITERIA_ALLOWED_DICT = { HAS_DATE_END=True, IS_AUTO_RENEW=True, HAS_SUCCESSOR=False, + PREDECESSOR_HAS_SUCCESSOR=None, CANCELED=False, ): ALLOWED( PLAN_SUCCESSOR=False, @@ -84,6 +96,7 @@ CRITERIA_ALLOWED_DICT = { HAS_DATE_END=True, IS_AUTO_RENEW=False, HAS_SUCCESSOR=True, + PREDECESSOR_HAS_SUCCESSOR=None, CANCELED=False, ): ALLOWED( PLAN_SUCCESSOR=False, @@ -97,6 +110,7 @@ CRITERIA_ALLOWED_DICT = { HAS_DATE_END=True, IS_AUTO_RENEW=False, HAS_SUCCESSOR=False, + PREDECESSOR_HAS_SUCCESSOR=None, CANCELED=False, ): ALLOWED( PLAN_SUCCESSOR=True, @@ -110,6 +124,7 @@ CRITERIA_ALLOWED_DICT = { HAS_DATE_END=False, IS_AUTO_RENEW=False, HAS_SUCCESSOR=False, + PREDECESSOR_HAS_SUCCESSOR=None, CANCELED=False, ): ALLOWED( PLAN_SUCCESSOR=False, @@ -123,6 +138,7 @@ CRITERIA_ALLOWED_DICT = { HAS_DATE_END=True, IS_AUTO_RENEW=True, HAS_SUCCESSOR=False, + PREDECESSOR_HAS_SUCCESSOR=None, CANCELED=False, ): ALLOWED( PLAN_SUCCESSOR=False, @@ -136,6 +152,7 @@ CRITERIA_ALLOWED_DICT = { HAS_DATE_END=True, IS_AUTO_RENEW=False, HAS_SUCCESSOR=True, + PREDECESSOR_HAS_SUCCESSOR=None, CANCELED=False, ): ALLOWED( PLAN_SUCCESSOR=False, @@ -149,6 +166,7 @@ CRITERIA_ALLOWED_DICT = { HAS_DATE_END=True, IS_AUTO_RENEW=False, HAS_SUCCESSOR=False, + PREDECESSOR_HAS_SUCCESSOR=None, CANCELED=False, ): ALLOWED( PLAN_SUCCESSOR=True, @@ -162,6 +180,7 @@ CRITERIA_ALLOWED_DICT = { HAS_DATE_END=None, IS_AUTO_RENEW=None, HAS_SUCCESSOR=None, + PREDECESSOR_HAS_SUCCESSOR=False, CANCELED=True, ): ALLOWED( PLAN_SUCCESSOR=False, @@ -170,6 +189,20 @@ CRITERIA_ALLOWED_DICT = { CANCEL=False, UN_CANCEL=True, ), + CRITERIA( + WHEN=None, + HAS_DATE_END=None, + IS_AUTO_RENEW=None, + HAS_SUCCESSOR=None, + PREDECESSOR_HAS_SUCCESSOR=True, + CANCELED=True, + ): ALLOWED( + PLAN_SUCCESSOR=False, + STOP_PLAN_SUCCESSOR=False, + STOP=False, + CANCEL=False, + UN_CANCEL=False, + ), } @@ -187,16 +220,31 @@ def compute_criteria( date_end, is_auto_renew, successor_contract_line_id, + predecessor_contract_line_id, is_canceled, ): if is_canceled: - return CRITERIA( - WHEN=None, - HAS_DATE_END=None, - IS_AUTO_RENEW=None, - HAS_SUCCESSOR=None, - CANCELED=True, - ) + if ( + not predecessor_contract_line_id + or not predecessor_contract_line_id.successor_contract_line_id + ): + return CRITERIA( + WHEN=None, + HAS_DATE_END=None, + IS_AUTO_RENEW=None, + HAS_SUCCESSOR=None, + PREDECESSOR_HAS_SUCCESSOR=False, + CANCELED=True, + ) + else: + return CRITERIA( + WHEN=None, + HAS_DATE_END=None, + IS_AUTO_RENEW=None, + HAS_SUCCESSOR=None, + PREDECESSOR_HAS_SUCCESSOR=True, + CANCELED=True, + ) when = compute_when(date_start, date_end) has_date_end = date_end if not date_end else True is_auto_renew = is_auto_renew @@ -207,6 +255,7 @@ def compute_criteria( HAS_DATE_END=has_date_end, IS_AUTO_RENEW=is_auto_renew, HAS_SUCCESSOR=has_successor, + PREDECESSOR_HAS_SUCCESSOR=None, CANCELED=canceled, ) @@ -216,6 +265,7 @@ def get_allowed( date_end, is_auto_renew, successor_contract_line_id, + predecessor_contract_line_id, is_canceled, ): criteria = compute_criteria( @@ -223,6 +273,7 @@ def get_allowed( date_end, is_auto_renew, successor_contract_line_id, + predecessor_contract_line_id, is_canceled, ) if criteria in CRITERIA_ALLOWED_DICT: diff --git a/contract/models/contract_line.py b/contract/models/contract_line.py index 9d96ac4e..6d4cb7ed 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -1,6 +1,7 @@ # Copyright 2017 LasLabs Inc. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from datetime import timedelta from dateutil.relativedelta import relativedelta from odoo import api, fields, models, _ @@ -91,25 +92,28 @@ class AccountAnalyticInvoiceLine(models.Model): 'date_end', 'is_auto_renew', 'successor_contract_line_id', + 'predecessor_contract_line_id', 'is_canceled', ) def _compute_allowed(self): for rec in self: - allowed = get_allowed( - rec.date_start, - rec.date_end, - rec.is_auto_renew, - rec.successor_contract_line_id, - rec.is_canceled, - ) - if allowed: - rec.is_plan_successor_allowed = allowed.PLAN_SUCCESSOR - rec.is_stop_plan_successor_allowed = ( - allowed.STOP_PLAN_SUCCESSOR + if rec.date_start: + allowed = get_allowed( + rec.date_start, + rec.date_end, + rec.is_auto_renew, + rec.successor_contract_line_id, + rec.predecessor_contract_line_id, + rec.is_canceled, ) - rec.is_stop_allowed = allowed.STOP - rec.is_cancel_allowed = allowed.CANCEL - rec.is_un_cancel_allowed = allowed.UN_CANCEL + if allowed: + rec.is_plan_successor_allowed = allowed.PLAN_SUCCESSOR + rec.is_stop_plan_successor_allowed = ( + allowed.STOP_PLAN_SUCCESSOR + ) + rec.is_stop_allowed = allowed.STOP + rec.is_cancel_allowed = allowed.CANCEL + rec.is_un_cancel_allowed = allowed.UN_CANCEL @api.constrains('is_auto_renew', 'successor_contract_line_id', 'date_end') def _check_allowed(self): @@ -185,8 +189,12 @@ class AccountAnalyticInvoiceLine(models.Model): """Date end should be auto-computed if a contract line is set to auto_renew""" for rec in self.filtered('is_auto_renew'): - rec.date_end = self.date_start + self.get_relative_delta( - rec.auto_renew_rule_type, rec.auto_renew_interval + rec.date_end = ( + self.date_start + + self.get_relative_delta( + rec.auto_renew_rule_type, rec.auto_renew_interval + ) + - relativedelta(days=1) ) @api.onchange( @@ -255,22 +263,24 @@ class AccountAnalyticInvoiceLine(models.Model): def _compute_create_invoice_visibility(self): today = fields.Date.today() for line in self: - if today < line.date_start: - line.create_invoice_visibility = False - elif not line.date_end: - line.create_invoice_visibility = True - elif line.recurring_next_date: - if line.recurring_invoicing_type == 'pre-paid': - line.create_invoice_visibility = ( - line.recurring_next_date <= line.date_end - ) - else: - line.create_invoice_visibility = ( - line.recurring_next_date - - line.get_relative_delta( - line.recurring_rule_type, line.recurring_interval + if line.date_start: + if today < line.date_start: + line.create_invoice_visibility = False + elif not line.date_end: + line.create_invoice_visibility = True + elif line.recurring_next_date: + if line.recurring_invoicing_type == 'pre-paid': + line.create_invoice_visibility = ( + line.recurring_next_date <= line.date_end ) - ) <= line.date_end + else: + line.create_invoice_visibility = ( + line.recurring_next_date + - line.get_relative_delta( + line.recurring_rule_type, + line.recurring_interval, + ) + ) <= line.date_end @api.model def recurring_create_invoice(self, contract=False): @@ -577,9 +587,9 @@ class AccountAnalyticInvoiceLine(models.Model): for rec in self: if rec.date_start >= date_start: if rec.date_start < date_end: - delay = date_end - rec.date_start + delay = (date_end - rec.date_start) + timedelta(days=1) else: - delay = date_end - date_start + delay = (date_end - date_start) + timedelta(days=1) rec.delay(delay) contract_line |= rec else: @@ -626,6 +636,9 @@ class AccountAnalyticInvoiceLine(models.Model): ) ) contract.message_post(body=msg) + self.mapped('predecessor_contract_line_id').write( + {'successor_contract_line_id': False} + ) return self.write({'is_canceled': True}) @api.multi @@ -644,9 +657,14 @@ class AccountAnalyticInvoiceLine(models.Model): ) ) contract.message_post(body=msg) - return self.write( - {'is_canceled': False, 'recurring_next_date': recurring_next_date} - ) + for rec in self: + if rec.predecessor_contract_line_id: + rec.predecessor_contract_line_id.successor_contract_line_id = ( + rec + ) + rec.is_canceled = False + rec.recurring_next_date = recurring_next_date + return True @api.multi def action_uncancel(self): @@ -739,9 +757,13 @@ class AccountAnalyticInvoiceLine(models.Model): @api.multi def _get_renewal_dates(self): self.ensure_one() - date_start = self.date_end - date_end = date_start + self.get_relative_delta( - self.auto_renew_rule_type, self.auto_renew_interval + date_start = self.date_end + relativedelta(days=1) + date_end = ( + date_start + + self.get_relative_delta( + self.auto_renew_rule_type, self.auto_renew_interval + ) + - relativedelta(days=1) ) return date_start, date_end diff --git a/contract/tests/test_contract.py b/contract/tests/test_contract.py index 283e5c85..2dea7bc2 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 datetime import timedelta from dateutil.relativedelta import relativedelta from odoo import fields from odoo.exceptions import ValidationError @@ -828,10 +829,11 @@ class TestContract(TestContractBase): ) self.assertEqual( self.acct_line.date_start, - start_date + (suspension_end - start_date), + start_date + (suspension_end - start_date) + timedelta(days=1), ) self.assertEqual( - self.acct_line.date_end, end_date + (suspension_end - start_date) + self.acct_line.date_end, + end_date + (suspension_end - start_date) + timedelta(days=1), ) new_line = self.env['account.analytic.invoice.line'].search( [('predecessor_contract_line_id', '=', self.acct_line.id)] @@ -860,10 +862,11 @@ class TestContract(TestContractBase): ) self.assertEqual( self.acct_line.date_start, - start_date + (suspension_end - start_date), + start_date + (suspension_end - start_date) + timedelta(days=1), ) self.assertEqual( - self.acct_line.date_end, end_date + (suspension_end - start_date) + self.acct_line.date_end, + end_date + (suspension_end - start_date) + timedelta(days=1), ) new_line = self.env['account.analytic.invoice.line'].search( [('predecessor_contract_line_id', '=', self.acct_line.id)] @@ -893,7 +896,7 @@ class TestContract(TestContractBase): ) self.assertEqual( self.acct_line.date_start, - start_date + (suspension_end - start_date), + start_date + (suspension_end - start_date) + timedelta(days=1), ) self.assertFalse(self.acct_line.date_end) new_line = self.env['account.analytic.invoice.line'].search( @@ -923,11 +926,13 @@ class TestContract(TestContractBase): ) self.assertEqual( self.acct_line.date_start, - start_date + (suspension_end - suspension_start), + start_date + + (suspension_end - suspension_start) + + timedelta(days=1), ) self.assertEqual( self.acct_line.date_end, - end_date + (suspension_end - suspension_start), + end_date + (suspension_end - suspension_start) + timedelta(days=1), ) new_line = self.env['account.analytic.invoice.line'].search( [('predecessor_contract_line_id', '=', self.acct_line.id)] @@ -957,7 +962,9 @@ class TestContract(TestContractBase): ) self.assertEqual( self.acct_line.date_start, - start_date + (suspension_end - suspension_start), + start_date + + (suspension_end - suspension_start) + + timedelta(days=1), ) self.assertFalse(self.acct_line.date_end) new_line = self.env['account.analytic.invoice.line'].search( @@ -988,11 +995,13 @@ class TestContract(TestContractBase): wizard.stop_plan_successor() self.assertEqual( self.acct_line.date_start, - start_date + (suspension_end - suspension_start), + start_date + + (suspension_end - suspension_start) + + timedelta(days=1), ) self.assertEqual( self.acct_line.date_end, - end_date + (suspension_end - suspension_start), + end_date + (suspension_end - suspension_start) + timedelta(days=1), ) new_line = self.env['account.analytic.invoice.line'].search( [('predecessor_contract_line_id', '=', self.acct_line.id)] @@ -1087,6 +1096,62 @@ class TestContract(TestContractBase): self.acct_line.uncancel(fields.Date.today()) self.assertFalse(self.acct_line.is_canceled) + def test_cancel_uncancel_with_predecessor(self): + suspension_start = fields.Date.today() + relativedelta(months=3) + suspension_end = fields.Date.today() + relativedelta(months=5) + start_date = fields.Date.today() + end_date = fields.Date.today() + relativedelta(months=4) + self.acct_line.write( + { + 'date_start': start_date, + 'recurring_next_date': start_date, + 'date_end': end_date, + } + ) + self.acct_line.stop_plan_successor( + suspension_start, suspension_end, True + ) + self.assertEqual(self.acct_line.date_end, suspension_start) + new_line = self.env['account.analytic.invoice.line'].search( + [('predecessor_contract_line_id', '=', self.acct_line.id)] + ) + self.assertEqual(self.acct_line.successor_contract_line_id, new_line) + new_line.cancel() + self.assertTrue(new_line.is_canceled) + self.assertFalse(self.acct_line.successor_contract_line_id) + self.assertEqual(new_line.predecessor_contract_line_id, self.acct_line) + new_line.uncancel(suspension_end) + self.assertFalse(new_line.is_canceled) + self.assertEqual(self.acct_line.successor_contract_line_id, new_line) + self.assertEqual(new_line.recurring_next_date, suspension_end) + + def test_cancel_uncancel_with_predecessor_has_successor(self): + suspension_start = fields.Date.today() + relativedelta(months=6) + suspension_end = fields.Date.today() + relativedelta(months=7) + start_date = fields.Date.today() + end_date = fields.Date.today() + relativedelta(months=8) + self.acct_line.write( + { + 'date_start': start_date, + 'recurring_next_date': start_date, + 'date_end': end_date, + } + ) + self.acct_line.stop_plan_successor( + suspension_start, suspension_end, True + ) + new_line = self.env['account.analytic.invoice.line'].search( + [('predecessor_contract_line_id', '=', self.acct_line.id)] + ) + new_line.cancel() + suspension_start = fields.Date.today() + relativedelta(months=4) + suspension_end = fields.Date.today() + relativedelta(months=5) + self.acct_line.stop_plan_successor( + suspension_start, suspension_end, True + ) + with self.assertRaises(ValidationError): + new_line.uncancel(suspension_end) + def test_check_has_not_date_end_has_successor(self): self.acct_line.write({'date_end': False, 'is_auto_renew': False}) with self.assertRaises(ValidationError): @@ -1126,8 +1191,10 @@ class TestContract(TestContractBase): ) def test_renew(self): + self.acct_line._onchange_is_auto_renew() + self.assertEqual(self.acct_line.date_end, to_date('2018-12-31')) new_line = self.acct_line.renew() self.assertFalse(self.acct_line.is_auto_renew) self.assertTrue(new_line.is_auto_renew) self.assertEqual(new_line.date_start, to_date('2019-01-01')) - self.assertEqual(new_line.date_end, to_date('2020-01-01')) + self.assertEqual(new_line.date_end, to_date('2019-12-31')) diff --git a/contract/views/contract_line.xml b/contract/views/contract_line.xml index 96875c9f..e2356306 100644 --- a/contract/views/contract_line.xml +++ b/contract/views/contract_line.xml @@ -131,6 +131,7 @@