mirror of
https://github.com/bringout/oca-ocb-technical.git
synced 2026-04-22 00:32:07 +02:00
19.0 vanilla
This commit is contained in:
parent
5faf7397c5
commit
2696f14ed7
721 changed files with 220375 additions and 91221 deletions
|
|
@ -20,36 +20,15 @@ pip install odoo-bringout-oca-ocb-calendar
|
|||
|
||||
## Dependencies
|
||||
|
||||
This addon depends on:
|
||||
- base
|
||||
- mail
|
||||
|
||||
## Manifest Information
|
||||
|
||||
- **Name**: Calendar
|
||||
- **Version**: 1.1
|
||||
- **Category**: Productivity/Calendar
|
||||
- **License**: LGPL-3
|
||||
- **Installable**: True
|
||||
|
||||
## Source
|
||||
|
||||
Based on [OCA/OCB](https://github.com/OCA/OCB) branch 16.0, addon `calendar`.
|
||||
- Repository: https://github.com/OCA/OCB
|
||||
- Branch: 19.0
|
||||
- Path: addons/calendar
|
||||
|
||||
## License
|
||||
|
||||
This package maintains the original LGPL-3 license from the upstream Odoo project.
|
||||
|
||||
## Documentation
|
||||
|
||||
- Overview: doc/OVERVIEW.md
|
||||
- Architecture: doc/ARCHITECTURE.md
|
||||
- Models: doc/MODELS.md
|
||||
- Controllers: doc/CONTROLLERS.md
|
||||
- Wizards: doc/WIZARDS.md
|
||||
- Install: doc/INSTALL.md
|
||||
- Usage: doc/USAGE.md
|
||||
- Configuration: doc/CONFIGURATION.md
|
||||
- Dependencies: doc/DEPENDENCIES.md
|
||||
- Troubleshooting: doc/TROUBLESHOOTING.md
|
||||
- FAQ: doc/FAQ.md
|
||||
This package preserves the original LGPL-3 license.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import controllers
|
||||
from . import models
|
||||
from . import populate
|
||||
from . import wizard
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
|
|
@ -33,28 +32,32 @@ If you need to manage your meetings, you should install the CRM module.
|
|||
'views/mail_activity_views.xml',
|
||||
'views/calendar_templates.xml',
|
||||
'views/calendar_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/res_partner_views.xml',
|
||||
'wizard/calendar_provider_config.xml'
|
||||
'views/res_users_views.xml',
|
||||
'wizard/calendar_provider_config.xml',
|
||||
'wizard/calendar_popover_delete_wizard.xml',
|
||||
'wizard/mail_activity_schedule_views.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': True,
|
||||
'assets': {
|
||||
'mail.assets_messaging': [
|
||||
'calendar/static/src/models/*.js',
|
||||
],
|
||||
'web.assets_backend': [
|
||||
'calendar/static/src/scss/calendar.scss',
|
||||
'calendar/static/src/js/base_calendar.js',
|
||||
'calendar/static/src/js/services/calendar_notification_service.js',
|
||||
'calendar/static/src/views/**/*',
|
||||
'calendar/static/src/components/*/*.xml',
|
||||
'calendar/static/src/**/*',
|
||||
],
|
||||
# Unit test files
|
||||
'web.assets_unit_tests': [
|
||||
'calendar/static/tests/**/*.js',
|
||||
('remove', 'calendar/static/tests/helpers/**/*'),
|
||||
('remove', 'calendar/static/tests/tours/**/*'),
|
||||
],
|
||||
'web.qunit_suite_tests': [
|
||||
'calendar/static/tests/**/*',
|
||||
'calendar/static/tests/helpers/**/*',
|
||||
],
|
||||
'web.assets_tests': [
|
||||
'calendar/static/tests/tours/calendar_tour.js',
|
||||
'calendar/static/tests/tours/**/*',
|
||||
],
|
||||
},
|
||||
'author': 'Odoo S.A.',
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import main
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import odoo.http as http
|
||||
|
|
@ -68,8 +67,8 @@ class CalendarController(http.Controller):
|
|||
|
||||
# If user is internal and logged, redirect to form view of event
|
||||
# otherwise, display the simplifyed web page with event informations
|
||||
if request.session.uid and request.env['res.users'].browse(request.session.uid).user_has_groups('base.group_user'):
|
||||
return request.redirect('/web?db=%s#id=%s&view_type=form&model=calendar.event' % (request.env.cr.dbname, id))
|
||||
if request.env.user._is_internal():
|
||||
return request.redirect('/odoo/calendar.event/%s?db=%s' % (id, request.env.cr.dbname))
|
||||
|
||||
# NOTE : we don't use request.render() since:
|
||||
# - we need a template rendering which is not lazy, to render before cursor closing
|
||||
|
|
@ -94,11 +93,11 @@ class CalendarController(http.Controller):
|
|||
return request.redirect('/calendar/meeting/view?token=%s&id=%s' % (attendee.access_token, event.id))
|
||||
|
||||
# Function used, in RPC to check every 5 minutes, if notification to do for an event or not
|
||||
@http.route('/calendar/notify', type='json', auth="user")
|
||||
@http.route('/calendar/notify', type='jsonrpc', auth="user")
|
||||
def notify(self):
|
||||
return request.env['calendar.alarm_manager'].get_next_notif()
|
||||
|
||||
@http.route('/calendar/notify_ack', type='json', auth="user")
|
||||
@http.route('/calendar/notify_ack', type='jsonrpc', auth="user")
|
||||
def notify_ack(self):
|
||||
return request.env['res.partner'].sudo()._set_calendar_last_notif_ack()
|
||||
|
||||
|
|
@ -113,3 +112,8 @@ class CalendarController(http.Controller):
|
|||
event._create_videocall_channel()
|
||||
|
||||
return request.redirect(event.videocall_channel_id.invitation_url)
|
||||
|
||||
@http.route('/calendar/check_credentials', type='jsonrpc', auth='user')
|
||||
def check_calendar_credentials(self):
|
||||
# method should be overwritten by sync providers
|
||||
return request.env['res.users'].check_calendar_credentials()
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@
|
|||
<field name="user_id" ref="base.user_root" />
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field eval="False" name="doall" />
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -45,5 +45,10 @@
|
|||
<field name="alarm_type">email</field>
|
||||
<field name="mail_template_id" ref="calendar.calendar_template_meeting_reminder"/>
|
||||
</record>
|
||||
|
||||
<record id="default_privacy" model="ir.config_parameter">
|
||||
<field name="key">calendar.default_privacy</field>
|
||||
<field name="value">public</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -6,132 +6,119 @@
|
|||
<field name="model_id" ref="calendar.model_calendar_attendee"/>
|
||||
<field name="subject">Invitation to {{ object.event_id.name }}</field>
|
||||
<field name="email_from">{{ (object.event_id.user_id.email_formatted or user.email_formatted or '') }}</field>
|
||||
<field name="email_to">{{ ('' if object.partner_id.email and object.partner_id.email == object.email else object.email) }}</field>
|
||||
<field name="partner_to">{{ object.partner_id.id if object.partner_id.email and object.partner_id.email == object.email else False }}</field>
|
||||
<field name="lang">{{ object.partner_id.lang }}</field>
|
||||
<field name="email_to" eval="False"/>
|
||||
<field name="partner_to" eval="False"/>
|
||||
<field name="use_default_to" eval="True"/>
|
||||
<field name="description">Invitation email to new attendees</field>
|
||||
<field name="body_html" type="html">
|
||||
<div>
|
||||
<t t-set="colors" t-value="{'needsAction': 'grey', 'accepted': 'green', 'tentative': '#FFFF00', 'declined': 'red'}"/>
|
||||
<t t-set="is_online" t-value="'appointment_type_id' in object.event_id and object.event_id.appointment_type_id"/>
|
||||
<div style="font-size: 13px; color: #374151;">
|
||||
<t t-set="customer" t-value=" object.event_id.find_partner_customer()"/>
|
||||
<t t-set="target_responsible" t-value="object.partner_id == object.event_id.partner_id"/>
|
||||
<t t-set="target_customer" t-value="object.partner_id == customer"/>
|
||||
<t t-set="recurrent" t-value="object.recurrence_id and not ctx.get('calendar_template_ignore_recurrence')"/>
|
||||
<t t-set="is_online" t-value="'appointment_type_id' in object.event_id and object.event_id.appointment_type_id"/>
|
||||
|
||||
<p>
|
||||
Hello <t t-out="object.common_name or ''">Wood Corner</t>,<br/><br/>
|
||||
<h1 style="font-size: 18px; font-weight: bold; color: #374151; margin-bottom: 28px;">
|
||||
Invitation
|
||||
</h1>
|
||||
<p style="margin: 0;">
|
||||
<span style="display: block; margin-bottom: 8px;">Hello <t t-out="object.common_name or ''">Wood Corner</t>,</span>
|
||||
|
||||
<t t-if="is_online and target_customer">
|
||||
Your appointment <strong t-out="object.event_id.appointment_type_id.name or ''">Schedule a Demo</strong> <t t-if="object.event_id.appointment_type_id.category != 'custom'"> with <t t-out="object.event_id.user_id.name or ''">Ready Mat</t></t> has been booked.
|
||||
</t>
|
||||
<t t-elif="is_online and target_responsible">
|
||||
<t t-if="customer">
|
||||
<t t-out="customer.name or ''"></t> scheduled the following appointment <strong t-out="object.event_id.appointment_type_id.name or ''">Schedule a Demo</strong> with you.
|
||||
<t t-if="not target_responsible">
|
||||
<t t-if="not object.event_id.user_id.active">
|
||||
You have been invited by Customer to the <strong t-out="object.event_id.name or ''">Follow-up for Project proposal</strong> meeting.
|
||||
</t>
|
||||
<t t-else="">
|
||||
Your appointment <strong t-out="object.event_id.appointment_type_id.name or ''">Schedule a Demo</strong> has been booked.
|
||||
<t t-out="object.event_id.user_id.partner_id.name or ''">Colleen Diaz</t> invited you for the <strong t-out="object.event_id.name or ''">Follow-up for Project proposal</strong> meeting.
|
||||
</t>
|
||||
</t>
|
||||
<t t-elif="not target_responsible">
|
||||
<t t-out="object.event_id.user_id.partner_id.name or ''">Colleen Diaz</t> invited you for the <strong t-out="object.event_id.name or ''">Follow-up for Project proposal</strong> meeting.
|
||||
</t>
|
||||
<t t-else="">
|
||||
Your meeting <strong t-out="object.event_id.name or ''">Follow-up for Project proposal</strong> has been booked.
|
||||
</t>
|
||||
|
||||
</p>
|
||||
<div style="text-align: center; padding: 16px 0px 16px 0px;">
|
||||
<t t-if="not is_online or object.state != 'accepted'">
|
||||
<a t-attf-href="/calendar/meeting/accept?token={{object.access_token}}&id={{object.event_id.id}}"
|
||||
style="padding: 5px 10px; color: #FFFFFF; text-decoration: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius: 3px">
|
||||
Accept</a>
|
||||
<a t-attf-href="/calendar/meeting/decline?token={{object.access_token}}&id={{object.event_id.id}}"
|
||||
style="padding: 5px 10px; color: #FFFFFF; text-decoration: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius: 3px">
|
||||
Decline</a>
|
||||
</t>
|
||||
<a t-attf-href="/calendar/meeting/accept?token={{object.access_token}}&id={{object.event_id.id}}"
|
||||
t-attf-style="display: inline-block; padding: 7px 14px; color: {{object.event_id.user_id.company_id.email_primary_color or '#FFFFFF'}}; text-decoration: none; background-color: {{object.event_id.user_id.company_id.email_secondary_color or '#875A7B'}}; border-radius: 3px;">
|
||||
Accept</a>
|
||||
<a t-attf-href="/calendar/meeting/decline?token={{object.access_token}}&id={{object.event_id.id}}"
|
||||
t-attf-style="display: inline-block; margin-left: 6px; padding: 7px 14px; color: {{object.event_id.user_id.company_id.email_primary_color or '#FFFFFF'}}; text-decoration: none; background-color: {{object.event_id.user_id.company_id.email_secondary_color or '#875A7B'}}; border-radius: 3px;">
|
||||
Decline</a>
|
||||
<a t-attf-href="/calendar/meeting/view?token={{object.access_token}}&id={{object.event_id.id}}"
|
||||
style="padding: 5px 10px; color: #FFFFFF; text-decoration: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius: 3px"
|
||||
><t t-out="'Reschedule' if is_online and target_customer else 'View'">View</t></a>
|
||||
t-attf-style="display: inline-block; margin-left: 12px; padding: 7px 14px; color: #111827; text-decoration: none; background-color: #E7E9ED; border-radius: 3px;">
|
||||
View</a>
|
||||
</div>
|
||||
<table border="0" cellpadding="0" cellspacing="0"><tr>
|
||||
<td width="130px;" style="min-width: 130px;">
|
||||
<div style="border-top-start-radius: 3px; border-top-end-radius: 3px; font-size: 12px; border-collapse: separate; text-align: center; font-weight: bold; color: #ffffff; min-height: 18px; background-color: #875A7B; border: 1px solid #875A7B;">
|
||||
<t t-out="format_datetime(dt=object.event_id.start, tz=object.mail_tz if not object.event_id.allday else None, dt_format='EEEE', lang_code=object.env.lang) or ''">Tuesday</t>
|
||||
</div>
|
||||
<div style="font-size: 48px; min-height: auto; font-weight: bold; text-align: center; color: #5F5F5F; background-color: #F8F8F8; border: 1px solid #875A7B;">
|
||||
<t t-out="format_datetime(dt=object.event_id.start, tz=object.mail_tz if not object.event_id.allday else None, dt_format='d', lang_code=object.env.lang) or ''">4</t>
|
||||
</div>
|
||||
<div style='font-size: 12px; text-align: center; font-weight: bold; color: #ffffff; background-color: #875A7B;'>
|
||||
<t t-out="format_datetime(dt=object.event_id.start, tz=object.mail_tz if not object.event_id.allday else None, dt_format='MMMM y', lang_code=object.env.lang) or ''">May 2021</t>
|
||||
</div>
|
||||
<div style="border-collapse: separate; color: #5F5F5F; text-align: center; font-size: 12px; border-bottom-end-radius: 3px; font-weight: bold ; border: 1px solid #875A7B; border-bottom-start-radius: 3px;">
|
||||
<t t-if="not object.event_id.allday">
|
||||
<div>
|
||||
<div style="margin: 16px 0 0;">
|
||||
<h2 t-attf-style="margin-bottom: 12px; font-size: 14px; font-weight: bold; color: {{user.company_id.email_secondary_color or '#875A7B'}};">Details</h2>
|
||||
<table cellpadding="0" cellspacing="0" border="0" style="font-size: 13px; color: #374151;">
|
||||
<tr>
|
||||
<td style="width: 100px; color: #676F7B;">Date & Time</td>
|
||||
<td style="font-weight: bold;">
|
||||
<t t-out="format_datetime(dt=object.event_id.start, tz=object.mail_tz if not object.event_id.allday else None, dt_format='EEEE', lang_code=object.env.lang) or ''">Tuesday</t>
|
||||
<t t-out="format_datetime(dt=object.event_id.start, tz=object.mail_tz if not object.event_id.allday else None, dt_format='d', lang_code=object.env.lang) or ''">4</t>
|
||||
<t t-out="format_datetime(dt=object.event_id.start, tz=object.mail_tz if not object.event_id.allday else None, dt_format='MMMM y', lang_code=object.env.lang) or ''">May 2021</t>
|
||||
<t t-if="not object.event_id.allday">
|
||||
<t t-out="format_time(time=object.event_id.start, tz=object.mail_tz, time_format='short', lang_code=object.env.lang) or ''">11:00 AM</t>
|
||||
</div>
|
||||
<t t-if="object.mail_tz">
|
||||
<div style="font-size: 10px; font-weight: normal">
|
||||
(<t t-out="object.mail_tz or ''">Europe/Brussels</t>)
|
||||
</div>
|
||||
<t t-if="object.mail_tz">
|
||||
<span style="font-size: 11px; font-weight: normal;">
|
||||
(<t t-out="object.mail_tz or ''">Europe/Brussels</t>)
|
||||
</span>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</td>
|
||||
<td width="20px;"/>
|
||||
<td style="padding-top: 5px;">
|
||||
<p><strong>Details of the event</strong></p>
|
||||
<ul>
|
||||
<t t-if="is_online">
|
||||
<li>Appointment Type: <t t-out="object.event_id.appointment_type_id.name or ''">Schedule a Demo</t></li>
|
||||
</t>
|
||||
<t t-if="object.event_id.location">
|
||||
<li>Location: <t t-out="object.event_id.location or ''">Bruxelles</t>
|
||||
(<a target="_blank" t-attf-href="http://maps.google.com/maps?oi=map&q={{object.event_id.location}}">View Map</a>)
|
||||
</li>
|
||||
</t>
|
||||
<t t-if="recurrent">
|
||||
<li>When: <t t-out="object.recurrence_id.get_recurrence_name()">Every 1 Weeks, for 3 events</t></li>
|
||||
</t>
|
||||
<t t-if="not object.event_id.allday and object.event_id.duration">
|
||||
<li>Duration: <t t-out="('%dH%02d' % (object.event_id.duration,round(object.event_id.duration*60)%60)) or ''">0H30</t></li>
|
||||
</t>
|
||||
<li>Attendees
|
||||
<ul>
|
||||
<li t-foreach="object.event_id.attendee_ids" t-as="attendee">
|
||||
<div t-attf-style="display: inline-block; border-radius: 50%; width: 10px; height: 10px; background:{{ colors.get(attendee.state) or 'white' }};"> </div>
|
||||
<t t-if="attendee.common_name != object.common_name">
|
||||
<span style="margin-left:5px" t-out="attendee.common_name or ''">Mitchell Admin</span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span style="margin-left:5px">You</span>
|
||||
</t>
|
||||
</li>
|
||||
</ul></li>
|
||||
<t t-if="object.event_id.videocall_location">
|
||||
<li>
|
||||
How to Join:
|
||||
<t t-if="object.get_base_url() in object.event_id.videocall_location"> Join with Odoo Discuss</t>
|
||||
<t t-else=""> Join at</t><br/>
|
||||
<a t-att-href="object.event_id.videocall_location" target="_blank" t-out="object.event_id.videocall_location or ''">www.mycompany.com/calendar/join_videocall/xyz</a>
|
||||
</li>
|
||||
</t>
|
||||
<t t-if="not is_html_empty(object.event_id.description)">
|
||||
<li>Description of the event:
|
||||
<t t-out="object.event_id.description">Internal meeting for discussion for new pricing for product and services.</t></li>
|
||||
</t>
|
||||
</ul>
|
||||
</td>
|
||||
</tr></table>
|
||||
<br/>
|
||||
Thank you,
|
||||
<t t-if="object.event_id.user_id.signature">
|
||||
<br />
|
||||
<t t-out="object.event_id.user_id.signature or ''">--<br/>Mitchell Admin</t>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-if="recurrent">
|
||||
<td style="width: 100px; padding-top: 6px; color: #676F7B;">When</td>
|
||||
<td style="padding-top: 6px;"><t t-out="object.recurrence_id.get_recurrence_name() or ''">Every 1 Weeks, for 3 events</t></td>
|
||||
</tr>
|
||||
<tr t-if="not object.event_id.allday and object.event_id.duration">
|
||||
<td style="width: 100px; padding-top: 6px; color: #676F7B;">Duration</td>
|
||||
<td style="padding-top: 6px;"><t t-out="('%dH%02d' % (object.event_id.duration,round(object.event_id.duration*60)%60)) or ''">0H30</t></td>
|
||||
</tr>
|
||||
<tr t-if="object.event_id.location">
|
||||
<td style="width: 100px; padding-top: 6px; color: #676F7B; vertical-align: top;">Location</td>
|
||||
<td style="padding-top: 6px;">
|
||||
<t t-out="object.event_id.location or ''">Bruxelles</t>
|
||||
<a target="_blank" t-if="not is_online or is_online and object.event_id.location != object.event_id.appointment_type_id.location_id.name" t-attf-href="http://maps.google.com/maps?oi=map&q={{object.event_id.location}}" style="margin-left: 12px; font-size: 12px; color: #008f8c; text-decoration: underline;">View Map</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-if="object.event_id.videocall_location">
|
||||
<td style="width: 100px; padding-top: 6px;color: #676F7B;">
|
||||
<t t-if="object.get_base_url() in object.event_id.videocall_location">Join with</t>
|
||||
<t t-else="">Join</t>
|
||||
</td>
|
||||
<td style="padding-top: 6px;">
|
||||
<a t-att-href="object.event_id.videocall_location" target="_blank" style="color: #008f8c;">
|
||||
<t t-if="object.get_base_url() in object.event_id.videocall_location">Odoo Discuss</t>
|
||||
<t t-else="">Video meeting</t>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div style="margin: 32px 0 0;">
|
||||
<h2 t-attf-style="margin-bottom: 12px; font-size: 14px; font-weight: bold; color: {{user.company_id.email_secondary_color or '#875A7B'}};">Attendees</h2>
|
||||
<div t-foreach="object.event_id.attendee_ids" t-as="attendee" style="margin-bottom: 5px;">
|
||||
<img t-if="attendee.state" t-attf-src="/calendar/static/src/img/state_{{ attendee.state }}.png" style="border:0; width: 15px; height: 15px; vertical-align: text-bottom;" />
|
||||
<t t-if="attendee != object">
|
||||
<span style="margin-left: 5px;" t-out="attendee.common_name or ''">Mitchell Admin</span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span style="margin-left: 5px;">You</span>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="not is_html_empty(object.event_id.description)" style="margin: 32px 0 0;">
|
||||
<h2 t-attf-style="margin-bottom: 12px; font-size: 14px; font-weight: bold; color: {{user.company_id.email_secondary_color or '#875A7B'}};">Description of the event</h2>
|
||||
<t t-out="object.event_id.description">Internal meeting for discussion for new pricing for product and services.</t>
|
||||
</div>
|
||||
<p style="margin: 32px 0 0;">
|
||||
Thank you!
|
||||
<t t-if="object.event_id.user_id.signature">
|
||||
<span style="display: block; margin-top: 8px;"><t t-out="object.event_id.user_id.signature or ''">Mitchell Admin</t></span>
|
||||
</t>
|
||||
</p>
|
||||
</div>
|
||||
</field>
|
||||
<field name="lang">{{ object.partner_id.lang }}</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
|
||||
|
|
@ -140,129 +127,128 @@
|
|||
<field name="model_id" ref="calendar.model_calendar_attendee"/>
|
||||
<field name="subject">{{ object.event_id.name }}: Date updated</field>
|
||||
<field name="email_from">{{ (object.event_id.user_id.email_formatted or user.email_formatted or '') }}</field>
|
||||
<field name="email_to">{{ ('' if object.partner_id.email and object.partner_id.email == object.email else object.email) }}</field>
|
||||
<field name="partner_to">{{ object.partner_id.id if object.partner_id.email and object.partner_id.email == object.email else False }}</field>
|
||||
<field name="lang">{{ object.partner_id.lang }}</field>
|
||||
<field name="email_to" eval="False"/>
|
||||
<field name="partner_to" eval="False"/>
|
||||
<field name="use_default_to" eval="True"/>
|
||||
<field name="description">Sent to all attendees if the schedule change</field>
|
||||
<field name="body_html" type="html">
|
||||
<div>
|
||||
<div style="font-size: 13px; color: #374151;">
|
||||
|
||||
<t t-set="colors" t-value="{'needsAction': 'grey', 'accepted': 'green', 'tentative': '#FFFF00', 'declined': 'red'}"/>
|
||||
<t t-set="is_online" t-value="'appointment_type_id' in object.event_id and object.event_id.appointment_type_id"/>
|
||||
<t t-set="customer" t-value="object.event_id.find_partner_customer()"/>
|
||||
<t t-set="target_responsible" t-value="object.partner_id == object.event_id.partner_id"/>
|
||||
<t t-set="target_customer" t-value="object.partner_id == customer"/>
|
||||
<t t-set="recurrent" t-value="object.recurrence_id and not ctx.get('calendar_template_ignore_recurrence')"/>
|
||||
<t t-set="recurrent" t-value="object.recurrence_id and not ctx.get('calendar_template_ignore_recurrence')"/>
|
||||
<t t-set="organizer" t-value="object.event_id.user_id"/>
|
||||
|
||||
<p>
|
||||
Hello <t t-out="object.common_name or ''">Ready Mat</t>,<br/><br/>
|
||||
<h1 style="font-size: 18px; font-weight: bold; color: #374151; margin-bottom: 28px;">
|
||||
Date Updated
|
||||
<img src="/calendar/static/src/img/sync.png" style="border:0; width: 24px; height: 24px; margin-left: 10px; text-decoration:none; vertical-align: bottom;" />
|
||||
</h1>
|
||||
<p style="margin: 0;">
|
||||
<span style="display: block; margin-bottom: 8px;">Hello <t t-out="object.common_name or ''">Ready Mat</t>,</span>
|
||||
<t t-if="is_online and target_responsible">
|
||||
<t t-if="customer">
|
||||
The date of your appointment with <t t-out="customer.name or ''">Jesse Brown</t> has been updated.
|
||||
Your appointment with <t t-out="customer.name or ''">Jesse Brown</t> has been updated:
|
||||
</t>
|
||||
<t t-else="">
|
||||
Your appointment has been updated.
|
||||
Your appointment has been updated:
|
||||
</t>
|
||||
The appointment <strong t-out="object.event_id.appointment_type_id.name or ''">Schedule a Demo</strong> is now scheduled for
|
||||
<t t-out="object.event_id.get_display_time_tz(tz=object.partner_id.tz) or ''">05/04/2021 at (11:00:00 To 11:30:00) (Europe/Brussels)</t>
|
||||
</t>
|
||||
<t t-elif="is_online and target_customer">
|
||||
The date of your appointment with <t t-out="object.event_id.user_id.partner_id.name or ''">Colleen Diaz</t> has been updated.
|
||||
The appointment <strong t-out="object.event_id.appointment_type_id.name or ''"></strong> is now scheduled for
|
||||
<strong t-out="object.event_id.appointment_type_id.name or ''">Schedule a Demo</strong> is now scheduled for
|
||||
<t t-out="object.event_id.get_display_time_tz(tz=object.partner_id.tz) or ''">05/04/2021 at (11:00:00 To 11:30:00) (Europe/Brussels)</t>.
|
||||
</t>
|
||||
<t t-elif="is_online and target_customer">
|
||||
Your appointment <strong t-out="object.event_id.appointment_type_id.name or ''"></strong> <t t-if="organizer">with </t><t t-out="organizer.partner_id.name or ''">Colleen Diaz</t>
|
||||
is now scheduled for <t t-out="object.event_id.get_display_time_tz(tz=object.partner_id.tz) or ''">05/04/2021 at (11:00:00 To 11:30:00) (Europe/Brussels)</t>.
|
||||
</t>
|
||||
<t t-else="">
|
||||
The date of the meeting has been updated.
|
||||
The meeting <strong t-out="object.event_id.name or ''">Follow-up for Project proposal</strong> created by <t t-out="object.event_id.user_id.partner_id.name or ''">Colleen Diaz</t> is now scheduled for
|
||||
The date of the meeting <strong t-out="object.event_id.name or ''">Follow-up for Project proposal</strong> <t t-if="organizer">created by </t><t t-out="organizer.partner_id.name or ''">Colleen Diaz</t> is now scheduled for
|
||||
<t t-out="object.event_id.get_display_time_tz(tz=object.partner_id.tz) or ''">05/04/2021 at (11:00:00 To 11:30:00) (Europe/Brussels)</t>.
|
||||
</t>
|
||||
</p>
|
||||
<div style="text-align: center; padding: 16px 0px 16px 0px;">
|
||||
<a t-attf-href="/calendar/meeting/accept?token={{ object.access_token }}&id={{ object.event_id.id }}"
|
||||
style="padding: 5px 10px; color: #FFFFFF; text-decoration: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius: 3px">
|
||||
t-attf-style="display: inline-block; padding: 7px 14px; color: {{organizer.company_id.email_primary_color or '#FFFFFF'}}; text-decoration: none; background-color: {{organizer.company_id.email_secondary_color or '#875A7B'}}; border-radius: 3px;">
|
||||
Accept</a>
|
||||
<a t-attf-href="/calendar/meeting/decline?token={{ object.access_token }}&id={{ object.event_id.id }}"
|
||||
style="padding: 5px 10px; color: #FFFFFF; text-decoration: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius: 3px">
|
||||
t-attf-style="display: inline-block; margin-left: 6px; padding: 7px 14px; color: {{organizer.company_id.email_primary_color or '#FFFFFF'}}; text-decoration: none; background-color: {{organizer.company_id.email_secondary_color or '#875A7B'}}; border-radius: 3px;">
|
||||
Decline</a>
|
||||
<a t-attf-href="/calendar/meeting/view?token={{ object.access_token }}&id={{ object.event_id.id }}"
|
||||
style="padding: 5px 10px; color: #FFFFFF; text-decoration: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius: 3px">
|
||||
t-attf-style="display: inline-block; margin-left: 12px; padding: 7px 14px; color: #111827; text-decoration: none; background-color: #E7E9ED; border-radius: 3px;">
|
||||
View</a>
|
||||
</div>
|
||||
<table border="0" cellpadding="0" cellspacing="0"><tr>
|
||||
<td width="130px;" style="min-width: 130px;">
|
||||
<div style="border-top-start-radius: 3px; border-top-end-radius: 3px; font-size: 12px; border-collapse: separate; text-align: center; font-weight: bold; color: #ffffff; min-height: 18px; background-color: #875A7B; border: 1px solid #875A7B;">
|
||||
<t t-out='format_datetime(dt=object.event_id.start, tz=object.mail_tz if not object.event_id.allday else None, dt_format="EEEE", lang_code=object.env.lang) or ""'>Tuesday</t>
|
||||
</div>
|
||||
<div style="font-size: 48px; min-height: auto; font-weight: bold; text-align: center; color: #5F5F5F; background-color: #F8F8F8; border: 1px solid #875A7B;">
|
||||
<t t-out="format_datetime(dt=object.event_id.start, tz=object.mail_tz if not object.event_id.allday else None, dt_format='d', lang_code=object.env.lang) or ''">4</t>
|
||||
</div>
|
||||
<div style='font-size: 12px; text-align: center; font-weight: bold; color: #ffffff; background-color: #875A7B;'>
|
||||
<t t-out='format_datetime(dt=object.event_id.start, tz=object.mail_tz if not object.event_id.allday else None, dt_format="MMMM y", lang_code=object.env.lang) or ""'>May 2021</t>
|
||||
</div>
|
||||
<div style="border-collapse: separate; color: #5F5F5F; text-align: center; font-size: 12px; border-bottom-end-radius: 3px; font-weight: bold; border: 1px solid #875A7B; border-bottom-start-radius: 3px;">
|
||||
<t t-if="not object.event_id.allday">
|
||||
<div>
|
||||
<t t-out='format_time(time=object.event_id.start, tz=object.mail_tz, time_format="short", lang_code=object.env.lang) or ""'>11:00 AM</t>
|
||||
</div>
|
||||
<t t-if="object.mail_tz">
|
||||
<div style="font-size: 10px; font-weight: normal">
|
||||
(<t t-out="object.mail_tz or ''">Europe/Brussels</t>)
|
||||
</div>
|
||||
<div style="margin: 16px 0 0;">
|
||||
<h2 t-attf-style="margin-bottom: 12px; font-size: 14px; font-weight: bold; color: {{user.company_id.email_secondary_color or '#875A7B'}};">Details</h2>
|
||||
<table cellpadding="0" cellspacing="0" border="0" style="font-size: 13px; color: #374151;">
|
||||
<tr>
|
||||
<td style="width: 100px; color: #676F7B;">Date & Time</td>
|
||||
<td style="font-weight: bold;">
|
||||
<t t-out="format_datetime(dt=object.event_id.start, tz=object.mail_tz if not object.event_id.allday else None, dt_format='EEEE', lang_code=object.env.lang) or ''">Tuesday</t>
|
||||
<t t-out="format_datetime(dt=object.event_id.start, tz=object.mail_tz if not object.event_id.allday else None, dt_format='d', lang_code=object.env.lang) or ''">4</t>
|
||||
<t t-out="format_datetime(dt=object.event_id.start, tz=object.mail_tz if not object.event_id.allday else None, dt_format='MMMM y', lang_code=object.env.lang) or ''">May 2021</t>
|
||||
<t t-if="not object.event_id.allday">
|
||||
<t t-out="format_time(time=object.event_id.start, tz=object.mail_tz, time_format='short', lang_code=object.env.lang) or ''">11:00 AM</t>
|
||||
<t t-if="object.mail_tz">
|
||||
<span style="font-size: 11px; font-weight: normal;">
|
||||
(<t t-out="object.mail_tz or ''">Europe/Brussels</t>)
|
||||
</span>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</td>
|
||||
<td width="20px;"/>
|
||||
<td style="padding-top: 5px;">
|
||||
<p><strong>Details of the event</strong></p>
|
||||
<ul>
|
||||
<t t-if="object.event_id.location">
|
||||
<li>Location: <t t-out="object.event_id.location or ''">Bruxelles</t>
|
||||
(<a target="_blank" t-attf-href="http://maps.google.com/maps?oi=map&q={{ object.event_id.location }}">View Map</a>)
|
||||
</li>
|
||||
</t>
|
||||
<t t-if="recurrent">
|
||||
<li>When: <t t-out="object.recurrence_id.get_recurrence_name()">Every 1 Weeks, for 3 events</t></li>
|
||||
</t>
|
||||
<t t-if="not object.event_id.allday and object.event_id.duration">
|
||||
<li>Duration: <t t-out="('%dH%02d' % (object.event_id.duration,round(object.event_id.duration*60)%60)) or ''">0H30</t></li>
|
||||
</t>
|
||||
<li>Attendees
|
||||
<ul>
|
||||
<li t-foreach="object.event_id.attendee_ids" t-as="attendee">
|
||||
<div t-attf-style="display: inline-block; border-radius: 50%; width: 10px; height: 10px; background: {{ colors.get(attendee.state) or 'white' }};"> </div>
|
||||
<t t-if="attendee.common_name != object.common_name">
|
||||
<span style="margin-left:5px" t-out="attendee.common_name or ''">Mitchell Admin</span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span style="margin-left:5px">You</span>
|
||||
</t>
|
||||
</li>
|
||||
</ul></li>
|
||||
<t t-if="object.event_id.videocall_location">
|
||||
<li>
|
||||
How to Join:
|
||||
<t t-if="object.get_base_url() in object.event_id.videocall_location"> Join with Odoo Discuss</t>
|
||||
<t t-else=""> Join at</t><br/>
|
||||
<a t-att-href="object.event_id.videocall_location" target="_blank" t-out="object.event_id.videocall_location or ''">www.mycompany.com/calendar/join_videocall/xyz</a>
|
||||
</li>
|
||||
</t>
|
||||
<t t-if="not is_html_empty(object.event_id.description)">
|
||||
<li>Description of the event:
|
||||
<t t-out="object.event_id.description">Internal meeting for discussion for new pricing for product and services.</t></li>
|
||||
</t>
|
||||
</ul>
|
||||
</td>
|
||||
</tr></table>
|
||||
<br/>
|
||||
Thank you,
|
||||
<t t-if="object.event_id.user_id.signature">
|
||||
<br />
|
||||
<t t-out="object.event_id.user_id.signature or ''">--<br/>Mitchell Admin</t>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-if="recurrent">
|
||||
<td style="width: 100px; padding-top: 6px; color: #676F7B;">When</td>
|
||||
<td style="padding-top: 6px;"><t t-out="object.recurrence_id.get_recurrence_name() or ''">Every 1 Weeks, for 3 events</t></td>
|
||||
</tr>
|
||||
<tr t-if="not object.event_id.allday and object.event_id.duration">
|
||||
<td style="width: 100px; padding-top: 6px; color: #676F7B;">Duration</td>
|
||||
<td style="padding-top: 6px;"><t t-out="('%dH%02d' % (object.event_id.duration,round(object.event_id.duration*60)%60)) or ''">0H30</t></td>
|
||||
</tr>
|
||||
<tr t-if="object.event_id.location">
|
||||
<td style="width: 100px; padding-top: 6px; color: #676F7B; vertical-align: top;">Location</td>
|
||||
<td style="padding-top: 6px;">
|
||||
<t t-out="object.event_id.location or ''">Bruxelles</t>
|
||||
<a target="_blank" t-if="not is_online or is_online and object.event_id.location != object.event_id.appointment_type_id.location_id.name" t-attf-href="http://maps.google.com/maps?oi=map&q={{object.event_id.location}}" style="margin-left: 12px; font-size: 12px; color: #008f8c; text-decoration: underline;">View Map</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-if="object.event_id.videocall_location">
|
||||
<td style="width: 100px; padding-top: 6px; color: #676F7B;">
|
||||
<t t-if="object.event_id.videocall_source == 'discuss'">Join with</t>
|
||||
<t t-else="">Join</t>
|
||||
</td>
|
||||
<td style="padding-top: 6px;">
|
||||
<a t-att-href="object.event_id.videocall_location" target="_blank" style="color: #008f8c;">
|
||||
<t t-if="object.event_id.videocall_source == 'discuss'">Odoo Discuss</t>
|
||||
<t t-else="">Video meeting</t>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div style="margin: 32px 0 0;">
|
||||
<h2 t-attf-style="margin-bottom: 12px; font-size: 14px; font-weight: bold; color: {{user.company_id.email_secondary_color or '#875A7B'}};">Attendees</h2>
|
||||
<div t-foreach="object.event_id.attendee_ids" t-as="attendee" style="margin-bottom: 5px;">
|
||||
<img t-if="attendee.state" t-attf-src="/calendar/static/src/img/state_{{ attendee.state }}.png" style="border:0; width: 15px; height: 15px; vertical-align: text-bottom;" />
|
||||
<t t-if="attendee != object">
|
||||
<span style="margin-left: 5px;" t-out="attendee.common_name or ''">Mitchell Admin</span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span style="margin-left: 5px;">You</span>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="not is_html_empty(object.event_id.description)" style="margin: 32px 0 0;">
|
||||
<h2 t-attf-style="margin-bottom: 12px; font-size: 14px; font-weight: bold; color: {{user.company_id.email_secondary_color or '#875A7B'}};">Description of the event</h2>
|
||||
<t t-out="object.event_id.description">Internal meeting for discussion for new pricing for product and services.</t>
|
||||
</div>
|
||||
<p style="margin: 32px 0 0;">
|
||||
Thank you!
|
||||
<t t-if="organizer.signature">
|
||||
<span style="display: block; margin-top: 8px;"><t t-out="organizer.signature or ''">Mitchell Admin</t></span>
|
||||
</t>
|
||||
</p>
|
||||
</div>
|
||||
</field>
|
||||
<field name="lang">{{ object.partner_id.lang }}</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
|
||||
|
|
@ -271,105 +257,109 @@
|
|||
<field name="model_id" ref="calendar.model_calendar_attendee"/>
|
||||
<field name="subject">{{ object.event_id.name }} - Reminder</field>
|
||||
<field name="email_from">{{ (object.event_id.user_id.email_formatted or user.email_formatted or '') }}</field>
|
||||
<field name="email_to">{{ ('' if object.partner_id.email and object.partner_id.email == object.email else object.email) }}</field>
|
||||
<field name="partner_to">{{ object.partner_id.id if object.partner_id.email and object.partner_id.email == object.email else False }}</field>
|
||||
<field name="lang">{{ object.partner_id.lang }}</field>
|
||||
<field name="email_to" eval="False"/>
|
||||
<field name="partner_to" eval="False"/>
|
||||
<field name="use_default_to" eval="True"/>
|
||||
<field name="description">Sent to all attendees if a reminder is set</field>
|
||||
<field name="body_html" type="html">
|
||||
<div>
|
||||
<t t-set="colors" t-value="{'needsAction': 'grey', 'accepted': 'green', 'tentative': '#FFFF00', 'declined': 'red'}" />
|
||||
<div style="font-size: 13px; color: #374151;">
|
||||
<t t-set="is_online" t-value="'appointment_type_id' in object.event_id and object.event_id.appointment_type_id" />
|
||||
<t t-set="recurrent" t-value="object.recurrence_id and not ctx.get('calendar_template_ignore_recurrence')" />
|
||||
<p>
|
||||
Hello <t t-out="object.common_name or ''">Gemini Furniture</t>,<br/><br/>
|
||||
This is a reminder for the below event :
|
||||
<h1 style="font-size: 18px; font-weight: bold; color: #374151; margin-bottom: 28px;">
|
||||
Reminder
|
||||
<img src="/calendar/static/src/img/bell.png" style="border:0; width: 21px; height: 24px; margin-left: 10px; text-decoration:none; vertical-align: bottom;" />
|
||||
</h1>
|
||||
<p style="margin: 0;">
|
||||
Hello <t t-out="object.common_name or ''">Gemini Furniture</t>,<br/>
|
||||
<span style="display: block; margin-top: 8px;">This is a reminder for the event below.</span>
|
||||
</p>
|
||||
<div style="text-align: center; padding: 16px 0px 16px 0px;">
|
||||
<a t-attf-href="/calendar/{{ 'recurrence' if recurrent else 'meeting' }}/accept?token={{ object.access_token }}&id={{ object.event_id.id }}"
|
||||
style="padding: 5px 10px; color: #FFFFFF; text-decoration: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius: 3px">
|
||||
t-attf-style="display: inline-block; padding: 7px 14px; color: {{object.event_id.user_id.company_id.email_primary_color or '#FFFFFF'}}; text-decoration: none; background-color: {{object.event_id.user_id.company_id.email_secondary_color or '#875A7B'}}; border-radius: 3px;">
|
||||
Accept</a>
|
||||
<a t-attf-href="/calendar/{{ 'recurrence' if recurrent else 'meeting' }}/decline?token={{ object.access_token }}&id={{ object.event_id.id }}"
|
||||
style="padding: 5px 10px; color: #FFFFFF; text-decoration: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius: 3px">
|
||||
t-attf-style="display: inline-block; margin-left: 6px; padding: 7px 14px; color: {{object.event_id.user_id.company_id.email_primary_color or '#FFFFFF'}}; text-decoration: none; background-color: {{object.event_id.user_id.company_id.email_secondary_color or '#875A7B'}}; border-radius: 3px;">
|
||||
Decline</a>
|
||||
<a t-attf-href="/calendar/meeting/view?token={{ object.access_token }}&id={{ object.event_id.id }}"
|
||||
style="padding: 5px 10px; color: #FFFFFF; text-decoration: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius: 3px">
|
||||
t-attf-style="display: inline-block; margin-left: 12px; padding: 7px 14px; color: #111827; text-decoration: none; background-color: #E7E9ED; border-radius: 3px;">
|
||||
View</a>
|
||||
</div>
|
||||
<table border="0" cellpadding="0" cellspacing="0"><tr>
|
||||
<td width="130px;" style="min-width: 130px;">
|
||||
<div style="border-top-start-radius: 3px; border-top-end-radius: 3px; font-size: 12px; border-collapse: separate; text-align: center; font-weight: bold; color: #ffffff; min-height: 18px; background-color: #875A7B; border: 1px solid #875A7B;">
|
||||
<t t-out='format_datetime(dt=object.event_id.start, tz=object.mail_tz if not object.event_id.allday else None, dt_format="EEEE", lang_code=object.env.lang) or ""'>Tuesday</t>
|
||||
</div>
|
||||
<div style="font-size: 48px; min-height: auto; font-weight: bold; text-align: center; color: #5F5F5F; background-color: #F8F8F8; border: 1px solid #875A7B;">
|
||||
<t t-out="format_datetime(dt=object.event_id.start, tz=object.mail_tz if not object.event_id.allday else None, dt_format='d', lang_code=object.env.lang) or ''">4</t>
|
||||
</div>
|
||||
<div style='font-size: 12px; text-align: center; font-weight: bold; color: #ffffff; background-color: #875A7B;'>
|
||||
<t t-out='format_datetime(dt=object.event_id.start, tz=object.mail_tz if not object.event_id.allday else None, dt_format="MMMM y", lang_code=object.env.lang) or ""'>May 2021</t>
|
||||
</div>
|
||||
<div style="border-collapse: separate; color: #5F5F5F; text-align: center; font-size: 12px; border-bottom-end-radius: 3px; font-weight: bold; border: 1px solid #875A7B; border-bottom-start-radius: 3px;">
|
||||
<t t-if="not object.event_id.allday">
|
||||
<div>
|
||||
<t t-out='format_time(time=object.event_id.start, tz=object.mail_tz, time_format="short", lang_code=object.env.lang) or ""'>11:00 AM</t>
|
||||
</div>
|
||||
<t t-if="object.mail_tz">
|
||||
<div style="font-size: 10px; font-weight: normal">
|
||||
(<t t-out="object.mail_tz or ''">Europe/Brussels</t>)
|
||||
</div>
|
||||
<div style="margin: 16px 0 0;">
|
||||
<h2 t-attf-style="margin-bottom: 12px; font-size: 14px; font-weight: bold; color: {{user.company_id.email_secondary_color or '#875A7B'}};">Details</h2>
|
||||
<table cellpadding="0" cellspacing="0" border="0" style="font-size: 13px; color: #374151;">
|
||||
<tr>
|
||||
<td style="width: 100px; color: #676F7B;">Date & Time</td>
|
||||
<td style="font-weight: bold;">
|
||||
<t t-out="format_datetime(dt=object.event_id.start, tz=object.mail_tz if not object.event_id.allday else None, dt_format='EEEE', lang_code=object.env.lang) or ''">Tuesday</t>
|
||||
<t t-out="format_datetime(dt=object.event_id.start, tz=object.mail_tz if not object.event_id.allday else None, dt_format='d', lang_code=object.env.lang) or ''">4</t>
|
||||
<t t-out="format_datetime(dt=object.event_id.start, tz=object.mail_tz if not object.event_id.allday else None, dt_format='MMMM y', lang_code=object.env.lang) or ''">May 2021</t>
|
||||
<t t-if="not object.event_id.allday">
|
||||
<t t-out="format_time(time=object.event_id.start, tz=object.mail_tz, time_format='short', lang_code=object.env.lang) or ''">11:00 AM</t>
|
||||
<t t-if="object.mail_tz">
|
||||
<span style="font-size: 11px; font-weight: normal;">
|
||||
(<t t-out="object.mail_tz or ''">Europe/Brussels</t>)
|
||||
</span>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</td>
|
||||
<td width="20px;"/>
|
||||
<td style="padding-top: 5px;">
|
||||
<p><strong>Details of the event</strong></p>
|
||||
<ul>
|
||||
<t t-if="object.event_id.location">
|
||||
<li>Location: <t t-out="object.event_id.location or ''">Bruxelles</t>
|
||||
(<a target="_blank" t-attf-href="http://maps.google.com/maps?oi=map&q={{ object.event_id.location }}">View Map</a>)
|
||||
</li>
|
||||
</t>
|
||||
<t t-if="recurrent">
|
||||
<li>When: <t t-out="object.recurrence_id.get_recurrence_name()">Every 1 Weeks, for 3 events</t></li>
|
||||
</t>
|
||||
<t t-if="not object.event_id.allday and object.event_id.duration">
|
||||
<li>Duration: <t t-out="('%dH%02d' % (object.event_id.duration,round(object.event_id.duration*60)%60)) or ''">0H30</t></li>
|
||||
</t>
|
||||
<li>Attendees
|
||||
<ul>
|
||||
<li t-foreach="object.event_id.attendee_ids" t-as="attendee">
|
||||
<div t-attf-style="display: inline-block; border-radius: 50%; width: 10px; height: 10px; background:{{ colors.get(attendee.state) or 'white' }};"> </div>
|
||||
<t t-if="attendee.common_name != object.common_name">
|
||||
<span style="margin-left:5px" t-out="attendee.common_name or ''">Mitchell Admin</span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span style="margin-left:5px">You</span>
|
||||
</t>
|
||||
</li>
|
||||
</ul></li>
|
||||
<t t-if="object.event_id.videocall_location">
|
||||
<li>
|
||||
How to Join:
|
||||
<t t-if="object.get_base_url() in object.event_id.videocall_location"> Join with Odoo Discuss</t>
|
||||
<t t-else=""> Join at</t><br/>
|
||||
<a t-att-href="object.event_id.videocall_location" target="_blank" t-out="object.event_id.videocall_location or ''">www.mycompany.com/calendar/join_videocall/xyz</a>
|
||||
</li>
|
||||
</t>
|
||||
<t t-if="not is_html_empty(object.event_id.description)">
|
||||
<li>Description of the event:
|
||||
<t t-out="object.event_id.description">Internal meeting for discussion for new pricing for product and services.</t></li>
|
||||
</t>
|
||||
</ul>
|
||||
</td>
|
||||
</tr></table>
|
||||
<br/>
|
||||
Thank you,
|
||||
<t t-if="object.event_id.user_id.signature">
|
||||
<br />
|
||||
<t t-out="object.event_id.user_id.signature or ''">--<br/>Mitchell Admin</t>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-if="recurrent">
|
||||
<td style="width: 100px; padding-top: 6px; color: #676F7B;">When</td>
|
||||
<td style="padding-top: 6px;">
|
||||
<t t-out="object.recurrence_id.get_recurrence_name() or ''">Every 1 Weeks, for 3 events</t>
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-if="not object.event_id.allday and object.event_id.duration">
|
||||
<td style="width: 100px; padding-top: 6px; color: #676F7B;">Duration</td>
|
||||
<td style="padding-top: 6px;">
|
||||
<t t-out="('%dH%02d' % (object.event_id.duration,round(object.event_id.duration*60)%60)) or ''">0H30</t>
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-if="object.event_id.location">
|
||||
<td style="width: 100px; padding-top: 6px; color: #676F7B; vertical-align: top;">Location</td>
|
||||
<td style="padding-top: 6px;">
|
||||
<t t-out="object.event_id.location or ''">Bruxelles</t>
|
||||
<a target="_blank" t-if="not is_online or is_online and object.event_id.location != object.event_id.appointment_type_id.location_id.name" t-attf-href="http://maps.google.com/maps?oi=map&q={{object.event_id.location}}" style="margin-left: 12px; font-size: 12px; color: #008f8c; text-decoration: underline;">View Map</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-if="object.event_id.videocall_location">
|
||||
<td style="width: 100px; padding-top: 6px; color: #676F7B;">
|
||||
<t t-if="object.get_base_url() in object.event_id.videocall_location">Join with</t>
|
||||
<t t-else="">Join</t>
|
||||
</td>
|
||||
<td style="padding-top: 6px;">
|
||||
<a t-att-href="object.event_id.videocall_location" target="_blank" style="color: #008f8c;">
|
||||
<t t-if="object.get_base_url() in object.event_id.videocall_location">Odoo Discuss</t>
|
||||
<t t-else="">Video meeting</t>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div style="margin: 32px 0 0;">
|
||||
<h2 t-attf-style="margin-bottom: 12px; font-size: 14px; font-weight: bold; color: {{user.company_id.email_secondary_color or '#875A7B'}};">Attendees</h2>
|
||||
<div t-foreach="object.event_id.attendee_ids" t-as="attendee" style="margin-bottom: 5px;">
|
||||
<img t-if="attendee.state" t-attf-src="/calendar/static/src/img/state_{{ attendee.state }}.png" style="border:0; width: 15px; height: 15px; vertical-align: text-bottom;" />
|
||||
<t t-if="attendee != object">
|
||||
<span style="margin-left: 5px;" t-out="attendee.common_name or ''">Mitchell Admin</span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span style="margin-left: 5px;">You</span>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="not is_html_empty(object.event_id.description)" style="margin: 32px 0 0;">
|
||||
<h2 t-attf-style="margin-bottom: 12px; font-size: 14px; font-weight: bold; color: {{user.company_id.email_secondary_color or '#875A7B'}};">Description of the event</h2>
|
||||
<t t-out="object.event_id.description">Internal meeting for discussion for new pricing for product and services.</t>
|
||||
</div>
|
||||
<p style="margin: 32px 0 0;">
|
||||
Thank you!
|
||||
<t t-if="object.event_id.user_id.signature">
|
||||
<span style="display: block; margin-top: 8px;"><t t-out="object.event_id.user_id.signature or ''">Mitchell Admin</t></span>
|
||||
</t>
|
||||
</p>
|
||||
</div>
|
||||
</field>
|
||||
<field name="lang">{{ object.partner_id.lang }}</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
|
||||
|
|
@ -378,85 +368,153 @@
|
|||
<field name="model_id" ref="calendar.model_calendar_event"/>
|
||||
<field name="subject">{{object.name}}: Event update</field>
|
||||
<field name="email_from">{{ (object.user_id.email_formatted or user.email_formatted or '') }}</field>
|
||||
<field name="email_to">{{ object._get_attendee_emails() }}</field>
|
||||
<field name="lang">{{ object.partner_id.lang }}</field>
|
||||
<field name="description">Used to manually notifiy attendees</field>
|
||||
<field name="email_to" eval="False"/>
|
||||
<field name="use_default_to" eval="True"/>
|
||||
<field name="description">Used to manually notify attendees</field>
|
||||
<field name="body_html" type="html">
|
||||
<div>
|
||||
<t t-set="colors" t-value="{'needsAction': 'grey', 'accepted': 'green', 'tentative': '#FFFF00', 'declined': 'red'}" />
|
||||
<div style="font-size: 13px; color: #374151;">
|
||||
<t t-set="is_online" t-value="'appointment_type_id' in object and object.appointment_type_id" />
|
||||
<t t-set="target_responsible" t-value="object.partner_id == object.partner_id" />
|
||||
<t t-set="target_customer" t-value="object.partner_id == customer" />
|
||||
<t t-set="recurrent" t-value="object.recurrence_id and not ctx.get('calendar_template_ignore_recurrence')" />
|
||||
<t t-set="mail_tz" t-value="object._get_mail_tz() or ctx.get('mail_tz')" />
|
||||
<div>
|
||||
<table border="0" cellpadding="0" cellspacing="0">
|
||||
<h1 style="font-size: 18px; font-weight: bold; color: #374151; margin-bottom: 28px;">
|
||||
Event updated
|
||||
<img src="/calendar/static/src/img/sync.png" style="border:0; width: 24px; height: 24px; margin-left: 10px; text-decoration:none; vertical-align: bottom;" />
|
||||
</h1>
|
||||
<p style="margin: 0;">
|
||||
<span style="display: block; margin-bottom: 8px;">Hello,</span>
|
||||
This meeting has been updated.
|
||||
</p>
|
||||
<div style="margin: 32px 0 0;">
|
||||
<h2 t-attf-style="margin-bottom: 12px; font-size: 14px; font-weight: bold; color: {{user.company_id.email_secondary_color or '#875A7B'}};">Details</h2>
|
||||
<table cellpadding="0" cellspacing="0" border="0" style="font-size: 13px; color: #374151;">
|
||||
<tr>
|
||||
<td width="130px;" style="min-width: 130px;">
|
||||
<div style="border-top-start-radius: 3px; border-top-end-radius: 3px; font-size: 12px; border-collapse: separate; text-align: center; font-weight: bold; color: #ffffff; min-height: 18px; background-color: #875A7B; border: 1px solid #875A7B;">
|
||||
<t t-out="format_datetime(dt=object.start, tz=mail_tz if not object.allday else None, dt_format='EEEE', lang_code=object.env.lang) "></t>
|
||||
</div>
|
||||
<div style="font-size: 48px; min-height: auto; font-weight: bold; text-align: center; color: #5F5F5F; background-color: #F8F8F8; border: 1px solid #875A7B;">
|
||||
<t t-out="format_datetime(dt=object.start, tz=mail_tz if not object.allday else None, dt_format='d', lang_code=object.env.lang)"></t>
|
||||
</div>
|
||||
<div style='font-size: 12px; text-align: center; font-weight: bold; color: #ffffff; background-color: #875A7B;'>
|
||||
<t t-out="format_datetime(dt=object.start, tz=mail_tz if not object.allday else None, dt_format='MMMM y', lang_code=object.env.lang)"></t>
|
||||
</div>
|
||||
<div style="border-collapse: separate; color: #5F5F5F; text-align: center; font-size: 12px; border-bottom-end-radius: 3px; font-weight: bold; border: 1px solid #875A7B; border-bottom-start-radius: 3px;">
|
||||
<t t-if="not object.allday">
|
||||
<div>
|
||||
<t t-out="format_time(time=object.start, tz=mail_tz, time_format='short', lang_code=object.env.lang)"></t>
|
||||
</div>
|
||||
<t t-if="mail_tz">
|
||||
<div style="font-size: 10px; font-weight: normal">
|
||||
(<t t-out="mail_tz"></t>)
|
||||
</div>
|
||||
</t>
|
||||
<td style="width: 100px; color: #676F7B;">Date & Time</td>
|
||||
<td style="font-weight: bold;">
|
||||
<t t-out="format_datetime(dt=object.start, tz=mail_tz if not object.allday else None, dt_format='EEEE', lang_code=object.env.lang)">Tuesday</t>
|
||||
<t t-out="format_datetime(dt=object.start, tz=mail_tz if not object.allday else None, dt_format='d', lang_code=object.env.lang)">4</t>
|
||||
<t t-out="format_datetime(dt=object.start, tz=mail_tz if not object.allday else None, dt_format='MMMM y', lang_code=object.env.lang)">May 2021</t>
|
||||
<t t-if="not object.allday">
|
||||
<t t-out="format_time(time=object.start, tz=mail_tz, time_format='short', lang_code=object.env.lang)">11:00 AM</t>
|
||||
<t t-if="mail_tz">
|
||||
<span style="font-size: 11px; font-weight: normal;">
|
||||
(<t t-out="mail_tz or ''">Europe/Brussels</t>)
|
||||
</span>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</td>
|
||||
<td width="20px;"/>
|
||||
<td style="padding-top: 5px;">
|
||||
<p>
|
||||
<strong>Details of the event</strong>
|
||||
</p>
|
||||
<ul>
|
||||
<t t-if="not is_html_empty(object.description)">
|
||||
<li>Description:
|
||||
<t t-out="object.description">Internal meeting for discussion for new pricing for product and services.</t></li>
|
||||
</t>
|
||||
<t t-if="object.videocall_location">
|
||||
<li>
|
||||
How to Join:
|
||||
<t t-if="object.get_base_url() in object.videocall_location"> Join with Odoo Discuss</t>
|
||||
<t t-else=""> Join at</t><br/>
|
||||
<a t-att-href="object.videocall_location" target="_blank" t-out="object.videocall_location or ''">www.mycompany.com/calendar/join_videocall/xyz</a>
|
||||
</li>
|
||||
</t>
|
||||
<t t-if="object.location">
|
||||
<li>Location: <t t-out="object.location or ''"></t>
|
||||
(<a target="_blank"
|
||||
t-attf-href="http://maps.google.com/maps?oi=map&q={{object.location}}">View Map</a>)
|
||||
</li>
|
||||
</t>
|
||||
<t t-if="recurrent">
|
||||
<li>When: <t t-out="object.recurrence_id.get_recurrence_name()"></t></li>
|
||||
</t>
|
||||
<t t-if="not object.allday and object.duration">
|
||||
<li>Duration:
|
||||
<t t-out="('%dH%02d' % (object.duration,round(object.duration*60)%60))"></t>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</tr>
|
||||
<tr t-if="recurrent">
|
||||
<td style="width: 100px; padding-top: 6px; color: #676F7B;">When</td>
|
||||
<td style="padding-top: 6px;"><t t-out="object.recurrence_id.get_recurrence_name() or ''">Every 1 Weeks, for 3 events</t></td>
|
||||
</tr>
|
||||
<tr t-if="not object.allday and object.duration">
|
||||
<td style="width: 100px; padding-top: 6px; color: #676F7B;">Duration</td>
|
||||
<td style="padding-top: 6px;"><t t-out="('%dH%02d' % (object.duration,round(object.duration*60)%60))">0H30</t></td>
|
||||
</tr>
|
||||
<tr t-if="object.location">
|
||||
<td style="width: 100px; padding-top: 6px; color: #676F7B;">Location</td>
|
||||
<td style="padding-top: 6px;">
|
||||
<t t-out="object.location or ''">Bruxelles</t>
|
||||
<a target="_blank" t-if="not is_online or is_online and object.location != object.appointment_type_id.location_id.name" t-attf-href="http://maps.google.com/maps?oi=map&q={{object.location}}" style="margin-left: 12px; font-size: 12px; color: #008f8c; text-decoration: underline;">View Map</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-if="object.videocall_location">
|
||||
<td style="width: 100px; padding-top: 6px; color: #676F7B;">
|
||||
<t t-if="object.videocall_source == 'discuss'">Join with</t>
|
||||
<t t-else="">Join</t>
|
||||
</td>
|
||||
<td style="padding-top: 6px;">
|
||||
<a t-att-href="object.videocall_location" target="_blank" style="color: #008f8c;">
|
||||
<t t-if="object.videocall_source == 'discuss'">Odoo Discuss</t>
|
||||
<t t-else="">Video meeting</t>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="user_input">
|
||||
<hr/>
|
||||
<p placeholder="Enter your message here"><br/></p>
|
||||
|
||||
<div style="margin: 32px 0 0;">
|
||||
<h2 t-attf-style="margin-bottom: 12px; font-size: 14px; font-weight: bold; color: {{user.company_id.email_secondary_color or '#875A7B'}};">Attendees</h2>
|
||||
<div t-foreach="object.attendee_ids" t-as="attendee" style="margin-bottom: 5px;">
|
||||
<img t-if="attendee.state" t-attf-src="/calendar/static/src/img/state_{{ attendee.state }}.png" style="border:0; width: 15px; height: 15px; vertical-align: text-bottom;" />
|
||||
<t t-if="attendee.common_name">
|
||||
<span style="margin-left: 5px" t-out="attendee.common_name or ''">Mitchell Admin</span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span style="margin-left: 5px;">You</span>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="not is_html_empty(object.description)" style="margin: 32px 0 0;">
|
||||
<h2 t-attf-style="margin-bottom: 12px; font-size: 14px; font-weight: bold; color: {{user.company_id.email_secondary_color or '#875A7B'}};">Description of the event</h2>
|
||||
<t t-out="object.description">Internal meeting for discussion for new pricing for product and services.</t>
|
||||
</div>
|
||||
<p style="margin: 32px 0 0;">
|
||||
Thank you!
|
||||
<t t-if="object.user_id.signature">
|
||||
<span style="display: block; margin-top: 8px;"><t t-out="object.user_id.signature or ''">Mitchell Admin</t></span>
|
||||
</t>
|
||||
</p>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="calendar_template_delete_event" model="mail.template">
|
||||
<field name="name">Calendar: Event Deleted</field>
|
||||
<field name="model_id" ref="calendar.model_calendar_event"/>
|
||||
<field name="subject">Deleted event: {{ object.name }}</field>
|
||||
<field name="email_from">{{ (object.user_id.email_formatted or user.email_formatted or '') }}</field>
|
||||
<field name="email_to" eval="False"/>
|
||||
<field name="use_default_to" eval="True"/>
|
||||
<field name="description">Used to manually notify attendees</field>
|
||||
<field name="body_html" type="html">
|
||||
<div style="font-size: 13px; color: #374151;">
|
||||
<t t-set="mail_tz" t-value="object._get_mail_tz() or ctx.get('mail_tz')" />
|
||||
<t t-set="event_name" t-value="object.name or ''"/>
|
||||
<t t-set="event_organizer" t-value="object.user_id.name or ''"/>
|
||||
<h1 style="font-size: 18px; font-weight: bold; color: #374151; margin-bottom: 28px;">
|
||||
Event canceled
|
||||
<img src="/calendar/static/src/img/times-circle.png" style="border:0; width: 24px; height: 24px; margin-left: 10px; text-decoration:none; vertical-align: bottom;" />
|
||||
</h1>
|
||||
<p style="margin: 0;">
|
||||
Hello,<br/>
|
||||
<span style="display: block; margin-top: 8px;">
|
||||
This is to inform you that the event <strong t-out="event_name or ''">Follow-up for Project proposal</strong> <t t-if="event_organizer">organized by </t><t t-out="event_organizer or ''">Colleen Diaz</t> has been canceled and removed from your calendar.
|
||||
</span>
|
||||
</p>
|
||||
<div style="margin: 32px 0 0;">
|
||||
<h2 t-attf-style="margin-bottom: 12px; font-size: 14px; font-weight: bold; color: {{user.company_id.email_secondary_color or '#875A7B'}};">Details</h2>
|
||||
<table cellpadding="0" cellspacing="0" border="0" style="font-size: 13px; color: #374151;">
|
||||
<tr>
|
||||
<td style="width: 100px; color: #676F7B;">Date & Time</td>
|
||||
<td style="font-weight: bold;">
|
||||
<t t-out="format_datetime(dt=object.start, tz=mail_tz if not object.allday else None, dt_format='EEEE', lang_code=object.env.lang)">Tuesday</t>
|
||||
<t t-out="format_datetime(dt=object.start, tz=mail_tz if not object.allday else None, dt_format='d', lang_code=object.env.lang)">4</t>
|
||||
<t t-out="format_datetime(dt=object.start, tz=mail_tz if not object.allday else None, dt_format='MMMM y', lang_code=object.env.lang)">May 2021</t>
|
||||
<t t-if="not object.allday">
|
||||
<t t-out="format_time(time=object.start, tz=mail_tz, time_format='short', lang_code=object.env.lang)">11:00 AM</t>
|
||||
<t t-if="mail_tz">
|
||||
<span style="font-size: 11px; font-weight: normal;">
|
||||
(<t t-out="mail_tz or ''">Europe/Brussels</t>)
|
||||
</span>
|
||||
</t>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-if="not object.allday and object.duration">
|
||||
<td style="width: 100px; padding-top: 6px; color: #676F7B;">Duration</td>
|
||||
<td style="padding-top: 6px;"><t t-out="('%dH%02d' % (object.duration,round(object.duration*60)%60))">0H30</t></td>
|
||||
</tr>
|
||||
<tr t-if="object.location">
|
||||
<td style="width: 100px; padding-top: 6px; color: #676F7B;">Location</td>
|
||||
<td style="padding-top: 6px;"><t t-out="object.location or ''">Bruxelles</t></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<p style="margin: 32px 0 0;">
|
||||
If you have any questions or concerns, please feel free to contact us. <br/>
|
||||
Best regards,
|
||||
<span style="display: block; margin-top: 8px;">The Calendar Team</span>
|
||||
</p>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
6999
odoo-bringout-oca-ocb-calendar/calendar/i18n/es_419.po
Normal file
6999
odoo-bringout-oca-ocb-calendar/calendar/i18n/es_419.po
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
3157
odoo-bringout-oca-ocb-calendar/calendar/i18n/ku.po
Normal file
3157
odoo-bringout-oca-ocb-calendar/calendar/i18n/ku.po
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
3158
odoo-bringout-oca-ocb-calendar/calendar/i18n/my.po
Normal file
3158
odoo-bringout-oca-ocb-calendar/calendar/i18n/my.po
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
3907
odoo-bringout-oca-ocb-calendar/calendar/i18n/uz.po
Normal file
3907
odoo-bringout-oca-ocb-calendar/calendar/i18n/uz.po
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import ir_http
|
||||
|
|
@ -10,7 +9,10 @@ from . import calendar_attendee
|
|||
from . import calendar_filter
|
||||
from . import calendar_event_type
|
||||
from . import calendar_recurrence
|
||||
from . import discuss_channel
|
||||
from . import mail_activity
|
||||
from . import mail_activity_mixin
|
||||
from . import mail_activity_type
|
||||
from . import res_users
|
||||
from . import res_users_settings
|
||||
from . import utils
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.fields import Domain
|
||||
|
||||
|
||||
class Alarm(models.Model):
|
||||
class CalendarAlarm(models.Model):
|
||||
_name = 'calendar.alarm'
|
||||
_description = 'Event Alarm'
|
||||
|
||||
|
|
@ -26,6 +26,7 @@ class Alarm(models.Model):
|
|||
compute='_compute_mail_template_id', readonly=False, store=True,
|
||||
help="Template used to render mail reminder content.")
|
||||
body = fields.Text("Additional Message", help="Additional message that would be sent with the notification for the reminder")
|
||||
notify_responsible = fields.Boolean("Notify Responsible", default=False)
|
||||
|
||||
@api.depends('interval', 'duration')
|
||||
def _compute_duration_minutes(self):
|
||||
|
|
@ -48,6 +49,14 @@ class Alarm(models.Model):
|
|||
alarm.mail_template_id = False
|
||||
|
||||
def _search_duration_minutes(self, operator, value):
|
||||
if operator == 'in':
|
||||
# recursive call with operator '='
|
||||
return Domain.OR(self._search_duration_minutes('=', v) for v in value)
|
||||
elif operator == '=':
|
||||
if not value:
|
||||
return Domain('duration', '=', False)
|
||||
elif operator not in ('>=', '<=', '<', '>'):
|
||||
return NotImplemented
|
||||
return [
|
||||
'|', '|',
|
||||
'&', ('interval', '=', 'minutes'), ('duration', operator, value),
|
||||
|
|
@ -55,10 +64,14 @@ class Alarm(models.Model):
|
|||
'&', ('interval', '=', 'days'), ('duration', operator, value / 60 / 24),
|
||||
]
|
||||
|
||||
@api.onchange('duration', 'interval', 'alarm_type')
|
||||
@api.onchange('duration', 'interval', 'alarm_type', 'notify_responsible')
|
||||
def _onchange_duration_interval(self):
|
||||
if self.notify_responsible and self.alarm_type in ('email', 'notification'):
|
||||
self.notify_responsible = False
|
||||
display_interval = self._interval_selection.get(self.interval, '')
|
||||
display_alarm_type = {
|
||||
key: value for key, value in self._fields['alarm_type']._description_selection(self.env)
|
||||
}[self.alarm_type]
|
||||
}.get(self.alarm_type, '')
|
||||
self.name = "%s - %s %s" % (display_alarm_type, self.duration, display_interval)
|
||||
if self.notify_responsible:
|
||||
self.name += " - " + _("Notify Responsible")
|
||||
|
|
|
|||
|
|
@ -1,17 +1,13 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools import plaintext2html
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
from odoo.tools.sql import SQL
|
||||
|
||||
|
||||
class AlarmManager(models.AbstractModel):
|
||||
class CalendarAlarm_Manager(models.AbstractModel):
|
||||
_name = 'calendar.alarm_manager'
|
||||
_description = 'Event Alarm Manager'
|
||||
|
||||
|
|
@ -23,7 +19,9 @@ class AlarmManager(models.AbstractModel):
|
|||
result = {}
|
||||
delta_request = """
|
||||
SELECT
|
||||
rel.calendar_event_id, max(alarm.duration_minutes) AS max_delta,min(alarm.duration_minutes) AS min_delta
|
||||
rel.calendar_event_id,
|
||||
max(alarm.duration_minutes) AS max_delta,
|
||||
min(alarm.duration_minutes) AS min_delta
|
||||
FROM
|
||||
calendar_alarm_calendar_event_rel AS rel
|
||||
LEFT JOIN calendar_alarm AS alarm ON alarm.id = rel.calendar_alarm_id
|
||||
|
|
@ -31,30 +29,24 @@ class AlarmManager(models.AbstractModel):
|
|||
GROUP BY rel.calendar_event_id
|
||||
"""
|
||||
base_request = """
|
||||
SELECT
|
||||
cal.id,
|
||||
cal.start - interval '1' minute * calcul_delta.max_delta AS first_alarm,
|
||||
CASE
|
||||
WHEN cal.recurrency THEN rrule.until - interval '1' minute * calcul_delta.min_delta
|
||||
ELSE cal.stop - interval '1' minute * calcul_delta.min_delta
|
||||
END as last_alarm,
|
||||
cal.start as first_event_date,
|
||||
CASE
|
||||
WHEN cal.recurrency THEN rrule.until
|
||||
ELSE cal.stop
|
||||
END as last_event_date,
|
||||
calcul_delta.min_delta,
|
||||
calcul_delta.max_delta,
|
||||
rrule.rrule AS rule
|
||||
FROM
|
||||
calendar_event AS cal
|
||||
RIGHT JOIN calcul_delta ON calcul_delta.calendar_event_id = cal.id
|
||||
LEFT JOIN calendar_recurrence as rrule ON rrule.id = cal.recurrence_id
|
||||
"""
|
||||
|
||||
SELECT
|
||||
cal.id,
|
||||
cal.start - interval '1' minute * calcul_delta.max_delta AS first_alarm,
|
||||
cal.stop - interval '1' minute * calcul_delta.min_delta AS last_alarm,
|
||||
cal.start AS first_meeting,
|
||||
cal.stop AS last_meeting,
|
||||
calcul_delta.min_delta,
|
||||
calcul_delta.max_delta
|
||||
FROM
|
||||
calendar_event AS cal
|
||||
INNER JOIN calcul_delta ON calcul_delta.calendar_event_id = cal.id
|
||||
WHERE cal.active = True
|
||||
"""
|
||||
filter_user = """
|
||||
RIGHT JOIN calendar_event_res_partner_rel AS part_rel ON part_rel.calendar_event_id = cal.id
|
||||
AND part_rel.res_partner_id IN %s
|
||||
INNER JOIN calendar_event_res_partner_rel AS part_rel
|
||||
ON part_rel.calendar_event_id = cal.id
|
||||
AND part_rel.res_partner_id IN %s
|
||||
WHERE cal.active = True
|
||||
"""
|
||||
|
||||
# Add filter on alarm type
|
||||
|
|
@ -62,7 +54,7 @@ class AlarmManager(models.AbstractModel):
|
|||
|
||||
# Add filter on partner_id
|
||||
if partners:
|
||||
base_request += filter_user
|
||||
base_request = base_request.replace("WHERE cal.active = True", filter_user)
|
||||
tuple_params += (tuple(partners.ids), )
|
||||
|
||||
# Upper bound on first_alarm of requested events
|
||||
|
|
@ -81,15 +73,15 @@ class AlarmManager(models.AbstractModel):
|
|||
tuple_params += (seconds,)
|
||||
|
||||
self.env.flush_all()
|
||||
self._cr.execute("""
|
||||
self.env.cr.execute("""
|
||||
WITH calcul_delta AS (%s)
|
||||
SELECT *
|
||||
FROM ( %s WHERE cal.active = True ) AS ALL_EVENTS
|
||||
WHERE ALL_EVENTS.first_alarm < %s
|
||||
AND ALL_EVENTS.last_event_date > (now() at time zone 'utc')
|
||||
FROM ( %s ) AS ALL_EVENTS
|
||||
WHERE ALL_EVENTS.first_alarm < %s
|
||||
AND ALL_EVENTS.last_alarm > (now() at time zone 'utc')
|
||||
""" % (delta_request, base_request, first_alarm_max_value), tuple_params)
|
||||
|
||||
for event_id, first_alarm, last_alarm, first_meeting, last_meeting, min_duration, max_duration, rule in self._cr.fetchall():
|
||||
for event_id, first_alarm, last_alarm, first_meeting, last_meeting, min_duration, max_duration in self.env.cr.fetchall():
|
||||
result[event_id] = {
|
||||
'event_id': event_id,
|
||||
'first_alarm': first_alarm,
|
||||
|
|
@ -98,14 +90,13 @@ class AlarmManager(models.AbstractModel):
|
|||
'last_meeting': last_meeting,
|
||||
'min_duration': min_duration,
|
||||
'max_duration': max_duration,
|
||||
'rrule': rule
|
||||
}
|
||||
|
||||
# determine accessible events
|
||||
events = self.env['calendar.event'].browse(result)
|
||||
result = {
|
||||
key: result[key]
|
||||
for key in set(events._filter_access_rules('read').ids)
|
||||
for key in events._filtered_access('read').ids
|
||||
}
|
||||
return result
|
||||
|
||||
|
|
@ -147,7 +138,7 @@ class AlarmManager(models.AbstractModel):
|
|||
To be overriden on inherited modules
|
||||
adding extra conditions to extract only the unsynced events
|
||||
"""
|
||||
return ""
|
||||
return SQL("")
|
||||
|
||||
def _get_events_by_alarm_to_notify(self, alarm_type):
|
||||
"""
|
||||
|
|
@ -159,21 +150,28 @@ class AlarmManager(models.AbstractModel):
|
|||
design. The attendees receive an invitation for any new event
|
||||
already.
|
||||
"""
|
||||
lastcall = self.env.context.get('lastcall', False) or fields.date.today() - relativedelta(weeks=1)
|
||||
self.env.cr.execute('''
|
||||
SELECT "alarm"."id", "event"."id"
|
||||
FROM "calendar_event" AS "event"
|
||||
JOIN "calendar_alarm_calendar_event_rel" AS "event_alarm_rel"
|
||||
ON "event"."id" = "event_alarm_rel"."calendar_event_id"
|
||||
JOIN "calendar_alarm" AS "alarm"
|
||||
ON "event_alarm_rel"."calendar_alarm_id" = "alarm"."id"
|
||||
WHERE
|
||||
"alarm"."alarm_type" = %s
|
||||
AND "event"."active"
|
||||
AND "event"."start" - CAST("alarm"."duration" || ' ' || "alarm"."interval" AS Interval) >= %s
|
||||
AND "event"."start" - CAST("alarm"."duration" || ' ' || "alarm"."interval" AS Interval) < now() at time zone 'utc'
|
||||
AND "event"."stop" > now() at time zone 'utc'
|
||||
''' + self._get_notify_alert_extra_conditions(), [alarm_type, lastcall])
|
||||
lastcall = self.env.context.get('lastcall', False) or fields.Date.today() - timedelta(weeks=1)
|
||||
# TODO MASTER: remove context and add a proper parameter
|
||||
extra_conditions = self.with_context(alarm_type=alarm_type)._get_notify_alert_extra_conditions()
|
||||
now = fields.Datetime.now()
|
||||
self.env.cr.execute(SQL("""
|
||||
SELECT alarm.id, event.id
|
||||
FROM calendar_event AS event
|
||||
JOIN calendar_alarm_calendar_event_rel AS event_alarm_rel
|
||||
ON event.id = event_alarm_rel.calendar_event_id
|
||||
JOIN calendar_alarm AS alarm
|
||||
ON event_alarm_rel.calendar_alarm_id = alarm.id
|
||||
WHERE alarm.alarm_type = %s
|
||||
AND event.active
|
||||
AND event.start - CAST(alarm.duration || ' ' || alarm.interval AS Interval) >= %s
|
||||
AND event.start - CAST(alarm.duration || ' ' || alarm.interval AS Interval) < %s
|
||||
%s
|
||||
""",
|
||||
alarm_type,
|
||||
lastcall,
|
||||
now,
|
||||
extra_conditions,
|
||||
))
|
||||
|
||||
events_by_alarm = {}
|
||||
for alarm_id, event_id in self.env.cr.fetchall():
|
||||
|
|
@ -187,21 +185,24 @@ class AlarmManager(models.AbstractModel):
|
|||
if not events_by_alarm:
|
||||
return
|
||||
|
||||
# force_send limit should apply to the total nb of attendees, not per alarm
|
||||
force_send_limit = int(self.env['ir.config_parameter'].sudo().get_param('mail.mail_force_send_limit', 100))
|
||||
|
||||
event_ids = list(set(event_id for event_ids in events_by_alarm.values() for event_id in event_ids))
|
||||
events = self.env['calendar.event'].browse(event_ids)
|
||||
attendees = events.attendee_ids.filtered(lambda a: a.state != 'declined')
|
||||
now = fields.Datetime.now()
|
||||
attendees = events.filtered(lambda e: e.stop > now).attendee_ids.filtered(lambda a: a.state != 'declined')
|
||||
alarms = self.env['calendar.alarm'].browse(events_by_alarm.keys())
|
||||
for alarm in alarms:
|
||||
alarm_attendees = attendees.filtered(lambda attendee: attendee.event_id.id in events_by_alarm[alarm.id])
|
||||
alarm_attendees.with_context(
|
||||
mail_notify_force_send=True,
|
||||
calendar_template_ignore_recurrence=True,
|
||||
mail_notify_author=True
|
||||
)._send_mail_to_attendees(
|
||||
alarm_attendees.with_context(calendar_template_ignore_recurrence=True)._notify_attendees(
|
||||
alarm.mail_template_id,
|
||||
force_send=True
|
||||
force_send=len(attendees) <= force_send_limit,
|
||||
notify_author=True,
|
||||
)
|
||||
|
||||
events._setup_event_recurrent_alarms(events_by_alarm)
|
||||
|
||||
@api.model
|
||||
def get_next_notif(self):
|
||||
partner = self.env.user.partner_id
|
||||
|
|
@ -245,13 +246,10 @@ class AlarmManager(models.AbstractModel):
|
|||
|
||||
def _notify_next_alarm(self, partner_ids):
|
||||
""" Sends through the bus the next alarm of given partners """
|
||||
notifications = []
|
||||
users = self.env['res.users'].search([
|
||||
('partner_id', 'in', tuple(partner_ids)),
|
||||
('groups_id', 'in', self.env.ref('base.group_user').ids),
|
||||
('group_ids', 'in', self.env.ref('base.group_user').ids),
|
||||
])
|
||||
for user in users:
|
||||
notif = self.with_user(user).with_context(allowed_company_ids=user.company_ids.ids).get_next_notif()
|
||||
notifications.append([user.partner_id, 'calendar.alarm', notif])
|
||||
if len(notifications) > 0:
|
||||
self.env['bus.bus']._sendmany(notifications)
|
||||
user._bus_send("calendar.alarm", notif)
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import uuid
|
||||
import base64
|
||||
import logging
|
||||
|
||||
from collections import defaultdict
|
||||
from odoo import api, Command, fields, models, _
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.addons.base.models.res_partner import _tz_get
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools.misc import clean_context
|
||||
from odoo.tools import split_every
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Attendee(models.Model):
|
||||
class CalendarAttendee(models.Model):
|
||||
""" Calendar Attendee Information """
|
||||
_name = 'calendar.attendee'
|
||||
_rec_name = 'common_name'
|
||||
|
|
@ -23,24 +24,24 @@ class Attendee(models.Model):
|
|||
return uuid.uuid4().hex
|
||||
|
||||
STATE_SELECTION = [
|
||||
('accepted', 'Yes'),
|
||||
('declined', 'No'),
|
||||
('tentative', 'Maybe'),
|
||||
('needsAction', 'Needs Action'),
|
||||
('tentative', 'Uncertain'),
|
||||
('declined', 'Declined'),
|
||||
('accepted', 'Accepted'),
|
||||
]
|
||||
|
||||
# event
|
||||
event_id = fields.Many2one('calendar.event', 'Meeting linked', required=True, ondelete='cascade')
|
||||
event_id = fields.Many2one('calendar.event', 'Meeting linked', required=True, index=True, ondelete='cascade')
|
||||
recurrence_id = fields.Many2one('calendar.recurrence', related='event_id.recurrence_id')
|
||||
# attendee
|
||||
partner_id = fields.Many2one('res.partner', 'Attendee', required=True, readonly=True)
|
||||
partner_id = fields.Many2one('res.partner', 'Attendee', required=True, readonly=True, ondelete='cascade')
|
||||
email = fields.Char('Email', related='partner_id.email')
|
||||
phone = fields.Char('Phone', related='partner_id.phone')
|
||||
common_name = fields.Char('Common name', compute='_compute_common_name', store=True)
|
||||
access_token = fields.Char('Invitation Token', default=_default_access_token)
|
||||
mail_tz = fields.Selection(_tz_get, compute='_compute_mail_tz', help='Timezone used for displaying time in the mail template')
|
||||
# state
|
||||
state = fields.Selection(STATE_SELECTION, string='Status', readonly=True, default='needsAction')
|
||||
state = fields.Selection(STATE_SELECTION, string='Status', default='needsAction')
|
||||
availability = fields.Selection(
|
||||
[('free', 'Available'), ('busy', 'Busy')], 'Available/Busy', readonly=True)
|
||||
|
||||
|
|
@ -66,90 +67,159 @@ class Attendee(models.Model):
|
|||
values['email'] = email[0] if email else ''
|
||||
values['common_name'] = values.get("common_name")
|
||||
attendees = super().create(vals_list)
|
||||
attendees._subscribe_partner()
|
||||
attendees.event_id.check_access('write')
|
||||
return attendees
|
||||
|
||||
def write(self, vals):
|
||||
attendees = super().write(vals)
|
||||
self.event_id.check_access('write')
|
||||
return attendees
|
||||
|
||||
def unlink(self):
|
||||
self._unsubscribe_partner()
|
||||
return super().unlink()
|
||||
|
||||
@api.returns('self', lambda value: value.id)
|
||||
def copy(self, default=None):
|
||||
raise UserError(_('You cannot duplicate a calendar attendee.'))
|
||||
|
||||
def _subscribe_partner(self):
|
||||
mapped_followers = defaultdict(lambda: self.env['calendar.event'])
|
||||
for event in self.event_id:
|
||||
partners = (event.attendee_ids & self).partner_id - event.message_partner_ids
|
||||
# current user is automatically added as followers, don't add it twice.
|
||||
partners -= self.env.user.partner_id
|
||||
mapped_followers[partners] |= event
|
||||
for partners, events in mapped_followers.items():
|
||||
events.message_subscribe(partner_ids=partners.ids)
|
||||
|
||||
def _unsubscribe_partner(self):
|
||||
for event in self.event_id:
|
||||
partners = (event.attendee_ids & self).partner_id & event.message_partner_ids
|
||||
event.message_unsubscribe(partner_ids=partners.ids)
|
||||
|
||||
def _send_mail_to_attendees(self, mail_template, force_send=False):
|
||||
""" Send mail for event invitation to event attendees.
|
||||
:param mail_template: a mail.template record
|
||||
:param force_send: if set to True, the mail(s) will be sent immediately (instead of the next queue processing)
|
||||
# ------------------------------------------------------------
|
||||
# MAILING
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@api.model
|
||||
def _mail_template_default_values(self):
|
||||
return {
|
||||
"email_from": "{{ (object.event_id.user_id.email_formatted or user.email_formatted or '') }}",
|
||||
"email_to": False,
|
||||
"partner_to": False,
|
||||
"lang": "{{ object.partner_id.lang }}",
|
||||
"use_default_to": True,
|
||||
}
|
||||
|
||||
def _message_add_default_recipients(self):
|
||||
# override: partner_id being the only stored field, we can currently
|
||||
# simplify computation, we have no other choice than relying on it
|
||||
return {
|
||||
attendee.id: {
|
||||
'partners': attendee.partner_id,
|
||||
'email_to_lst': [],
|
||||
'email_cc_lst': [],
|
||||
} for attendee in self
|
||||
}
|
||||
|
||||
def _send_invitation_emails(self):
|
||||
""" Hook to be able to override the invitation email sending process.
|
||||
Notably inside appointment to use a different mail template from the appointment type. """
|
||||
self._notify_attendees(
|
||||
self.env.ref('calendar.calendar_template_meeting_invitation', raise_if_not_found=False),
|
||||
force_send=True,
|
||||
)
|
||||
|
||||
def _notify_attendees(self, mail_template, notify_author=False, force_send=False):
|
||||
""" Notify attendees about event main changes (invite, cancel, ...) based
|
||||
on template.
|
||||
|
||||
:param mail_template: a mail.template record
|
||||
:param force_send: if set to True, the mail(s) will be sent immediately (instead of the next queue processing)
|
||||
"""
|
||||
# TDE FIXME: check this
|
||||
if force_send:
|
||||
force_send_limit = int(self.env['ir.config_parameter'].sudo().get_param('mail.mail_force_send_limit', 100))
|
||||
notified_attendees_ids = set(self.ids)
|
||||
for event, attendees in self.grouped('event_id').items():
|
||||
if event._skip_send_mail_status_update():
|
||||
notified_attendees_ids -= set(attendees.ids)
|
||||
notified_attendees = self.browse(notified_attendees_ids)
|
||||
if isinstance(mail_template, str):
|
||||
raise ValueError('Template should be a template record, not an XML ID anymore.')
|
||||
if self.env['ir.config_parameter'].sudo().get_param('calendar.block_mail') or self._context.get("no_mail_to_attendees"):
|
||||
if self.env['ir.config_parameter'].sudo().get_param('calendar.block_mail') or self.env.context.get("no_mail_to_attendees"):
|
||||
return False
|
||||
if not mail_template:
|
||||
_logger.warning("No template passed to %s notification process. Skipped.", self)
|
||||
return False
|
||||
|
||||
# get ics file for all meetings
|
||||
ics_files = self.mapped('event_id')._get_ics_file()
|
||||
ics_files = notified_attendees.event_id._get_ics_file()
|
||||
|
||||
for attendee in self:
|
||||
if attendee.email and attendee._should_notify_attendee():
|
||||
# If the mail template has attachments, prepare copies for each attendee (to be added to each attendee's mail)
|
||||
if mail_template.attachment_ids:
|
||||
|
||||
# Setting res_model to ensure attachments are linked to the msg (otherwise only internal users are allowed link attachments)
|
||||
attachments_values = [a.copy_data({'res_id': 0, 'res_model': 'mail.compose.message'})[0] for a in mail_template.attachment_ids]
|
||||
attachments_values *= len(self)
|
||||
attendee_attachment_ids = self.env['ir.attachment'].create(attachments_values).ids
|
||||
|
||||
# Map attendees to their respective attachments
|
||||
template_attachment_count = len(mail_template.attachment_ids)
|
||||
attendee_id_attachment_id_map = dict(zip(self.ids, split_every(template_attachment_count, attendee_attachment_ids, list)))
|
||||
|
||||
mail_messages = self.env['mail.message']
|
||||
for attendee in notified_attendees:
|
||||
if attendee.email and attendee._should_notify_attendee(notify_author=notify_author):
|
||||
event_id = attendee.event_id.id
|
||||
ics_file = ics_files.get(event_id)
|
||||
|
||||
attachment_values = [Command.set(mail_template.attachment_ids.ids)]
|
||||
# Add template attachments copies to the attendee's email, if available
|
||||
attachment_ids = attendee_id_attachment_id_map[attendee.id] if mail_template.attachment_ids else []
|
||||
|
||||
if ics_file:
|
||||
attachment_values += [
|
||||
(0, 0, {'name': 'invitation.ics',
|
||||
'mimetype': 'text/calendar',
|
||||
'res_id': event_id,
|
||||
'res_model': 'calendar.event',
|
||||
'datas': base64.b64encode(ics_file)})
|
||||
]
|
||||
context = {
|
||||
**clean_context(self.env.context),
|
||||
'no_document': True, # An ICS file must not create a document
|
||||
}
|
||||
attachment_ids += self.env['ir.attachment'].with_context(context).create({
|
||||
'datas': base64.b64encode(ics_file),
|
||||
'description': 'invitation.ics',
|
||||
'mimetype': 'text/calendar',
|
||||
'res_id': 0,
|
||||
'res_model': 'mail.compose.message',
|
||||
'name': 'invitation.ics',
|
||||
}).ids
|
||||
|
||||
body = mail_template._render_field(
|
||||
'body_html',
|
||||
attendee.ids,
|
||||
compute_lang=True,
|
||||
post_process=True)[attendee.id]
|
||||
compute_lang=True)[attendee.id]
|
||||
subject = mail_template._render_field(
|
||||
'subject',
|
||||
attendee.ids,
|
||||
compute_lang=True)[attendee.id]
|
||||
attendee.event_id.with_context(no_document=True).sudo().message_notify(
|
||||
email_from=attendee.event_id.user_id.email_formatted or self.env.user.email_formatted,
|
||||
email_from = mail_template._render_field(
|
||||
'email_from',
|
||||
attendee.ids)[attendee.id]
|
||||
mail_messages += attendee.event_id.with_context(no_document=True).sudo().message_notify(
|
||||
email_from=email_from or None, # use None to trigger fallback sender
|
||||
author_id=attendee.event_id.user_id.partner_id.id or self.env.user.partner_id.id,
|
||||
body=body,
|
||||
subject=subject,
|
||||
notify_author=notify_author,
|
||||
partner_ids=attendee.partner_id.ids,
|
||||
email_layout_xmlid='mail.mail_notification_light',
|
||||
attachment_ids=attachment_values,
|
||||
force_send=force_send,
|
||||
attachment_ids=attachment_ids,
|
||||
force_send=False,
|
||||
)
|
||||
# batch sending at the end
|
||||
if force_send and len(notified_attendees) < force_send_limit:
|
||||
mail_messages.sudo().mail_ids.send_after_commit()
|
||||
|
||||
def _should_notify_attendee(self):
|
||||
def _should_notify_attendee(self, notify_author=False):
|
||||
""" Utility method that determines if the attendee should be notified.
|
||||
By default, we do not want to notify (aka no message and no mail) the current user
|
||||
if he is part of the attendees.
|
||||
if he is part of the attendees. But for reminders, mail_notify_author could be forced
|
||||
(Override in appointment to ignore that rule and notify all attendees if it's an appointment)
|
||||
"""
|
||||
self.ensure_one()
|
||||
return self.partner_id != self.env.user.partner_id
|
||||
partner_not_sender = self.partner_id != self.env.user.partner_id
|
||||
return partner_not_sender or notify_author
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# STATE MANAGEMENT
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def do_tentative(self):
|
||||
""" Makes event invitation as Tentative. """
|
||||
|
|
@ -160,7 +230,7 @@ class Attendee(models.Model):
|
|||
for attendee in self:
|
||||
attendee.event_id.message_post(
|
||||
author_id=attendee.partner_id.id,
|
||||
body=_("%s has accepted the invitation") % (attendee.common_name),
|
||||
body=_("%s has accepted the invitation", attendee.common_name),
|
||||
subtype_xmlid="calendar.subtype_invitation",
|
||||
)
|
||||
return self.write({'state': 'accepted'})
|
||||
|
|
@ -170,7 +240,7 @@ class Attendee(models.Model):
|
|||
for attendee in self:
|
||||
attendee.event_id.message_post(
|
||||
author_id=attendee.partner_id.id,
|
||||
body=_("%s has declined the invitation") % (attendee.common_name),
|
||||
body=_("%s has declined the invitation", attendee.common_name),
|
||||
subtype_xmlid="calendar.subtype_invitation",
|
||||
)
|
||||
return self.write({'state': 'declined'})
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from random import randint
|
||||
|
|
@ -6,9 +5,9 @@ from random import randint
|
|||
from odoo import fields, models
|
||||
|
||||
|
||||
class MeetingType(models.Model):
|
||||
|
||||
class CalendarEventType(models.Model):
|
||||
_name = 'calendar.event.type'
|
||||
|
||||
_description = 'Event Meeting Type'
|
||||
|
||||
def _default_color(self):
|
||||
|
|
@ -17,6 +16,7 @@ class MeetingType(models.Model):
|
|||
name = fields.Char('Name', required=True)
|
||||
color = fields.Integer('Color', default=_default_color)
|
||||
|
||||
_sql_constraints = [
|
||||
('name_uniq', 'unique (name)', "Tag name already exists !"),
|
||||
]
|
||||
_name_uniq = models.Constraint(
|
||||
'unique (name)',
|
||||
'Tag name already exists!',
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class Contacts(models.Model):
|
||||
class CalendarFilters(models.Model):
|
||||
_name = 'calendar.filters'
|
||||
_description = 'Calendar Filters'
|
||||
|
||||
user_id = fields.Many2one('res.users', 'Me', required=True, default=lambda self: self.env.user, index=True)
|
||||
user_id = fields.Many2one('res.users', 'Me', required=True, default=lambda self: self.env.user, index=True, ondelete='cascade')
|
||||
partner_id = fields.Many2one('res.partner', 'Employee', required=True, index=True)
|
||||
active = fields.Boolean('Active', default=True)
|
||||
partner_checked = fields.Boolean('Checked', default=True) # used to know if the partner is checked in the filter of the calendar view for the user_id.
|
||||
|
||||
_sql_constraints = [
|
||||
('user_id_partner_id_unique', 'UNIQUE(user_id, partner_id)', 'A user cannot have the same contact twice.')
|
||||
]
|
||||
_user_id_partner_id_unique = models.Constraint(
|
||||
'UNIQUE(user_id, partner_id)',
|
||||
'A user cannot have the same contact twice.',
|
||||
)
|
||||
|
||||
@api.model
|
||||
def unlink_from_partner_id(self, partner_id):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime, time
|
||||
|
|
@ -10,6 +9,7 @@ from dateutil.relativedelta import relativedelta
|
|||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools.misc import clean_context
|
||||
|
||||
from odoo.addons.base.models.res_partner import _tz_get
|
||||
|
||||
|
|
@ -90,7 +90,7 @@ def weekday_to_field(weekday_index):
|
|||
return RRULE_WEEKDAY_TO_FIELD.get(weekday_index)
|
||||
|
||||
|
||||
class RecurrenceRule(models.Model):
|
||||
class CalendarRecurrence(models.Model):
|
||||
_name = 'calendar.recurrence'
|
||||
_description = 'Event Recurrence Rule'
|
||||
|
||||
|
|
@ -119,16 +119,18 @@ class RecurrenceRule(models.Model):
|
|||
weekday = fields.Selection(WEEKDAY_SELECTION, string='Weekday')
|
||||
byday = fields.Selection(BYDAY_SELECTION, string='By day')
|
||||
until = fields.Date('Repeat Until')
|
||||
trigger_id = fields.Many2one('ir.cron.trigger')
|
||||
|
||||
_sql_constraints = [
|
||||
('month_day',
|
||||
"CHECK (rrule_type != 'monthly' "
|
||||
"OR month_by != 'day' "
|
||||
"OR day >= 1 AND day <= 31 "
|
||||
"OR weekday in %s AND byday in %s)"
|
||||
% (tuple(wd[0] for wd in WEEKDAY_SELECTION), tuple(bd[0] for bd in BYDAY_SELECTION)),
|
||||
"The day must be between 1 and 31"),
|
||||
]
|
||||
_month_day = models.Constraint("""CHECK (
|
||||
rrule_type != 'monthly'
|
||||
OR month_by != 'day'
|
||||
OR day >= 1 AND day <= 31
|
||||
OR weekday IN %s AND byday IN %s)""" % (
|
||||
tuple(wd[0] for wd in WEEKDAY_SELECTION),
|
||||
tuple(bd[0] for bd in BYDAY_SELECTION),
|
||||
),
|
||||
"The day must be between 1 and 31",
|
||||
)
|
||||
|
||||
def _get_daily_recurrence_name(self):
|
||||
if self.end_type == 'count':
|
||||
|
|
@ -197,11 +199,8 @@ class RecurrenceRule(models.Model):
|
|||
|
||||
@api.depends('calendar_event_ids.start')
|
||||
def _compute_dtstart(self):
|
||||
groups = self.env['calendar.event'].read_group([('recurrence_id', 'in', self.ids)], ['start:min'], ['recurrence_id'])
|
||||
start_mapping = {
|
||||
group['recurrence_id'][0]: group['start']
|
||||
for group in groups
|
||||
}
|
||||
groups = self.env['calendar.event']._read_group([('recurrence_id', 'in', self.ids)], ['recurrence_id'], ['start:min'])
|
||||
start_mapping = {recurrence.id: start_min for recurrence, start_min in groups}
|
||||
for recurrence in self:
|
||||
recurrence.dtstart = start_mapping.get(recurrence.id)
|
||||
|
||||
|
|
@ -275,9 +274,44 @@ class RecurrenceRule(models.Model):
|
|||
|
||||
events = self.calendar_event_ids - keep
|
||||
detached_events = self._detach_events(events)
|
||||
self.env['calendar.event'].with_context(no_mail_to_attendees=True, mail_create_nolog=True).create(event_vals)
|
||||
context = {
|
||||
**clean_context(self.env.context),
|
||||
**{'no_mail_to_attendees': True, 'mail_create_nolog': True},
|
||||
}
|
||||
self.env['calendar.event'].with_context(context).create(event_vals)
|
||||
return detached_events
|
||||
|
||||
def _setup_alarms(self, recurrence_update=False):
|
||||
""" Schedule cron triggers for future events
|
||||
Create one ir.cron.trigger per recurrence.
|
||||
:param recurrence_update: boolean: if true, update all recurrences in self, else only the recurrences
|
||||
without trigger
|
||||
"""
|
||||
now = self.env.context.get('date') or fields.Datetime.now()
|
||||
# get next events
|
||||
self.env['calendar.event'].flush_model(fnames=['recurrence_id', 'start'])
|
||||
if not self.calendar_event_ids.ids:
|
||||
return
|
||||
|
||||
self.env.cr.execute("""
|
||||
SELECT DISTINCT ON (recurrence_id) id event_id, recurrence_id
|
||||
FROM calendar_event
|
||||
WHERE start > %s
|
||||
AND id IN %s
|
||||
ORDER BY recurrence_id,start ASC;
|
||||
""", (now, tuple(self.calendar_event_ids.ids)))
|
||||
result = self.env.cr.dictfetchall()
|
||||
if not result:
|
||||
return
|
||||
events = self.env['calendar.event'].browse(value['event_id'] for value in result)
|
||||
triggers_by_events = events._setup_alarms()
|
||||
for vals in result:
|
||||
trigger_id = triggers_by_events.get(vals['event_id'])
|
||||
if not trigger_id:
|
||||
continue
|
||||
recurrence = self.env['calendar.recurrence'].browse(vals['recurrence_id'])
|
||||
recurrence.trigger_id = trigger_id
|
||||
|
||||
def _split_from(self, event, recurrence_values=None):
|
||||
"""Stops the current recurrence at the given event and creates a new one starting
|
||||
with the event.
|
||||
|
|
@ -331,7 +365,7 @@ class RecurrenceRule(models.Model):
|
|||
def _detach_events(self, events):
|
||||
events.with_context(dont_notify=True).write({
|
||||
'recurrence_id': False,
|
||||
'recurrency': False,
|
||||
'recurrency': True,
|
||||
})
|
||||
return events
|
||||
|
||||
|
|
@ -395,14 +429,9 @@ class RecurrenceRule(models.Model):
|
|||
data['month_by'] = 'day'
|
||||
data['rrule_type'] = 'monthly'
|
||||
|
||||
if rule._bymonthday:
|
||||
if rule._bymonthday and data['rrule_type'] == 'monthly':
|
||||
data['day'] = list(rule._bymonthday)[0]
|
||||
data['month_by'] = 'date'
|
||||
data['rrule_type'] = 'monthly'
|
||||
|
||||
# Repeat yearly but for odoo it's monthly, take same information as monthly but interval is 12 times
|
||||
if rule._bymonth:
|
||||
data['interval'] *= 12
|
||||
|
||||
if data.get('until'):
|
||||
data['end_type'] = 'end_date'
|
||||
|
|
@ -413,7 +442,7 @@ class RecurrenceRule(models.Model):
|
|||
return data
|
||||
|
||||
def _get_lang_week_start(self):
|
||||
lang = self.env['res.lang']._lang_get(self.env.user.lang)
|
||||
lang = self.env['res.lang']._get_data(code=self.env.user.lang)
|
||||
week_start = int(lang.week_start) # lang.week_start ranges from '1' to '7'
|
||||
return rrule.weekday(week_start - 1) # rrule expects an int from 0 to 6
|
||||
|
||||
|
|
@ -574,3 +603,19 @@ class RecurrenceRule(models.Model):
|
|||
return rrule.rrule(
|
||||
freq_to_rrule(freq), **rrule_params
|
||||
)
|
||||
|
||||
def _is_event_over(self):
|
||||
"""Check if all events in this recurrence are in the past.
|
||||
:return: True if all events are over, False otherwise
|
||||
"""
|
||||
self.ensure_one()
|
||||
if not self.calendar_event_ids:
|
||||
return False
|
||||
|
||||
now = fields.Datetime.now()
|
||||
today = fields.Date.today()
|
||||
|
||||
return all(
|
||||
(event.stop_date < today if event.allday else event.stop < now)
|
||||
for event in self.calendar_event_ids
|
||||
)
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue