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)],
+ }
+ )