diff --git a/date_range/__manifest__.py b/date_range/__manifest__.py index d5309e0b0..9076cd663 100644 --- a/date_range/__manifest__.py +++ b/date_range/__manifest__.py @@ -4,7 +4,7 @@ { "name": "Date Range", "summary": "Manage all kind of date range", - "version": "10.0.2.0.1", + "version": "10.0.3.0.0", "category": "Uncategorized", "website": "https://odoo-community.org/", "author": "ACSONE SA/NV, Odoo Community Association (OCA)", diff --git a/date_range/models/date_range.py b/date_range/models/date_range.py index 29da5b600..55cdedb19 100644 --- a/date_range/models/date_range.py +++ b/date_range/models/date_range.py @@ -31,6 +31,12 @@ class DateRange(models.Model): active = fields.Boolean( help="The active field allows you to hide the date range without " "removing it.", default=True) + parent_type_id = fields.Many2one( + related='type_id.parent_type_id', + store=True, + readonly=True) + parent_id = fields.Many2one( + comodel_name='date.range', string="Parent", index=1) _sql_constraints = [ ('date_range_uniq', 'unique (name,type_id, company_id)', @@ -53,6 +59,45 @@ class DateRange(models.Model): _('The Company in the Date Range and in ' 'Date Range Type must be the same.')) + @api.constrains('parent_id', 'date_start', 'date_end') + def _validate_child_range(self): + for this in self: + if not this.parent_id: + continue + start = this.parent_id.date_start <= this.date_start + end = this.parent_id.date_end >= this.date_end + child_range = start and end + if not child_range: + text_dict = { + 'name': this.name, + 'start': this.date_start, + 'end': this.date_end, + 'parent_name': this.parent_id.name, + 'parent_start': this.parent_id.date_start, + 'parent_end': this.parent_id.date_end, + } + if (not start) and end: + text = _( + "Start date %(start)s of %(name)s must be greater than" + " or equal to " + "start date %(parent_start)s of %(parent_name)s" + ) % text_dict + elif (not end) and start: + text = _( + "End date %(end)s of %(name)s must be smaller than" + " or equal to " + "end date %(parent_end)s of %(parent_name)s" + ) % text_dict + else: + text = _( + "%(name)s range not in " + "%(parent_start)s - %(parent_end)s" + ) % text_dict + raise ValidationError( + _("%(name)s not a subrange of" + " %(parent_name)s: " % text_dict) + text + ) + @api.constrains('type_id', 'date_start', 'date_end', 'company_id') def _validate_range(self): for this in self: @@ -95,3 +140,29 @@ class DateRange(models.Model): self.ensure_one() return [(field_name, '>=', self.date_start), (field_name, '<=', self.date_end)] + + @api.multi + @api.onchange('company_id', 'type_id', 'date_start', 'date_end') + def onchange_type_id(self): + """The type_id and the dates determine the choices for parent.""" + domain = [] + if self.company_id: + domain.append(('company_id', '=', self.company_id.id)) + if self.parent_type_id: + domain.append(('type_id', '=', self.parent_type_id.id)) + if self.date_start: + domain.append('|') + domain.append(('date_start', '<=', self.date_start)) + domain.append(('date_start', '=', False)) + if self.date_end: + domain.append('|') + domain.append(('date_end', '>=', self.date_end)) + domain.append(('date_end', '=', False)) + if domain: + # If user did not select a parent already, autoselect the last + # (ordered by date_start) or only parent that applies. + if self.type_id and self.date_start and not self.parent_id: + possible_parent = self.search( + domain, limit=1, order='date_start desc') + self.parent_id = possible_parent # can be empty! + return {'domain': {'parent_id': domain}} diff --git a/date_range/models/date_range_type.py b/date_range/models/date_range_type.py index 3d14ff467..6d83ab848 100644 --- a/date_range/models/date_range_type.py +++ b/date_range/models/date_range_type.py @@ -26,6 +26,9 @@ class DateRangeType(models.Model): comodel_name='res.company', string='Company', index=1, default=_default_company) date_range_ids = fields.One2many('date.range', 'type_id', string='Ranges') + parent_type_id = fields.Many2one( + comodel_name='date.range.type', + index=1) _sql_constraints = [ ('date_range_type_uniq', 'unique (name,company_id)', @@ -44,3 +47,15 @@ class DateRangeType(models.Model): _('You cannot change the company, as this ' 'Date Range Type is assigned to Date Range ' '(%s).') % (rec.date_range_ids.name_get()[0][1])) + + @api.constrains('parent_type_id') + def _validate_parent_type_id(self): + for record in self: + parent = record + while parent: + if not parent.parent_type_id: + break + if parent.parent_type_id == record: + raise ValidationError( + _("A type can not have itself as parent or child")) + parent = parent.parent_type_id diff --git a/date_range/readme/CONTRIBUTORS.rst b/date_range/readme/CONTRIBUTORS.rst index 38b10b316..72039de31 100644 --- a/date_range/readme/CONTRIBUTORS.rst +++ b/date_range/readme/CONTRIBUTORS.rst @@ -2,3 +2,4 @@ * Alexis de Lattre * Miquel Raïch * Andrea Stirpe +* Nikos Tsirintanis diff --git a/date_range/readme/HISTORY.rst b/date_range/readme/HISTORY.rst index 9dcf0ba1d..4b5578a13 100644 --- a/date_range/readme/HISTORY.rst +++ b/date_range/readme/HISTORY.rst @@ -1,3 +1,9 @@ +10.0.3.0.0 (2019-11-20) +~~~~~~~~~~~~~~~~~~~~~~~ + +* [IMP] Added hierarchical feature for date_range. + (`#1380 `_) + 10.0.2.0.1 (2018-11-19) ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/date_range/tests/test_date_range.py b/date_range/tests/test_date_range.py index 1b774b6f2..9babe6a1d 100644 --- a/date_range/tests/test_date_range.py +++ b/date_range/tests/test_date_range.py @@ -129,3 +129,142 @@ class DateRangeTest(TransactionCase): 'type_id': self.typeB.id, 'company_id': self.company_2.id, }) + + def test_parent_id(self): + date_range_type = self.env['date.range.type'] + parent = self.date_range.create({ + 'name': 'FS2018', + 'date_start': '2018-01-01', + 'date_end': '2018-12-31', + 'type_id': self.type.id, + }) + type_block = date_range_type.create({ + 'name': 'FS2018-type_block', + 'company_id': False, + 'allow_overlap': False, + }) + # Check here all three validation errors thrown + # when child range is not a subrange of parent + with self.assertRaises(ValidationError) as cm, self.env.cr.savepoint(): + self.date_range.create({ + 'name': 'FS2018-period1', + 'date_start': '2018-06-06', + 'date_end': '2019-01-02', + 'type_id': type_block.id, + 'parent_id': parent.id, + }) + self.assertEqual( + cm.exception.name, + 'FS2018-period1 not a subrange of FS2018: ' + 'End date 2019-01-02 of FS2018-period1 must be ' + 'smaller than or equal to end date 2018-12-31 of FS2018' + ) + with self.assertRaises(ValidationError) as cm, self.env.cr.savepoint(): + self.date_range.create({ + 'name': 'FS2018-period1', + 'date_start': '2017-06-06', + 'date_end': '2018-01-02', + 'type_id': type_block.id, + 'parent_id': parent.id, + }) + self.assertEqual( + cm.exception.name, + 'FS2018-period1 not a subrange of FS2018: ' + 'Start date 2017-06-06 of FS2018-period1 must be ' + 'greater than or equal to start date 2018-01-01 of FS2018' + ) + with self.assertRaises(ValidationError) as cm, self.env.cr.savepoint(): + self.date_range.create({ + 'name': 'FS2018-period1', + 'date_start': '2017-06-06', + 'date_end': '2019-01-02', + 'type_id': type_block.id, + 'parent_id': parent.id, + }) + self.assertEqual( + cm.exception.name, + 'FS2018-period1 not a subrange of FS2018: ' + 'FS2018-period1 range not in 2018-01-01 - 2018-12-31' + ) + self.date_range.create({ + 'name': 'FS2018-period1', + 'date_start': '2018-01-01', + 'date_end': '2018-04-30', + 'type_id': type_block.id, + 'parent_id': parent.id, + }) + # Check here that a validation error is thrown + # when child periods overlap + with self.assertRaises(ValidationError) as cm, self.env.cr.savepoint(): + self.date_range.create({ + 'name': 'FS2018-period2', + 'date_start': '2018-02-01', + 'date_end': '2018-03-15', + 'type_id': type_block.id, + 'parent_id': parent.id, + }) + self.assertEqual( + cm.exception.name, + 'FS2018-period2 overlaps FS2018-period1' + ) + + def test_parent_type_id(self): + """Check domain and constraint between parent and child types""" + date_range_type = self.env['date.range.type'] + # First create a parent type + parent_type = date_range_type.create({ + 'name': 'FS2018_parent_type', + 'parent_type_id': False, + }) + # catch here the validation error when assigning + # a child as its own parent, or vise-versa (predestination) + with self.assertRaises(ValidationError)as cm, self.env.cr.savepoint(): + parent_type.write({ + 'parent_type_id': parent_type.id, + }) + self.assertEqual( + cm.exception.name, + 'A type can not have itself as parent or child' + ) + # Then, add a child type + child_type = date_range_type.create({ + 'name': 'FS2018_child_type', + 'parent_type_id': parent_type.id, + }) + # Now create a parent range + parent_range = self.date_range.create({ + 'name': 'FS2018', + 'date_start': '2018-01-01', + 'date_end': '2018-12-31', + 'type_id': parent_type.id, + }) + # and two child ranges + child_range1 = self.date_range.create({ + 'name': 'FS2018-child1', + 'date_start': '2018-01-01', + 'date_end': '2018-04-30', + 'type_id': child_type.id, + 'parent_id': parent_range.id, + }) + child_range2 = self.date_range.create({ + 'name': 'FS2018-child2', + 'date_start': '2018-05-01', + 'date_end': '2018-06-30', + 'type_id': child_type.id, + 'parent_id': parent_range.id, + }) + # and check how parent_type_id behaves + self.assertEqual(parent_type, child_range1.parent_type_id) + self.assertEqual( + child_range1.parent_type_id, + child_range2.parent_type_id + ) + # Ensure here that parent and children are of different type + self.assertNotEqual( + parent_range.type_id, + child_range1.type_id + ) + self.assertEqual( + parent_range.type_id, + child_range2.parent_id.type_id + ) diff --git a/date_range/tests/test_date_range_generator.py b/date_range/tests/test_date_range_generator.py index 7764fd3e3..2ee643b6e 100644 --- a/date_range/tests/test_date_range_generator.py +++ b/date_range/tests/test_date_range_generator.py @@ -72,3 +72,41 @@ class DateRangeGeneratorTest(TransactionCase): 'count': 4, 'company_id': self.company_2.id, }) + + def test_generator_partner_id_domain(self): + """Check here domain returned for partner_id + in both date.range and date.range.generator""" + date_range = self.env['date.range'] + generator = self.env['date.range.generator'] + date_type = self.env['date.range.type'] + month_type = date_type.create({ + 'name': 'month type' + }) + day_type = date_type.create({ + 'name': 'day type', + 'parent_type_id': month_type.id, + }) + month_range = date_range.create({ + 'name': 'month range', + 'type_id': month_type.id, + 'date_start': '01-01-2050', + 'date_end': '02-01-2050', + }) + # now trigger onchange in generator, + # which would also trigger onchange in date_range + values = { + 'date_start': month_range.date_start, + 'type_id': day_type.id, + } + on_change = generator._onchange_spec() + domain = generator.onchange( + values, + ['type_id', 'date_start'], + on_change, + ) + # check that with this search domain, + # only the month_range record is returned. + self.assertEqual( + date_range.search(domain['domain']['parent_id']), + month_range, + ) diff --git a/date_range/views/date_range_view.xml b/date_range/views/date_range_view.xml index 97e627e6a..a965dc2a8 100644 --- a/date_range/views/date_range_view.xml +++ b/date_range/views/date_range_view.xml @@ -10,6 +10,11 @@ + + @@ -25,6 +30,11 @@ + + @@ -38,6 +48,7 @@ + @@ -51,6 +62,7 @@ + diff --git a/date_range/wizard/date_range_generator.py b/date_range/wizard/date_range_generator.py index 534263e0c..a32f42ea9 100644 --- a/date_range/wizard/date_range_generator.py +++ b/date_range/wizard/date_range_generator.py @@ -38,6 +38,8 @@ class DateRangeGenerator(models.TransientModel): duration_count = fields.Integer('Duration', required=True) count = fields.Integer( string="Number of ranges to generate", required=True) + parent_id = fields.Many2one( + comodel_name='date.range', string="Parent", index=1) @api.multi def _compute_date_ranges(self): @@ -60,7 +62,8 @@ class DateRangeGenerator(models.TransientModel): 'date_start': date_start, 'date_end': date_end, 'type_id': self.type_id.id, - 'company_id': self.company_id.id}) + 'company_id': self.company_id.id, + 'parent_id': self.parent_id.id}) return date_ranges @api.onchange('company_id') @@ -88,3 +91,20 @@ class DateRangeGenerator(models.TransientModel): self.env['date.range'].create(dr) return self.env['ir.actions.act_window'].for_xml_id( module='date_range', xml_id='date_range_action') + + @api.multi + @api.onchange('type_id', 'date_start') + def onchange_type_id(self): + self.ensure_one() + date_range = self.env['date.range'] + values = { + 'date_start': self.date_start, + 'type_id': self.type_id.id, + } + on_change = date_range._onchange_spec() + domain = date_range.onchange( + values, + ['type_id', 'date_start'], + on_change, + ) + return domain diff --git a/date_range/wizard/date_range_generator.xml b/date_range/wizard/date_range_generator.xml index 73551151b..aea993b12 100644 --- a/date_range/wizard/date_range_generator.xml +++ b/date_range/wizard/date_range_generator.xml @@ -15,6 +15,7 @@ +