mirror of
https://github.com/bringout/oca-mrp.git
synced 2026-04-21 22:32:05 +02:00
Initial commit: OCA Mrp packages (117 packages)
This commit is contained in:
commit
277e84fd7a
4403 changed files with 395154 additions and 0 deletions
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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 {}
|
||||
|
|
@ -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",
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue