19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:32:34 +01:00
parent 5faf7397c5
commit 2696f14ed7
721 changed files with 220375 additions and 91221 deletions

View file

@ -1,13 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_access_rights
from . import test_attendees
from . import test_calendar
from . import test_calendar_activity
from . import test_calendar_controller
from . import test_calendar_recurrent_event_case2
from . import test_calendar_tour
from . import test_event_recurrence
from . import test_event_notifications
from . import test_mail_activity_mixin
from . import test_res_partner
from . import test_recurrence_rule
from . import test_res_users

View file

@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, timedelta
from odoo.tests.common import TransactionCase, new_test_user
from odoo.tests import Form
from odoo.exceptions import AccessError
from odoo.tools import mute_logger
@ -19,6 +19,7 @@ class TestAccessRights(TransactionCase):
cls.george = new_test_user(cls.env, login='george', groups='base.group_user')
cls.portal = new_test_user(cls.env, login='pot', groups='base.group_portal')
cls.admin_user = new_test_user(cls.env, login='admin_user', groups='base.group_partner_manager,base.group_user')
cls.admin_system_user = new_test_user(cls.env, login='admin_system_user', groups='base.group_system')
def create_event(self, user, **values):
return self.env['calendar.event'].with_user(user).create({
@ -94,32 +95,32 @@ class TestAccessRights(TransactionCase):
privacy='public',
location='In Hell',
)
# invalidate cache before reading, otherwise read() might leak private data
self.env.invalidate_all()
[private_location, public_location] = self.read_event(self.raoul, private + public, 'location')
self.assertFalse(private_location, "Private value should be obfuscated")
self.assertEqual(public_location, 'In Hell', "Public value should not be obfuscated")
def test_read_group_public(self):
event = self.create_event(self.john)
data = self.env['calendar.event'].with_user(self.raoul).read_group([('id', '=', event.id)], fields=['start'], groupby='start')
data = self.env['calendar.event'].with_user(self.raoul)._read_group([('id', '=', event.id)], groupby=['start:month'])
self.assertTrue(data, "It should be able to read group")
data = self.env['calendar.event'].with_user(self.raoul).read_group([('id', '=', event.id)], fields=['name'],
groupby='name')
data = self.env['calendar.event'].with_user(self.raoul)._read_group([('id', '=', event.id)], groupby=['name'])
self.assertTrue(data, "It should be able to read group")
def test_read_group_private(self):
event = self.create_event(self.john, privacy='private')
result = self.env['calendar.event'].with_user(self.raoul).read_group([('id', '=', event.id)], fields=['name'], groupby='name')
result = self.env['calendar.event'].with_user(self.raoul)._read_group([('id', '=', event.id)], groupby=['name'])
self.assertFalse(result, "Private events should not be fetched")
def test_read_group_agg(self):
event = self.create_event(self.john)
data = self.env['calendar.event'].with_user(self.raoul).read_group([('id', '=', event.id)], fields=['start'], groupby='start:week')
data = self.env['calendar.event'].with_user(self.raoul)._read_group([('id', '=', event.id)], groupby=['start:week'])
self.assertTrue(data, "It should be able to read group")
def test_read_group_list(self):
event = self.create_event(self.john)
data = self.env['calendar.event'].with_user(self.raoul).read_group([('id', '=', event.id)], fields=['start'], groupby=['start'])
data = self.env['calendar.event'].with_user(self.raoul)._read_group([('id', '=', event.id)], groupby=['start:month'])
self.assertTrue(data, "It should be able to read group")
def test_private_attendee(self):
@ -158,69 +159,216 @@ class TestAccessRights(TransactionCase):
'stop': datetime.now() + timedelta(days=2, hours=2),
})
def test_event_default_privacy_as_private(self):
""" Check the privacy of events with owner's event default privacy as 'private'. """
# Set organizer default privacy as 'private' and create event privacies default, public, private and confidential.
self.george.with_user(self.george).calendar_default_privacy = 'private'
default_event = self.create_event(self.george)
public_event = self.create_event(self.george, privacy='public')
private_event = self.create_event(self.george, privacy='private')
confidential_event = self.create_event(self.george, privacy='confidential')
# With another user who is not an event attendee, try accessing the events.
query_default_event = self.env['calendar.event'].with_user(self.raoul)._read_group([('id', '=', default_event.id)], groupby=['name'])
query_public_event = self.env['calendar.event'].with_user(self.raoul)._read_group([('id', '=', public_event.id)], groupby=['name'])
query_private_event = self.env['calendar.event'].with_user(self.raoul)._read_group([('id', '=', private_event.id)], groupby=['name'])
query_confidential_event = self.env['calendar.event'].with_user(self.raoul)._read_group([('id', '=', confidential_event.id)], groupby=['name'])
# Ensure that each event is accessible or not according to its privacy.
self.assertFalse(query_default_event, "Event must be inaccessible because the user has default privacy as 'private'.")
self.assertTrue(query_public_event, "Public event must be accessible to other users.")
self.assertFalse(query_private_event, "Private event must be inaccessible to other users.")
self.assertTrue(query_confidential_event, "Confidential event must be accessible to other internal users.")
def test_edit_private_event_of_other_user(self):
"""
Ensure that it is not possible editing the private event of another user when the current user is not an
attendee/organizer of that event. Attendees should be able to edit it, others will receive AccessError on write.
"""
def ensure_user_can_update_event(self, event, user):
event.with_user(user).write({'name': user.name})
self.assertEqual(event.name, user.name, 'Event name should be updated by user %s' % user.name)
# Prepare events attendees/partners including organizer (john) and another user (raoul).
events_attendees = [
(0, 0, {'partner_id': self.john.partner_id.id, 'state': 'accepted'}),
(0, 0, {'partner_id': self.raoul.partner_id.id, 'state': 'accepted'})
]
events_partners = [self.john.partner_id.id, self.raoul.partner_id.id]
# Set calendar default privacy as private and create a normal event, only attendees/organizer can edit it.
self.john.with_user(self.john).calendar_default_privacy = 'private'
johns_default_privacy_event = self.create_event(self.john, name='my event with default privacy', attendee_ids=events_attendees, partner_ids=events_partners)
ensure_user_can_update_event(self, johns_default_privacy_event, self.john)
ensure_user_can_update_event(self, johns_default_privacy_event, self.raoul)
with self.assertRaises(AccessError):
self.assertEqual(len(self.john.res_users_settings_id), 1, "Res Users Settings for the user is not defined.")
self.assertEqual(self.john.res_users_settings_id.calendar_default_privacy, 'private', "Privacy field update was lost.")
johns_default_privacy_event.with_user(self.george).write({'name': 'blocked-update-by-non-attendee'})
# Set calendar default privacy as public and create a private event, only attendees/organizer can edit it.
self.john.with_user(self.john).calendar_default_privacy = 'public'
johns_private_event = self.create_event(self.john, name='my private event', privacy='private', attendee_ids=events_attendees, partner_ids=events_partners)
ensure_user_can_update_event(self, johns_private_event, self.john)
ensure_user_can_update_event(self, johns_private_event, self.raoul)
with self.assertRaises(AccessError):
self.assertEqual(len(self.john.res_users_settings_id), 1, "Res Users Settings for the user is not defined.")
self.assertEqual(self.john.res_users_settings_id.calendar_default_privacy, 'public', "Privacy field update was lost.")
johns_private_event.with_user(self.george).write({'name': 'blocked-update-by-non-attendee'})
def test_admin_cant_fetch_uninvited_private_events(self):
"""
Administrators must not be able to fetch information from private events which
they are not attending (i.e. events which it is not an event partner). The privacy
of the event information must always be kept. Public events can be read normally.
"""
john_private_evt = self.create_event(self.john, name='priv', privacy='private', location='loc_1', description='priv')
john_public_evt = self.create_event(self.john, name='pub', privacy='public', location='loc_2', description='pub')
self.env.invalidate_all()
# For the private event, ensure that no private field can be read, such as: 'name', 'location' and 'description'.
for (field, value) in [('name', 'Busy'), ('location', False), ('description', False)]:
hidden_information = self.read_event(self.admin_user, john_private_evt, field)
self.assertEqual(hidden_information, value, "The field '%s' information must be hidden, even for uninvited admins." % field)
# For the public event, ensure that the same fields can be read by the admin.
for (field, value) in [
('name', 'pub'),
('location', 'loc_2'),
('description', '<div>pub<br>'
'<strong>Organized by</strong><br>'
'john (base.group_user)<br><a href="mailto:j.j@example.com">j.j@example.com</a><br><br>'
'<strong>Contact Details</strong><br>'
'george (base.group_user)<br><a href="mailto:g.g@example.com">g.g@example.com</a></div>',
),
]:
field_information = self.read_event(self.admin_user, john_public_evt, field)
self.assertEqual(str(field_information), value, "The field '%s' information must be readable by the admin." % field)
def test_admin_cant_edit_uninvited_private_events(self):
"""
Administrators must not be able to edit private events that they are not attending.
The event is property of the organizer and its attendees only (for private events in the backend).
"""
john_private_evt = self.create_event(self.john, name='priv', privacy='private', location='loc_1', description='priv')
# Ensure that uninvited admin can not edit the event since it is not an event partner (attendee).
with self.assertRaises(AccessError):
john_private_evt.with_user(self.admin_user)._compute_user_can_edit()
# Ensure that AccessError is raised when trying to update the uninvited event.
with self.assertRaises(AccessError):
john_private_evt.with_user(self.admin_user).write({'name': 'forbidden-update'})
def test_admin_edit_uninvited_non_private_events(self):
"""
Administrators must be able to edit (public, confidential) events that they are not attending.
This feature is widely used for customers since it is useful editing normal user's events on their behalf.
"""
for privacy in ['public', 'confidential']:
john_event = self.create_event(self.john, name='event', privacy=privacy, location='loc')
# Ensure that uninvited admin can edit this type of event.
john_event.with_user(self.admin_user)._compute_user_can_edit()
self.assertTrue(john_event.user_can_edit, f"Event of type {privacy} must be editable by uninvited admins.")
john_event.with_user(self.admin_user).write({'name': 'update'})
self.assertEqual(john_event.name, 'update', f"Simple write must be allowed for uninvited admins in {privacy} events.")
def test_hide_sensitive_fields_private_events_from_uninvited_admins(self):
"""
Ensure that it is not possible fetching sensitive fields for uninvited administrators,
i.e. admins who are not attendees of private events. Sensitive fields are fields that
could contain sensitive information, such as 'name', 'description', 'location', etc.
"""
sensitive_fields = [
'location', 'attendee_ids', 'partner_ids', 'description',
'videocall_location', 'categ_ids', 'message_ids',
]
sensitive_fields = {
'name', 'location', 'attendee_ids', 'description', 'alarm_ids',
'categ_ids', 'message_ids', 'partner_ids', 'videocall_location'
}
# Create event with all sensitive fields defined on it.
event_type = self.env['calendar.event.type'].create({'name': 'type'})
john_private_evt = self.create_event(
self.john,
name='private-event',
privacy='private',
location='private-location',
description='private-description',
attendee_status='accepted',
partner_ids=[self.john.partner_id.id, self.raoul.partner_id.id],
categ_ids=[event_type.id],
videocall_location='private-url.com'
)
john_private_evt.message_post(body="Message to be hidden.")
# Read the event as an uninvited administrator and ensure that the sensitive fields were hidden.
# Do the same for the search_read method: the information of sensitive fields must be hidden.
# Search_fetch the event as an uninvited administrator and ensure that the sensitive fields were hidden.
# This method goes through the _fetch_query method which covers all variations of read(), search_read() and export_data().
private_event_domain = ('id', '=', john_private_evt.id)
readed_event = john_private_evt.with_user(self.admin_user).read(sensitive_fields + ['name'])
search_readed_event = self.env['calendar.event'].with_user(self.admin_user).search_read([private_event_domain])
for event in [readed_event, search_readed_event]:
self.assertEqual(len(event), 1, "The event itself must be fetched since the record is not hidden from uninvited admins.")
self.assertEqual(event[0]['name'], "Busy", "Event name must be 'Busy', hiding the information from uninvited administrators.")
for field in sensitive_fields:
self.assertFalse(event[0][field], "Field %s contains private information, it must be hidden from uninvited administrators." % field)
# Ensure that methods like 'mapped', 'filtered', 'filtered_domain', '_search' and 'read_group' do not
# bypass the override of read, which will hide the private information of the events from uninvited administrators.
sensitive_stored_fields = ['name', 'location', 'description', 'videocall_location']
searched_event = self.env['calendar.event'].with_user(self.admin_user).search([private_event_domain])
for field in sensitive_stored_fields:
# For each method, fetch the information of the private event as an uninvited administrator.
check_mapped_event = searched_event.with_user(self.admin_user).mapped(field)
check_filtered_event = searched_event.with_user(self.admin_user).filtered(lambda ev: ev.id == john_private_evt.id)
check_filtered_domain = searched_event.with_user(self.admin_user).filtered_domain([private_event_domain])
check_search_query = self.env['calendar.event'].with_user(self.admin_user)._search([private_event_domain])
check_search_object = self.env['calendar.event'].with_user(self.admin_user).browse(check_search_query)
check_read_group = self.env['calendar.event'].with_user(self.admin_user).read_group([private_event_domain], [field], [field])
search_fetch_event = self.env['calendar.event'].with_user(self.admin_user).search_fetch([private_event_domain], sensitive_fields)
self.assertEqual(len(search_fetch_event), 1, "The event itself must be fetched since the record is not hidden from uninvited admins.")
for field in sensitive_fields:
if field == 'name':
# The 'name' field is manually changed to 'Busy' by default. We need to ensure it is shown as 'Busy' in all following methods.
self.assertEqual(check_mapped_event, ['Busy'], 'Private event name should be shown as Busy using the mapped function.')
self.assertEqual(check_filtered_event.name, 'Busy', 'Private event name should be shown as Busy using the filtered function.')
self.assertEqual(check_filtered_domain.name, 'Busy', 'Private event name should be shown as Busy using the filtered_domain function.')
self.assertEqual(check_search_object.name, 'Busy', 'Private event name should be shown as Busy using the _search function.')
self.assertEqual(search_fetch_event['name'], "Busy", "Event name must be 'Busy', hiding the information from uninvited administrators.")
else:
# The remaining private fields should be falsy for uninvited administrators.
self.assertFalse(check_mapped_event[0], 'Private event field "%s" should be hidden when using the mapped function.' % field)
self.assertFalse(check_filtered_event[field], 'Private event field "%s" should be hidden when using the filtered function.' % field)
self.assertFalse(check_filtered_domain[field], 'Private event field "%s" should be hidden when using the filtered_domain function.' % field)
self.assertFalse(check_search_object[field], 'Private event field "%s" should be hidden when using the _search function.' % field)
self.assertFalse(search_fetch_event[field], "Field %s contains private information, it must be hidden from uninvited administrators." % field)
# Private events are excluded from read_group by default, ensure that we do not fetch it.
self.assertFalse(len(check_read_group), 'Private event should be hidden using the function _read_group.')
def test_user_update_calendar_default_privacy(self):
"""
Ensure that administrators and normal users can update their own calendar
default privacy from the 'res.users' related field without throwing any error.
Updates from others users are blocked during write (except for Default User Template from admins).
"""
for privacy in ['public', 'private', 'confidential']:
# Update normal user and administrator 'calendar_default_privacy' simulating their own update.
self.john.with_user(self.john).write({'calendar_default_privacy': privacy})
self.admin_system_user.with_user(self.admin_system_user).write({'calendar_default_privacy': privacy})
self.assertEqual(self.john.calendar_default_privacy, privacy, 'Normal user must be able to update its calendar default privacy.')
self.assertEqual(self.admin_system_user.calendar_default_privacy, privacy, 'Admin must be able to update its calendar default privacy.')
# Update the Default 'calendar.default_privacy' as an administrator.
self.env['ir.config_parameter'].sudo().set_param("calendar.default_privacy", privacy)
# All calendar default privacy updates must be blocked during write.
with self.assertRaises(AccessError):
self.john.with_user(self.admin_system_user).write({'calendar_default_privacy': privacy})
with self.assertRaises(AccessError):
self.admin_system_user.with_user(self.john).write({'calendar_default_privacy': privacy})
def test_check_private_event_conditions_by_internal_user(self):
""" Ensure that internal user (non-admin) will see that admin's event is private. """
# Update admin calendar_default_privacy with 'private' option. Create private event for admin.
self.admin_user.with_user(self.admin_user).write({'calendar_default_privacy': 'private'})
admin_user_private_evt = self.create_event(self.admin_user, name='My Event', privacy=False, partner_ids=[self.admin_user.partner_id.id])
# Ensure that intrnal user will see the admin's event as private.
self.assertTrue(
admin_user_private_evt.with_user(self.raoul)._check_private_event_conditions(),
"Privacy check must be True since the new event is private (following John's calendar default privacy)."
)
def test_recurring_event_with_alarms_for_non_admin(self):
"""
Test that non-admin user can modify recurring events with alarms
without triggering access errors when accessing ir.cron.trigger records.
"""
alarm = self.env['calendar.alarm'].create({
'name': '15 minutes before',
'alarm_type': 'email',
'duration': 15,
'interval': 'minutes',
})
with Form(self.env['calendar.event'].with_user(self.john)) as event_form:
event_form.name = 'yearly Team Meeting'
event_form.start = datetime(2024, 1, 15, 9, 0)
event_form.stop = datetime(2024, 1, 15, 10, 0)
event_form.recurrency = True
event_form.rrule_type_ui = 'yearly'
event_form.count = 3
event_form.alarm_ids.add(alarm)
event_form.partner_ids.add(self.john.partner_id)
recurring_event = event_form.save()
self.assertTrue(recurring_event.recurrence_id, "Recurrence should be created")
with Form(recurring_event.with_user(self.john)) as form:
form.partner_ids.add(self.raoul.partner_id)
self.assertIn(self.raoul.partner_id.id, recurring_event.partner_ids.ids, "Partner should be added as attendee")

