server-tools/base_time_window/models/time_window_mixin.py

138 lines
5.4 KiB
Python

# 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(
_("%(end_time)s must be > %(start_time)s")
% (
{
"end_time": self.float_to_time_repr(record.time_window_end),
"start_time": 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(
_("%(record_name)s overlaps %(other_name)s")
% (
{
"record_name": record.display_name,
"other_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.constrains("time_window_start", "time_window_end")
def _check_window_under_twenty_four_hours(self):
error_msg = _("Hour should be between 00 and 23")
for record in self:
if record.time_window_start:
hour, minute = self._get_hour_min_from_value(record.time_window_start)
if hour > 23:
raise ValidationError(error_msg)
if record.time_window_end:
hour, minute = self._get_hour_min_from_value(record.time_window_end)
if hour > 23:
raise ValidationError(error_msg)
@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)