mirror of
https://github.com/bringout/oca-ocb-technical.git
synced 2026-04-22 22:32:08 +02:00
19.0 vanilla
This commit is contained in:
parent
5faf7397c5
commit
2696f14ed7
721 changed files with 220375 additions and 91221 deletions
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import ir_http
|
||||
|
|
@ -10,7 +9,10 @@ from . import calendar_attendee
|
|||
from . import calendar_filter
|
||||
from . import calendar_event_type
|
||||
from . import calendar_recurrence
|
||||
from . import discuss_channel
|
||||
from . import mail_activity
|
||||
from . import mail_activity_mixin
|
||||
from . import mail_activity_type
|
||||
from . import res_users
|
||||
from . import res_users_settings
|
||||
from . import utils
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.fields import Domain
|
||||
|
||||
|
||||
class Alarm(models.Model):
|
||||
class CalendarAlarm(models.Model):
|
||||
_name = 'calendar.alarm'
|
||||
_description = 'Event Alarm'
|
||||
|
||||
|
|
@ -26,6 +26,7 @@ class Alarm(models.Model):
|
|||
compute='_compute_mail_template_id', readonly=False, store=True,
|
||||
help="Template used to render mail reminder content.")
|
||||
body = fields.Text("Additional Message", help="Additional message that would be sent with the notification for the reminder")
|
||||
notify_responsible = fields.Boolean("Notify Responsible", default=False)
|
||||
|
||||
@api.depends('interval', 'duration')
|
||||
def _compute_duration_minutes(self):
|
||||
|
|
@ -48,6 +49,14 @@ class Alarm(models.Model):
|
|||
alarm.mail_template_id = False
|
||||
|
||||
def _search_duration_minutes(self, operator, value):
|
||||
if operator == 'in':
|
||||
# recursive call with operator '='
|
||||
return Domain.OR(self._search_duration_minutes('=', v) for v in value)
|
||||
elif operator == '=':
|
||||
if not value:
|
||||
return Domain('duration', '=', False)
|
||||
elif operator not in ('>=', '<=', '<', '>'):
|
||||
return NotImplemented
|
||||
return [
|
||||
'|', '|',
|
||||
'&', ('interval', '=', 'minutes'), ('duration', operator, value),
|
||||
|
|
@ -55,10 +64,14 @@ class Alarm(models.Model):
|
|||
'&', ('interval', '=', 'days'), ('duration', operator, value / 60 / 24),
|
||||
]
|
||||
|
||||
@api.onchange('duration', 'interval', 'alarm_type')
|
||||
@api.onchange('duration', 'interval', 'alarm_type', 'notify_responsible')
|
||||
def _onchange_duration_interval(self):
|
||||
if self.notify_responsible and self.alarm_type in ('email', 'notification'):
|
||||
self.notify_responsible = False
|
||||
display_interval = self._interval_selection.get(self.interval, '')
|
||||
display_alarm_type = {
|
||||
key: value for key, value in self._fields['alarm_type']._description_selection(self.env)
|
||||
}[self.alarm_type]
|
||||
}.get(self.alarm_type, '')
|
||||
self.name = "%s - %s %s" % (display_alarm_type, self.duration, display_interval)
|
||||
if self.notify_responsible:
|
||||
self.name += " - " + _("Notify Responsible")
|
||||
|
|
|
|||
|
|
@ -1,17 +1,13 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools import plaintext2html
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
from odoo.tools.sql import SQL
|
||||
|
||||
|
||||
class AlarmManager(models.AbstractModel):
|
||||
class CalendarAlarm_Manager(models.AbstractModel):
|
||||
_name = 'calendar.alarm_manager'
|
||||
_description = 'Event Alarm Manager'
|
||||
|
||||
|
|
@ -23,7 +19,9 @@ class AlarmManager(models.AbstractModel):
|
|||
result = {}
|
||||
delta_request = """
|
||||
SELECT
|
||||
rel.calendar_event_id, max(alarm.duration_minutes) AS max_delta,min(alarm.duration_minutes) AS min_delta
|
||||
rel.calendar_event_id,
|
||||
max(alarm.duration_minutes) AS max_delta,
|
||||
min(alarm.duration_minutes) AS min_delta
|
||||
FROM
|
||||
calendar_alarm_calendar_event_rel AS rel
|
||||
LEFT JOIN calendar_alarm AS alarm ON alarm.id = rel.calendar_alarm_id
|
||||
|
|
@ -31,30 +29,24 @@ class AlarmManager(models.AbstractModel):
|
|||
GROUP BY rel.calendar_event_id
|
||||
"""
|
||||
base_request = """
|
||||
SELECT
|
||||
cal.id,
|
||||
cal.start - interval '1' minute * calcul_delta.max_delta AS first_alarm,
|
||||
CASE
|
||||
WHEN cal.recurrency THEN rrule.until - interval '1' minute * calcul_delta.min_delta
|
||||
ELSE cal.stop - interval '1' minute * calcul_delta.min_delta
|
||||
END as last_alarm,
|
||||
cal.start as first_event_date,
|
||||
CASE
|
||||
WHEN cal.recurrency THEN rrule.until
|
||||
ELSE cal.stop
|
||||
END as last_event_date,
|
||||
calcul_delta.min_delta,
|
||||
calcul_delta.max_delta,
|
||||
rrule.rrule AS rule
|
||||
FROM
|
||||
calendar_event AS cal
|
||||
RIGHT JOIN calcul_delta ON calcul_delta.calendar_event_id = cal.id
|
||||
LEFT JOIN calendar_recurrence as rrule ON rrule.id = cal.recurrence_id
|
||||
"""
|
||||
|
||||
SELECT
|
||||
cal.id,
|
||||
cal.start - interval '1' minute * calcul_delta.max_delta AS first_alarm,
|
||||
cal.stop - interval '1' minute * calcul_delta.min_delta AS last_alarm,
|
||||
cal.start AS first_meeting,
|
||||
cal.stop AS last_meeting,
|
||||
calcul_delta.min_delta,
|
||||
calcul_delta.max_delta
|
||||
FROM
|
||||
calendar_event AS cal
|
||||
INNER JOIN calcul_delta ON calcul_delta.calendar_event_id = cal.id
|
||||
WHERE cal.active = True
|
||||
"""
|
||||
filter_user = """
|
||||
RIGHT JOIN calendar_event_res_partner_rel AS part_rel ON part_rel.calendar_event_id = cal.id
|
||||
AND part_rel.res_partner_id IN %s
|
||||
INNER JOIN calendar_event_res_partner_rel AS part_rel
|
||||
ON part_rel.calendar_event_id = cal.id
|
||||
AND part_rel.res_partner_id IN %s
|
||||
WHERE cal.active = True
|
||||
"""
|
||||
|
||||
# Add filter on alarm type
|
||||
|
|
@ -62,7 +54,7 @@ class AlarmManager(models.AbstractModel):
|
|||
|
||||
# Add filter on partner_id
|
||||
if partners:
|
||||
base_request += filter_user
|
||||
base_request = base_request.replace("WHERE cal.active = True", filter_user)
|
||||
tuple_params += (tuple(partners.ids), )
|
||||
|
||||
# Upper bound on first_alarm of requested events
|
||||
|
|
@ -81,15 +73,15 @@ class AlarmManager(models.AbstractModel):
|
|||
tuple_params += (seconds,)
|
||||
|
||||
self.env.flush_all()
|
||||
self._cr.execute("""
|
||||
self.env.cr.execute("""
|
||||
WITH calcul_delta AS (%s)
|
||||
SELECT *
|
||||
FROM ( %s WHERE cal.active = True ) AS ALL_EVENTS
|
||||
WHERE ALL_EVENTS.first_alarm < %s
|
||||
AND ALL_EVENTS.last_event_date > (now() at time zone 'utc')
|
||||
FROM ( %s ) AS ALL_EVENTS
|
||||
WHERE ALL_EVENTS.first_alarm < %s
|
||||
AND ALL_EVENTS.last_alarm > (now() at time zone 'utc')
|
||||
""" % (delta_request, base_request, first_alarm_max_value), tuple_params)
|
||||
|
||||
for event_id, first_alarm, last_alarm, first_meeting, last_meeting, min_duration, max_duration, rule in self._cr.fetchall():
|
||||
for event_id, first_alarm, last_alarm, first_meeting, last_meeting, min_duration, max_duration in self.env.cr.fetchall():
|
||||
result[event_id] = {
|
||||
'event_id': event_id,
|
||||
'first_alarm': first_alarm,
|
||||
|
|
@ -98,14 +90,13 @@ class AlarmManager(models.AbstractModel):
|
|||
'last_meeting': last_meeting,
|
||||
'min_duration': min_duration,
|
||||
'max_duration': max_duration,
|
||||
'rrule': rule
|
||||
}
|
||||
|
||||
# determine accessible events
|
||||
events = self.env['calendar.event'].browse(result)
|
||||
result = {
|
||||
key: result[key]
|
||||
for key in set(events._filter_access_rules('read').ids)
|
||||
for key in events._filtered_access('read').ids
|
||||
}
|
||||
return result
|
||||
|
||||
|
|
@ -147,7 +138,7 @@ class AlarmManager(models.AbstractModel):
|
|||
To be overriden on inherited modules
|
||||
adding extra conditions to extract only the unsynced events
|
||||
"""
|
||||
return ""
|
||||
return SQL("")
|
||||
|
||||
def _get_events_by_alarm_to_notify(self, alarm_type):
|
||||
"""
|
||||
|
|
@ -159,21 +150,28 @@ class AlarmManager(models.AbstractModel):
|
|||
design. The attendees receive an invitation for any new event
|
||||
already.
|
||||
"""
|
||||
lastcall = self.env.context.get('lastcall', False) or fields.date.today() - relativedelta(weeks=1)
|
||||
self.env.cr.execute('''
|
||||
SELECT "alarm"."id", "event"."id"
|
||||
FROM "calendar_event" AS "event"
|
||||
JOIN "calendar_alarm_calendar_event_rel" AS "event_alarm_rel"
|
||||
ON "event"."id" = "event_alarm_rel"."calendar_event_id"
|
||||
JOIN "calendar_alarm" AS "alarm"
|
||||
ON "event_alarm_rel"."calendar_alarm_id" = "alarm"."id"
|
||||
WHERE
|
||||
"alarm"."alarm_type" = %s
|
||||
AND "event"."active"
|
||||
AND "event"."start" - CAST("alarm"."duration" || ' ' || "alarm"."interval" AS Interval) >= %s
|
||||
AND "event"."start" - CAST("alarm"."duration" || ' ' || "alarm"."interval" AS Interval) < now() at time zone 'utc'
|
||||
AND "event"."stop" > now() at time zone 'utc'
|
||||
''' + self._get_notify_alert_extra_conditions(), [alarm_type, lastcall])
|
||||
lastcall = self.env.context.get('lastcall', False) or fields.Date.today() - timedelta(weeks=1)
|
||||
# TODO MASTER: remove context and add a proper parameter
|
||||
extra_conditions = self.with_context(alarm_type=alarm_type)._get_notify_alert_extra_conditions()
|
||||
now = fields.Datetime.now()
|
||||
self.env.cr.execute(SQL("""
|
||||
SELECT alarm.id, event.id
|
||||
FROM calendar_event AS event
|
||||
JOIN calendar_alarm_calendar_event_rel AS event_alarm_rel
|
||||
ON event.id = event_alarm_rel.calendar_event_id
|
||||
JOIN calendar_alarm AS alarm
|
||||
ON event_alarm_rel.calendar_alarm_id = alarm.id
|
||||
WHERE alarm.alarm_type = %s
|
||||
AND event.active
|
||||
AND event.start - CAST(alarm.duration || ' ' || alarm.interval AS Interval) >= %s
|
||||
AND event.start - CAST(alarm.duration || ' ' || alarm.interval AS Interval) < %s
|
||||
%s
|
||||
""",
|
||||
alarm_type,
|
||||
lastcall,
|
||||
now,
|
||||
extra_conditions,
|
||||
))
|
||||
|
||||
events_by_alarm = {}
|
||||
for alarm_id, event_id in self.env.cr.fetchall():
|
||||
|
|
@ -187,21 +185,24 @@ class AlarmManager(models.AbstractModel):
|
|||
if not events_by_alarm:
|
||||
return
|
||||
|
||||
# force_send limit should apply to the total nb of attendees, not per alarm
|
||||
force_send_limit = int(self.env['ir.config_parameter'].sudo().get_param('mail.mail_force_send_limit', 100))
|
||||
|
||||
event_ids = list(set(event_id for event_ids in events_by_alarm.values() for event_id in event_ids))
|
||||
events = self.env['calendar.event'].browse(event_ids)
|
||||
attendees = events.attendee_ids.filtered(lambda a: a.state != 'declined')
|
||||
now = fields.Datetime.now()
|
||||
attendees = events.filtered(lambda e: e.stop > now).attendee_ids.filtered(lambda a: a.state != 'declined')
|
||||
alarms = self.env['calendar.alarm'].browse(events_by_alarm.keys())
|
||||
for alarm in alarms:
|
||||
alarm_attendees = attendees.filtered(lambda attendee: attendee.event_id.id in events_by_alarm[alarm.id])
|
||||
alarm_attendees.with_context(
|
||||
mail_notify_force_send=True,
|
||||
calendar_template_ignore_recurrence=True,
|
||||
mail_notify_author=True
|
||||
)._send_mail_to_attendees(
|
||||
alarm_attendees.with_context(calendar_template_ignore_recurrence=True)._notify_attendees(
|
||||
alarm.mail_template_id,
|
||||
force_send=True
|
||||
force_send=len(attendees) <= force_send_limit,
|
||||
notify_author=True,
|
||||
)
|
||||
|
||||
events._setup_event_recurrent_alarms(events_by_alarm)
|
||||
|
||||
@api.model
|
||||
def get_next_notif(self):
|
||||
partner = self.env.user.partner_id
|
||||
|
|
@ -245,13 +246,10 @@ class AlarmManager(models.AbstractModel):
|
|||
|
||||
def _notify_next_alarm(self, partner_ids):
|
||||
""" Sends through the bus the next alarm of given partners """
|
||||
notifications = []
|
||||
users = self.env['res.users'].search([
|
||||
('partner_id', 'in', tuple(partner_ids)),
|
||||
('groups_id', 'in', self.env.ref('base.group_user').ids),
|
||||
('group_ids', 'in', self.env.ref('base.group_user').ids),
|
||||
])
|
||||
for user in users:
|
||||
notif = self.with_user(user).with_context(allowed_company_ids=user.company_ids.ids).get_next_notif()
|
||||
notifications.append([user.partner_id, 'calendar.alarm', notif])
|
||||
if len(notifications) > 0:
|
||||
self.env['bus.bus']._sendmany(notifications)
|
||||
user._bus_send("calendar.alarm", notif)
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import uuid
|
||||
import base64
|
||||
import logging
|
||||
|
||||
from collections import defaultdict
|
||||
from odoo import api, Command, fields, models, _
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.addons.base.models.res_partner import _tz_get
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools.misc import clean_context
|
||||
from odoo.tools import split_every
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Attendee(models.Model):
|
||||
class CalendarAttendee(models.Model):
|
||||
""" Calendar Attendee Information """
|
||||
_name = 'calendar.attendee'
|
||||
_rec_name = 'common_name'
|
||||
|
|
@ -23,24 +24,24 @@ class Attendee(models.Model):
|
|||
return uuid.uuid4().hex
|
||||
|
||||
STATE_SELECTION = [
|
||||
('accepted', 'Yes'),
|
||||
('declined', 'No'),
|
||||
('tentative', 'Maybe'),
|
||||
('needsAction', 'Needs Action'),
|
||||
('tentative', 'Uncertain'),
|
||||
('declined', 'Declined'),
|
||||
('accepted', 'Accepted'),
|
||||
]
|
||||
|
||||
# event
|
||||
event_id = fields.Many2one('calendar.event', 'Meeting linked', required=True, ondelete='cascade')
|
||||
event_id = fields.Many2one('calendar.event', 'Meeting linked', required=True, index=True, ondelete='cascade')
|
||||
recurrence_id = fields.Many2one('calendar.recurrence', related='event_id.recurrence_id')
|
||||
# attendee
|
||||
partner_id = fields.Many2one('res.partner', 'Attendee', required=True, readonly=True)
|
||||
partner_id = fields.Many2one('res.partner', 'Attendee', required=True, readonly=True, ondelete='cascade')
|
||||
email = fields.Char('Email', related='partner_id.email')
|
||||
phone = fields.Char('Phone', related='partner_id.phone')
|
||||
common_name = fields.Char('Common name', compute='_compute_common_name', store=True)
|
||||
access_token = fields.Char('Invitation Token', default=_default_access_token)
|
||||
mail_tz = fields.Selection(_tz_get, compute='_compute_mail_tz', help='Timezone used for displaying time in the mail template')
|
||||
# state
|
||||
state = fields.Selection(STATE_SELECTION, string='Status', readonly=True, default='needsAction')
|
||||
state = fields.Selection(STATE_SELECTION, string='Status', default='needsAction')
|
||||
availability = fields.Selection(
|
||||
[('free', 'Available'), ('busy', 'Busy')], 'Available/Busy', readonly=True)
|
||||
|
||||
|
|
@ -66,90 +67,159 @@ class Attendee(models.Model):
|
|||
values['email'] = email[0] if email else ''
|
||||
values['common_name'] = values.get("common_name")
|
||||
attendees = super().create(vals_list)
|
||||
attendees._subscribe_partner()
|
||||
attendees.event_id.check_access('write')
|
||||
return attendees
|
||||
|
||||
def write(self, vals):
|
||||
attendees = super().write(vals)
|
||||
self.event_id.check_access('write')
|
||||
return attendees
|
||||
|
||||
def unlink(self):
|
||||
self._unsubscribe_partner()
|
||||
return super().unlink()
|
||||
|
||||
@api.returns('self', lambda value: value.id)
|
||||
def copy(self, default=None):
|
||||
raise UserError(_('You cannot duplicate a calendar attendee.'))
|
||||
|
||||
def _subscribe_partner(self):
|
||||
mapped_followers = defaultdict(lambda: self.env['calendar.event'])
|
||||
for event in self.event_id:
|
||||
partners = (event.attendee_ids & self).partner_id - event.message_partner_ids
|
||||
# current user is automatically added as followers, don't add it twice.
|
||||
partners -= self.env.user.partner_id
|
||||
mapped_followers[partners] |= event
|
||||
for partners, events in mapped_followers.items():
|
||||
events.message_subscribe(partner_ids=partners.ids)
|
||||
|
||||
def _unsubscribe_partner(self):
|
||||
for event in self.event_id:
|
||||
partners = (event.attendee_ids & self).partner_id & event.message_partner_ids
|
||||
event.message_unsubscribe(partner_ids=partners.ids)
|
||||
|
||||
def _send_mail_to_attendees(self, mail_template, force_send=False):
|
||||
""" Send mail for event invitation to event attendees.
|
||||
:param mail_template: a mail.template record
|
||||
:param force_send: if set to True, the mail(s) will be sent immediately (instead of the next queue processing)
|
||||
# ------------------------------------------------------------
|
||||
# MAILING
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@api.model
|
||||
def _mail_template_default_values(self):
|
||||
return {
|
||||
"email_from": "{{ (object.event_id.user_id.email_formatted or user.email_formatted or '') }}",
|
||||
"email_to": False,
|
||||
"partner_to": False,
|
||||
"lang": "{{ object.partner_id.lang }}",
|
||||
"use_default_to": True,
|
||||
}
|
||||
|
||||
def _message_add_default_recipients(self):
|
||||
# override: partner_id being the only stored field, we can currently
|
||||
# simplify computation, we have no other choice than relying on it
|
||||
return {
|
||||
attendee.id: {
|
||||
'partners': attendee.partner_id,
|
||||
'email_to_lst': [],
|
||||
'email_cc_lst': [],
|
||||
} for attendee in self
|
||||
}
|
||||
|
||||
def _send_invitation_emails(self):
|
||||
""" Hook to be able to override the invitation email sending process.
|
||||
Notably inside appointment to use a different mail template from the appointment type. """
|
||||
self._notify_attendees(
|
||||
self.env.ref('calendar.calendar_template_meeting_invitation', raise_if_not_found=False),
|
||||
force_send=True,
|
||||
)
|
||||
|
||||
def _notify_attendees(self, mail_template, notify_author=False, force_send=False):
|
||||
""" Notify attendees about event main changes (invite, cancel, ...) based
|
||||
on template.
|
||||
|
||||
:param mail_template: a mail.template record
|
||||
:param force_send: if set to True, the mail(s) will be sent immediately (instead of the next queue processing)
|
||||
"""
|
||||
# TDE FIXME: check this
|
||||
if force_send:
|
||||
force_send_limit = int(self.env['ir.config_parameter'].sudo().get_param('mail.mail_force_send_limit', 100))
|
||||
notified_attendees_ids = set(self.ids)
|
||||
for event, attendees in self.grouped('event_id').items():
|
||||
if event._skip_send_mail_status_update():
|
||||
notified_attendees_ids -= set(attendees.ids)
|
||||
notified_attendees = self.browse(notified_attendees_ids)
|
||||
if isinstance(mail_template, str):
|
||||
raise ValueError('Template should be a template record, not an XML ID anymore.')
|
||||
if self.env['ir.config_parameter'].sudo().get_param('calendar.block_mail') or self._context.get("no_mail_to_attendees"):
|
||||
if self.env['ir.config_parameter'].sudo().get_param('calendar.block_mail') or self.env.context.get("no_mail_to_attendees"):
|
||||
return False
|
||||
if not mail_template:
|
||||
_logger.warning("No template passed to %s notification process. Skipped.", self)
|
||||
return False
|
||||
|
||||
# get ics file for all meetings
|
||||
ics_files = self.mapped('event_id')._get_ics_file()
|
||||
ics_files = notified_attendees.event_id._get_ics_file()
|
||||
|
||||
for attendee in self:
|
||||
if attendee.email and attendee._should_notify_attendee():
|
||||
# If the mail template has attachments, prepare copies for each attendee (to be added to each attendee's mail)
|
||||
if mail_template.attachment_ids:
|
||||
|
||||
# Setting res_model to ensure attachments are linked to the msg (otherwise only internal users are allowed link attachments)
|
||||
attachments_values = [a.copy_data({'res_id': 0, 'res_model': 'mail.compose.message'})[0] for a in mail_template.attachment_ids]
|
||||
attachments_values *= len(self)
|
||||
attendee_attachment_ids = self.env['ir.attachment'].create(attachments_values).ids
|
||||
|
||||
# Map attendees to their respective attachments
|
||||
template_attachment_count = len(mail_template.attachment_ids)
|
||||
attendee_id_attachment_id_map = dict(zip(self.ids, split_every(template_attachment_count, attendee_attachment_ids, list)))
|
||||
|
||||
mail_messages = self.env['mail.message']
|
||||
for attendee in notified_attendees:
|
||||
if attendee.email and attendee._should_notify_attendee(notify_author=notify_author):
|
||||
event_id = attendee.event_id.id
|
||||
ics_file = ics_files.get(event_id)
|
||||
|
||||
attachment_values = [Command.set(mail_template.attachment_ids.ids)]
|
||||
# Add template attachments copies to the attendee's email, if available
|
||||
attachment_ids = attendee_id_attachment_id_map[attendee.id] if mail_template.attachment_ids else []
|
||||
|
||||
if ics_file:
|
||||
attachment_values += [
|
||||
(0, 0, {'name': 'invitation.ics',
|
||||
'mimetype': 'text/calendar',
|
||||
'res_id': event_id,
|
||||
'res_model': 'calendar.event',
|
||||
'datas': base64.b64encode(ics_file)})
|
||||
]
|
||||
context = {
|
||||
**clean_context(self.env.context),
|
||||
'no_document': True, # An ICS file must not create a document
|
||||
}
|
||||
attachment_ids += self.env['ir.attachment'].with_context(context).create({
|
||||
'datas': base64.b64encode(ics_file),
|
||||
'description': 'invitation.ics',
|
||||
'mimetype': 'text/calendar',
|
||||
'res_id': 0,
|
||||
'res_model': 'mail.compose.message',
|
||||
'name': 'invitation.ics',
|
||||
}).ids
|
||||
|
||||
body = mail_template._render_field(
|
||||
'body_html',
|
||||
attendee.ids,
|
||||
compute_lang=True,
|
||||
post_process=True)[attendee.id]
|
||||
compute_lang=True)[attendee.id]
|
||||
subject = mail_template._render_field(
|
||||
'subject',
|
||||
attendee.ids,
|
||||
compute_lang=True)[attendee.id]
|
||||
attendee.event_id.with_context(no_document=True).sudo().message_notify(
|
||||
email_from=attendee.event_id.user_id.email_formatted or self.env.user.email_formatted,
|
||||
email_from = mail_template._render_field(
|
||||
'email_from',
|
||||
attendee.ids)[attendee.id]
|
||||
mail_messages += attendee.event_id.with_context(no_document=True).sudo().message_notify(
|
||||
email_from=email_from or None, # use None to trigger fallback sender
|
||||
author_id=attendee.event_id.user_id.partner_id.id or self.env.user.partner_id.id,
|
||||
body=body,
|
||||
subject=subject,
|
||||
notify_author=notify_author,
|
||||
partner_ids=attendee.partner_id.ids,
|
||||
email_layout_xmlid='mail.mail_notification_light',
|
||||
attachment_ids=attachment_values,
|
||||
force_send=force_send,
|
||||
attachment_ids=attachment_ids,
|
||||
force_send=False,
|
||||
)
|
||||
# batch sending at the end
|
||||
if force_send and len(notified_attendees) < force_send_limit:
|
||||
mail_messages.sudo().mail_ids.send_after_commit()
|
||||
|
||||
def _should_notify_attendee(self):
|
||||
def _should_notify_attendee(self, notify_author=False):
|
||||
""" Utility method that determines if the attendee should be notified.
|
||||
By default, we do not want to notify (aka no message and no mail) the current user
|
||||
if he is part of the attendees.
|
||||
if he is part of the attendees. But for reminders, mail_notify_author could be forced
|
||||
(Override in appointment to ignore that rule and notify all attendees if it's an appointment)
|
||||
"""
|
||||
self.ensure_one()
|
||||
return self.partner_id != self.env.user.partner_id
|
||||
partner_not_sender = self.partner_id != self.env.user.partner_id
|
||||
return partner_not_sender or notify_author
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# STATE MANAGEMENT
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def do_tentative(self):
|
||||
""" Makes event invitation as Tentative. """
|
||||
|
|
@ -160,7 +230,7 @@ class Attendee(models.Model):
|
|||
for attendee in self:
|
||||
attendee.event_id.message_post(
|
||||
author_id=attendee.partner_id.id,
|
||||
body=_("%s has accepted the invitation") % (attendee.common_name),
|
||||
body=_("%s has accepted the invitation", attendee.common_name),
|
||||
subtype_xmlid="calendar.subtype_invitation",
|
||||
)
|
||||
return self.write({'state': 'accepted'})
|
||||
|
|
@ -170,7 +240,7 @@ class Attendee(models.Model):
|
|||
for attendee in self:
|
||||
attendee.event_id.message_post(
|
||||
author_id=attendee.partner_id.id,
|
||||
body=_("%s has declined the invitation") % (attendee.common_name),
|
||||
body=_("%s has declined the invitation", attendee.common_name),
|
||||
subtype_xmlid="calendar.subtype_invitation",
|
||||
)
|
||||
return self.write({'state': 'declined'})
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from random import randint
|
||||
|
|
@ -6,9 +5,9 @@ from random import randint
|
|||
from odoo import fields, models
|
||||
|
||||
|
||||
class MeetingType(models.Model):
|
||||
|
||||
class CalendarEventType(models.Model):
|
||||
_name = 'calendar.event.type'
|
||||
|
||||
_description = 'Event Meeting Type'
|
||||
|
||||
def _default_color(self):
|
||||
|
|
@ -17,6 +16,7 @@ class MeetingType(models.Model):
|
|||
name = fields.Char('Name', required=True)
|
||||
color = fields.Integer('Color', default=_default_color)
|
||||
|
||||
_sql_constraints = [
|
||||
('name_uniq', 'unique (name)', "Tag name already exists !"),
|
||||
]
|
||||
_name_uniq = models.Constraint(
|
||||
'unique (name)',
|
||||
'Tag name already exists!',
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class Contacts(models.Model):
|
||||
class CalendarFilters(models.Model):
|
||||
_name = 'calendar.filters'
|
||||
_description = 'Calendar Filters'
|
||||
|
||||
user_id = fields.Many2one('res.users', 'Me', required=True, default=lambda self: self.env.user, index=True)
|
||||
user_id = fields.Many2one('res.users', 'Me', required=True, default=lambda self: self.env.user, index=True, ondelete='cascade')
|
||||
partner_id = fields.Many2one('res.partner', 'Employee', required=True, index=True)
|
||||
active = fields.Boolean('Active', default=True)
|
||||
partner_checked = fields.Boolean('Checked', default=True) # used to know if the partner is checked in the filter of the calendar view for the user_id.
|
||||
|
||||
_sql_constraints = [
|
||||
('user_id_partner_id_unique', 'UNIQUE(user_id, partner_id)', 'A user cannot have the same contact twice.')
|
||||
]
|
||||
_user_id_partner_id_unique = models.Constraint(
|
||||
'UNIQUE(user_id, partner_id)',
|
||||
'A user cannot have the same contact twice.',
|
||||
)
|
||||
|
||||
@api.model
|
||||
def unlink_from_partner_id(self, partner_id):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime, time
|
||||
|
|
@ -10,6 +9,7 @@ from dateutil.relativedelta import relativedelta
|
|||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools.misc import clean_context
|
||||
|
||||
from odoo.addons.base.models.res_partner import _tz_get
|
||||
|
||||
|
|
@ -90,7 +90,7 @@ def weekday_to_field(weekday_index):
|
|||
return RRULE_WEEKDAY_TO_FIELD.get(weekday_index)
|
||||
|
||||
|
||||
class RecurrenceRule(models.Model):
|
||||
class CalendarRecurrence(models.Model):
|
||||
_name = 'calendar.recurrence'
|
||||
_description = 'Event Recurrence Rule'
|
||||
|
||||
|
|
@ -119,16 +119,18 @@ class RecurrenceRule(models.Model):
|
|||
weekday = fields.Selection(WEEKDAY_SELECTION, string='Weekday')
|
||||
byday = fields.Selection(BYDAY_SELECTION, string='By day')
|
||||
until = fields.Date('Repeat Until')
|
||||
trigger_id = fields.Many2one('ir.cron.trigger')
|
||||
|
||||
_sql_constraints = [
|
||||
('month_day',
|
||||
"CHECK (rrule_type != 'monthly' "
|
||||
"OR month_by != 'day' "
|
||||
"OR day >= 1 AND day <= 31 "
|
||||
"OR weekday in %s AND byday in %s)"
|
||||
% (tuple(wd[0] for wd in WEEKDAY_SELECTION), tuple(bd[0] for bd in BYDAY_SELECTION)),
|
||||
"The day must be between 1 and 31"),
|
||||
]
|
||||
_month_day = models.Constraint("""CHECK (
|
||||
rrule_type != 'monthly'
|
||||
OR month_by != 'day'
|
||||
OR day >= 1 AND day <= 31
|
||||
OR weekday IN %s AND byday IN %s)""" % (
|
||||
tuple(wd[0] for wd in WEEKDAY_SELECTION),
|
||||
tuple(bd[0] for bd in BYDAY_SELECTION),
|
||||
),
|
||||
"The day must be between 1 and 31",
|
||||
)
|
||||
|
||||
def _get_daily_recurrence_name(self):
|
||||
if self.end_type == 'count':
|
||||
|
|
@ -197,11 +199,8 @@ class RecurrenceRule(models.Model):
|
|||
|
||||
@api.depends('calendar_event_ids.start')
|
||||
def _compute_dtstart(self):
|
||||
groups = self.env['calendar.event'].read_group([('recurrence_id', 'in', self.ids)], ['start:min'], ['recurrence_id'])
|
||||
start_mapping = {
|
||||
group['recurrence_id'][0]: group['start']
|
||||
for group in groups
|
||||
}
|
||||
groups = self.env['calendar.event']._read_group([('recurrence_id', 'in', self.ids)], ['recurrence_id'], ['start:min'])
|
||||
start_mapping = {recurrence.id: start_min for recurrence, start_min in groups}
|
||||
for recurrence in self:
|
||||
recurrence.dtstart = start_mapping.get(recurrence.id)
|
||||
|
||||
|
|
@ -275,9 +274,44 @@ class RecurrenceRule(models.Model):
|
|||
|
||||
events = self.calendar_event_ids - keep
|
||||
detached_events = self._detach_events(events)
|
||||
self.env['calendar.event'].with_context(no_mail_to_attendees=True, mail_create_nolog=True).create(event_vals)
|
||||
context = {
|
||||
**clean_context(self.env.context),
|
||||
**{'no_mail_to_attendees': True, 'mail_create_nolog': True},
|
||||
}
|
||||
self.env['calendar.event'].with_context(context).create(event_vals)
|
||||
return detached_events
|
||||
|
||||
def _setup_alarms(self, recurrence_update=False):
|
||||
""" Schedule cron triggers for future events
|
||||
Create one ir.cron.trigger per recurrence.
|
||||
:param recurrence_update: boolean: if true, update all recurrences in self, else only the recurrences
|
||||
without trigger
|
||||
"""
|
||||
now = self.env.context.get('date') or fields.Datetime.now()
|
||||
# get next events
|
||||
self.env['calendar.event'].flush_model(fnames=['recurrence_id', 'start'])
|
||||
if not self.calendar_event_ids.ids:
|
||||
return
|
||||
|
||||
self.env.cr.execute("""
|
||||
SELECT DISTINCT ON (recurrence_id) id event_id, recurrence_id
|
||||
FROM calendar_event
|
||||
WHERE start > %s
|
||||
AND id IN %s
|
||||
ORDER BY recurrence_id,start ASC;
|
||||
""", (now, tuple(self.calendar_event_ids.ids)))
|
||||
result = self.env.cr.dictfetchall()
|
||||
if not result:
|
||||
return
|
||||
events = self.env['calendar.event'].browse(value['event_id'] for value in result)
|
||||
triggers_by_events = events._setup_alarms()
|
||||
for vals in result:
|
||||
trigger_id = triggers_by_events.get(vals['event_id'])
|
||||
if not trigger_id:
|
||||
continue
|
||||
recurrence = self.env['calendar.recurrence'].browse(vals['recurrence_id'])
|
||||
recurrence.trigger_id = trigger_id
|
||||
|
||||
def _split_from(self, event, recurrence_values=None):
|
||||
"""Stops the current recurrence at the given event and creates a new one starting
|
||||
with the event.
|
||||
|
|
@ -331,7 +365,7 @@ class RecurrenceRule(models.Model):
|
|||
def _detach_events(self, events):
|
||||
events.with_context(dont_notify=True).write({
|
||||
'recurrence_id': False,
|
||||
'recurrency': False,
|
||||
'recurrency': True,
|
||||
})
|
||||
return events
|
||||
|
||||
|
|
@ -395,14 +429,9 @@ class RecurrenceRule(models.Model):
|
|||
data['month_by'] = 'day'
|
||||
data['rrule_type'] = 'monthly'
|
||||
|
||||
if rule._bymonthday:
|
||||
if rule._bymonthday and data['rrule_type'] == 'monthly':
|
||||
data['day'] = list(rule._bymonthday)[0]
|
||||
data['month_by'] = 'date'
|
||||
data['rrule_type'] = 'monthly'
|
||||
|
||||
# Repeat yearly but for odoo it's monthly, take same information as monthly but interval is 12 times
|
||||
if rule._bymonth:
|
||||
data['interval'] *= 12
|
||||
|
||||
if data.get('until'):
|
||||
data['end_type'] = 'end_date'
|
||||
|
|
@ -413,7 +442,7 @@ class RecurrenceRule(models.Model):
|
|||
return data
|
||||
|
||||
def _get_lang_week_start(self):
|
||||
lang = self.env['res.lang']._lang_get(self.env.user.lang)
|
||||
lang = self.env['res.lang']._get_data(code=self.env.user.lang)
|
||||
week_start = int(lang.week_start) # lang.week_start ranges from '1' to '7'
|
||||
return rrule.weekday(week_start - 1) # rrule expects an int from 0 to 6
|
||||
|
||||
|
|
@ -574,3 +603,19 @@ class RecurrenceRule(models.Model):
|
|||
return rrule.rrule(
|
||||
freq_to_rrule(freq), **rrule_params
|
||||
)
|
||||
|
||||
def _is_event_over(self):
|
||||
"""Check if all events in this recurrence are in the past.
|
||||
:return: True if all events are over, False otherwise
|
||||
"""
|
||||
self.ensure_one()
|
||||
if not self.calendar_event_ids:
|
||||
return False
|
||||
|
||||
now = fields.Datetime.now()
|
||||
today = fields.Date.today()
|
||||
|
||||
return all(
|
||||
(event.stop_date < today if event.allday else event.stop < now)
|
||||
for event in self.calendar_event_ids
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class DiscussChannel(models.Model):
|
||||
_inherit = "discuss.channel"
|
||||
|
||||
calendar_event_ids = fields.One2many("calendar.event", "videocall_channel_id")
|
||||
|
||||
def _should_invite_members_to_join_call(self):
|
||||
if self.calendar_event_ids:
|
||||
return False
|
||||
return super()._should_invite_members_to_join_call()
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from werkzeug.exceptions import BadRequest
|
||||
|
|
@ -12,7 +11,7 @@ class IrHttp(models.AbstractModel):
|
|||
|
||||
@classmethod
|
||||
def _auth_method_calendar(cls):
|
||||
token = request.get_http_params().get('token', '')
|
||||
token = request.httprequest.args.get('token', '')
|
||||
|
||||
error_message = False
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,38 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import pytz
|
||||
|
||||
from odoo import models, fields, tools, _
|
||||
from odoo.tools import is_html_empty
|
||||
from odoo.addons.mail.tools.discuss import Store
|
||||
|
||||
|
||||
class MailActivity(models.Model):
|
||||
_inherit = "mail.activity"
|
||||
|
||||
calendar_event_id = fields.Many2one('calendar.event', string="Calendar Meeting", ondelete='cascade')
|
||||
calendar_event_id = fields.Many2one('calendar.event', string="Calendar Meeting", index='btree_not_null', ondelete='cascade')
|
||||
|
||||
def write(self, vals):
|
||||
# synchronize calendar events
|
||||
res = super().write(vals)
|
||||
# protect against loops in case of ill-managed timezones
|
||||
if 'date_deadline' in vals and not self.env.context.get('calendar_event_meeting_update') and self.calendar_event_id:
|
||||
date_deadline = self[0].date_deadline # updated, hence all same value
|
||||
# also protect against loops in case of ill-managed timezones
|
||||
events = self.calendar_event_id.with_context(mail_activity_meeting_update=True)
|
||||
user_tz = self.env.context.get('tz') or 'UTC'
|
||||
for event in events:
|
||||
# allday: just apply diff between dates
|
||||
if event.allday and event.start_date != date_deadline:
|
||||
event.start = event.start + (date_deadline - event.start_date)
|
||||
# otherwise: we have to check if day did change, based on TZ
|
||||
elif not event.allday:
|
||||
# old start in user timezone
|
||||
old_deadline_dt = pytz.utc.localize(event.start).astimezone(pytz.timezone(user_tz))
|
||||
date_diff = date_deadline - old_deadline_dt.date()
|
||||
event.start = event.start + date_diff
|
||||
|
||||
return res
|
||||
|
||||
def action_create_calendar_event(self):
|
||||
self.ensure_one()
|
||||
|
|
@ -17,32 +41,33 @@ class MailActivity(models.Model):
|
|||
'default_activity_type_id': self.activity_type_id.id,
|
||||
'default_res_id': self.env.context.get('default_res_id'),
|
||||
'default_res_model': self.env.context.get('default_res_model'),
|
||||
'default_name': self.summary or self.res_name,
|
||||
'default_name': self.res_name,
|
||||
'default_description': self.note if not is_html_empty(self.note) else '',
|
||||
'default_activity_ids': [(6, 0, self.ids)],
|
||||
'orig_activity_ids': self,
|
||||
'default_partner_ids': self.user_id.partner_id.ids,
|
||||
'default_user_id': self.user_id.id,
|
||||
'initial_date': self.date_deadline,
|
||||
'default_calendar_event_id': self.calendar_event_id.id,
|
||||
'orig_activity_ids': self.ids,
|
||||
'return_to_parent_breadcrumb': True,
|
||||
}
|
||||
return action
|
||||
|
||||
def _action_done(self, feedback=False, attachment_ids=False):
|
||||
events = self.calendar_event_id
|
||||
# To avoid the feedback to be included in the activity note (due to the synchronization in event.write
|
||||
# that updates the related activity note each time the event description is updated),
|
||||
# when the activity is written as a note in the chatter in _action_done (leading to duplicate feedback),
|
||||
# we call super before updating the description. As self is deleted in super, we load the related events before.
|
||||
messages, activities = super(MailActivity, self)._action_done(feedback=feedback, attachment_ids=attachment_ids)
|
||||
# Add feedback to the internal event 'notes', which is not synchronized with the activity's 'note'
|
||||
if feedback:
|
||||
for event in events:
|
||||
description = event.description
|
||||
description = '%s<br />%s' % (
|
||||
description if not tools.is_html_empty(description) else '',
|
||||
_('Feedback: %(feedback)s', feedback=tools.plaintext2html(feedback)) if feedback else '',
|
||||
)
|
||||
event.write({'description': description})
|
||||
return messages, activities
|
||||
for event in self.calendar_event_id:
|
||||
notes = event.notes if not tools.is_html_empty(event.notes) else ''
|
||||
notes_feedback = _('Feedback: %s', tools.plaintext2html(feedback))
|
||||
notes = f'{notes}<br />{notes_feedback}'
|
||||
event.write({'notes': notes})
|
||||
return super()._action_done(feedback=feedback, attachment_ids=attachment_ids)
|
||||
|
||||
def unlink_w_meeting(self):
|
||||
events = self.mapped('calendar_event_id')
|
||||
res = self.unlink()
|
||||
events.unlink()
|
||||
return res
|
||||
|
||||
def _to_store_defaults(self, target):
|
||||
return super()._to_store_defaults(target) + [Store.One("calendar_event_id", [])]
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class MailActivityMixin(models.AbstractModel):
|
||||
_inherit = 'mail.activity.mixin'
|
||||
|
||||
|
|
@ -15,4 +15,6 @@ class MailActivityMixin(models.AbstractModel):
|
|||
"""This computes the calendar event of the next activity.
|
||||
It evaluates to false if there is no such event."""
|
||||
for record in self:
|
||||
record.activity_calendar_event_id = fields.first(record.activity_ids).calendar_event_id
|
||||
activities = record.activity_ids
|
||||
activity = next(iter(activities), activities)
|
||||
record.activity_calendar_event_id = activity.calendar_event_id
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, fields
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.tools import SQL
|
||||
|
||||
|
||||
class Partner(models.Model):
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
meeting_count = fields.Integer("# Meetings", compute='_compute_meeting_count')
|
||||
|
|
@ -23,20 +24,21 @@ class Partner(models.Model):
|
|||
|
||||
def _compute_meeting(self):
|
||||
if self.ids:
|
||||
all_partners = self.with_context(active_test=False).search([('id', 'child_of', self.ids)])
|
||||
# prefetch 'parent_id'
|
||||
all_partners = self.with_context(active_test=False).search_fetch(
|
||||
[('id', 'child_of', self.ids)], ['parent_id'],
|
||||
)
|
||||
|
||||
event_id = self.env['calendar.event']._search([]) # ir.rules will be applied
|
||||
subquery_string, subquery_params = event_id.select()
|
||||
subquery = self.env.cr.mogrify(subquery_string, subquery_params).decode()
|
||||
|
||||
self.env.cr.execute("""
|
||||
query = self.env['calendar.event']._search([]) # ir.rules will be applied
|
||||
meeting_data = self.env.execute_query(SQL("""
|
||||
SELECT res_partner_id, calendar_event_id, count(1)
|
||||
FROM calendar_event_res_partner_rel
|
||||
WHERE res_partner_id IN %s AND calendar_event_id IN ({})
|
||||
WHERE res_partner_id IN %s AND calendar_event_id IN %s
|
||||
GROUP BY res_partner_id, calendar_event_id
|
||||
""".format(subquery), [tuple(p["id"] for p in all_partners)])
|
||||
|
||||
meeting_data = self.env.cr.fetchall()
|
||||
""",
|
||||
all_partners._ids,
|
||||
query.subselect(),
|
||||
))
|
||||
|
||||
# Create a dict {partner_id: event_ids} and fill with events linked to the partner
|
||||
meetings = {}
|
||||
|
|
@ -44,20 +46,28 @@ class Partner(models.Model):
|
|||
meetings.setdefault(p_id, set()).add(m_id)
|
||||
|
||||
# Add the events linked to the children of the partner
|
||||
for meeting_pid in set(meetings):
|
||||
partner = self.browse(meeting_pid)
|
||||
while partner:
|
||||
for p in self.browse(meetings.keys()):
|
||||
partner = p
|
||||
while partner.parent_id:
|
||||
partner = partner.parent_id
|
||||
if partner in self:
|
||||
meetings[partner.id] = meetings.get(partner.id, set()) | meetings[meeting_pid]
|
||||
meetings[partner.id] = meetings.get(partner.id, set()) | meetings[p.id]
|
||||
return {p_id: list(meetings.get(p_id, set())) for p_id in self.ids}
|
||||
return {}
|
||||
|
||||
def _compute_application_statistics_hook(self):
|
||||
data_list = super()._compute_application_statistics_hook()
|
||||
for partner in self.filtered('meeting_count'):
|
||||
stat_info = {'iconClass': 'fa-calendar', 'value': partner.meeting_count, 'label': _('Meetings'), 'tagClass': 'o_tag_color_3'}
|
||||
data_list[partner.id].append(stat_info)
|
||||
return data_list
|
||||
|
||||
def get_attendee_detail(self, meeting_ids):
|
||||
""" Return a list of dict of the given meetings with the attendees details
|
||||
Used by:
|
||||
- base_calendar.js : Many2ManyAttendee
|
||||
- calendar_model.js (calendar.CalendarModel)
|
||||
|
||||
- many2many_attendee.js: Many2ManyAttendee
|
||||
- calendar_model.js (calendar.CalendarModel)
|
||||
"""
|
||||
attendees_details = []
|
||||
meetings = self.env['calendar.event'].browse(meeting_ids)
|
||||
|
|
@ -92,3 +102,21 @@ class Partner(models.Model):
|
|||
}
|
||||
action['domain'] = ['|', ('id', 'in', self._compute_meeting()[self.id]), ('partner_ids', 'in', self.ids)]
|
||||
return action
|
||||
|
||||
def _get_busy_calendar_events(self, start_datetime, end_datetime):
|
||||
"""Get a mapping from partner id to attended events intersecting with the time interval.
|
||||
|
||||
:rtype: dict[int, <calendar.event>]
|
||||
"""
|
||||
events = self.env['calendar.event'].search([
|
||||
('stop', '>=', start_datetime.replace(tzinfo=None)),
|
||||
('start', '<=', end_datetime.replace(tzinfo=None)),
|
||||
('partner_ids', 'in', self.ids),
|
||||
('show_as', '=', 'busy'),
|
||||
])
|
||||
|
||||
event_by_partner_id = defaultdict(lambda: self.env['calendar.event'])
|
||||
for event in events:
|
||||
for partner in event.partner_ids:
|
||||
event_by_partner_id[partner.id] |= event
|
||||
return dict(event_by_partner_id)
|
||||
|
|
|
|||
|
|
@ -3,12 +3,102 @@
|
|||
import datetime
|
||||
|
||||
from odoo import api, fields, models, modules, _
|
||||
from odoo.exceptions import AccessError
|
||||
|
||||
from pytz import timezone, UTC
|
||||
|
||||
|
||||
class Users(models.Model):
|
||||
class ResUsers(models.Model):
|
||||
_inherit = 'res.users'
|
||||
|
||||
calendar_default_privacy = fields.Selection(
|
||||
[('public', 'Public by default'),
|
||||
('private', 'Private by default'),
|
||||
('confidential', 'Internal users only')],
|
||||
compute="_compute_calendar_default_privacy",
|
||||
inverse="_inverse_calendar_res_users_settings",
|
||||
)
|
||||
|
||||
@property
|
||||
def SELF_READABLE_FIELDS(self):
|
||||
return super().SELF_READABLE_FIELDS + ['calendar_default_privacy']
|
||||
|
||||
@property
|
||||
def SELF_WRITEABLE_FIELDS(self):
|
||||
return super().SELF_WRITEABLE_FIELDS + ['calendar_default_privacy']
|
||||
|
||||
def get_selected_calendars_partner_ids(self, include_user=True):
|
||||
"""
|
||||
Retrieves the partner IDs of the attendees selected in the calendar view.
|
||||
|
||||
:param bool include_user: Determines whether to include the current user's partner ID in the results.
|
||||
:return: A list of integer IDs representing the partners selected in the calendar view.
|
||||
If 'include_user' is True, the list will also include the current user's partner ID.
|
||||
:rtype: list
|
||||
"""
|
||||
self.ensure_one()
|
||||
partner_ids = self.env['calendar.filters'].search([
|
||||
('user_id', '=', self.id),
|
||||
('partner_checked', '=', True)
|
||||
]).partner_id.ids
|
||||
|
||||
if include_user:
|
||||
partner_ids += [self.env.user.partner_id.id]
|
||||
return partner_ids
|
||||
|
||||
@api.model
|
||||
def _default_user_calendar_default_privacy(self):
|
||||
""" Get the calendar default privacy from the Default User Template, set public as default. """
|
||||
return self.env['ir.config_parameter'].sudo().get_param('calendar.default_privacy', 'public')
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
""" Set the calendar default privacy as the same as Default User Template when defined. """
|
||||
default_privacy = self._default_user_calendar_default_privacy()
|
||||
# Update the dictionaries in vals_list with the calendar default privacy.
|
||||
for vals_dict in vals_list:
|
||||
if not vals_dict.get('calendar_default_privacy'):
|
||||
vals_dict.update(calendar_default_privacy=default_privacy)
|
||||
|
||||
res = super().create(vals_list)
|
||||
return res
|
||||
|
||||
def write(self, vals):
|
||||
""" Forbid the calendar default privacy update from different users for keeping private events secured. """
|
||||
privacy_update = 'calendar_default_privacy' in vals
|
||||
if privacy_update and self != self.env.user:
|
||||
raise AccessError(_("You are not allowed to change the calendar default privacy of another user due to privacy constraints."))
|
||||
return super().write(vals)
|
||||
|
||||
@api.depends("res_users_settings_id.calendar_default_privacy")
|
||||
def _compute_calendar_default_privacy(self):
|
||||
"""
|
||||
Compute the calendar default privacy of the users, pointing to its ResUsersSettings.
|
||||
When any user doesn't have its setting from ResUsersSettings defined, fallback to Default User Template's.
|
||||
"""
|
||||
fallback_default_privacy = 'public'
|
||||
# sudo: any user has access to other users calendar_default_privacy setting
|
||||
if any(not user.sudo().res_users_settings_id.calendar_default_privacy for user in self):
|
||||
fallback_default_privacy = self._default_user_calendar_default_privacy()
|
||||
|
||||
for user in self:
|
||||
user.calendar_default_privacy = user.sudo().res_users_settings_id.calendar_default_privacy or fallback_default_privacy
|
||||
|
||||
def _inverse_calendar_res_users_settings(self):
|
||||
"""
|
||||
Updates the values of the calendar fields in 'res_users_settings_ids' to have the same values as their related
|
||||
fields in 'res.users'. If there is no 'res.users.settings' record for the user, then the record is created.
|
||||
"""
|
||||
for user in self.filtered(lambda user: user._is_internal()):
|
||||
settings = self.env["res.users.settings"].sudo()._find_or_create_for_user(user)
|
||||
configuration = {field: user[field] for field in self._get_user_calendar_configuration_fields()}
|
||||
settings.sudo().update(configuration)
|
||||
|
||||
@api.model
|
||||
def _get_user_calendar_configuration_fields(self) -> list[str]:
|
||||
""" Return the list of configurable fields for the user related to the res.users.settings table. """
|
||||
return ['calendar_default_privacy']
|
||||
|
||||
def _systray_get_calendar_event_domain(self):
|
||||
# Determine the domain for which the users should be notified. This method sends notification to
|
||||
# events occurring between now and the end of the day. Note that "now" needs to be computed in the
|
||||
|
|
@ -37,7 +127,7 @@ class Users(models.Model):
|
|||
# | | <--- `stop_dt_utc` = `stop_dt` if user lives in an area of West longitude (positive shift compared to UTC, America for example)
|
||||
# | |
|
||||
start_dt_utc = start_dt = datetime.datetime.now(UTC)
|
||||
stop_dt_utc = UTC.localize(datetime.datetime.combine(start_dt.date(), datetime.time.max))
|
||||
stop_dt_utc = UTC.localize(datetime.datetime.combine(start_dt_utc.date(), datetime.time.max))
|
||||
|
||||
tz = self.env.user.tz
|
||||
if tz:
|
||||
|
|
@ -48,6 +138,11 @@ class Users(models.Model):
|
|||
|
||||
start_date = start_dt.date()
|
||||
|
||||
current_user_non_declined_attendee_ids = self.env['calendar.attendee']._search([
|
||||
('partner_id', '=', self.env.user.partner_id.id),
|
||||
('state', '!=', 'declined'),
|
||||
])
|
||||
|
||||
return ['&', '|',
|
||||
'&',
|
||||
'|',
|
||||
|
|
@ -57,17 +152,16 @@ class Users(models.Model):
|
|||
'&',
|
||||
['allday', '=', True],
|
||||
['start_date', '=', fields.Date.to_string(start_date)],
|
||||
('attendee_ids.partner_id', '=', self.env.user.partner_id.id)]
|
||||
('attendee_ids', 'in', current_user_non_declined_attendee_ids)]
|
||||
|
||||
@api.model
|
||||
def systray_get_activities(self):
|
||||
res = super(Users, self).systray_get_activities()
|
||||
|
||||
meetings_lines = self.env['calendar.event'].search_read(
|
||||
def _get_activity_groups(self):
|
||||
res = super()._get_activity_groups()
|
||||
EventModel = self.env['calendar.event']
|
||||
meetings_lines = EventModel.search_read(
|
||||
self._systray_get_calendar_event_domain(),
|
||||
['id', 'start', 'name', 'allday', 'attendee_status'],
|
||||
['id', 'start', 'name', 'allday'],
|
||||
order='start')
|
||||
meetings_lines = [line for line in meetings_lines if line['attendee_status'] != 'declined']
|
||||
if meetings_lines:
|
||||
meeting_label = _("Today's Meetings")
|
||||
meetings_systray = {
|
||||
|
|
@ -75,9 +169,25 @@ class Users(models.Model):
|
|||
'type': 'meeting',
|
||||
'name': meeting_label,
|
||||
'model': 'calendar.event',
|
||||
'icon': modules.module.get_module_icon(self.env['calendar.event']._original_module),
|
||||
'icon': modules.module.get_module_icon(EventModel._original_module),
|
||||
'domain': [('active', 'in', [True, False])],
|
||||
'meetings': meetings_lines,
|
||||
"view_type": EventModel._systray_view,
|
||||
}
|
||||
res.insert(0, meetings_systray)
|
||||
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def check_calendar_credentials(self):
|
||||
return {}
|
||||
|
||||
def check_synchronization_status(self):
|
||||
return {}
|
||||
|
||||
def _has_any_active_synchronization(self):
|
||||
"""
|
||||
Overridable method for checking if user has any synchronization active in inherited modules.
|
||||
|
||||
:return: boolean indicating if any synchronization is active.
|
||||
"""
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ResUsersSettings(models.Model):
|
||||
_inherit = "res.users.settings"
|
||||
|
||||
# Calendar module settings.
|
||||
calendar_default_privacy = fields.Selection(
|
||||
[('public', 'Public'),
|
||||
('private', 'Private'),
|
||||
('confidential', 'Only internal users')],
|
||||
'Calendar Default Privacy', default='public', required=True,
|
||||
store=True, readonly=False, help="Default privacy setting for whom the calendar events will be visible."
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _get_fields_blacklist(self):
|
||||
""" Get list of calendar fields that won't be formatted in session_info. """
|
||||
calendar_fields_blacklist = ['calendar_default_privacy']
|
||||
return super()._get_fields_blacklist() + calendar_fields_blacklist
|
||||
11
odoo-bringout-oca-ocb-calendar/calendar/models/utils.py
Normal file
11
odoo-bringout-oca-ocb-calendar/calendar/models/utils.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
from odoo.tools.intervals import Intervals
|
||||
|
||||
|
||||
def interval_from_events(event_ids):
|
||||
"""Group events with contiguous and/or overlapping time slots.
|
||||
|
||||
Can be used to avoid doing time-related querries on long stretches of time with no relevant event.
|
||||
:param <calendar.event> event_ids: Any recordset of events
|
||||
:return Intervals|Iterable[tuple[datetime, datetime, <calendar.event>]]:
|
||||
"""
|
||||
return Intervals([(event.start, event.stop, event) for event in event_ids if event.start and event.stop])
|
||||
Loading…
Add table
Add a link
Reference in a new issue