View file

@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from dateutil.relativedelta import relativedelta
from odoo.tests.common import TransactionCase, new_test_user, Form
from odoo.tests.common import TransactionCase, new_test_user
from odoo.tests import Form
from odoo import fields, Command
from freezegun import freeze_time
@ -37,7 +37,7 @@ class TestEventNotifications(TransactionCase):
})
self.assertTrue(event.attendee_ids, "It should have created an attendee")
self.assertEqual(event.attendee_ids.partner_id, self.partner, "It should be linked to the partner")
self.assertIn(self.partner, event.message_follower_ids.partner_id, "He should be follower of the event")
self.assertNotIn(self.partner, event.message_follower_ids.partner_id, "He should not be automatically added in followers, no need")
def test_attendee_added_create_with_specific_states(self):
"""
@ -99,6 +99,31 @@ class TestEventNotifications(TransactionCase):
self.assertNotIn(self.partner, self.event.message_follower_ids.partner_id, "It should have unsubscribed the partner")
self.assertIn(partner_bis, self.event.attendee_ids.partner_id, "It should have left the attendee")
def test_attendee_unavailabilities(self):
partner1, partner2 = self.env['res.partner'].create([{
'name': 'Test partner 1',
'email': 'test1@example.com',
}, {
'name': 'Test partner 2',
'email': 'test2@example.com',
}])
event1 = self.env['calendar.event'].create({
'name': 'Meeting 1',
'start': datetime(2020, 12, 13, 17),
'stop': datetime(2020, 12, 13, 22),
'partner_ids': [(4, partner.id) for partner in (partner1, partner2)]
})
self.assertFalse(event1.unavailable_partner_ids)
event2 = self.env['calendar.event'].create({
'name': 'Meeting 2',
'start': datetime(2020, 12, 13, 17),
'stop': datetime(2020, 12, 13, 22),
'partner_ids': [(4, partner1.id)],
})
event1.invalidate_recordset()
self.assertEqual(event1.unavailable_partner_ids, partner1)
self.assertEqual(event2.unavailable_partner_ids, partner1)
def test_attendee_without_email(self):
self.partner.email = False
self.event.partner_ids = self.partner

View file

@ -1,16 +1,14 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import datetime
from datetime import date, datetime, timedelta
import base64
import freezegun
from datetime import datetime, timedelta
from odoo import fields, Command
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
from odoo.tests import Form, tagged, new_test_user
from odoo.exceptions import AccessError
from odoo.tests import Form, new_test_user
from odoo.addons.base.tests.common import SavepointCaseWithUserDemo
import pytz
import re
import base64
class TestCalendar(SavepointCaseWithUserDemo):
@ -76,128 +74,6 @@ class TestCalendar(SavepointCaseWithUserDemo):
events = self.CalendarEvent.search(domain, order='start desc, name desc')
self.assertEqual(list(events), [foo2, bar2, bar1, foo1])
def test_event_activity(self):
# ensure meeting activity type exists
meeting_act_type = self.env['mail.activity.type'].search([('category', '=', 'meeting')], limit=1)
if not meeting_act_type:
meeting_act_type = self.env['mail.activity.type'].create({
'name': 'Meeting Test',
'category': 'meeting',
})
# have a test model inheriting from activities
test_record = self.env['res.partner'].create({
'name': 'Test',
})
now = datetime.now()
test_user = self.user_demo
test_name, test_description, test_description2 = 'Test-Meeting', 'Test-Description', 'NotTest'
test_note, test_note2 = '<p>Test-Description</p>', '<p>NotTest</p>'
# create using default_* keys
test_event = self.env['calendar.event'].with_user(test_user).with_context(
default_res_model=test_record._name,
default_res_id=test_record.id,
).create({
'name': test_name,
'description': test_description,
'start': fields.Datetime.to_string(now + timedelta(days=-1)),
'stop': fields.Datetime.to_string(now + timedelta(hours=2)),
'user_id': self.env.user.id,
})
self.assertEqual(test_event.res_model, test_record._name)
self.assertEqual(test_event.res_id, test_record.id)
self.assertEqual(len(test_record.activity_ids), 1)
self.assertEqual(test_record.activity_ids.summary, test_name)
self.assertEqual(test_record.activity_ids.note, test_note)
self.assertEqual(test_record.activity_ids.user_id, self.env.user)
self.assertEqual(test_record.activity_ids.date_deadline, (now + timedelta(days=-1)).date())
# updating event should update activity
test_event.write({
'name': '%s2' % test_name,
'description': test_description2,
'start': fields.Datetime.to_string(now + timedelta(days=-2)),
'user_id': test_user.id,
})
self.assertEqual(test_record.activity_ids.summary, '%s2' % test_name)
self.assertEqual(test_record.activity_ids.note, test_note2)
self.assertEqual(test_record.activity_ids.user_id, test_user)
self.assertEqual(test_record.activity_ids.date_deadline, (now + timedelta(days=-2)).date())
# update event with a description that have a special character and a new line
test_description3 = 'Test & <br> Description'
test_note3 = '<p>Test &amp; <br> Description</p>'
test_event.write({
'description': test_description3,
})
self.assertEqual(test_record.activity_ids.note, test_note3)
# deleting meeting should delete its activity
test_record.activity_ids.unlink()
self.assertEqual(self.env['calendar.event'], self.env['calendar.event'].search([('name', '=', test_name)]))
# create using active_model keys
test_event = self.env['calendar.event'].with_user(self.user_demo).with_context(
active_model=test_record._name,
active_id=test_record.id,
).create({
'name': test_name,
'description': test_description,
'start': now + timedelta(days=-1),
'stop': now + timedelta(hours=2),
'user_id': self.env.user.id,
})
self.assertEqual(test_event.res_model, test_record._name)
self.assertEqual(test_event.res_id, test_record.id)
self.assertEqual(len(test_record.activity_ids), 1)
def test_activity_event_multiple_meetings(self):
# Creating multiple meetings from an activity creates additional activities
# ensure meeting activity type exists
meeting_act_type = self.env['mail.activity.type'].search([('category', '=', 'meeting')], limit=1)
if not meeting_act_type:
meeting_act_type = self.env['mail.activity.type'].create({
'name': 'Meeting Test',
'category': 'meeting',
})
# have a test model inheriting from activities
test_record = self.env['res.partner'].create({
'name': 'Test',
})
activity_id = self.env['mail.activity'].create({
'summary': 'Meeting with partner',
'activity_type_id': meeting_act_type.id,
'res_model_id': self.env['ir.model']._get_id('res.partner'),
'res_id': test_record.id,
})
calendar_action = activity_id.with_context(default_res_model='res.partner', default_res_id=test_record.id).action_create_calendar_event()
event_1 = self.env['calendar.event'].with_context(calendar_action['context']).create({
'name': 'Meeting 1',
'start': datetime(2025, 3, 10, 17),
'stop': datetime(2025, 3, 10, 22),
})
self.assertEqual(event_1.activity_ids, activity_id)
total_activities = self.env['mail.activity'].search_count(domain=[])
event_2 = self.env['calendar.event'].with_context(calendar_action['context']).create({
'name': 'Meeting 2',
'start': datetime(2025, 3, 11, 17),
'stop': datetime(2025, 3, 11, 22),
})
self.assertEqual(event_1.activity_ids, activity_id, "Event 1's activity should still be the first activity")
self.assertEqual(activity_id.calendar_event_id, event_1, "The first activity's event should still be event 1")
self.assertEqual(total_activities + 1, self.env['mail.activity'].search_count(domain=[]), "1 more activity record should have been created (by event 2)")
self.assertNotEqual(event_2.activity_ids, activity_id, "Event 2's activity should not be the first activity")
self.assertEqual(event_2.activity_ids.activity_type_id, activity_id.activity_type_id, "Event 2's activity should be the same activity type as the first activity")
self.assertEqual(test_record.activity_ids, activity_id | event_2.activity_ids, "Resource record should now have both activities")
def test_event_allday(self):
self.env.user.tz = 'Pacific/Honolulu'
@ -235,8 +111,9 @@ class TestCalendar(SavepointCaseWithUserDemo):
self.assertEqual(d.minute, 30)
def test_recurring_ny(self):
self.env.user.tz = 'America/New_York'
f = Form(self.CalendarEvent.with_context(tz='America/New_York'))
self.user_demo.tz = 'America/New_York'
event = self.CalendarEvent.create({'user_id': self.user_demo.id, 'name': 'test', 'partner_ids': [Command.link(self.user_demo.partner_id.id)]})
f = Form(event.with_context(tz='America/New_York').with_user(self.user_demo))
f.name = 'test'
f.start = '2022-07-07 01:00:00' # This is in UTC. In NY, it corresponds to the 6th of july at 9pm.
f.recurrency = True
@ -248,76 +125,10 @@ class TestCalendar(SavepointCaseWithUserDemo):
self.assertEqual(f.end_type, "count", "The default value should be displayed")
self.assertEqual(f.rrule_type, "weekly", "The default value should be displayed")
def test_event_activity_timezone(self):
activty_type = self.env['mail.activity.type'].create({
'name': 'Meeting',
'category': 'meeting'
})
activity_id = self.env['mail.activity'].create({
'summary': 'Meeting with partner',
'activity_type_id': activty_type.id,
'res_model_id': self.env['ir.model']._get_id('res.partner'),
'res_id': self.env['res.partner'].create({'name': 'A Partner'}).id,
})
calendar_event = self.env['calendar.event'].create({
'name': 'Meeting with partner',
'activity_ids': [(6, False, activity_id.ids)],
'start': '2018-11-12 21:00:00',
'stop': '2018-11-13 00:00:00',
})
# Check output in UTC
self.assertEqual(str(activity_id.date_deadline), '2018-11-12')
# Check output in the user's tz
# write on the event to trigger sync of activities
calendar_event.with_context({'tz': 'Australia/Brisbane'}).write({
'start': '2018-11-12 21:00:00',
})
self.assertEqual(str(activity_id.date_deadline), '2018-11-13')
def test_event_allday_activity_timezone(self):
# Covers use case of commit eef4c3b48bcb4feac028bf640b545006dd0c9b91
# Also, read the comment in the code at calendar.event._inverse_dates
activty_type = self.env['mail.activity.type'].create({
'name': 'Meeting',
'category': 'meeting'
})
activity_id = self.env['mail.activity'].create({
'summary': 'Meeting with partner',
'activity_type_id': activty_type.id,
'res_model_id': self.env['ir.model']._get_id('res.partner'),
'res_id': self.env['res.partner'].create({'name': 'A Partner'}).id,
})
calendar_event = self.env['calendar.event'].create({
'name': 'All Day',
'start': "2018-10-16 00:00:00",
'start_date': "2018-10-16",
'stop': "2018-10-18 00:00:00",
'stop_date': "2018-10-18",
'allday': True,
'activity_ids': [(6, False, activity_id.ids)],
})
# Check output in UTC
self.assertEqual(str(activity_id.date_deadline), '2018-10-16')
# Check output in the user's tz
# write on the event to trigger sync of activities
calendar_event.with_context({'tz': 'Pacific/Honolulu'}).write({
'start': '2018-10-16 00:00:00',
'start_date': '2018-10-16',
})
self.assertEqual(str(activity_id.date_deadline), '2018-10-16')
@freezegun.freeze_time('2023-10-06 10:00:00')
def test_event_creation_mail(self):
"""
Freezegun used because we don't send mail for past events
Check that mail are sent to the attendees on event creation
Check that mail are sent to the added attendees on event edit
Check that mail are NOT sent to the attendees when the event date is past
@ -332,36 +143,39 @@ class TestCalendar(SavepointCaseWithUserDemo):
])
self.assertEqual(len(mail), 1)
def _test_emails_has_attachment(self, partners):
# check that every email has an attachment
def _test_emails_has_attachment(self, partners, attachments_names=["fileText_attachment.txt"]):
# check that every email has specified extra attachments
for partner in partners:
mail = self.env['mail.message'].sudo().search([
('notified_partner_ids', 'in', partner.id),
])
extra_attachment = mail.attachment_ids.filtered(lambda attachment: attachment.name == "fileText_attachment.txt")
self.assertEqual(len(extra_attachment), 1)
extra_attachments = mail.attachment_ids.filtered(lambda attachment: attachment.name in attachments_names)
self.assertEqual(len(extra_attachments), len(attachments_names))
attachment = self.env['ir.attachment'].create({
attachments = self.env['ir.attachment'].create([{
'datas': base64.b64encode(bytes("Event Attachment", 'utf-8')),
'name': 'fileText_attachment.txt',
'mimetype': 'text/plain'
})
self.env.ref('calendar.calendar_template_meeting_invitation').attachment_ids = attachment
}, {
'datas': base64.b64encode(bytes("Event Attachment 2", 'utf-8')),
'name': 'fileText_attachment_2.txt',
'mimetype': 'text/plain'
}])
self.env.ref('calendar.calendar_template_meeting_invitation').attachment_ids = attachments
partners = [
self.env['res.partner'].create({'name': 'testuser0', 'email': u'bob@example.com'}),
self.env['res.partner'].create({'name': 'testuser1', 'email': u'alice@example.com'}),
]
partner_ids = [(6, False, [p.id for p in partners]),]
now = fields.Datetime.context_timestamp(partners[0], fields.Datetime.now())
m = self.CalendarEvent.create({
'name': "mailTest1",
'allday': False,
'rrule': u'FREQ=DAILY;INTERVAL=1;COUNT=5',
'recurrency': True,
'partner_ids': partner_ids,
'start': fields.Datetime.to_string(now + timedelta(days=10)),
'stop': fields.Datetime.to_string(now + timedelta(days=15)),
'start': "2023-10-29 08:00:00",
'stop': "2023-11-03 08:00:00",
})
# every partner should have 1 mail sent
@ -389,13 +203,34 @@ class TestCalendar(SavepointCaseWithUserDemo):
'allday': False,
'recurrency': False,
'partner_ids': partner_ids,
'start': fields.Datetime.to_string(now - timedelta(days=10)),
'stop': fields.Datetime.to_string(now - timedelta(days=9)),
'start': "2023-10-04 08:00:00",
'stop': "2023-10-10 08:00:00",
})
# no more email should be sent
_test_one_mail_per_attendee(self, partners)
partner_staff, new_partner = self.env['res.partner'].create([{
'name': 'partner_staff',
'email': 'partner_staff@example.com',
}, {
'name': 'partner_created_on_the_spot_by_the_appointment_form',
'email': 'partner_created_on_the_spot_by_the_appointment_form@example.com',
}])
test_user = self.env['res.users'].with_context({'no_reset_password': True}).create({
'name': 'test_user',
'login': 'test_user',
'email': 'test_user@example.com',
})
self.CalendarEvent.with_user(self.env.ref('base.public_user')).sudo().create({
'name': "publicUserEvent",
'partner_ids': [(6, False, [partner_staff.id, new_partner.id])],
'start': "2023-10-06 12:00:00",
'stop': "2023-10-06 13:00:00",
'user_id': test_user.id,
})
_test_emails_has_attachment(self, partners=[partner_staff, new_partner], attachments_names=[a.name for a in attachments])
def test_event_creation_internal_user_invitation_ics(self):
""" Check that internal user can read invitation.ics attachment """
internal_user = new_test_user(self.env, login='internal_user', groups='base.group_user')
@ -473,82 +308,211 @@ class TestCalendar(SavepointCaseWithUserDemo):
})
self.assertTrue(set(new_partners) == set(self.event_tech_presentation.videocall_channel_id.channel_partner_ids.ids), 'new partners must be invited to the channel')
def test_search_current_attendee_status(self):
""" Test searching for events based on the current user's attendance status. """
# Create a second user to ensure the filter is specific to the current user
user_test = new_test_user(self.env, login='user_test_calendar_filter')
@tagged('post_install', '-at_install')
class TestCalendarTours(HttpCaseWithUserDemo):
def test_calendar_month_view_start_hour_displayed(self):
""" Test that the time is displayed in the month view. """
self.start_tour("/web", 'calendar_appointments_hour_tour', login="demo")
def test_calendar_delete_tour(self):
"""
Check that we can delete events with the "Everybody's calendars" filter.
"""
user_admin = self.env.ref('base.user_admin')
start = datetime.combine(date.today(), datetime.min.time()).replace(hour=9)
stop = datetime.combine(date.today(), datetime.min.time()).replace(hour=12)
event = self.env['calendar.event'].with_user(user_admin).create({
'name': 'Test Event',
'description': 'Test Description',
'start': start.strftime("%Y-%m-%d %H:%M:%S"),
'stop': stop.strftime("%Y-%m-%d %H:%M:%S"),
'duration': 3,
'location': 'Odoo S.A.',
'privacy': 'public',
'show_as': 'busy',
# Create events with different attendee statuses for both users
event_accepted = self.env['calendar.event'].create({
'name': 'Event Demo Accepted',
'start': datetime(2025, 1, 1, 10, 0),
'stop': datetime(2025, 1, 1, 11, 0),
'attendee_ids': [
Command.create({'partner_id': self.user_demo.partner_id.id, 'state': 'accepted'}),
Command.create({'partner_id': user_test.partner_id.id, 'state': 'needsAction'}),
]
})
event_declined = self.env['calendar.event'].create({
'name': 'Event Demo Declined',
'start': datetime(2025, 1, 2, 10, 0),
'stop': datetime(2025, 1, 2, 11, 0),
'attendee_ids': [
Command.create({'partner_id': self.user_demo.partner_id.id, 'state': 'declined'}),
Command.create({'partner_id': user_test.partner_id.id, 'state': 'accepted'}),
]
})
event_tentative = self.env['calendar.event'].create({
'name': 'Event Demo Tentative',
'start': datetime(2025, 1, 3, 10, 0),
'stop': datetime(2025, 1, 3, 11, 0),
'attendee_ids': [
Command.create({'partner_id': self.user_demo.partner_id.id, 'state': 'tentative'}),
Command.create({'partner_id': user_test.partner_id.id, 'state': 'declined'}),
]
})
event_other_user = self.env['calendar.event'].create({
'name': 'Event Other User Only',
'start': datetime(2025, 1, 4, 10, 0),
'stop': datetime(2025, 1, 4, 11, 0),
'attendee_ids': [
Command.create({'partner_id': user_test.partner_id.id, 'state': 'accepted'}),
]
})
action_id = self.env.ref('calendar.action_calendar_event')
url = "/web#action=" + str(action_id.id) + '&view_type=calendar'
self.start_tour(url, 'test_calendar_delete_tour', login='admin')
event = self.env['calendar.event'].search([('name', '=', 'Test Event')])
self.assertFalse(event) # Check if the event has been correctly deleted
def test_calendar_decline_tour(self):
"""
Check that we can decline events.
"""
user_admin = self.env.ref('base.user_admin')
# Perform searches as the demo user and assert the results
CalendarEvent_Demo = self.env['calendar.event'].with_user(self.user_demo)
# Search for 'Yes' (accepted)
accepted_events = CalendarEvent_Demo.search([('current_status', '=', 'accepted')])
self.assertEqual(accepted_events, event_accepted, "Should find only the event where the demo user has accepted.")
# Search for 'No' (declined)
declined_events = CalendarEvent_Demo.search([('current_status', '=', 'declined')])
self.assertEqual(declined_events, event_declined, "Should find only the event where the demo user has declined.")
# Search for 'Maybe' (tentative)
tentative_events = CalendarEvent_Demo.search([('current_status', '=', 'tentative')])
self.assertEqual(tentative_events, event_tentative, "Should find only the event where the demo user is tentative.")
# Search for events where status is not 'No' (declined)
not_declined_events = CalendarEvent_Demo.search([('current_status', '!=', 'declined')])
self.assertIn(event_accepted, not_declined_events, "Accepted events should be in the result.")
self.assertIn(event_tentative, not_declined_events, "Tentative events should be in the result.")
self.assertNotIn(event_declined, not_declined_events, "Declined events should NOT be in the result.")
self.assertNotIn(event_other_user, not_declined_events, "Events where the user is not an attendee should NOT be in the result.")
# Search using the 'in' operator
in_events = CalendarEvent_Demo.search([('current_status', 'in', ['accepted', 'tentative'])])
self.assertEqual(len(in_events), 2, "Should find two events for 'accepted' or 'tentative'.")
self.assertIn(event_accepted, in_events, "Should find the accepted event in the 'in' search.")
self.assertIn(event_tentative, in_events, "Should find the tentative event in the 'in' search.")
def test_event_duplication_allday(self):
"""Test that a calendar event is successfully duplicated with dates."""
# Create an event
calendar_event = self.env['calendar.event'].create({
'name': 'All Day',
'start': "2018-10-16 00:00:00",
'start_date': "2018-10-16",
'stop': "2018-10-18 00:00:00",
'stop_date': "2018-10-18",
'allday': True,
})
# Duplicate the event with explicit defaults for start_date and stop_date
new_calendar_event = calendar_event.copy()
# Ensure the copied event exists and retains the correct dates
self.assertTrue(new_calendar_event, "Event should be duplicated.")
self.assertEqual(new_calendar_event.start_date, calendar_event.start_date, "Start date should match the original.")
self.assertEqual(new_calendar_event.stop_date, calendar_event.stop_date, "Stop date should match the original.")
def test_event_privacy_domain(self):
"""Test privacy domain filtering in _read_group for events with user_id=False and default privacy (False)"""
now = datetime.now()
test_user = self.user_demo
self.env['calendar.event'].create([
{
'name': 'event_a',
'start': now + timedelta(days=-1),
'stop': now + timedelta(days=-1, hours=2),
'user_id': False,
'privacy': 'public',
},
{
'name': 'event_b',
'start': now + timedelta(days=1),
'stop': now + timedelta(days=1, hours=1),
'user_id': False,
'privacy': False,
},
{
'name': 'event_c',
'start': now + timedelta(days=-1, hours=3),
'stop': now + timedelta(days=-1, hours=5),
'user_id': False,
'privacy': 'private',
}
])
meetings = self.env['calendar.event'].with_user(test_user)
result = meetings._read_group(
domain=[['user_id', '=', False]],
aggregates=["__count", "duration:sum"],
groupby=['create_date:month']
)
# Verify privacy domain filtered out the private event only
total_visible_events = sum(group[1] for group in result)
self.assertEqual(total_visible_events, 2,
"Should see 2 events (public and no-privacy), private event filtered out")
def test_unauthorized_user_cannot_add_attendee(self):
""" Check that a user that doesn't have access to a private event cannot add attendees to it """
attendee_model = self.env['calendar.attendee'].with_user(self.user_demo.id)
# event_id in values
with self.assertRaises(AccessError):
attendee_model.create([{
'event_id': self.event_tech_presentation.id,
'partner_id': self.partner_demo.id,
}])
# event_id via context (default_event_id)
with self.assertRaises(AccessError):
attendee_model.with_context(default_event_id=self.event_tech_presentation.id).create([{
'partner_id': self.partner_demo.id,
}])
def test_default_duration(self):
# Check the default duration depending on various parameters
user_demo = self.user_demo
start = datetime.combine(date.today(), datetime.min.time()).replace(hour=9)
stop = datetime.combine(date.today(), datetime.min.time()).replace(hour=12)
event = self.env['calendar.event'].with_user(user_admin).create({
'name': 'Test Event',
'description': 'Test Description',
'start': start.strftime("%Y-%m-%d %H:%M:%S"),
'stop': stop.strftime("%Y-%m-%d %H:%M:%S"),
'duration': 3,
'location': 'Odoo S.A.',
'privacy': 'public',
'show_as': 'busy',
})
event.partner_ids = [Command.link(user_demo.partner_id.id)]
action_id = self.env.ref('calendar.action_calendar_event')
url = "/web#action=" + str(action_id.id) + '&view_type=calendar'
self.start_tour(url, 'test_calendar_decline_tour', login='demo')
attendee = self.env['calendar.attendee'].search([('event_id', '=', event.id), ('partner_id', '=', user_demo.partner_id.id)])
self.assertEqual(attendee.state, 'declined') # Check if the event has been correctly declined
second_company = self.env['res.company'].sudo().create({'name': "Second Company"})
def test_calendar_decline_with_everybody_filter_tour(self):
"""
Check that we can decline events with the "Everybody's calendars" filter.
"""
duration = self.env['calendar.event'].get_default_duration()
self.assertEqual(duration, 1, "By default, the duration is 1 hour")
IrDefault = self.env['ir.default'].sudo()
IrDefault.with_user(user_demo).set('calendar.event', 'duration', 2, user_id=True, company_id=False)
IrDefault.with_company(second_company).set('calendar.event', 'duration', 8, company_id=True)
duration = self.env['calendar.event'].with_user(user_demo).get_default_duration()
self.assertEqual(duration, 2, "Custom duration is 2 hours")
duration = self.env['calendar.event'].with_company(second_company).get_default_duration()
self.assertEqual(duration, 8, "Custom duration is 8 hours in the other company")
def test_discuss_videocall_not_ringing_with_event(self):
self.event_tech_presentation._set_discuss_videocall_location()
self.event_tech_presentation._create_videocall_channel()
self.event_tech_presentation.write(
{
"start": fields.Datetime.to_string(datetime.now() + timedelta(hours=2)),
}
)
partner1, partner2 = self.env["res.partner"].create(
[{"name": "Bob", "email": "bob@gm.co"}, {"name": "Jack", "email": "jack@gm.co"}]
)
new_partners = [partner1.id, partner2.id]
# invite partners to meeting
self.event_tech_presentation.write(
{"partner_ids": [Command.link(new_partner) for new_partner in new_partners]}
)
channel_member = self.event_tech_presentation.videocall_channel_id.channel_member_ids[0]
channel_member_2 = self.event_tech_presentation.videocall_channel_id.channel_member_ids[1]
channel_member._rtc_join_call()
self.assertFalse(channel_member_2.rtc_inviting_session_id)
def test_calendar_res_id_fallback_when_res_id_is_0(self):
user_admin = self.env.ref('base.user_admin')
user_demo = self.user_demo
start = datetime.combine(date.today(), datetime.min.time()).replace(hour=9)
stop = datetime.combine(date.today(), datetime.min.time()).replace(hour=12)
event = self.env['calendar.event'].with_user(user_admin).create({
'name': 'Test Event',
'description': 'Test Description',
'start': start.strftime("%Y-%m-%d %H:%M:%S"),
'stop': stop.strftime("%Y-%m-%d %H:%M:%S"),
'duration': 3,
'location': 'Odoo S.A.',
'privacy': 'public',
'show_as': 'busy',
context_defaults = {
'default_res_model': user_admin._name,
'default_res_id': user_admin.id,
}
self.env['mail.activity.type'].create({
'name': 'Meeting',
'category': 'meeting'
})
event.partner_ids = [Command.link(user_demo.partner_id.id)]
action_id = self.env.ref('calendar.action_calendar_event')
url = "/web#action=" + str(action_id.id) + '&view_type=calendar'
self.start_tour(url, 'test_calendar_decline_with_everybody_filter_tour', login='demo')
attendee = self.env['calendar.attendee'].search([('event_id', '=', event.id), ('partner_id', '=', user_demo.partner_id.id)])
self.assertEqual(attendee.state, 'declined') # Check if the event has been correctly declined
event = self.env['calendar.event'].with_user(user_admin).with_context(**context_defaults).create({
'name': 'All Day',
'start': "2018-10-16 00:00:00",
'start_date': "2018-10-16",
'stop': "2018-10-18 00:00:00",
'stop_date': "2018-10-18",
'allday': True,
'res_id': 0,
})
self.assertTrue(event.res_id)

View file

@ -0,0 +1,259 @@
from datetime import datetime, date, timedelta
from freezegun import freeze_time
from odoo.addons.mail.tests.common_activity import ActivityScheduleCase
from odoo.tests import tagged, users
@tagged('mail_activity_mixin', 'mail_activity', 'post_install', '-at_install')
class TestCalendarActivity(ActivityScheduleCase):
def test_event_activity(self):
# ensure meeting activity type exists
meeting_act_type = self.env['mail.activity.type'].search([('category', '=', 'meeting')], limit=1)
if not meeting_act_type:
meeting_act_type = self.env['mail.activity.type'].create({
'name': 'Meeting Test',
'category': 'meeting',
})
# have a test model inheriting from activities
test_record = self.env['res.partner'].create({
'name': 'Test',
})
now = datetime.now()
# test with escaping
test_name, test_description, test_description2 = 'Test-Meeting', 'Test-Description', 'Test & <br> Description'
test_note, test_note2 = '<p>Test-Description</p>', '<p>Test &amp; <br> Description</p>'
# create using default_* keys
test_event = self.env['calendar.event'].with_user(self.user_employee).with_context(
default_res_model=test_record._name,
default_res_id=test_record.id,
).create({
'name': test_name,
'description': test_description,
'start': now + timedelta(days=-1),
'stop': now + timedelta(hours=2),
'user_id': self.env.user.id,
})
self.assertEqual(test_event.res_model, test_record._name)
self.assertEqual(test_event.res_id, test_record.id)
self.assertEqual(len(test_record.activity_ids), 1)
self.assertEqual(test_record.activity_ids.summary, test_name)
self.assertEqual(test_record.activity_ids.note, test_note)
self.assertEqual(test_record.activity_ids.user_id, self.env.user)
self.assertEqual(test_record.activity_ids.date_deadline, (now + timedelta(days=-1)).date())
# updating event should update activity
test_event.write({
'name': '%s2' % test_name,
'description': test_description2,
'start': now + timedelta(days=-2),
'user_id': self.user_employee.id,
})
activity = test_record.activity_ids[0]
self.assertEqual(activity.summary, '%s2' % test_name)
self.assertEqual(activity.note, test_note2)
self.assertEqual(activity.user_id, self.user_employee)
self.assertEqual(activity.date_deadline, (now + timedelta(days=-2)).date())
# deleting meeting should delete its activity
test_record.activity_ids.unlink()
self.assertEqual(self.env['calendar.event'], self.env['calendar.event'].search([('name', '=', test_name)]))
# create using active_model keys
test_event = self.env['calendar.event'].with_user(self.user_employee).with_context(
active_model=test_record._name,
active_id=test_record.id,
).create({
'name': test_name,
'description': test_description,
'start': now + timedelta(days=-1),
'stop': now + timedelta(hours=2),
'user_id': self.env.user.id,
})
self.assertEqual(test_event.res_model, test_record._name)
self.assertEqual(test_event.res_id, test_record.id)
self.assertEqual(len(test_record.activity_ids), 1)
def test_activity_event_multiple_meetings(self):
# Creating multiple meetings from an activity creates additional activities
# ensure meeting activity type exists
meeting_act_type = self.env.ref('mail.mail_activity_data_meeting')
# have a test model inheriting from activities
test_record = self.env['res.partner'].create({
'name': 'Test',
})
activity_1 = self.env['mail.activity'].create({
'summary': 'Meeting 1 with partner',
'activity_type_id': meeting_act_type.id,
'res_model_id': self.env['ir.model']._get_id('res.partner'),
'res_id': test_record.id,
})
# default usage in successive create
event_1_1 = self.env['calendar.event'].with_context(default_activity_ids=[(6, 0, activity_1.ids)]).create({
'name': 'Meeting 1',
'start': datetime(2025, 3, 10, 17),
'stop': datetime(2025, 3, 10, 22),
})
self.assertEqual(event_1_1.activity_ids, activity_1)
self.assertEqual(activity_1.calendar_event_id, event_1_1)
self.assertEqual(activity_1.date_deadline, date(2025, 3, 10))
event_1_2 = self.env['calendar.event'].with_context(default_activity_ids=[(6, 0, activity_1.ids)]).create({
'name': 'Meeting 2',
'start': datetime(2025, 3, 12, 17),
'stop': datetime(2025, 3, 12, 22),
})
self.assertFalse(event_1_1.activity_ids, 'Changes activity ownership')
self.assertEqual(event_1_2.activity_ids, activity_1, 'Changes activity ownership')
self.assertEqual(activity_1.calendar_event_id, event_1_2)
self.assertEqual(activity_1.date_deadline, date(2025, 3, 12))
activity_2 = self.env['mail.activity'].create({
'summary': 'Meeting 2 with partner',
'activity_type_id': meeting_act_type.id,
'res_model_id': self.env['ir.model']._get_id('res.partner'),
'res_id': test_record.id,
})
existing_activities = self.env['mail.activity'].search([])
# specific action that creates activities instead of replacing
calendar_action = activity_2.with_context(default_res_model='res.partner', default_res_id=test_record.id).action_create_calendar_event()
event_2_1 = self.env['calendar.event'].with_context(calendar_action['context']).create({
'name': 'Meeting 1',
'start': datetime(2025, 4, 10, 17),
'stop': datetime(2025, 4, 10, 22),
})
self.assertEqual(event_2_1.activity_ids, activity_2)
self.assertEqual(activity_2.calendar_event_id, event_2_1)
self.assertEqual(activity_2.date_deadline, date(2025, 4, 10))
event_2_2 = self.env['calendar.event'].with_context(calendar_action['context']).create({
'name': 'Meeting 2',
'start': datetime(2025, 4, 11, 17),
'stop': datetime(2025, 4, 11, 22),
})
new_existing_activities = self.env['mail.activity'].search([])
new_activity = new_existing_activities - existing_activities
self.assertEqual(event_2_1.activity_ids, activity_2, "Event 1's activity should still be the first activity")
self.assertEqual(activity_2.calendar_event_id, event_2_1, "The first activity's event should still be event 1")
self.assertEqual(len(new_activity), 1, "1 more activity record should have been created (by event 2)")
self.assertEqual(event_2_2.activity_ids, new_activity, "Event 2's activity should not be the first activity")
self.assertEqual(event_2_2.activity_ids.activity_type_id, activity_2.activity_type_id, "Event 2's activity should be the same activity type as the first activity")
self.assertEqual(test_record.activity_ids, activity_1 + activity_2 + new_activity, "Resource record should now have all activities")
def test_event_activity_user_sync(self):
# ensure phonecall activity type exists
activty_type = self.env['mail.activity.type'].create({
'name': 'Call',
'category': 'phonecall'
})
activity = self.env['mail.activity'].create({
'summary': 'Call with Demo',
'activity_type_id': activty_type.id,
'note': 'Schedule call with Admin',
'res_model_id': self.env['ir.model']._get_id('res.partner'),
'res_id': self.env['res.partner'].create({'name': 'Test Partner'}).id,
'user_id': self.user_employee.id,
})
action_context = activity.action_create_calendar_event().get('context', {})
event_from_activity = self.env['calendar.event'].with_context(action_context).create({
'start': '2022-07-27 14:30:00',
'stop': '2022-07-27 16:30:00',
})
# Check that assignation of the activity hasn't changed, and event is having
# correct values set in attendee and organizer related fields
self.assertEqual(activity.user_id, self.user_employee)
self.assertEqual(event_from_activity.partner_ids, activity.user_id.partner_id)
self.assertEqual(event_from_activity.attendee_ids.partner_id, activity.user_id.partner_id)
self.assertEqual(event_from_activity.user_id, activity.user_id)
@users('employee')
def test_synchronize_activity_timezone(self):
activity_type = self.activity_type_todo.with_user(self.env.user)
with freeze_time(datetime(2025, 6, 19, 12, 44, 00)):
activity = self.env['mail.activity'].create({
'activity_type_id': activity_type.id,
'res_model_id': self.env['ir.model']._get_id('res.partner'),
'res_id': self.env['res.partner'].create({'name': 'A Partner'}).id,
'summary': 'Meeting with partner',
})
self.assertEqual(activity.date_deadline, date(2025, 6, 19))
with freeze_time(datetime(2025, 6, 19, 12, 44, 00)):
calendar_event = self.env['calendar.event'].create({
'activity_ids': [(6, 0, activity.ids)],
'name': 'Meeting with partner',
'start': datetime(2025, 6, 21, 21, 0, 0),
'stop': datetime(2025, 6, 22, 0, 0, 0),
})
# Check output in UTC
self.assertEqual(activity.date_deadline, date(2025, 6, 21))
# Check output in the user's tz
# write on the event to trigger sync of activities
calendar_event.with_context({'tz': 'Australia/Brisbane'}).write({
'start': datetime(2025, 6, 27, 21, 0, 0),
})
self.assertEqual(activity.date_deadline, date(2025, 6, 28),
'Next day due to timezone')
# now change from activity, timezone should be taken into account when
# converting into UTC for event
activity.with_context({'tz': 'Australia/Brisbane'}).date_deadline = date(2025, 6, 30)
self.assertEqual(calendar_event.start, datetime(2025, 6, 29, 21, 0, 0),
'Should apply diff in days, taking into account timezone')
def test_synchronize_activity_timezone_allday(self):
# Covers use case of commit eef4c3b48bcb4feac028bf640b545006dd0c9b91
# Also, read the comment in the code at calendar.event._inverse_dates
activity_type = self.activity_type_todo.with_user(self.env.user)
with freeze_time(datetime(2025, 6, 19, 12, 44, 00)):
activity = self.env['mail.activity'].create({
'activity_type_id': activity_type.id,
'res_id': self.env['res.partner'].create({'name': 'A Partner'}).id,
'res_model_id': self.env['ir.model']._get_id('res.partner'),
'summary': 'Meeting with partner',
})
self.assertEqual(activity.date_deadline, date(2025, 6, 19))
with freeze_time(datetime(2025, 6, 19, 12, 44, 00)):
calendar_event = self.env['calendar.event'].create({
'activity_ids': [(6, False, activity.ids)],
'allday': True,
'name': 'All Day',
'start': datetime(2025, 6, 21, 0, 0, 0),
'start_date': date(2025, 6, 21),
'stop': datetime(2025, 6, 23, 0, 0, 0),
'stop_date': date(2025, 6, 23),
})
# Check output in UTC
self.assertEqual(activity.date_deadline, date(2025, 6, 21))
# Check output in the user's tz
# write on the event to trigger sync of activities
calendar_event.with_context({'tz': 'Pacific/Honolulu'}).write({
'start': datetime(2025, 6, 22, 0, 0, 0),
'start_date': date(2025, 6, 22),
})
self.assertEqual(calendar_event.start, datetime(2025, 6, 22, 8, 0, 0),
'Calendar datetime updated with timezone')
self.assertEqual(activity.date_deadline, date(2025, 6, 22),
'Same day, as taking all day, do not care about timezone')
# now change from activity, timezone should not be taken into account
# and just update the starting day
activity.with_context({'tz': 'Australia/Brisbane'}).date_deadline = date(2025, 6, 30)
self.assertEqual(calendar_event.start, datetime(2025, 6, 30, 8, 0, 0),
'Just apply days diff, timezone do not matter')
self.assertEqual(calendar_event.start_date, date(2025, 6, 30),
'Just apply days diff, timezone do not matter')

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import common
@ -21,13 +20,13 @@ class TestRecurrentEvent(common.TransactionCase):
'duration': 1.0,
'name': 'Test Meeting',
'recurrency': True,
'rrule_type': 'daily'
'rrule_type': 'daily',
})
# I search for all the recurrent meetings
meetings_count = self.CalendarEvent.with_context({'virtual_id': True}).search_count([
('start', '>=', '2011-03-13'), ('stop', '<=', '2011-05-13')
])
self.assertEqual(meetings_count, 5, 'Recurrent daily meetings are not created !')
self.assertEqual(meetings_count, 5, 'Recurrent daily meetings are not created!')
def test_recurrent_meeting2(self):
# I create a weekly meeting till a particular end date.
@ -45,11 +44,11 @@ class TestRecurrentEvent(common.TransactionCase):
'wed': True,
'name': 'Review code with programmer',
'recurrency': True,
'rrule_type': 'weekly'
'rrule_type': 'weekly',
})
# I search for all the recurrent weekly meetings.
meetings_count = self.CalendarEvent.search_count([
('start', '>=', '2011-03-13'), ('stop', '<=', '2011-05-13')
])
self.assertEqual(meetings_count, 10, 'Recurrent weekly meetings are not created !')
self.assertEqual(meetings_count, 10, 'Recurrent weekly meetings are not created!')

