Skip to content

Commit

Permalink
Merge PR #1798 into 13.0
Browse files Browse the repository at this point in the history
Signed-off-by jgrandguillaume
  • Loading branch information
OCA-git-bot committed Apr 15, 2020
2 parents 8084db2 + ac84cb7 commit b20e6bf
Show file tree
Hide file tree
Showing 25 changed files with 610 additions and 0 deletions.
1 change: 1 addition & 0 deletions base_time_window/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
14 changes: 14 additions & 0 deletions base_time_window/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copyright 2020 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
{
"name": "Base Time Window",
"summary": "Base model to handle time windows",
"version": "13.0.1.0.0",
"category": "Technical Settings",
"author": "ACSONE SA/NV, Camptocamp, Odoo Community Association (OCA)",
"license": "AGPL-3",
"website": "https://github.com/OCA/server-tools",
"depends": ["base"],
"data": ["data/time_weekday.xml", "security/ir.model.access.xml"],
"installable": True,
}
26 changes: 26 additions & 0 deletions base_time_window/data/time_weekday.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2020 ACSONE SA/NV
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo noupdate="1">
<record model="time.weekday" id="time_weekday_monday">
<field name="name">0</field>
</record>
<record model="time.weekday" id="time_weekday_tuesday">
<field name="name">1</field>
</record>
<record model="time.weekday" id="time_weekday_wednesday">
<field name="name">2</field>
</record>
<record model="time.weekday" id="time_weekday_thursday">
<field name="name">3</field>
</record>
<record model="time.weekday" id="time_weekday_friday">
<field name="name">4</field>
</record>
<record model="time.weekday" id="time_weekday_saturday">
<field name="name">5</field>
</record>
<record model="time.weekday" id="time_weekday_sunday">
<field name="name">6</field>
</record>
</odoo>
2 changes: 2 additions & 0 deletions base_time_window/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import time_weekday
from . import time_window_mixin
62 changes: 62 additions & 0 deletions base_time_window/models/time_weekday.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# 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 TimeWeekday(models.Model):

_name = "time.weekday"
_description = "Time 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]

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().create(vals)
self._get_id_by_name.clear_cache(self)
return result

def write(self, vals):
result = super().write(vals)
self._get_id_by_name.clear_cache(self)
return result

def unlink(self):
result = super().unlink()
self._get_id_by_name.clear_cache(self)
return result
115 changes: 115 additions & 0 deletions base_time_window/models/time_window_mixin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Copyright 2020 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

import math
from datetime import time

from psycopg2.extensions import AsIs

from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools.misc import format_time


class TimeWindowMixin(models.AbstractModel):

_name = "time.window.mixin"
_description = "Time Window"
_order = "time_window_start"

# TODO patch api.constrains with field here?
_time_window_overlap_check_field = False

time_window_start = fields.Float("From", required=True)
time_window_end = fields.Float("To", required=True)
time_window_weekday_ids = fields.Many2many(
comodel_name="time.weekday", required=True
)

