Initial commit: OCA Mrp packages (117 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:05 +02:00
commit 277e84fd7a
4403 changed files with 395154 additions and 0 deletions

View file

@ -0,0 +1,8 @@
from . import event_event
from . import event_mail_registration
from . import event_mail_session
from . import event_mail
from . import event_registration
from . import event_session
from . import event_session_timeslot
from . import event_type

View file

@ -0,0 +1,140 @@
# Copyright 2017 David Vidal<david.vidal@tecnativa.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class EventEvent(models.Model):
_inherit = "event.event"
use_sessions = fields.Boolean(
string="Event Sessions",
help="Manage multiple sessions per event",
compute="_compute_use_sessions",
store=True,
readonly=False,
)
session_ids = fields.One2many(
comodel_name="event.session",
inverse_name="event_id",
string="Sessions",
)
session_count = fields.Integer(
string="Sessions Count",
compute="_compute_session_count",
)
date_begin = fields.Datetime(
compute="_compute_date_begin",
store=True,
readonly=False,
)
date_end = fields.Datetime(
compute="_compute_date_end",
store=True,
readonly=False,
)
@api.depends("event_type_id")
def _compute_use_sessions(self):
for rec in self:
rec.use_sessions = rec.event_type_id.use_sessions
@api.onchange("use_sessions")
def _onchange_use_sessions(self):
"""
Automatically fill date_begin and date_end if it's a use_session event.
These fields are required but computed from sessions anyway.
"""
if self.use_sessions and not self.date_begin:
self.date_begin = fields.Datetime.now()
if self.use_sessions and not self.date_end:
self.date_end = fields.Datetime.now()
@api.depends("session_ids")
def _compute_session_count(self):
groups = self.env["event.session"].read_group(
domain=[("event_id", "in", self.ids)],
fields=["event_id"],
groupby=["event_id"],
)
result = {g["event_id"][0]: g["event_id_count"] for g in groups}
for rec in self:
rec.session_count = result.get(rec.id, 0)
@api.depends("use_sessions", "session_ids.date_begin")
def _compute_date_begin(self):
session_records = self.filtered("use_sessions")
regular_records = self - session_records
# This is a core field. Play nice with other modules.
# It is also why we compute date_begin and date_end separately.
if hasattr(super(), "_compute_date_begin"): # pragma: no cover
super(EventEvent, regular_records)._compute_date_begin()
if not session_records: # pragma: no cover
return
groups = self.env["event.session"].read_group(
domain=[("event_id", "in", session_records.ids)],
fields=["event_id", "date_begin:min"],
groupby=["event_id"],
)
data = {d["event_id"][0]: d["date_begin"] for d in groups}
for rec in session_records:
if data.get(rec.id):
rec.date_begin = data.get(rec.id)
@api.depends("use_sessions", "session_ids.date_end")
def _compute_date_end(self):
session_records = self.filtered("use_sessions")
regular_records = self - session_records
# This is a core field. Play nice with other modules.
# It is also why we compute date_begin and date_end separately.
if hasattr(super(), "_compute_date_end"): # pragma: no cover
super(EventEvent, regular_records)._compute_date_end()
if not session_records: # pragma: no cover
return
groups = self.env["event.session"].read_group(
domain=[("event_id", "in", session_records.ids)],
fields=["event_id", "date_end:max"],
groupby=["event_id"],
)
data = {d["event_id"][0]: d["date_end"] for d in groups}
for rec in session_records:
if data.get(rec.id):
rec.date_end = data.get(rec.id)
def _check_seats_availability(self, minimal_availability=0): # pragma: no cover
# OVERRIDE to ignore this constraint for event with sessions
# Seat availability is checked on each session, not here.
session_records = self.filtered("use_sessions")
regular_records = self - session_records
return super(EventEvent, regular_records)._check_seats_availability(
minimal_availability=minimal_availability
)
@api.model_create_multi
def create(self, vals_list):
# OVERRIDE to automatically fill date_begin and date_end if they're
# missing and it's a use_session event.
# These fields are required but computed from sessions anyway.
for vals in vals_list:
if vals.get("use_sessions"):
vals["date_begin"] = fields.Datetime.now()
vals["date_end"] = fields.Datetime.now()
return super().create(vals_list)
def write(self, vals):
# OVERRIDE to prevent the switch of use_sessions if the event has registrations
# and to automatically subscribe the organizer to sessions, if it changes.
if "use_sessions" in vals:
if any(
rec.use_sessions != vals["use_sessions"] and rec.registration_ids
for rec in self
):
raise ValidationError(
_("You can't enable/disable sessions on events with registrations.")
)
if not vals["use_sessions"]:
self.with_context(active_test=False).session_ids.unlink()
if vals.get("organizer_id"):
self.session_ids.message_subscribe([vals["organizer_id"]])
return super().write(vals)

View file

@ -0,0 +1,59 @@
# Copyright 2017 David Vidal<david.vidal@tecnativa.com>
# Copyright 2021 Moka Tourisme (https://www.mokatourisme.fr).
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import logging
from odoo import api, fields, models
_logger = logging.getLogger(__name__)
class EventMail(models.Model):
_inherit = "event.mail"
use_sessions = fields.Boolean(
related="event_id.use_sessions",
)
session_scheduler_ids = fields.One2many(
comodel_name="event.mail.session",
inverse_name="scheduler_id",
string="Session Mails",
)
@api.depends("event_id.use_sessions")
def _compute_scheduled_date(self):
# OVERRIDE to handle event session mail schedulers.
# We set scheduled_date to False because it doesn't make sense for sessions,
# as we use them only as "templates" to be copied/synced to the sessions as
# `event.mail.session` records. Their scheduled_dates are then computed from
# the dates of the related session.
# By doing it, we get the additional benefit of having them automatically
# ignored by the scheduled_date domain leaf of the core's mail scheduler cron.
session_records = self.filtered("use_sessions")
session_records.scheduled_date = False
regular_records = self - session_records
return super(EventMail, regular_records)._compute_scheduled_date()
@api.model
def schedule_communications(self, autocommit=False):
# OVERRIDE to also process session mail schedulers
res = super().schedule_communications(autocommit=autocommit)
self.env["event.mail.session"].schedule_communications(autocommit=autocommit)
return res
def execute(self): # pragma: no cover
# OVERRIDE. Just in case, prevent execution of schedulers linked to event.event
# that are using sessions. They manage that through event.mail.session.
# This should never happen because they always have scheduled_date = False.
session_records = self.filtered("use_sessions")
regular_records = self - session_records
if session_records: # pragma: no cover
_logger.error("Trying to execute event.mail linked to a session event.")
return super(EventMail, regular_records).execute()
def _prepare_session_mail_scheduler_vals(self, session):
return {
"scheduler_id": self.id,
"session_id": session.id,
}

View file

@ -0,0 +1,35 @@
# Copyright 2021 Moka Tourisme (https://www.mokatourisme.fr).
# @author Iván Todorovich <ivan.todorovich@gmail.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
from odoo.addons.event.models.event_mail import _INTERVALS
class EventMailRegistration(models.Model):
_inherit = "event.mail.registration"
session_scheduler_id = fields.Many2one(
comodel_name="event.mail.session",
string="Session Mail",
ondelete="cascade",
)
@api.depends(
"session_scheduler_id.interval_unit",
"session_scheduler_id.interval_type",
)
def _compute_scheduled_date(self):
# OVERRIDE to handle session mail registrations
session_records = self.filtered("session_scheduler_id")
regular_records = self - session_records
for rec in session_records:
if rec.registration_id:
date_open = rec.registration_id.create_date or fields.Datetime.now()
scheduler = rec.session_scheduler_id
delta = _INTERVALS[scheduler.interval_unit](scheduler.interval_nbr)
rec.scheduled_date = date_open + delta
else: # pragma: no cover
rec.scheduled_date = False
return super(EventMailRegistration, regular_records)._compute_scheduled_date()

View file

@ -0,0 +1,164 @@
# Copyright 2021 Moka Tourisme (https://www.mokatourisme.fr).
# @author Iván Todorovich <ivan.todorovich@gmail.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import logging
import threading
from odoo import api, fields, models
from odoo.addons.event.models.event_mail import _INTERVALS
_logger = logging.getLogger(__name__)
class EventMailSession(models.Model):
_name = "event.mail.session"
_inherits = {"event.mail": "scheduler_id"}
_description = "Event Session Automated Mailing"
scheduler_id = fields.Many2one(
comodel_name="event.mail",
string="Event Mail Scheduler",
ondelete="cascade",
auto_join=True,
required=True,
)
session_id = fields.Many2one(
comodel_name="event.session",
string="Session",
ondelete="cascade",
required=True,
)
mail_registration_ids = fields.One2many(
comodel_name="event.mail.registration",
inverse_name="session_scheduler_id",
)
scheduled_date = fields.Datetime(
compute="_compute_scheduled_date",
store=True,
)
mail_done = fields.Boolean("Sent", copy=False, readonly=True)
mail_count_done = fields.Integer("# Sent", copy=False, readonly=True)
@api.depends(
"session_id",
"session_id.date_begin",
"session_id.date_end",
"scheduler_id",
"interval_type",
"interval_unit",
"interval_nbr",
)
def _compute_scheduled_date(self):
"""
Similar to core's :meth:`event.models.event_mail._compute_scheduled_date`,
only here we take values from the `event.session` instead.
"""
for scheduler in self:
if scheduler.interval_type == "after_sub":
date, sign = scheduler.session_id.create_date, 1
elif scheduler.interval_type == "before_event":
date, sign = scheduler.session_id.date_begin, -1
else:
date, sign = scheduler.session_id.date_end, 1
delta = _INTERVALS[scheduler.interval_unit](sign * scheduler.interval_nbr)
scheduler.scheduled_date = date + delta if date else False
def _get_new_event_registrations(self):
registrations = self.session_id.registration_ids.filtered_domain(
[("state", "not in", ("cancel", "draft"))]
)
return registrations - self.mail_registration_ids.registration_id
def _prepare_mail_registration_vals(self, registration):
self.ensure_one()
return {
"registration_id": registration.id,
"scheduler_id": self.scheduler_id.id,
"session_scheduler_id": self.id,
}
def _create_missing_mail_registrations(self, registrations):
vals_list = []
for scheduler in self:
vals_list += [
scheduler._prepare_mail_registration_vals(registration)
for registration in registrations
]
if vals_list:
return self.env["event.mail.registration"].create(vals_list)
return self.env["event.mail.registration"]
def execute(self):
"""
Similar to core's :meth:`event.models.event_mail.execute`, only here we
take values from the `event.session` instead.
"""
for scheduler in self:
now = fields.Datetime.now()
if scheduler.interval_type == "after_sub":
new_registrations = self._get_new_event_registrations()
scheduler._create_missing_mail_registrations(new_registrations)
# execute scheduler on registrations
scheduler.mail_registration_ids.execute()
total_sent = len(
scheduler.mail_registration_ids.filtered(lambda reg: reg.mail_sent)
)
scheduler.update(
{
"mail_done": total_sent
>= (
scheduler.session_id.seats_reserved
+ scheduler.session_id.seats_used
),
"mail_count_done": total_sent,
}
)
else:
# before or after event -> one shot email
if scheduler.mail_done or scheduler.notification_type != "mail":
continue # pragma: no cover
# no template -> ill configured, skip and avoid crash
if not scheduler.template_ref: # pragma: no cover
continue
# do not send emails if the mailing was scheduled before the event
# but the event is over
if scheduler.scheduled_date <= now and (
scheduler.interval_type != "before_event"
or scheduler.session_id.date_end > now
):
scheduler.session_id.mail_attendees(scheduler.template_ref.id)
scheduler.update(
{
"mail_done": True,
"mail_count_done": scheduler.session_id.seats_reserved
+ scheduler.session_id.seats_used,
}
)
return True
@api.model
def schedule_communications(self, autocommit=False):
"""
Similar to core's :meth:`event.models.event_mail.schedule_communications`.
"""
schedulers = self.search(
[("mail_done", "=", False), ("scheduled_date", "<=", fields.Datetime.now())]
)
for scheduler in schedulers:
try:
# Prevent a mega prefetch of the registration ids of all the events
# of all the schedulers
self.browse(scheduler.id).execute()
except Exception as e: # pragma: no cover
_logger.exception(e)
self.invalidate_cache()
self.env["event.mail"]._warn_template_error(scheduler, e)
else:
if autocommit and not getattr(
threading.currentThread(), "testing", False
): # pragma: no cover
self.env.cr.commit() # pylint: disable=invalid-commit
return True

View file

@ -0,0 +1,80 @@
# Copyright 2017 David Vidal<david.vidal@tecnativa.com>
# Copyright 2021 Moka Tourisme (https://www.mokatourisme.fr).
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import SUPERUSER_ID, _, api, fields, models
from odoo.exceptions import ValidationError
class EventRegistration(models.Model):
_inherit = "event.registration"
use_sessions = fields.Boolean(
related="event_id.use_sessions",
)
session_id = fields.Many2one(
comodel_name="event.session",
string="Session",
ondelete="restrict",
)
# NOTE: Originally these fields are related to event_id.
# We make them computed to get the date from the session if needed.
event_begin_date = fields.Datetime(
related=None, compute="_compute_event_begin_date"
)
event_end_date = fields.Datetime(related=None, compute="_compute_event_end_date")
@api.depends("event_id.date_begin", "session_id.date_begin", "use_sessions")
def _compute_event_begin_date(self):
for rec in self:
if rec.use_sessions:
rec.event_begin_date = rec.session_id.date_begin
else:
rec.event_begin_date = rec.event_id.date_begin
@api.depends("event_id.date_end", "session_id.date_end", "use_sessions")
def _compute_event_end_date(self):
for rec in self:
if rec.use_sessions:
rec.event_end_date = rec.session_id.date_end
else:
rec.event_end_date = rec.event_id.date_end
@api.constrains("session_id")
def _check_seats_limit(self):
# Needed to check if the registration can be created
# when we try to save it.
session_records = self.filtered("session_id")
for rec in session_records:
session = rec.session_id
if (
session.seats_limited
and session.seats_max
and session.seats_available < (1 if rec.state == "draft" else 0)
):
raise ValidationError(_("No more seats available for this session."))
def _update_mail_schedulers(self):
# OVERRIDE to handle sessions' mail scheduler, not event ones.
session_records = self.filtered("session_id")
regular_records = self - session_records
res = super(EventRegistration, regular_records)._update_mail_schedulers()
# Similar to super, only we find the schedulers linked to the session
open_registrations = session_records.filtered(lambda r: r.state == "open")
if not open_registrations:
return res
onsubscribe_schedulers = (
self.env["event.mail.session"]
.sudo()
.search(
[
("session_id", "in", open_registrations.session_id.ids),
("interval_type", "=", "after_sub"),
]
)
)
if not onsubscribe_schedulers:
return res
onsubscribe_schedulers.mail_done = False
onsubscribe_schedulers.with_user(SUPERUSER_ID).execute()
return res

View file

@ -0,0 +1,550 @@
# Copyright 2017 David Vidal<david.vidal@tecnativa.com>
# Copyright 2017 Tecnativa - Pedro M. Baeza
# Copyright 2021 Moka Tourisme (https://www.mokatourisme.fr).
# @author Iván Todorovich <ivan.todorovich@gmail.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from collections import defaultdict
import pytz
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools import format_datetime
from odoo.addons.event.models.event_event import vobject
class EventSession(models.Model):
_name = "event.session"
_inherits = {"event.event": "event_id"}
_inherit = ["mail.thread", "mail.activity.mixin"]
_description = "Event session"
_order = "date_begin"
active = fields.Boolean(
default=True,
)
event_id = fields.Many2one(
comodel_name="event.event",
string="Parent Event",
domain=[("use_sessions", "=", True)],
ondelete="cascade",
auto_join=True,
index=True,
required=True,
)
date_begin = fields.Datetime(
string="Start Date",
required=True,
)
date_end = fields.Datetime(
string="End Date",
required=True,
)
date_begin_located = fields.Char(
string="Start Date Located",
compute="_compute_date_begin_located",
)
date_end_located = fields.Char(
string="End Date Located",
compute="_compute_date_end_located",
)
is_ongoing = fields.Boolean(
compute="_compute_is_ongoing",
search="_search_is_ongoing",
)
is_finished = fields.Boolean(
compute="_compute_is_finished",
search="_search_is_finished",
)
is_one_day = fields.Boolean(
compute="_compute_is_one_day",
)
registration_ids = fields.One2many(
comodel_name="event.registration",
inverse_name="session_id",
string="Attendees",
)
seats_reserved = fields.Integer(
string="Reserved Seats",
compute="_compute_seats",
store=True,
)
seats_available = fields.Integer(
string="Available Seats",
compute="_compute_seats_available",
store=True,
)
seats_unconfirmed = fields.Integer(
string="Unconfirmed Seat Reservations",
compute="_compute_seats",
store=True,
)
seats_used = fields.Integer(
string="Number of Participants",
compute="_compute_seats",
store=True,
)
seats_expected = fields.Integer(
string="Number of Expected Attendees",
compute="_compute_seats_expected",
compute_sudo=True,
)
seats_available_unexpected = fields.Integer(
string="Number of seats non allocated by an attendee of any kind",
compute="_compute_seats_available_unexpected",
compute_sudo=True,
)
event_registrations_open = fields.Boolean(
string="Registration open",
compute="_compute_event_registrations_open",
compute_sudo=True,
)
event_registrations_sold_out = fields.Boolean(
string="Sold Out",
compute="_compute_event_registrations_sold_out",
compute_sudo=True,
)
event_mail_ids = fields.One2many(
comodel_name="event.mail.session",
inverse_name="session_id",
string="Mail Schedule",
compute="_compute_event_mail_ids",
store=True,
)
stage_id = fields.Many2one(
comodel_name="event.stage",
default=lambda self: self.env["event.event"]._get_default_stage_id(),
group_expand="_read_group_stage_ids",
tracking=True,
copy=False,
ondelete="restrict",
)
kanban_state = fields.Selection(
selection=lambda self: self.env["event.event"]
._fields["kanban_state"]
.selection,
default="normal",
copy=False,
)
kanban_state_label = fields.Char(
compute="_compute_kanban_state_label",
store=True,
tracking=True,
)
session_update = fields.Selection(
[
("this", "This session"),
("subsequent", "This and following event sessions"),
("all", "All event sessions"),
],
help="Choose what to do with other event sessions",
default="this",
store=False,
)
session_update_message = fields.Text(
compute="_compute_session_update_message",
)
def onchange(self, values, field_name, field_onchange):
# OVERRIDE to workaround this issue: https://github.com/odoo/odoo/pull/91373
# This can/should be removed if a FIX is merged on odoo core
first_call = not field_name
res = super().onchange(values, field_name, field_onchange)
if (
first_call
and "default_event_id" in self.env.context
and "event_id" in res["value"]
and not res["value"]["event_id"]
):
res["value"]["event_id"] = (
self.env["event.event"]
.browse(self.env.context["default_event_id"])
.name_get()[0]
)
return res
@api.depends("stage_id", "kanban_state")
def _compute_kanban_state_label(self):
for event in self:
if event.kanban_state == "normal":
event.kanban_state_label = event.stage_id.legend_normal
elif event.kanban_state == "blocked":
event.kanban_state_label = event.stage_id.legend_blocked
else:
event.kanban_state_label = event.stage_id.legend_done
@api.depends("date_begin_located", "date_tz")
def _compute_display_name(self):
with_event_name = self.env.context.get("with_event_name", True)
for rec in self:
name = f"{rec.event_id.name}, " if with_event_name else ""
name += rec.date_begin_located
if rec.date_tz != self.env.user.tz:
name += f" ({rec.date_tz})"
rec.display_name = name
def name_get(self):
return [(rec.id, rec.display_name) for rec in self]
@api.model
def _map_registration_state_to_seats_fields(self):
return {
"draft": "seats_unconfirmed",
"open": "seats_reserved",
"done": "seats_used",
}
@api.depends("seats_max", "registration_ids.state")
def _compute_seats(self):
"""Determine reserved, available, reserved but unconfirmed and used seats."""
# Aggregate registrations by session and by state
state_field = self._map_registration_state_to_seats_fields()
results = defaultdict(lambda: defaultdict(lambda: 0))
if self.ids:
query = """
SELECT session_id, state, count(session_id)
FROM event_registration
WHERE session_id IN %s
AND state IN %s
GROUP BY session_id, state
"""
self.env["event.registration"].flush_model(
["session_id", "state", "active"]
)
self.env.cr.execute(query, (tuple(self.ids), tuple(state_field.keys())))
for session_id, state, num in self.env.cr.fetchall():
results[session_id][state_field[state]] = num
# Compute seats
for rec in self:
rec.update(
{
fname: results[rec._origin.id or rec.id][fname]
for fname in state_field.values()
}
)
@api.depends("seats_unconfirmed", "seats_reserved", "seats_used", "seats_max")
def _compute_seats_available(self):
for rec in self:
rec.seats_available = (
rec.seats_max - (rec.seats_reserved + rec.seats_used)
if rec.seats_max > 0
else 0
)
@api.depends("seats_unconfirmed", "seats_reserved", "seats_used")
def _compute_seats_expected(self):
for rec in self:
rec.seats_expected = (
rec.seats_unconfirmed + rec.seats_reserved + rec.seats_used
)
@api.depends("seats_max", "seats_expected")
def _compute_seats_available_unexpected(self):
"""How many non allocated free seats we've got?"""
for rec in self:
rec.seats_available_unexpected = rec.seats_max - rec.seats_expected
@api.depends("date_tz", "date_begin")
def _compute_date_begin_located(self):
for rec in self:
if rec.date_begin:
rec.date_begin_located = format_datetime(
self.env,
rec.date_begin,
tz=rec.date_tz,
dt_format="medium",
)
else: # pragma: no cover
rec.date_begin_located = False
@api.depends("date_tz", "date_end")
def _compute_date_end_located(self):
for rec in self:
if rec.date_end:
rec.date_end_located = format_datetime(
self.env,
rec.date_end,
tz=rec.date_tz,
dt_format="medium",
)
else: # pragma: no cover
rec.date_end_located = False
def _set_tz_context(self):
"""Similar to core's :meth:`event_event._set_tz_context`"""
return self.with_context(**self.event_id._set_tz_context().env.context)
@api.depends("date_begin", "date_end")
def _compute_is_ongoing(self):
"""Similar to core's :meth:`event_event._compute_is_ongoing`"""
now = fields.Datetime.now()
for rec in self:
rec.is_ongoing = rec.date_begin <= now < rec.date_end
def _search_is_ongoing(self, operator, value):
"""Similar to core's :meth:`event_event._search_is_ongoing`"""
if operator not in ["=", "!="]: # pragma: no cover
raise ValueError(_("This operator is not supported"))
if not isinstance(value, bool): # pragma: no cover
raise ValueError(_("Value should be True or False (not %s)", value))
now = fields.Datetime.now()
if (operator == "=" and value) or (operator == "!=" and not value):
domain = [("date_begin", "<=", now), ("date_end", ">", now)]
else:
domain = ["|", ("date_begin", ">", now), ("date_end", "<=", now)]
return domain
@api.depends("date_begin", "date_end", "date_tz")
def _compute_is_one_day(self):
"""Similar to core's :meth:`event_event._compute_is_one_day`"""
for rec in self:
rec = rec._set_tz_context()
begin_tz = fields.Datetime.context_timestamp(rec, rec.date_begin)
end_tz = fields.Datetime.context_timestamp(rec, rec.date_end)
rec.is_one_day = begin_tz.date() == end_tz.date()
@api.depends("date_end")
def _compute_is_finished(self):
"""Similar to core's :meth:`event_event._compute_is_finished`"""
now = fields.Datetime.now()
for rec in self:
rec.is_finished = rec.date_end and rec.date_end <= now
def _search_is_finished(self, operator, value):
"""Similar to core's :meth:`event_event._search_is_finished`"""
if operator not in ["=", "!="]: # pragma: no cover
raise ValueError(_("This operator is not supported"))
if not isinstance(value, bool): # pragma: no cover
raise ValueError(_("Value should be True or False (not %s)", value))
now = fields.Datetime.now()
if (operator == "=" and value) or (operator == "!=" and not value):
domain = [("date_end", "<=", now)]
else:
domain = [("date_end", ">", now)]
return domain
@api.depends(
"date_tz",
"date_end",
"event_registrations_started",
"seats_available",
"seats_limited",
"event_ticket_ids.sale_available",
)
def _compute_event_registrations_open(self):
"""Similar to core's :meth:`event_event._compute_event_registrations_open`"""
now = fields.Datetime.now()
for rec in self:
rec.event_registrations_open = (
rec.event_registrations_started
and (not rec.date_end or rec.date_end >= now)
and (not rec.seats_limited or not rec.seats_max or rec.seats_available)
and (
not rec.event_ticket_ids
or any(ticket.sale_available for ticket in rec.event_ticket_ids)
)
)
@api.depends(
"event_ticket_ids.seats_available",
"seats_limited",
"seats_available",
)
def _compute_event_registrations_sold_out(self):
"""Similar to core's :meth:`event_event._compute_event_registrations_sold_out`"""
for rec in self:
rec.event_registrations_sold_out = (
rec.seats_limited and rec.seats_max and not rec.seats_available
) or (
rec.event_ticket_ids
and all(ticket.is_sold_out for ticket in rec.event_ticket_ids)
)
@api.depends("event_id.event_mail_ids")
def _compute_event_mail_ids(self):
"""Compute event mail ids from its parent event
The email schedulers for sessions are used to track their independent states,
but the management is done directly from the parent event.event.
This method takes care of synchronizing the session's schedulers with those
of their parent events.
"""
for rec in self:
existing_schedulers = rec.event_mail_ids.scheduler_id
event_schedulers = rec.event_id.event_mail_ids
# Unlink the ones no-longer in sync
to_unlink = rec.event_mail_ids.filtered(
lambda r: r.scheduler_id not in event_schedulers
)
if to_unlink:
rec.event_mail_ids = [
fields.Command.unlink(scheduler.id) for scheduler in to_unlink
]
# Create missing ones
to_create = event_schedulers - existing_schedulers
if to_create:
rec.event_mail_ids = [
fields.Command.create(
scheduler._prepare_session_mail_scheduler_vals(rec)
)
for scheduler in to_create
]
# Force recomputation of scheduled date
rec.event_mail_ids._compute_scheduled_date()
@api.model
def _read_group_stage_ids(self, stages, domain, order): # pragma: no cover
return self.env["event.event"]._read_group_stage_ids(stages, domain, order)
@api.constrains("seats_max", "seats_available", "seats_limited")
def _check_seats_availability(self, minimal_availability=0):
sold_out_events = []
for session in self:
if (
session.seats_limited
and session.seats_max
and session.seats_available < minimal_availability
):
sold_out_events.append(
_(
'- "%(event_name)s": Missing %(nb_too_many)i seats.',
event_name=session.name,
nb_too_many=-session.seats_available,
)
)
if sold_out_events:
raise ValidationError(
_("There are not enough seats available for:")
+ "\n%s\n" % "\n".join(sold_out_events)
)
@api.constrains("date_begin", "date_end")
def _check_closing_date(self):
for rec in self:
if rec.date_end <= rec.date_begin:
raise ValidationError(
_("The closing date cannot be earlier than the beginning date.")
)
def mail_attendees(
self,
template_id,
force_send=False,
filter_func=lambda self: self.state != "cancel",
):
"""Mail session attendees
Similar to core's :meth:`event.models.event.mail_attendees`, but here we take
only the session's attendees into account.
"""
template = self.env["mail.template"].browse(template_id)
for rec in self:
for attendee in rec.registration_ids.filtered(filter_func):
template.send_mail(attendee.id, force_send=force_send)
def action_open_registrations(self):
"""Open session registrations"""
self.ensure_one()
action = self.env["ir.actions.actions"]._for_xml_id(
"event.act_event_registration_from_event"
)
action["domain"] = [("id", "in", self.registration_ids.ids)]
action["context"] = {
"default_event_id": self.event_id.id,
"default_session_id": self.id,
}
return action
def action_set_done(self):
"""Similar to core's :meth:`event_event.action_set_done`"""
first_ended_stage = self.env["event.stage"].search(
[("pipe_end", "=", True)], limit=1, order="sequence"
)
if first_ended_stage:
self.stage_id = first_ended_stage
def _get_ics_file(self):
"""Similar to core's :meth:`event_event._get_ics_file`"""
result = {}
if not vobject: # pragma: no cover
return result
for rec in self:
cal = vobject.iCalendar()
cal_event = cal.add("vevent")
cal_event.add("created").value = fields.Datetime.now().replace(
tzinfo=pytz.timezone("UTC")
)
cal_event.add("dtstart").value = fields.Datetime.from_string(
rec.date_begin
).replace(tzinfo=pytz.timezone("UTC"))
cal_event.add("dtend").value = fields.Datetime.from_string(
rec.date_end
).replace(tzinfo=pytz.timezone("UTC"))
cal_event.add("summary").value = rec.name
if rec.address_id:
cal_event.add("location").value = rec.sudo().address_id.contact_address
result[rec.id] = cal.serialize().encode("utf-8")
return result
@api.model_create_multi
def create(self, vals_list):
records = super().create(vals_list)
# Subscribe the organizer to sessions. Similar to core's behaviour for events.
for rec in records:
if rec.organizer_id:
rec.message_subscribe([rec.organizer_id.id])
return records
@api.model
def _session_update_fields(self):
"""List of fields that could be synced with session_update"""
return ["active"]
def _compute_session_update_message(self):
"""Human readable list of fields that could be synced with session_update"""
fnames = self._session_update_fields()
fdescs = map(lambda fname: self._fields[fname].string, fnames)
self.session_update_message = "\n".join(map(lambda s: f"* {s}", fdescs))
def _sync_session_update(self, vals):
"""Handles write on multiple sessions at once from the UX"""
update = vals.pop("session_update", "this")
if update not in ("subsequent", "all"):
return
if len(self) > 1:
raise ValidationError(
_("You cannot use session_update when writing on recordsets")
)
to_sync = self._session_update_fields()
to_sync_vals = {k: v for k, v in vals.items() if k in to_sync}
if not to_sync_vals:
return
domain = [("event_id", "=", self.event_id.id)]
if update == "subsequent":
domain.append(("date_begin", ">", self.date_begin))
records = self.search(domain)
records.write(to_sync_vals)
def write(self, vals):
# OVERRIDE to apply session_update mechanism
self._sync_session_update(vals)
return super().write(vals)
@api.autovacuum
def _gc_mark_events_done(self):
"""Move every ended sessions in the next 'ended stage'
Similar to core's :meth:`event_event._gc_mark_events_done`
"""
ended = self.search(
[
("date_end", "<", fields.Datetime.now()),
("stage_id.pipe_end", "=", False),
]
)
if ended:
ended.action_set_done()

View file

@ -0,0 +1,51 @@
# Copyright 2021 Moka Tourisme (https://www.mokatourisme.fr).
# @author Iván Todorovich <ivan.todorovich@gmail.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import time
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools.misc import format_duration
def time_as_float_time(tm):
hours, minutes = tm.tm_hour, tm.tm_min
return hours + (minutes / 60)
class EventSessionTimeslot(models.Model):
_name = "event.session.timeslot"
_description = "Event Session Timeslot"
_order = "time"
_rec_name = "time"
_sql_constraints = [
("unique_time", "UNIQUE(time)", "The timeslot has to be unique"),
(
"valid_time",
"CHECK(time >= 0 AND time <= 24)",
"Time has to be between 0:00 and 23:59",
),
]
time = fields.Float(required=True)
def name_get(self):
return [(rec.id, format_duration(rec.time)) for rec in self]
@api.model
def name_create(self, name):
try:
tm = time.strptime(name.strip(), "%H:%M")
except ValueError as e:
raise ValidationError(
_("The timeslot has to be defined in HH:MM format")
) from e
vals = {"time": time_as_float_time(tm)}
return self.create(vals).name_get()[0]
def _prepare_session_extra_vals(self):
"""Hook to prepare values to apply on sessions created from this timeslot"""
self.ensure_one()
return {}

View file

@ -0,0 +1,14 @@
# Copyright 2021 Moka Tourisme (https://www.mokatourisme.fr).
# @author Iván Todorovich <ivan.todorovich@gmail.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
class EventType(models.Model):
_inherit = "event.type"
use_sessions = fields.Boolean(
string="Event Sessions",
help="Manage multiple sessions per event",
)