View file

@ -0,0 +1,69 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import date, datetime
from odoo import Command
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
from odoo.tests import tagged
@tagged('post_install', '-at_install')
class TestCalendarTours(HttpCaseWithUserDemo):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env.ref('base.user_admin').write({
'email': 'mitchell.admin@example.com',
})
def test_calendar_month_view_start_hour_displayed(self):
""" Test that the time is displayed in the month view. """
self.start_tour("/odoo", 'calendar_appointments_hour_tour', login="demo")
def test_calendar_delete_tour(self):
"""
Check that we can delete events with the "Everybody's calendars" filter.
"""
user_admin = self.env.ref('base.user_admin')
start = datetime.combine(date.today(), datetime.min.time()).replace(hour=9)
stop = datetime.combine(date.today(), datetime.min.time()).replace(hour=12)
event = self.env['calendar.event'].with_user(user_admin).create({
'name': 'Test Event',
'description': 'Test Description',
'start': start.strftime("%Y-%m-%d %H:%M:%S"),
'stop': stop.strftime("%Y-%m-%d %H:%M:%S"),
'duration': 3,
'location': 'Odoo S.A.',
'privacy': 'public',
'show_as': 'busy',
})
action_id = self.env.ref('calendar.action_calendar_event')
url = "/odoo/action-" + str(action_id.id)
self.start_tour(url, 'test_calendar_delete_tour', login='admin')
event = self.env['calendar.event'].search([('name', '=', 'Test Event')])
self.assertFalse(event) # Check if the event has been correctly deleted
def test_calendar_decline_tour(self):
"""
Check that we can decline events.
"""
user_admin = self.env.ref('base.user_admin')
user_demo = self.user_demo
start = datetime.combine(date.today(), datetime.min.time()).replace(hour=9)
stop = datetime.combine(date.today(), datetime.min.time()).replace(hour=12)
event = self.env['calendar.event'].with_user(user_admin).create({
'name': 'Test Event',
'description': 'Test Description',
'start': start.strftime("%Y-%m-%d %H:%M:%S"),
'stop': stop.strftime("%Y-%m-%d %H:%M:%S"),
'duration': 3,
'location': 'Odoo S.A.',
'privacy': 'public',
'show_as': 'busy',
})
event.partner_ids = [Command.link(user_demo.partner_id.id)]
action_id = self.env.ref('calendar.action_calendar_event')
url = "/odoo/action-" + str(action_id.id)
self.start_tour(url, 'test_calendar_decline_tour', login='demo')
attendee = self.env['calendar.attendee'].search([('event_id', '=', event.id), ('partner_id', '=', user_demo.partner_id.id)])
self.assertEqual(attendee.state, 'declined') # Check if the event has been correctly declined

