mirror of
https://github.com/bringout/oca-ocb-technical.git
synced 2026-04-22 08:52:04 +02:00
19.0 vanilla
This commit is contained in:
parent
5faf7397c5
commit
2696f14ed7
721 changed files with 220375 additions and 91221 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 & <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)
|
||||
|
|
|
|||
|
|
@ -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 & <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')
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from datetime import datetime
|
||||
|
||||
|
|
|
|||
|
|
@ -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!')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue