Initial commit: Odoo 18.0-20251222 extra-addons
This commit is contained in:
2
base_time_window/models/__init__.py
Executable file
2
base_time_window/models/__init__.py
Executable file
@@ -0,0 +1,2 @@
|
||||
from . import time_weekday
|
||||
from . import time_window_mixin
|
||||
81
base_time_window/models/time_weekday.py
Executable file
81
base_time_window/models/time_weekday.py
Executable file
@@ -0,0 +1,81 @@
|
||||
# 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]
|
||||
|
||||
@api.model
|
||||
@tools.ormcache("name")
|
||||
def _get_id_by_name(self, name):
|
||||
return self.search([("name", "=", name)], limit=1).id
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
records = super().create(vals_list)
|
||||
self.env.registry.clear_cache()
|
||||
return records
|
||||
|
||||
def write(self, vals):
|
||||
result = super().write(vals)
|
||||
self.env.registry.clear_cache()
|
||||
return result
|
||||
|
||||
def unlink(self):
|
||||
result = super().unlink()
|
||||
self.env.registry.clear_cache()
|
||||
return result
|
||||
|
||||
def _get_next_weekday_date(self, date_from=False, include_date_from=True):
|
||||
"""Returns the next Date matching weekday
|
||||
|
||||
:param date_from: Date object from which we start searching for next weekday
|
||||
:param include_date_from: Allows to return date from if it's same weekday
|
||||
:return Date object matching the weekday
|
||||
"""
|
||||
self.ensure_one()
|
||||
if not date_from:
|
||||
date_from = fields.Date.today()
|
||||
next_weekday_delta = int(self.name) - date_from.weekday()
|
||||
if next_weekday_delta < 0:
|
||||
next_weekday_delta += 7
|
||||
elif next_weekday_delta == 0 and not include_date_from:
|
||||
next_weekday_delta += 7
|
||||
return fields.Date.add(date_from, days=next_weekday_delta)
|
||||
|
||||
def _get_next_weekdays_date(self, date_from=False, include_date_from=True):
|
||||
return min(
|
||||
[
|
||||
weekday._get_next_weekday_date(
|
||||
date_from=date_from, include_date_from=include_date_from
|
||||
)
|
||||
for weekday in self
|
||||
]
|
||||
)
|
||||
133
base_time_window/models/time_window_mixin.py
Executable file
133
base_time_window/models/time_window_mixin.py
Executable file
@@ -0,0 +1,133 @@
|
||||
# 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(
|
||||
self.env._(
|
||||
"%(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(
|
||||
self.env._("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(
|
||||
self.env._(
|
||||
"%(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 = self.env._("{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 = self.env._("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)
|
||||
Reference in New Issue
Block a user