View file

@ -6,31 +6,155 @@ from dateutil.relativedelta import relativedelta
from freezegun import freeze_time
from odoo import fields
from odoo.tests.common import TransactionCase, new_test_user
from odoo.tests import Form, tagged
from odoo.tests.common import new_test_user
from odoo.addons.base.tests.test_ir_cron import CronMixinCase
from odoo.addons.mail.tests.common import MailCase
class TestEventNotifications(TransactionCase, MailCase, CronMixinCase):
class CalendarMailCommon(MailCase, CronMixinCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
# give default values for all email aliases and domain
cls._init_mail_gateway()
cls._init_mail_servers()
cls.test_alias = cls.env['mail.alias'].create({
'alias_domain_id': cls.mail_alias_domain.id,
'alias_model_id': cls.env['ir.model']._get_id('calendar.event'),
'alias_name': 'test.alias.event',
})
cls.event = cls.env['calendar.event'].create({
'name': "Doom's day",
'start': datetime(2019, 10, 25, 8, 0),
'stop': datetime(2019, 10, 27, 18, 0),
}).with_context(mail_notrack=True)
cls.user = new_test_user(cls.env, 'xav', email='em@il.com', notification_type='inbox')
})
cls.user = new_test_user(
cls.env,
'xav',
email='em@il.com',
notification_type='inbox',
)
cls.user_employee_2 = new_test_user(
cls.env,
email='employee.2@test.mycompany.com',
groups='base.group_user,base.group_partner_manager',
login='employee.2@test.mycompany.com',
notification_type='email',
)
cls.user_admin = cls.env.ref('base.user_admin')
cls.user_root = cls.env.ref('base.user_root')
cls.customers = cls.env['res.partner'].create([
{
'email': 'test.customer@example.com',
'name': 'Customer Email',
}, {
'email': 'wrong',
'name': 'Wrong Email',
}, {
'email': f'"Alias Customer" <{cls.test_alias.alias_full_name}>',
'name': 'Alias Email',
},
])
@tagged('post_install', '-at_install', 'mail_flow')
class TestCalendarMail(CalendarMailCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.user_employee = cls.user
cls.test_template_event = cls.env['mail.template'].with_user(cls.user_admin).create({
'auto_delete': True,
'body_html': '<p>Hello <t t-out="object.partner_id.name"/></p>',
'email_from': '{{ object.user_id.email_formatted or user.email_formatted or "" }}',
'model_id': cls.env['ir.model']._get_id('calendar.event'),
'name': 'Test Event Template',
'subject': 'Test {{ object.name }}',
'use_default_to': True,
})
cls.event.write({
'partner_ids': [(4, p.id) for p in cls.user_employee_2.partner_id + cls.customers],
})
def test_assert_initial_values(self):
self.assertFalse(self.event.message_partner_ids)
self.assertEqual(self.event.partner_ids, self.user_employee_2.partner_id + self.customers)
self.assertEqual(self.event.user_id, self.user_root)
def test_event_get_default_recipients(self):
event = self.event.with_user(self.user_employee)
defaults = event._message_get_default_recipients()
self.assertDictEqual(
defaults[event.id],
{'email_cc': '', 'email_to': '', 'partner_ids': (self.customers[0] + self.user_employee_2.partner_id).ids},
'Correctly filters out robodoo and aliases'
)
def test_event_get_suggested_recipients(self):
event = self.event.with_user(self.user_employee)
suggested = event._message_get_suggested_recipients()
self.assertListEqual(suggested, [
{
'create_values': {},
'email': self.customers[0].email_normalized,
'name': self.customers[0].name,
'partner_id': self.customers[0].id,
}, { # wrong email suggested, can be corrected ?
'create_values': {},
'email': self.customers[1].email_normalized,
'name': self.customers[1].name,
'partner_id': self.customers[1].id,
}, {
'create_values': {},
'email': self.user_employee_2.partner_id.email_normalized,
'name': self.user_employee_2.partner_id.name,
'partner_id': self.user_employee_2.partner_id.id,
},
], 'Correctly filters out robodoo and aliases')
def test_event_template(self):
event, template = self.event.with_user(self.user_employee), self.test_template_event.with_user(self.user_employee)
message = event.message_post_with_source(
template,
message_type='comment',
subtype_id=self.env.ref('mail.mt_comment').id,
)
self.assertEqual(
message.notified_partner_ids, self.customers[0] + self.customers[1] + self.user_employee_2.partner_id,
'Matches suggested recipients',
)
@tagged('post_install', '-at_install', 'mail_flow')
class TestEventNotifications(CalendarMailCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.partner = cls.user.partner_id
def test_assert_initial_values(self):
self.assertFalse(self.event.partner_ids)
def test_message_invite(self):
self.env['ir.config_parameter'].sudo().set_param('mail.mail_force_send_limit', None)
with self.assertSinglePostNotifications([{'partner': self.partner, 'type': 'inbox'}], {
'message_type': 'user_notification',
'subtype': 'mail.mt_note',
}):
self.event.partner_ids = self.partner
# remove custom threshold, sends immediately instead of queuing
email_partner = self.env['res.partner'].create({'name': 'bob invitee', 'email': 'bob.invitee@test.lan'})
with self.mock_mail_gateway(mail_unlink_sent=False):
self.event.partner_ids += email_partner
self.assertMailMail(email_partner, 'sent', author=self.env.ref('base.partner_root'))
def test_message_invite_allday(self):
with self.assertSinglePostNotifications([{'partner': self.partner, 'type': 'inbox'}], {
'message_type': 'user_notification',
@ -44,6 +168,23 @@ class TestEventNotifications(TransactionCase, MailCase, CronMixinCase):
'partner_ids': [(4, self.partner.id)],
}])
def test_message_invite_email_notif_mass_queued(self):
"""Check that more than 20 notified attendees means mails are queued."""
self.env['ir.config_parameter'].sudo().set_param('mail.mail_force_send_limit', None)
additional_attendees = self.env['res.partner'].create([{
'name': f'test{n}',
'email': f'test{n}@example.com'} for n in range(101)])
with self.mock_mail_gateway(mail_unlink_sent=False), self.mock_mail_app():
self.event.partner_ids = additional_attendees
self.assertNotified(
self._new_msgs,
[{
'is_read': True,
'partner': partner,
'type': 'email',
} for partner in additional_attendees],
)
def test_message_invite_self(self):
with self.assertNoNotifications():
@ -137,20 +278,29 @@ class TestEventNotifications(TransactionCase, MailCase, CronMixinCase):
'duration': 30,
})
now = fields.Datetime.now()
def get_bus_params():
return (
[(self.env.cr.dbname, "res.partner", self.partner.id)],
[
{
"type": "calendar.alarm",
"payload": [
{
"alarm_id": alarm.id,
"event_id": self.event.id,
"title": "Doom's day",
"message": self.event.display_time,
"timer": 20 * 60,
"notify_at": fields.Datetime.to_string(now + relativedelta(minutes=20)),
},
],
},
],
)
with patch.object(fields.Datetime, 'now', lambda: now):
with self.assertBus([(self.env.cr.dbname, 'res.partner', self.partner.id)], [
{
"type": "calendar.alarm",
"payload": [{
"alarm_id": alarm.id,
"event_id": self.event.id,
"title": "Doom's day",
"message": self.event.display_time,
"timer": 20 * 60,
"notify_at": fields.Datetime.to_string(now + relativedelta(minutes=20)),
}],
},
]):
with self.assertBus(get_params=get_bus_params):
self.event.with_context(no_mail_to_attendees=True).write({
'start': now + relativedelta(minutes=50),
'stop': now + relativedelta(minutes=55),
@ -161,13 +311,13 @@ class TestEventNotifications(TransactionCase, MailCase, CronMixinCase):
def test_email_alarm(self):
now = fields.Datetime.now()
with self.capture_triggers('calendar.ir_cron_scheduler_alarm') as capt:
alarm = self.env['calendar.alarm'].create({
alarm = self.env['calendar.alarm'].with_user(self.user).create({
'name': 'Alarm',
'alarm_type': 'email',
'interval': 'minutes',
'duration': 20,
})
self.event.write({
self.event.with_user(self.user).write({
'name': 'test event',
'start': now + relativedelta(minutes=15),
'stop': now + relativedelta(minutes=18),
@ -176,25 +326,167 @@ class TestEventNotifications(TransactionCase, MailCase, CronMixinCase):
})
self.env.flush_all() # flush is required to make partner_ids be present in the event
capt.records.ensure_one()
self.assertEqual(len(capt.records), 1)
self.assertLessEqual(capt.records.call_at, now)
with patch.object(fields.Datetime, 'now', lambda: now):
with self.assertSinglePostNotifications([{'partner': self.partner, 'type': 'inbox'}], {
'message_type': 'user_notification',
'subtype': 'mail.mt_note',
}):
self.event.user_id = self.user
old_messages = self.event.message_ids
self.env['calendar.alarm_manager'].with_context(lastcall=now - relativedelta(minutes=15))._send_reminder()
messages = self.env["mail.message"].search([
("model", "=", self.event._name),
("res_id", "=", self.event.id),
("message_type", "=", "user_notification")
])
new_messages = messages - old_messages
user_message = new_messages.filtered(lambda x: self.event.user_id.partner_id in x.partner_ids)
self.assertTrue(user_message.notification_ids, "Organizer must receive a reminder")
self.env['calendar.alarm_manager'].with_context(lastcall=now - relativedelta(minutes=25))._send_reminder()
self.env.flush_all()
new_messages = self.env['mail.message'].search([('model', '=', 'calendar.event'), ('res_id', '=', self.event.id), ('subject', '=', 'test event - Reminder')])
user_message = new_messages.filtered(lambda x: self.event.user_id.partner_id in x.partner_ids)
self.assertTrue(user_message, "Organizer must receive a reminder")
def test_email_alarm_recurrence(self):
# test that only a single cron trigger is created for recurring events.
# Once a notification has been sent, the next one should be created.
# It prevent creating hunderds of cron trigger at event creation
alarm = self.env['calendar.alarm'].create({
'name': 'Alarm',
'alarm_type': 'email',
'interval': 'minutes',
'duration': 1,
})
cron = self.env.ref('calendar.ir_cron_scheduler_alarm')
cron.lastcall = False
with self.capture_triggers('calendar.ir_cron_scheduler_alarm') as capt:
with freeze_time('2022-04-13 10:00+0000'):
now = fields.Datetime.now()
self.env['calendar.event'].create({
'name': "Single Doom's day",
'start': now + relativedelta(minutes=15),
'stop': now + relativedelta(minutes=20),
'alarm_ids': [fields.Command.link(alarm.id)],
}).with_context(mail_notrack=True)
self.env.flush_all()
self.assertEqual(len(capt.records), 1)
with self.capture_triggers('calendar.ir_cron_scheduler_alarm') as capt:
with freeze_time('2022-04-13 10:00+0000'):
self.env['calendar.event'].create({
'name': "Recurring Doom's day",
'start': now + relativedelta(minutes=15),
'stop': now + relativedelta(minutes=20),
'recurrency': True,
'rrule_type': 'monthly',
'month_by': 'date',
'day': 13,
'count': 5,
'alarm_ids': [fields.Command.link(alarm.id)],
}).with_context(mail_notrack=True)
self.env.flush_all()
self.assertEqual(len(capt.records), 1, "1 trigger should have been created for the whole recurrence")
self.assertEqual(capt.records.call_at, datetime(2022, 4, 13, 10, 14))
self.env['calendar.alarm_manager']._send_reminder()
self.assertEqual(len(capt.records), 1)
self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)]).unlink()
with freeze_time('2022-05-16 10:00+0000'):
self.env['calendar.alarm_manager']._send_reminder()
self.assertEqual(capt.records.mapped('call_at'), [datetime(2022, 6, 13, 10, 14)])
self.assertEqual(len(capt.records), 1, "1 more trigger should have been created")
with self.capture_triggers('calendar.ir_cron_scheduler_alarm') as capt:
with freeze_time('2022-04-13 10:00+0000'):
now = fields.Datetime.now()
self.env['calendar.event'].create({
'name': "Single Doom's day",
'start_date': now.date(),
'stop_date': now.date() + relativedelta(days=1),
'allday': True,
'alarm_ids': [fields.Command.link(alarm.id)],
}).with_context(mail_notrack=True)
self.env.flush_all()
self.assertEqual(len(capt.records), 1)
with self.capture_triggers('calendar.ir_cron_scheduler_alarm') as capt:
with freeze_time('2022-04-13 10:00+0000'):
now = fields.Datetime.now()
self.env['calendar.event'].create({
'name': "Single Doom's day",
'start_date': now.date(),
'stop_date': now.date() + relativedelta(days=1),
'allday': True,
'recurrency': True,
'rrule_type': 'monthly',
'month_by': 'date',
'day': 13,
'count': 5,
'alarm_ids': [fields.Command.link(alarm.id)],
}).with_context(mail_notrack=True)
self.env.flush_all()
self.assertEqual(len(capt.records), 1)
with self.capture_triggers('calendar.ir_cron_scheduler_alarm') as capt:
# Create alarm with one hour interval.
alarm_hour = self.env['calendar.alarm'].create({
'name': 'Alarm',
'alarm_type': 'email',
'interval': 'hours',
'duration': 1,
})
# Create monthly recurrence, ensure the next alarm is set to the first event
# and then one month later must be set one hour before to the last event.
with freeze_time('2024-04-16 10:00+0000'):
now = fields.Datetime.now()
self.env['calendar.event'].create({
'name': "Single Doom's day",
'start': now + relativedelta(hours=2),
'stop': now + relativedelta(hours=3),
'recurrency': True,
'rrule_type': 'monthly',
'count': 2,
'day': 16,
'alarm_ids': [fields.Command.link(alarm_hour.id)],
}).with_context(mail_notrack=True)
self.env.flush_all()
# Ensure that there is only one alarm set, exactly for one hour previous the event.
self.assertEqual(len(capt.records), 1, "Only one trigger must be created for the entire recurrence.")
self.assertEqual(capt.records.mapped('call_at'), [datetime(2024, 4, 16, 11, 0)], "Alarm must be one hour before the first event.")
self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)]).unlink()
with freeze_time('2024-04-22 10:00+0000'):
# The next alarm will be set through the next_date selection for the next event.
# Ensure that there is only one alarm set, exactly for one hour previous the event.
self.env['calendar.alarm_manager']._send_reminder()
self.assertEqual(len(capt.records), 1, "Only one trigger must be created for the entire recurrence.")
self.assertEqual(capt.records.mapped('call_at'), [datetime(2024, 5, 16, 11, 0)], "Alarm must be one hour before the second event.")
def test_email_alarm_daily_recurrence(self):
# test email alarm is sent correctly on daily recurrence
alarm = self.env['calendar.alarm'].create({
'name': 'Alarm',
'alarm_type': 'email',
'interval': 'minutes',
'duration': 5,
})
cron = self.env.ref('calendar.ir_cron_scheduler_alarm')
cron.lastcall = False
with self.capture_triggers('calendar.ir_cron_scheduler_alarm') as capt:
with freeze_time('2022-04-13 10:00+0000'):
now = fields.Datetime.now()
self.env['calendar.event'].create({
'name': "Recurring Event",
'start': now + relativedelta(minutes=15),
'stop': now + relativedelta(minutes=20),
'recurrency': True,
'rrule_type': 'daily',
'count': 3,
'alarm_ids': [fields.Command.link(alarm.id)],
}).with_context(mail_notrack=True)
self.env.flush_all()
self.assertEqual(len(capt.records), 1, "1 trigger should have been created for the whole recurrence (1)")
self.assertEqual(capt.records.call_at, datetime(2022, 4, 13, 10, 10))
with self.capture_triggers('calendar.ir_cron_scheduler_alarm') as capt:
with freeze_time('2022-04-13 10:11+0000'):
self.env['calendar.alarm_manager']._send_reminder()
self.assertEqual(len(capt.records), 1)
with self.capture_triggers('calendar.ir_cron_scheduler_alarm') as capt:
with freeze_time('2022-04-14 10:11+0000'):
self.env['calendar.alarm_manager']._send_reminder()
self.assertEqual(len(capt.records), 1, "1 trigger should have been created for the whole recurrence (2)")
self.assertEqual(capt.records.call_at, datetime(2022, 4, 15, 10, 10))
def test_notification_event_timezone(self):
"""
@ -279,3 +571,165 @@ class TestEventNotifications(TransactionCase, MailCase, CronMixinCase):
with freeze_time('2023-11-15 16:00:00'):
self.assertEqual(len(search_event()), 3)
events.unlink()
def test_recurring_meeting_reminder_notification(self):
alarm = self.env['calendar.alarm'].create({
'name': 'Alarm',
'alarm_type': 'notification',
'interval': 'minutes',
'duration': 30,
})
self.event._apply_recurrence_values({
'interval': 2,
'rrule_type': 'weekly',
'tue': True,
'count': 2,
})
now = fields.Datetime.now()
def get_bus_params():
return (
[(self.env.cr.dbname, "res.partner", self.partner.id)],
[
{
"type": "calendar.alarm",
"payload": [
{
"alarm_id": alarm.id,
"event_id": self.event.id,
"title": "Doom's day",
"message": self.event.display_time,
"timer": 20 * 60,
"notify_at": fields.Datetime.to_string(now + relativedelta(minutes=20)),
},
],
},
],
)
with patch.object(fields.Datetime, 'now', lambda: now):
with self.assertBus(get_params=get_bus_params):
self.event.with_context(no_mail_to_attendees=True).write({
'start': now + relativedelta(minutes=50),
'stop': now + relativedelta(minutes=55),
'partner_ids': [(4, self.partner.id)],
'alarm_ids': [(4, alarm.id)]
})
def test_calendar_recurring_event_delete_notification(self):
""" Ensure that we can delete a specific occurrence of a recurring event
and notify the participants about the cancellation. """
# Setup for creating a test event with recurring properties.
user_admin = self.env.ref('base.user_admin')
start = datetime.combine(date.today(), datetime.min.time()).replace(hour=9)
stop = datetime.combine(date.today(), datetime.min.time()).replace(hour=12)
event = self.env['calendar.event'].create({
'name': 'Test Event Delete Notification',
'description': 'Test Description',
'start': start.strftime("%Y-%m-%d %H:%M:%S"),
'stop': stop.strftime("%Y-%m-%d %H:%M:%S"),
'duration': 3,
'recurrency': True,
'rrule_type': 'daily',
'count': 3,
'location': 'Odoo S.A.',
'privacy': 'public',
'show_as': 'busy',
})
# Deleting the next occurrence of the event using the delete wizard.
wizard = self.env['calendar.popover.delete.wizard'].with_context(
form_view_ref='calendar.calendar_popover_delete_view').create({'calendar_event_id': event.id})
form = Form(wizard)
form.delete = 'next'
form.save()
wizard.close()
# Unlink the event and send a cancellation notification.
event.action_unlink_event()
wizard = self.env['calendar.popover.delete.wizard'].create({
'calendar_event_id': event.id,
'subject': 'Event Cancellation',
'body': 'The event has been cancelled.',
'recipient_ids': [(6, 0, [user_admin.partner_id.id])],
})
# Simulate sending the email and ensure one email was sent.
with self.mock_mail_gateway():
wizard.action_send_mail_and_delete()
self.assertEqual(len(self._new_mails), 1)
def test_get_next_potential_limit_alarm(self):
"""
Test that the next potential limit alarm is correctly computed for notification alarms.
"""
now = fields.Datetime.now()
start = now - relativedelta(days=1)
while start.weekday() > 4:
start -= relativedelta(days=1)
stop = start + relativedelta(hours=1)
next_month = now + relativedelta(days=30)
weekday_flags = ['mon', 'tue', 'wed', 'thu', 'fri']
weekday_flag = weekday_flags[start.weekday()]
weekday_dict = {flag: False for flag in weekday_flags}
weekday_dict[weekday_flag] = True
partner = self.user.partner_id
# until_date event first alarm
alarm = self.env['calendar.alarm'].create({
'name': 'Alarm',
'alarm_type': 'notification',
'interval': 'minutes',
'duration': 15,
})
event_vals = {
'start': start,
'stop': stop,
'name': 'Weekly Sales Meeting',
'alarm_ids': [[6, 0, [alarm.id]]],
'partner_ids': [(4, self.partner.id)],
}
self.event.write(event_vals)
self.event._apply_recurrence_values({
'interval': 1,
'rrule_type': 'weekly',
'end_type': 'end_date',
'until': next_month.date().isoformat(),
**weekday_dict,
})
events = self.env['calendar.event'].search([('name', '=', 'Weekly Sales Meeting')])
self.env.flush_all()
result = self.env['calendar.alarm_manager']._get_next_potential_limit_alarm('notification', partners=partner)
for alarm_data in result.values():
first_alarm = alarm_data.get('first_alarm')
self.assertLess(now, first_alarm)
events.unlink()
# count event last alarm
recurrence_count = 5
start = now - relativedelta(days=1)
stop = start + relativedelta(hours=1)
event_vals = {
'start': start,
'stop': stop,
'name': 'Daily Sales Meeting',
'alarm_ids': [[6, 0, [alarm.id]]],
'partner_ids': [(4, self.partner.id)],
}
self.event = self.env['calendar.event'].create(event_vals)
self.event._apply_recurrence_values({
'interval': 1,
'rrule_type': 'daily',
'end_type': 'count',
'count': recurrence_count
})
self.env.flush_all()
result = self.env['calendar.alarm_manager']._get_next_potential_limit_alarm('notification', partners=partner)
expected_alarms = sorted([stop + relativedelta(days=offset) - relativedelta(minutes=15) for offset in range(1, recurrence_count)])
actual_alarms = sorted([data.get('last_alarm') for data in result.values()])
for expected, actual in zip(expected_alarms, actual_alarms):
self.assertEqual(actual, expected)

View file

@ -1,12 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.exceptions import UserError
import pytz
from datetime import datetime, date
from dateutil.relativedelta import relativedelta
from odoo.exceptions import UserError
from odoo.tests.common import TransactionCase
from odoo.tests import Form, TransactionCase
from freezegun import freeze_time
@ -22,8 +21,7 @@ class TestRecurrentEvents(TransactionCase):
events = events.sorted('start')
self.assertEqual(len(events), len(dates), "Wrong number of events in the recurrence")
self.assertTrue(all(events.mapped('active')), "All events should be active")
for event, dates in zip(events, dates):
start, stop = dates
for event, (start, stop) in zip(events, dates):
self.assertEqual(event.start, start)
self.assertEqual(event.stop, stop)
@ -353,6 +351,60 @@ class TestCreateRecurrentEvents(TestRecurrentEvents):
(datetime(2023, 4, 27, 7, 00), datetime(2023, 4, 27, 8, 00)),
])
def test_all_day_date(self):
recurrence = self.env['calendar.event'].with_context(
default_start=datetime(2019, 10, 22),
default_stop=datetime(2019, 10, 22),
default_start_date=date(2019, 10, 22),
default_stop_date=date(2019, 10, 22),
).create({
'name': 'Recurrent Event',
'start': datetime(2019, 10, 22, 8, 0),
'stop': datetime(2019, 10, 22, 18, 0),
'start_date': date(2019, 10, 22),
'stop_date': date(2019, 10, 22),
'recurrency': True,
'rrule_type': 'weekly',
'tue': True,
'interval': 1,
'count': 2,
'event_tz': 'UTC',
'allday': True,
}).recurrence_id
events = recurrence.calendar_event_ids
self.assertEqual(events[0].start_date, date(2019, 10, 22), "The first event has the initial start date")
self.assertEqual(events[1].start_date, date(2019, 10, 29), "The start date of the second event is one week later")
def test_recurrency_with_this_event(self):
"""
1) Create an event with a recurrence set on it
2) Try updating the event with a different recurrence without specifying 'recurrence_update'
3) Update the recurrence of one of the events, this time using the 'recurrence_update' as future_events
4) Finally, check that the updated event correctly reflects the recurrence
"""
event = self.env['calendar.event'].create({
'name': "Test Event",
'allday': False,
'rrule': u'FREQ=DAILY;INTERVAL=1;COUNT=10',
'recurrency': True,
'start': datetime(2023, 7, 28, 1, 0),
'stop': datetime(2023, 7, 29, 18, 0),
})
events = self.env['calendar.recurrence'].search([('base_event_id', '=', event.id)]).calendar_event_ids
self.assertEqual(len(events), 10, "It should have 10 events in the recurrence")
# Update the recurrence without without specifying 'recurrence_update'
with self.assertRaises(UserError):
event.write({'rrule': u'FREQ=DAILY;INTERVAL=2;COUNT=5'})
# Update the recurrence of the earlier event
events[5].write({
'recurrence_update': 'future_events',
'count': 2,
})
updated_events = self.env['calendar.recurrence'].search([('base_event_id', '=', events[5].id)]).calendar_event_ids
self.assertEqual(len(updated_events), 2, "It should have 2 events in the recurrence")
self.assertTrue(updated_events[1].recurrency, "It should have recurrency in the updated events")
class TestUpdateRecurrentEvents(TestRecurrentEvents):
@classmethod
@ -443,7 +495,7 @@ class TestUpdateRecurrentEvents(TestRecurrentEvents):
'start': event.start + relativedelta(days=4),
'stop': event.stop + relativedelta(days=5),
})
recurrence = self.env['calendar.recurrence'].search([])
recurrence = self.env['calendar.recurrence'].search([], limit=1)
self.assertEventDates(recurrence.calendar_event_ids, [
(datetime(2019, 10, 26, 1, 0), datetime(2019, 10, 29, 18, 0)),
(datetime(2019, 11, 2, 1, 0), datetime(2019, 11, 5, 18, 0)),
@ -489,7 +541,7 @@ class TestUpdateRecurrentEvents(TestRecurrentEvents):
'start': event.start + relativedelta(days=4),
'stop': event.stop + relativedelta(days=5),
})
self.assertFalse(self.recurrence.calendar_event_ids, "Inactive event should not create recurrent events")
self.assertFalse(self.recurrence.exists(), "Inactive event should not create recurrent events")
def test_shift_all_with_outlier(self):
outlier = self.events[1]
@ -571,7 +623,7 @@ class TestUpdateRecurrentEvents(TestRecurrentEvents):
'recurrence_update': 'all_events',
'mon': True, # recurrence is now Tuesday AND Monday
})
recurrence = self.env['calendar.recurrence'].search([])
recurrence = self.env['calendar.recurrence'].search([], limit=1)
self.assertEventDates(recurrence.calendar_event_ids, [
(datetime(2019, 10, 22, 1, 0), datetime(2019, 10, 24, 18, 0)),
(datetime(2019, 10, 28, 1, 0), datetime(2019, 10, 30, 18, 0)),
@ -692,6 +744,60 @@ class TestUpdateRecurrentEvents(TestRecurrentEvents):
self.assertTrue(self.recurrence)
self.assertEqual(self.events.exists(), self.events[0])
def test_unlink_recurrence_wizard_next(self):
""" Test unlinking the next recurrent event using the delete wizard. """
# Retrieve the recurring event to delete the next event occurrence.
event = self.events[1]
# Step 1: Use the popover delete wizard to delete the next occurrence of the event.
wizard = self.env['calendar.popover.delete.wizard'].with_context(
form_view_ref='calendar.calendar_popover_delete_view').create({'calendar_event_id': event.id})
form = Form(wizard)
form.delete = 'next'
form.save()
wizard.close()
# Step 2: Use another delete wizard to handle the deletion of the next occurrence.
wizard_delete = self.env['calendar.popover.delete.wizard'].with_context(
form_view_ref='calendar.view_event_delete_wizard_form',
default_recurrence='next'
).create({'calendar_event_id': event.id})
form_delete = Form(wizard_delete)
form_delete.save()
# Step 3: Send cancellation notifications and delete the next occurrence.
# Ensure that the recurrence still exists but the next event occurrence is deleted.
wizard_delete.action_send_mail_and_delete()
self.assertTrue(self.recurrence)
self.assertEqual(self.events.exists(), self.events[0])
def test_unlink_recurrence_wizard_all(self):
""" Test unlinking all recurrences using the delete wizard. """
# Step 0: Retrieve the recurring event to be deleted.
event = self.events[1]
# Step 1: Use the popover delete wizard to delete all occurrences of the event.
wizard = self.env['calendar.popover.delete.wizard'].with_context(
form_view_ref='calendar.calendar_popover_delete_view').create({'calendar_event_id': event.id})
form = Form(wizard)
form.delete = 'all'
form.save()
wizard.close()
# Step 2: Use another delete wizard to handle the deletion of the event recurrence.
wizard_delete = self.env['calendar.popover.delete.wizard'].with_context(
form_view_ref='calendar.view_event_delete_wizard_form',
default_recurrence='all'
).create({'calendar_event_id': event.id})
form_delete = Form(wizard_delete)
form_delete.save()
# Step 3: Send cancellation notifications and delete all recurrences.
# Ensure that the recurrence and all related events have been deleted.
wizard_delete.action_send_mail_and_delete()
self.assertFalse(self.recurrence.exists())
self.assertFalse(self.events.exists())
def test_recurrence_update_all_first_archived(self):
"""Test to check the flow when a calendar event is
created from a day that does not belong to the recurrence.
@ -734,7 +840,6 @@ class TestUpdateRecurrentEvents(TestRecurrentEvents):
(datetime(2019, 11, 6, 1, 0), datetime(2019, 11, 6, 2, 0)),
])
class TestUpdateMultiDayWeeklyRecurrentEvents(TestRecurrentEvents):
@classmethod
@ -769,7 +874,7 @@ class TestUpdateMultiDayWeeklyRecurrentEvents(TestRecurrentEvents):
'start': event.start + relativedelta(days=2),
'stop': event.stop + relativedelta(days=2),
})
recurrence = self.env['calendar.recurrence'].search([])
recurrence = self.env['calendar.recurrence'].search([], limit=1)
# We don't try to do magic tricks. First event is moved, other remain
self.assertEventDates(recurrence.calendar_event_ids, [
(datetime(2019, 10, 24, 1, 0), datetime(2019, 10, 26, 18, 0)),
@ -787,7 +892,7 @@ class TestUpdateMultiDayWeeklyRecurrentEvents(TestRecurrentEvents):
'start': event.start + relativedelta(days=2),
'stop': event.stop + relativedelta(days=3),
})
recurrence = self.env['calendar.recurrence'].search([])
recurrence = self.env['calendar.recurrence'].search([], limit=1)
self.assertEventDates(recurrence.calendar_event_ids, [
(datetime(2019, 10, 24, 1, 0), datetime(2019, 10, 27, 18, 0)),
(datetime(2019, 10, 31, 1, 0), datetime(2019, 11, 3, 18, 0)),
@ -840,7 +945,7 @@ class TestUpdateMonthlyByDay(TestRecurrentEvents):
'start': event.start + relativedelta(hours=5),
'stop': event.stop + relativedelta(hours=5),
})
recurrence = self.env['calendar.recurrence'].search([])
recurrence = self.env['calendar.recurrence'].search([], limit=1)
self.assertEventDates(recurrence.calendar_event_ids, [
(datetime(2019, 10, 15, 6, 0), datetime(2019, 10, 16, 23, 0)),
(datetime(2019, 11, 19, 6, 0), datetime(2019, 11, 20, 23, 0)),
@ -898,3 +1003,116 @@ class TestUpdateMonthlyByDate(TestRecurrentEvents):
(datetime(2019, 11, 25, 1, 0), datetime(2019, 11, 27, 18, 0)),
(datetime(2019, 12, 25, 1, 0), datetime(2019, 12, 27, 18, 0)),
])
def test_recurring_ui_options_daily(self):
with Form(self.env['calendar.event']) as calendar_form:
calendar_form.name = 'test recurrence daily'
calendar_form.recurrency = True
calendar_form.rrule_type_ui = 'daily'
calendar_form.count = 2
calendar_form.start = datetime(2019, 6, 23, 16)
calendar_form.stop = datetime(2019, 6, 23, 17)
event = calendar_form.save()
self.assertEventDates(event.recurrence_id.calendar_event_ids, [
(datetime(2019, 6, 23, 16), datetime(2019, 6, 23, 17)),
(datetime(2019, 6, 24, 16, 0), datetime(2019, 6, 24, 17)),
])
self.assertEqual(event.rrule_type_ui, 'daily')
self.assertEqual(event.count, 2)
def test_recurring_ui_options_monthly(self):
with Form(self.env['calendar.event']) as calendar_form:
calendar_form.name = 'test recurrence monthly'
calendar_form.recurrency = True
calendar_form.rrule_type_ui = 'monthly'
calendar_form.count = 2
calendar_form.start = datetime(2019, 6, 11, 16)
calendar_form.stop = datetime(2019, 6, 11, 17)
calendar_form.day = 11
event = calendar_form.save()
self.assertEventDates(event.recurrence_id.calendar_event_ids, [
(datetime(2019, 6, 11, 16), datetime(2019, 6, 11, 17)),
(datetime(2019, 7, 11, 16), datetime(2019, 7, 11, 17)),
])
self.assertEqual(event.rrule_type_ui, 'monthly')
self.assertEqual(event.count, 2)
def test_recurring_ui_options_yearly(self):
with Form(self.env['calendar.event']) as calendar_form:
calendar_form.name = 'test recurrence yearly'
calendar_form.recurrency = True
calendar_form.rrule_type_ui = 'yearly'
calendar_form.count = 2
calendar_form.start = datetime(2019, 6, 11, 16)
calendar_form.stop = datetime(2019, 6, 11, 17)
event = calendar_form.save()
self.assertEventDates(event.recurrence_id.calendar_event_ids, [
(datetime(2019, 6, 11, 16), datetime(2019, 6, 11, 17)),
(datetime(2020, 6, 11, 16), datetime(2020, 6, 11, 17)),
])
# set to custom because a yearly recurrence, becomes a monthly recurrence every 12 months
self.assertEqual(event.rrule_type_ui, 'yearly')
self.assertEqual(event.count, 2)
self.assertEqual(event.interval, 1)
self.assertEqual(event.rrule_type, 'yearly')
def test_attendees_state_after_update(self):
""" Ensure that after the organizer updates a recurrence, the attendees state will be pending and current user accepted. """
# Create events with organizer and attendee state set as accepted.
organizer = self.env.ref('base.user_admin')
attendee_partner = self.env['res.partner'].create({'name': "attendee", "email": 'attendee@email.com'})
first_event = self.env['calendar.event'].with_user(organizer).create({
'name': "Recurrence",
'start': datetime(2023, 10, 18, 8, 0),
'stop': datetime(2023, 10, 18, 10, 0),
'rrule': u'FREQ=WEEKLY;COUNT=5;BYDAY=WE',
'recurrency': True,
'partner_ids': [(4, organizer.partner_id.id), (4, attendee_partner.id)],
})
recurrence_id = first_event.recurrence_id.id
# Accept all events for all attendees and ensure their acceptance.
for event in first_event.recurrence_id.calendar_event_ids:
for attendee in event.attendee_ids:
attendee.state = 'accepted'
# Change time fields of the recurrence by organizer in "all_events" mode. Events must reset attendee status to 'needsAction'.
first_event.with_user(organizer).write({
'start': first_event.start + relativedelta(hours=2),
'stop': first_event.stop + relativedelta(hours=2),
'recurrence_update': 'all_events',
})
first_event = self.env['calendar.recurrence'].search([('id', '>', recurrence_id)]).base_event_id
recurrence_id = first_event.recurrence_id.id
# Ensure that attendee status is pending after organizer (current user) update time values.
for event in first_event.recurrence_id.calendar_event_ids:
for attendee in event.attendee_ids:
if attendee.partner_id == organizer.partner_id:
self.assertEqual(attendee.state, "accepted", "Organizer must remain accepted after time values update.")
else:
self.assertEqual(attendee.state, "needsAction", "Attendees state except organizer must be pending after update.")
# Accept all events again for all attendes.
for event in first_event.recurrence_id.calendar_event_ids:
for attendee in event.attendee_ids:
attendee.state = 'accepted'
# Change time fields of the recurrence by organizer in "future_events" mode. Events must reset attendee status to 'needsAction'.
second_event = first_event.recurrence_id.calendar_event_ids.sorted('start')[1]
second_event.with_user(organizer).write({
'start': second_event.start + relativedelta(hours=2),
'stop': second_event.stop + relativedelta(hours=2),
'recurrence_update': 'future_events',
})
second_event = self.env['calendar.recurrence'].search([('id', '>', recurrence_id)]).base_event_id
# Ensure that first event is accepted for everyone and also from the second event on, the state in pending for attendees except organizer.
self.assertTrue(first_event.active, "Event from previous recurrence must remain active after the second event got updated.")
self.assertTrue(all(attendee.state == 'accepted' for attendee in first_event.attendee_ids), "Attendees state from previous event must remain accepted.")
for event in second_event.recurrence_id.calendar_event_ids:
for attendee in event.attendee_ids:
if attendee.partner_id == organizer.partner_id:
self.assertEqual(attendee.state, "accepted", "Current user must remain accepted after time values update.")
else:
self.assertEqual(attendee.state, "needsAction", "Attendees state except current user must be pending after update.")

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, time
@ -6,7 +5,6 @@ from dateutil.relativedelta import relativedelta
import pytz
from odoo import Command
from odoo import tests
from odoo.addons.mail.tests.common import MailCommon
@ -44,14 +42,12 @@ class TestMailActivityMixin(MailCommon):
})
def schedule_meeting_activity(record, date_deadline, calendar_event=False):
meeting = record.activity_schedule('calendar.calendar_activity_test_default', date_deadline=date_deadline)
meeting = record.activity_schedule('calendar.calendar_activity_test_default', date_deadline=date_deadline, user_id=self.env.uid)
meeting.calendar_event_id = calendar_event
return meeting
group_partner_manager = self.env['ir.model.data']._xmlid_to_res_id('base.group_partner_manager')
self.user_employee.write({
'tz': self.user_admin.tz,
'groups_id': [Command.link(group_partner_manager)]
})
with self.with_user('employee'):
test_record = self.env['res.partner'].browse(self.test_record.id)
@ -78,5 +74,5 @@ class TestMailActivityMixin(MailCommon):
act1._action_done(feedback="Mark activity as done with text")
self.assertFalse(act1.exists(), "activity marked as done should be deleted")
self.assertFalse(act1.active, "activity marked as done should be archived")
self.assertTrue(ev1.exists(), "event of done activity must not be deleted")

