diff --git a/base_time_window/data/time_weekday.xml b/base_time_window/data/time_weekday.xml new file mode 100644 index 000000000..6d1f14bf4 --- /dev/null +++ b/base_time_window/data/time_weekday.xml @@ -0,0 +1,34 @@ + + + + + + + 0 + + + + 1 + + + + 2 + + + + 3 + + + + 4 + + + + 5 + + + + 6 + + diff --git a/base_time_window/models/base_time_weekday.py b/base_time_window/models/base_time_weekday.py new file mode 100644 index 000000000..59ef2eda2 --- /dev/null +++ b/base_time_window/models/base_time_weekday.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models, tools + + +class DeliveryWeekDay(models.Model): + + _name = "delivery.week.day" + _description = "Delivery Week Day" + + name = fields.Selection( + selection=[ + ("0", "Monday"), + ("1", "Tuesday"), + ("2", "Wednesday"), + ("3", "Thursday"), + ("4", "Friday"), + ("5", "Saturday"), + ("6", "Sunday"), + ], + required=True, + ) + _sql_constraints = [ + ("name_uniq", "UNIQUE(name)", _("Name must be unique")) + ] + + @api.depends("name") + def _compute_display_name(self): + """ + WORKAROUND since Odoo doesn't handle properly records where name is + a selection + """ + translated_values = dict( + self._fields["name"]._description_selection(self.env) + ) + for record in self: + record.display_name = translated_values[record.name] + + @api.multi + def name_get(self): + """ + WORKAROUND since Odoo doesn't handle properly records where name is + a selection + """ + return [(r.id, r.display_name) for r in self] + + @api.model + @tools.ormcache("name") + def _get_id_by_name(self, name): + return self.search([("name", "=", name)], limit=1).id + + @api.model + def create(self, vals): + result = super(DeliveryWeekDay, self).create(vals) + self._get_id_by_name.clear_cache(self) + return result + + @api.multi + def write(self, vals): + result = super(DeliveryWeekDay, self).write(vals) + self._get_id_by_name.clear_cache(self) + return result + + @api.multi + def unlink(self): + result = super(DeliveryWeekDay, self).unlink() + self._get_id_by_name.clear_cache(self) + return result diff --git a/base_time_window/models/base_time_window.py b/base_time_window/models/base_time_window.py new file mode 100644 index 000000000..4786cfcd0 --- /dev/null +++ b/base_time_window/models/base_time_window.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import math + +from psycopg2.extensions import AsIs + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class DeliveryWindow(models.Model): + + _name = "delivery.window" + _description = "Delivery Window" + _order = "partner_id, start" + + start = fields.Float("From", required=True) + end = fields.Float("To", required=True) + week_day_ids = fields.Many2many( + comodel_name="alc.delivery.week.day", required=True + ) + partner_id = fields.Many2one( + "res.partner", required=True, index=True, ondelete='cascade' + ) + + @api.constrains("start", "end", "week_day_ids") + def check_window_no_onverlaps(self): + week_days_field = self._fields["week_day_ids"] + for record in self: + if record.start > record.end: + raise ValidationError( + _("%s must be > %s") + % ( + self.float_to_time_repr(record.end), + self.float_to_time_repr(record.start), + ) + ) + # here we use a plain SQL query to benefit of the numrange + # function available in PostgresSQL + # (http://www.postgresql.org/docs/current/static/rangetypes.html) + SQL = """ + SELECT + id + FROM + %(table)s w + join %(relation)s as d + on d.%(relation_window_fkey)s = w.id + WHERE + NUMRANGE(w.start::numeric, w.end::numeric) && + NUMRANGE(%(start)s::numeric, %(end)s::numeric) + AND w.id != %(window_id)s + AND d.%(relation_week_day_fkey)s in %(week_day_ids)s + AND w.partner_id = %(partner_id)s""" + self.env.cr.execute( + SQL, + dict( + table=AsIs(self._table), + relation=AsIs(week_days_field.relation), + relation_window_fkey=AsIs(week_days_field.column1), + relation_week_day_fkey=AsIs(week_days_field.column2), + start=record.start, + end=record.end, + window_id=record.id, + week_day_ids=tuple(record.week_day_ids.ids), + partner_id=record.partner_id.id, + ), + ) + res = self.env.cr.fetchall() + if res: + other = self.browse(res[0][0]) + raise ValidationError( + _("%s overlaps %s") + % (record.display_name, other.display_name) + ) + + @api.depends("start", "end", "week_day_ids") + def _compute_display_name(self): + for record in self: + "{days}: From {start} to {end}".format( + days=", ".join(record.week_day_ids.mapped("display_name")), + start=self.float_to_time_repr(record.start), + end=self.float_to_time_repr(record.end), + ) + + @api.model + def float_to_time_repr(self, value): + pattern = "%02d:%02d" + hour = math.floor(value) + min = round((value % 1) * 60) + if min == 60: + min = 0 + hour += 1 + + return pattern % (hour, min) diff --git a/base_time_window/security/ir.model.access.xml b/base_time_window/security/ir.model.access.xml new file mode 100644 index 000000000..a6b3fd50f --- /dev/null +++ b/base_time_window/security/ir.model.access.xml @@ -0,0 +1,35 @@ + + + + + + delivery.window.day access read + + + + + + + + + + delivery.window access read + + + + + + + + + + delivery.window access read + + + + + + + + diff --git a/base_time_window/tests/test_delivery_window.py b/base_time_window/tests/test_delivery_window.py new file mode 100644 index 000000000..0462f131e --- /dev/null +++ b/base_time_window/tests/test_delivery_window.py @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.exceptions import ValidationError +from odoo.tests import SavepointCase + + +class TestDeliveryWindow(SavepointCase): + @classmethod + def setUpClass(cls): + super(TestDeliveryWindow, cls).setUpClass() + cls.partner_1 = cls.env['res.partner'].create({'name': 'partner 1'}) + cls.partner_2 = cls.env['res.partner'].create({'name': 'patner 2'}) + cls.DeliveryWindow = cls.env["delivery.window"] + cls.monday = cls.env.ref( + "partner_delivery_window.delivery_weed_day_monday" + ) + cls.sunday = cls.env.ref( + "partner_delivery_window.delivery_weed_day_sunday" + ) + + def test_00(self): + """ + Data: + A partner without delivery window + Test Case: + Add a delivery window + Expected result: + A delivery window is created for the partner + """ + + self.assertFalse(self.partner_1.delivery_window_ids) + self.DeliveryWindow.create( + { + "partner_id": self.partner_1.id, + "start": 10.0, + "end": 12.0, + "week_day_ids": [(4, self.monday.id)], + } + ) + self.assertTrue(self.partner_1.delivery_window_ids) + delivery_window = self.partner_1.delivery_window_ids + self.assertEqual(delivery_window.start, 10.0) + self.assertEqual(delivery_window.end, 12.0) + self.assertEqual(delivery_window.week_day_ids, self.monday) + + def test_01(self): + """ + Data: + A partner without delivery window + Test Case: + 1 Add a delivery window + 2 unlink the partner + Expected result: + 1 A delivery window is created for the partner + 2 The delivery window is removed + """ + partner_id = self.partner_1.id + self.assertFalse(self.partner_1.delivery_window_ids) + self.DeliveryWindow.create( + { + "partner_id": self.partner_1.id, + "start": 10.0, + "end": 12.0, + "week_day_ids": [(4, self.monday.id)], + } + ) + self.assertTrue(self.partner_1.delivery_window_ids) + delivery_window = self.DeliveryWindow.search( + [("partner_id", "=", partner_id)] + ) + self.assertTrue(delivery_window) + self.partner_1.unlink() + self.assertFalse(delivery_window.exists()) + + def test_02(self): + """ + Data: + A partner without delivery window + Test Case: + 1 Add a delivery window + 2 Add a second delivery window that overlaps the first one (same day) + Expected result: + 1 A delivery window is created for the partner + 2 ValidationError is raised + """ + self.DeliveryWindow.create( + { + "partner_id": self.partner_1.id, + "start": 10.0, + "end": 12.0, + "week_day_ids": [(4, self.monday.id)], + } + ) + with self.assertRaises(ValidationError): + self.DeliveryWindow.create( + { + "partner_id": self.partner_1.id, + "start": 11.0, + "end": 13.0, + "week_day_ids": [(4, self.monday.id), (4, self.sunday.id)], + } + ) + + def test_03(self): + """ + Data: + A partner without delivery window + Test Case: + 1 Add a delivery window + 2 Add a second delivery window that overlaps the first one (another day) + Expected result: + 1 A delivery window is created for the partner + 2 A second delivery window is created for the partner + """ + self.assertFalse(self.partner_1.delivery_window_ids) + self.DeliveryWindow.create( + { + "partner_id": self.partner_1.id, + "start": 10.0, + "end": 12.0, + "week_day_ids": [(4, self.monday.id)], + } + ) + self.assertTrue(self.partner_1.delivery_window_ids) + self.DeliveryWindow.create( + { + "partner_id": self.partner_1.id, + "start": 11.0, + "end": 13.0, + "week_day_ids": [(4, self.sunday.id)], + } + ) + self.assertEquals(len(self.partner_1.delivery_window_ids), 2) + + def test_04(self): + """ + Data: + Partner 1 without delivery window + Partner 2 without delivery window + Test Case: + 1 Add a delivery window to partner 1 + 2 Add the same delivery window to partner 2 + Expected result: + 1 A delivery window is created for the partner 1 + 1 A delivery window is created for the partner 2 + """ + self.assertFalse(self.partner_1.delivery_window_ids) + self.DeliveryWindow.create( + { + "partner_id": self.partner_1.id, + "start": 10.0, + "end": 12.0, + "week_day_ids": [(4, self.monday.id)], + } + ) + self.assertTrue(self.partner_1.delivery_window_ids) + self.assertFalse(self.partner_2.delivery_window_ids) + self.DeliveryWindow.create( + { + "partner_id": self.partner_2.id, + "start": 10.0, + "end": 12.0, + "week_day_ids": [(4, self.monday.id)], + } + ) + self.assertTrue(self.partner_2.delivery_window_ids) + + def test_05(self): + """"" + Data: + Partner 1 without delivery window + Test Case: + Add a delivery window to partner 1 with end > start + Expected result: + ValidationError is raised + """ + with self.assertRaises(ValidationError): + self.DeliveryWindow.create( + { + "partner_id": self.partner_1.id, + "start": 14.0, + "end": 12.0, + "week_day_ids": [(4, self.monday.id)], + } + )