@api.constrains("time_window_start", "time_window_end", "time_window_weekday_ids")
def check_window_no_overlaps(self):
weekdays_field = self._fields["time_window_weekday_ids"]
for record in self:
if record.time_window_start > record.time_window_end:
raise ValidationError(
_("%s must be > %s")
% (
self.float_to_time_repr(record.time_window_end),
self.float_to_time_repr(record.time_window_start),
)
)
if not record.time_window_weekday_ids:
raise ValidationError(_("At least one time.weekday is required"))
# 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.time_window_start::numeric,
w.time_window_end::numeric) &&
NUMRANGE(%(start)s::numeric, %(end)s::numeric)
AND w.id != %(window_id)s
AND d.%(relation_week_day_fkey)s in %(weekday_ids)s
AND w.%(check_field)s = %(check_field_id)s;"""
self.env.cr.execute(
SQL,
dict(
table=AsIs(self._table),
relation=AsIs(weekdays_field.relation),
relation_window_fkey=AsIs(weekdays_field.column1),
relation_week_day_fkey=AsIs(weekdays_field.column2),
start=record.time_window_start,
end=record.time_window_end,
window_id=record.id,
weekday_ids=tuple(record.time_window_weekday_ids.ids),
check_field=AsIs(self._time_window_overlap_check_field),
check_field_id=record[self._time_window_overlap_check_field].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("time_window_start", "time_window_end", "time_window_weekday_ids")
def _compute_display_name(self):
for record in self:
record.display_name = _("{days}: From {start} to {end}").format(
days=", ".join(record.time_window_weekday_ids.mapped("display_name")),
start=format_time(self.env, record.get_time_window_start_time()),
end=format_time(self.env, record.get_time_window_end_time()),
)

@api.model
def _get_hour_min_from_value(self, value):
hour = math.floor(value)
minute = round((value % 1) * 60)
if minute == 60:
minute = 0
hour += 1
return hour, minute

@api.model
def float_to_time_repr(self, value):
pattern = "%02d:%02d"
hour, minute = self._get_hour_min_from_value(value)
return pattern % (hour, minute)

@api.model
def float_to_time(self, value):
hour, minute = self._get_hour_min_from_value(value)
return time(hour=hour, minute=minute)

def get_time_window_start_time(self):
return self.float_to_time(self.time_window_start)

def get_time_window_end_time(self):
return self.float_to_time(self.time_window_end)
2 changes: 2 additions & 0 deletions base_time_window/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
* Laurent Mignon <laurent.mignon@acsone.eu>
* Akim Juillerat <akim.juillerat@camptocamp.com>
2 changes: 2 additions & 0 deletions base_time_window/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
This module provides base classes and models to manage time windows through
`time.window.mixin`.
11 changes: 11 additions & 0 deletions base_time_window/readme/ROADMAP.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
* Storing times using `float_time` widget requires extra processing to ensure
computations are done in the right timezone, because the value is not stored
as UTC in the database, and must therefore be related to a `tz` field.

`float_time` in this sense should only be used for durations and not for a
"point in time" as this is always needs a Date for a timezone conversion to
be done properly. (Because a conversion from UTC to e.g. Europe/Brussels won't
give the same result in winter or summer because of Daylight Saving Time).

Therefore the right move would be to use a `resource.calendar` to define time
windows using Datetime with recurrences.
30 changes: 30 additions & 0 deletions base_time_window/readme/USAGE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
Example implementation for the mixin can be found in module `test_base_time_window`.

As a time window will always be linked to a related model thourgh a M2o relation,
when defining the new model inheriting the mixin, one should pay attention to the
following points in order to have the overlapping check work properly:

- Define class property `_overlap_check_field`: This must state the M2o field to
use for the to check of overlapping time window records linked to a specific
record of the related model.

- Add the M2o field to the related model in the `api.constrains`:


For example:

.. code-block:: python
class PartnerTimeWindow(models.Model):
_name = 'partner.time.window'
_inherit = 'time.window.mixin'
partner_id = fields.Many2one(
res.partner', required=True, index=True, ondelete='cascade'
)
_overlap_check_field = 'partner_id'
@api.constrains('partner_id')
def check_window_no_overlaps(self):
return super().check_window_no_overlaps()
14 changes: 14 additions & 0 deletions base_time_window/security/ir.model.access.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2020 ACSONE SA/NV
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record model="ir.model.access" id="time_weekday_access_read">
<field name="name">time.weekday access read</field>
<field name="model_id" ref="model_time_weekday" />
<field name="group_id" ref="base.group_user" />
<field name="perm_read" eval="1" />
<field name="perm_create" eval="0" />
<field name="perm_write" eval="0" />
<field name="perm_unlink" eval="0" />
</record>
</odoo>
1 change: 1 addition & 0 deletions setup/base_time_window/odoo/addons/base_time_window
6 changes: 6 additions & 0 deletions setup/base_time_window/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import setuptools

setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)
6 changes: 6 additions & 0 deletions setup/test_base_time_window/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import setuptools

setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)
1 change: 1 addition & 0 deletions test_base_time_window/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
14 changes: 14 additions & 0 deletions test_base_time_window/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copyright 2020 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
{
"name": "Test Base Time Window",
"summary": "Test Base model to handle time windows",
"version": "13.0.1.0.0",
"category": "Technical Settings",
"author": "ACSONE SA/NV, Camptocamp, Odoo Community Association (OCA)",
"license": "AGPL-3",
"website": "https://github.com/OCA/server-tools",
"depends": ["base_time_window"],
"data": ["security/ir.model.access.xml"],
"installable": True,
}
2 changes: 2 additions & 0 deletions test_base_time_window/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import partner_time_window
from . import res_partner
20 changes: 20 additions & 0 deletions test_base_time_window/models/partner_time_window.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copyright 2020 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models


class TestPartnerTimeWindow(models.Model):

_name = "test.partner.time.window"
_inherit = "time.window.mixin"
_description = "Test partner time Window"

_time_window_overlap_check_field = "partner_id"

partner_id = fields.Many2one(
"res.partner", required=True, index=True, ondelete="cascade"
)

@api.constrains("partner_id")
def check_window_no_overlaps(self):
return super().check_window_no_overlaps()
36 changes: 36 additions & 0 deletions test_base_time_window/models/res_partner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Copyright 2020 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from collections import defaultdict

from odoo import fields, models


class ResPartner(models.Model):

_inherit = "res.partner"

time_window_ids = fields.One2many(
comodel_name="test.partner.time.window",
inverse_name="partner_id",
string="Time windows",
)

def get_delivery_windows(self, day_name):
"""
Return the list of delivery windows by partner id for the given day
:param day: The day name (see time.weekday, ex: 0,1,2,...)
:return: dict partner_id:[time_window_ids, ]
"""
weekday_id = self.env["time.weekday"]._get_id_by_name(day_name)
res = defaultdict(list)
windows = self.env["test.partner.time.window"].search(
[
("partner_id", "in", self.ids),
("time_window_weekday_ids", "in", weekday_id),
]
)
for window in windows:
res[window.partner_id.id].append(window)
return res
2 changes: 2 additions & 0 deletions test_base_time_window/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
* Laurent Mignon <laurent.mignon@acsone.eu>
* Akim Juillerat <akim.juillerat@camptocamp.com>
1 change: 1 addition & 0 deletions test_base_time_window/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This module provides unittests for module `base_time_window`.
Loading

0 comments on commit b20e6bf

Please sign in to comment.