View file

@ -1,3 +1,5 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from odoo.tests.common import TransactionCase

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests.common import TransactionCase
@ -61,12 +60,12 @@ class TestResPartner(TransactionCase):
'perm_read': True,
'perm_create': False,
'perm_write': False})
Event.create({'name': 'event_9',
# create generally requires read -> prevented by above test rule
Event.sudo().create({'name': 'event_9',
'partner_ids': [(6, 0, [test_partner_2.id,
test_partner_3.id])]})
Event.create({'name': 'event_10',
Event.sudo().create({'name': 'event_10',
'partner_ids': [(6, 0, [test_partner_5.id])]})
self.assertEqual(test_partner_1.meeting_count, 7)

View file

@ -0,0 +1,83 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests.common import TransactionCase
class TestResUsers(TransactionCase):
def test_same_calendar_default_privacy_as_user_template(self):
"""
The 'calendar default privacy' variable can be set in the Default User Template
for defining which privacy the new user's calendars will have when creating a
user. Ensure that when creating a new user, its calendar default privacy will
have the same value as defined in the template.
"""
def create_user(name, login, email, privacy=None):
vals = {'name': name, 'login': login, 'email': email}
if privacy is not None:
vals['calendar_default_privacy'] = privacy
return self.env['res.users'].create(vals)
# Get Default User Template and define expected outputs for each privacy update test.
privacy_and_output = [
(False, 'public'),
('public', 'public'),
('private', 'private'),
('confidential', 'confidential')
]
for (privacy, expected_output) in privacy_and_output:
# Update default privacy.
if privacy:
self.env['ir.config_parameter'].set_param("calendar.default_privacy", privacy)
# If Calendar Default Privacy isn't defined in vals: get the privacy from Default User Template.
username = 'test_%s_%s' % (str(privacy), expected_output)
new_user = create_user(username, username, username + '@user.com')
self.assertEqual(
new_user.calendar_default_privacy,
expected_output,
'Calendar default privacy %s should be %s, same as in the Default User Template.'
% (new_user.calendar_default_privacy, expected_output)
)
# If Calendar Default Privacy is defined in vals: override the privacy from Default User Template.
for custom_privacy in ['public', 'private', 'confidential']:
custom_name = str(custom_privacy) + username
custom_user = create_user(custom_name, custom_name, custom_name + '@user.com', privacy=custom_privacy)
self.assertEqual(
custom_user.calendar_default_privacy,
custom_privacy,
'Custom %s privacy from in vals must override the privacy %s from Default User Template.'
% (custom_privacy, privacy)
)
def test_avoid_res_users_settings_creation_portal(self):
"""
This test ensures that 'res.users.settings' entries are not created for portal
and public users through the new 'calendar_default_privacy' field, since it is
not useful tracking these fields for non-internal users.
"""
username_and_group = {
'PORTAL': 'base.group_portal',
'PUBLIC': 'base.group_public',
}
for username, group in username_and_group.items():
# Create user and impersonate it as sudo for triggering the compute.
user = self.env['res.users'].create({
'name': username,
'login': username,
'email': username + '@email.com',
'group_ids': [(6, 0, [self.env.ref(group).id])]
})
user.with_user(user).sudo()._compute_calendar_default_privacy()
# Ensure default privacy fallback and also that no 'res.users.settings' entry got created.
self.assertEqual(
user.calendar_default_privacy, 'public',
"Calendar default privacy of %s users must fallback to 'public'." % (username)
)
self.assertFalse(
user.sudo().res_users_settings_id,
"No res.users.settings record must be created for '%s' users." % (username)
)