[IMP] calendar,calendar_sms,crm,google_calendar: Improve calendars

This commit improves multiple small details in the calendar application.
UI, UX mostly:
  * some labels were confusing
  * the invisible attribute were not set in popover view
  * the sample data were missing
  * ...

taskid: 2342252
This commit is contained in:
Arnaud Joset
2021-02-25 08:25:51 +00:00
parent 6b6d5931a8
commit 2f4a91c0ce
11 changed files with 173 additions and 49 deletions
+2 -1
View File
@@ -28,11 +28,12 @@ class Attendee(models.Model):
event_id = fields.Many2one(
'calendar.event', 'Meeting linked', required=True, ondelete='cascade')
partner_id = fields.Many2one('res.partner', 'Contact', required=True, readonly=True)
partner_id = fields.Many2one('res.partner', 'Attendee', required=True, readonly=True)
state = fields.Selection(STATE_SELECTION, string='Status', readonly=True, default='needsAction',
help="Status of the attendee's participation")
common_name = fields.Char('Common name', compute='_compute_common_name', store=True)
email = fields.Char('Email', related='partner_id.email', help="Email of Invited Person")
phone = fields.Char('Phone', related='partner_id.phone', help="Phone number of Invited Person")
availability = fields.Selection(
[('free', 'Available'), ('busy', 'Busy')], 'Available/Busy', readonly=True)
access_token = fields.Char('Invitation Token', default=_default_access_token)
+37 -7
View File
@@ -127,7 +127,7 @@ class Meeting(models.Model):
return self._get_recurrent_fields() | self._get_time_fields() | self._get_custom_fields() | {
'id', 'active', 'allday',
'duration', 'user_id', 'interval',
'count', 'rrule', 'recurrence_id', 'show_as'}
'count', 'rrule', 'recurrence_id', 'show_as', 'privacy'}
@api.model
def _get_display_time(self, start, stop, zduration, zallday):
@@ -217,15 +217,21 @@ class Meeting(models.Model):
duration = fields.Float('Duration', compute='_compute_duration', store=True, readonly=False)
description = fields.Text('Description')
privacy = fields.Selection(
[('public', 'Everyone'),
('private', 'Only me'),
[('public', 'Public'),
('private', 'Private'),
('confidential', 'Only internal users')],
'Privacy', default='public', required=True)
'Privacy', default='public', required=True,
help="People to whom this event will be visible.")
location = fields.Char('Location', tracking=True, help="Location of Event")
videocall_location = fields.Char('Join Video Call', default=_default_videocall_location)
show_as = fields.Selection(
[('free', 'Available'),
('busy', 'Busy')], 'Show Time as', default='busy', required=True)
('busy', 'Busy')], 'Show as', default='busy', required=True,
help="If the time is shown as 'busy', this event will be visible to other people with either the full \
information or simply 'busy' written depending on its privacy. Use this option to let other people know \
that you are unavailable during that period of time. \n If the time is shown as 'free', this event won't \
be visible to other people at all. Use this option to let other people know that you are available during \
that period of time.")
# linked document
# LUL TODO use fields.Reference ?
@@ -253,12 +259,13 @@ class Meeting(models.Model):
string='Attendees', default=_default_partners)
alarm_ids = fields.Many2many(
'calendar.alarm', 'calendar_alarm_calendar_event_rel',
string='Reminders', ondelete="restrict")
string='Reminders', ondelete="restrict",
help="Notifications sent to all attendees to remind of the meeting.")
is_highlighted = fields.Boolean(
compute='_compute_is_highlighted', string='Is the Event Highlighted')
# RECURRENCE FIELD
recurrency = fields.Boolean('Recurrent', help="Recurrent Event")
recurrency = fields.Boolean('Recurrent')
recurrence_id = fields.Many2one(
'calendar.recurrence', string="Recurrence Rule", index=True)
follow_recurrence = fields.Boolean(default=False) # Indicates if an event follows the recurrence, i.e. is not an exception
@@ -556,6 +563,29 @@ class Meeting(models.Model):
meeting.attendee_ids._send_mail_to_attendees('calendar.calendar_template_meeting_invitation')
return True
def action_mass_mailing(self):
partners_ids = self.mapped('partner_ids')
if not partners_ids:
raise UserError(_("There are no attendees on these events"))
compose_form = self.env.ref('mail.email_compose_message_wizard_form', False)
default_partners = partners_ids and partners_ids.ids
compose_ctx = dict(
default_use_template=False,
default_composition_mode='mass_mail',
default_partner_ids=default_partners,
default_subject=_("Event update")
)
return {
'name': _('Contact Attendees'),
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'mail.compose.message',
'views': [(compose_form.id, 'form')],
'view_id': compose_form.id,
'target': 'new',
'context': compose_ctx,
}
def _apply_recurrence_values(self, values, future=True):
"""Apply the new recurrence rules in `values`. Create a recurrence if it does not exist
and create all missing events according to the rrule.
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from random import randint
from odoo import fields, models
@@ -9,7 +11,11 @@ class MeetingType(models.Model):
_name = 'calendar.event.type'
_description = 'Event Meeting Type'
def _default_color(self):
return randint(1, 11)
name = fields.Char('Name', required=True)
color = fields.Integer('Color', default=_default_color)
_sql_constraints = [
('name_uniq', 'unique (name)', "Tag name already exists !"),
@@ -4,9 +4,22 @@ odoo.define('calendar.CalendarController', function (require) {
const Controller = require('web.CalendarController');
const Dialog = require('web.Dialog');
const { qweb, _t } = require('web.core');
const core = require('web.core');
const QWeb = core.qweb;
const CalendarController = Controller.extend({
renderButtons: function ($node) {
this._super.apply(this, arguments);
const $addButton = $(QWeb.render('Calendar.calendar_add_buttons'));
this.$buttons.prepend($addButton)
const self = this;
// When clicking on "Add", create a new record in form view
this.$buttons.on('click', 'button.o-calendar-button-new', () => {
return self.do_action('calendar.action_calendar_event_notify');
});
},
_askRecurrenceUpdatePolicy() {
return new Promise((resolve, reject) => {
new Dialog(this, {
@@ -25,7 +38,6 @@ odoo.define('calendar.CalendarController', function (require) {
});
},
// TODO factorize duplicated code
/**
* @override
* @private
@@ -33,12 +45,7 @@ odoo.define('calendar.CalendarController', function (require) {
*/
async _onDropRecord(event) {
const _super = this._super; // reference to this._super is lost after async call
if (event.data.record.recurrency) {
const recurrenceUpdate = await this._askRecurrenceUpdatePolicy();
event.data = _.extend({}, event.data, {
'recurrenceUpdate': recurrenceUpdate,
});
}
await this._dropdUpdateRecord(event);
_super.apply(this, arguments);
},
@@ -49,14 +56,18 @@ odoo.define('calendar.CalendarController', function (require) {
*/
async _onUpdateRecord(event) {
const _super = this._super; // reference to this._super is lost after async call
await this._dropdUpdateRecord(event);
_super.apply(this, arguments);
},
async _dropdUpdateRecord(event) {
if (event.data.record.recurrency) {
const recurrenceUpdate = await this._askRecurrenceUpdatePolicy();
event.data = _.extend({}, event.data, {
'recurrenceUpdate': recurrenceUpdate,
});
}
_super.apply(this, arguments);
},
}
});
@@ -77,3 +77,7 @@
.o_cw_body .o_clipboard_button {
padding-top: 0px !important;
}
.o_calendar_attendees {
max-width:80% !important;
}
@@ -56,4 +56,10 @@
</t>
</t>
<t t-name="Calendar.calendar_add_buttons">
<span class="o_calendar_create_buttons">
<button class="btn btn-primary o-calendar-button-new" aria-label="Add" title="Add">Add</button>
</span>
</t>
</template>
+68 -23
View File
@@ -6,7 +6,7 @@
<field name="name">calendar.event.type</field>
<field name="model">calendar.event.type</field>
<field name="arch" type="xml">
<tree string="Meeting Types" editable="bottom">
<tree string="Meeting Types" sample="1" editable="bottom">
<field name="name"/>
</tree>
</field>
@@ -23,7 +23,7 @@
<field name="name">calendar.alarm.tree</field>
<field name="model">calendar.alarm</field>
<field name="arch" type="xml">
<tree string="Calendar Alarm" editable="bottom">
<tree string="Calendar Alarm" sample="1" editable="bottom">
<field name="name" invisible="1"/>
<field name="alarm_type"/>
<field name="duration"/>
@@ -65,14 +65,25 @@
<field name="name">calendar.event.tree</field>
<field name="model">calendar.event</field>
<field name="arch" type="xml">
<tree string="Meetings" multi_edit="1">
<field name="name" string="Subject" readonly="1"/>
<field name="allday" invisible="True"/>
<field name="start" string="Start Date"/>
<field name="stop" string="End Date"/>
<field name="partner_ids" readonly="1"/>
<field name="location"/>
<tree string="Meetings" sample="1" multi_edit="1">
<header>
<button name="action_mass_mailing" type="object"
string="Send Mail"/>
</header>
<field name="name" string="Subject" decoration-bf="1" attrs="{'readonly':[('recurrency','=',True)]}"/>
<field name="start" string="Start Date" readonly="1"/>
<field name="stop" string="End Date" readonly="1"/>
<field name="user_id" widget="many2one_avatar_user" attrs="{'readonly':[('recurrency','=',True)]}" optional="hide"/>
<field name="partner_ids" widget="many2many_tags" attrs="{'readonly':[('recurrency','=',True)]}" optional="show"/>
<field name="alarm_ids" widget="many2many_tags" optional="hide" attrs="{'readonly':[('recurrency','=',True)]}"/>
<field name="categ_ids" widget="many2many_tags" optional="hide" attrs="{'readonly':[('recurrency','=',True)]}"/>
<field name="recurrency" optional="hide" readonly="1"/>
<field name="privacy" optional="hide" attrs="{'readonly':[('recurrency','=',True)]}"/>
<field name="show_as" optional="hide" attrs="{'readonly':[('recurrency','=',True)]}"/>
<field name="location" optional="show" attrs="{'readonly':[('recurrency','=',True)]}"/>
<field name="duration" widget="float_time" readonly="1"/>
<field name="description" optional="hide" attrs="{'readonly':[('recurrency','=',True)]}"/>
<field name="allday" invisible="1"/>
<field name="message_needaction" invisible="1"/>
</tree>
</field>
@@ -104,12 +115,16 @@
<h1>
<field name="name" placeholder="e.g. Business Lunch"/>
</h1>
<h2>
<div class="d-flex">
<field name="partner_ids" widget="many2manyattendee"
placeholder="Select attendees..."
context="{'force_email':True}"
class="oe_inline"/>
</h2>
class="oe_inline o_calendar_attendees"
/>
<div name="send_buttons">
<button name="action_mass_mailing" help="Send Email to attendees" type="object" string=" EMAIl" icon="fa-envelope"/>
</div>
</div>
</div>
<group>
<group>
@@ -125,20 +140,19 @@
<field name="stop_date" string="Ending at" attrs="{'required': [('allday','=',True)],'invisible': [('allday','=',False)]}" force_save="1"/>
<field name="start" string="Starting at" attrs="{'required': [('allday','=',False)], 'invisible': [('allday','=',True)]}"/>
<field name="stop" invisible="1"/>
<field name="stop" string="Ending At" attrs="{'invisible': [('allday','=',True)]}"/>
<label for="duration" attrs="{'invisible': [('allday','=',True)]}"/>
<div attrs="{'invisible': [('allday','=',True)]}">
<field name="duration" widget="float_time" string="Duration" class="oe_inline" attrs="{'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/>
<span> hours</span>
</div>
<field name="event_tz" attrs="{'invisible': [('recurrency', '=', False)]}"/>
<field name="allday" attrs="{'readonly': [('id', '!=', False), ('recurrency','=',True)]}" force_save="1"/>
</group>
<group>
<field name="categ_ids" widget="many2many_tags" options="
{'no_create_edit': True}"/>
<field name="alarm_ids" widget="many2many_tags" />
<field name="alarm_ids" widget="many2many_tags" options="{'no_quick_create': True}"/>
<field name="location" />
<field name="event_tz" attrs="{'invisible': [('recurrency', '=', False)]}"/>
<field name="categ_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_create_edit': True}"/>
</group>
</group>
@@ -200,12 +214,13 @@
</page>
<page name="page_invitations" string="Invitations" groups="base.group_no_one">
<button name="action_sendmail" type="object" string="Send mail" icon="fa-envelope" class="oe_link"/>
<button name="action_sendmail" type="object" string="Send Invitations" icon="fa-envelope" class="oe_link"/>
<field name="attendee_ids" widget="one2many" mode="tree,kanban">
<tree string="Invitation details" editable="top" create="false" delete="false">
<field name="partner_id" />
<field name="state" />
<field name="email" widget="email"/>
<field name="phone" widget="phone"/>
<field name="state" />
<button name="do_tentative" states="needsAction,declined,accepted" string="Uncertain" type="object" icon="fa-asterisk" />
<button name="do_accept" string="Accept" states="needsAction,tentative,declined" type="object" icon="fa-check text-success"/>
@@ -259,13 +274,17 @@
event_open_popup="true"
event_limit="5"
color="user_id">
<field name="location" attrs="{'invisible': [('location', '=', False)]}"/>
<field name="videocall_location" string="Join Video Call" widget="CopyClipboardURL" />
<field name="attendee_status"/>
<field name="attendee_status" invisible="1"/>
<field name="user_id" string="Responsible" filters="1" widget="many2one_avatar_user"/>
<field name="partner_ids" widget="many2manyattendee" write_model="calendar.contacts" write_field="partner_id" avatar_field="image_128"/>
<field name="is_highlighted" invisible="1"/>
<field name="description"/>
<field name="description" attrs="{'invisible': [('description', '=', False)]}"/>
<field name="privacy"/>
<field name="show_as"/>
<field name="alarm_ids" attrs="{'invisible': [('alarm_ids', '=', [])]}"/>
<field name="categ_ids" attrs="{'invisible': [('categ_ids', '=', [])]}"/>
<!-- For recurrence update Dialog -->
<field name="recurrency" invisible="1"/>
<field name="recurrence_update" invisible="1"/>
@@ -280,13 +299,24 @@
<search string="Search Meetings">
<field name="name" string="Meeting" filter_domain="[('name', 'ilike', self)]"/>
<field name="partner_ids"/>
<field name="categ_ids"/>
<field name="user_id"/>
<field name="location"/>
<field name="show_as"/>
<field name="categ_ids"/>
<field name="description"/>
<filter string="My Meetings" help="My Meetings" name="mymeetings" domain="[('partner_ids.user_ids', 'in', [uid])]"/>
<separator/>
<filter string="Date" name="filter_start_date" date="start"/>
<separator/>
<filter string="Busy" name="busy" domain="[('show_as', '=', 'busy')]"/>
<filter string="Free" name="free" domain="[('show_as', '=', 'free')]"/>
<separator/>
<filter string="Public" name="public" domain="[('privacy', '=', 'public')]"/>
<filter string="Private" name="private" domain="[('privacy', '=', 'private')]"/>
<filter string="Only Internal Users" name="confidential" domain="[('privacy', '=', 'confidential')]"/>
<separator/>
<filter string="Recurrent" name="recurrent" domain="[('recurrency', '=', True)]"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
<group expand="0" string="Group By">
<filter string="Responsible" name="responsible" domain="[]" context="{'group_by': 'user_id'}"/>
@@ -304,7 +334,7 @@
<field name="search_view_id" ref="view_calendar_event_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Schedule a new meeting
No meetings found. Let's schedule one!
</p><p>
The calendar is shared between employees and fully integrated with
other applications such as the employee leaves or the business
@@ -357,6 +387,21 @@
web_icon="calendar,static/description/icon.png"
groups="base.group_user"/>
<menuitem
id="calendar_menu_config"
parent="calendar.mail_menu_calendar"
name="Configuration"
sequence="40"
action="calendar.action_calendar_event"
groups="base.group_no_one"/>
<menuitem
id="calendar_submenu_reminders"
parent="calendar_menu_config"
name="Reminders"
action="action_calendar_alarm"
groups="base.group_no_one"/>
<menuitem
id="menu_calendar_configuration"
name="Calendar"
+13
View File
@@ -27,6 +27,19 @@ class CalendarEvent(models.Model):
put_in_queue=False
)
def action_send_sms(self):
return {
'type': 'ir.actions.act_window',
'res_model': 'sms.composer',
'view_mode': 'form',
'target': 'new',
'context': {
'default_composition_mode': 'mass',
'default_res_model': 'res.partner',
'default_res_ids': self.partner_ids.ids,
},
}
def _get_trigger_alarm_types(self):
return super()._get_trigger_alarm_types() + ['sms']
+16 -7
View File
@@ -3,12 +3,12 @@
<!-- Add action entry in the Action Menu for Events -->
<record id="calendar_event_act_window_sms_composer_single" model="ir.actions.act_window">
<field name="name">Send SMS to attendees</field>
<field name="name">Send SMS</field>
<field name="res_model">sms.composer</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="context">{
'sms_composition_mode': 'guess',
'default_composition_mode': 'guess',
'default_res_id': active_id,
'default_res_ids': active_ids,
}</field>
@@ -19,11 +19,20 @@
<field name="model">calendar.event</field>
<field name="inherit_id" ref="calendar.view_calendar_event_tree"/>
<field name="arch" type="xml">
<xpath expr="//tree" position="inside">
<header>
<button name="%(calendar_event_act_window_sms_composer_single)d" type="action"
string="Send SMS to attendees"/>
</header>
<xpath expr="//header" position="inside">
<button name="%(calendar_event_act_window_sms_composer_single)d" type="action"
string="Send SMS"/>
</xpath>
</field>
</record>
<record id="view_calendar_event_form_inherited" model="ir.ui.view">
<field name="name">calendar.event.form.calendar_sms</field>
<field name="model">calendar.event</field>
<field name="inherit_id" ref="calendar.view_calendar_event_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='send_buttons']" position="inside">
<button name="action_send_sms" help="Send SMS to attendees" type="object" string="SMS" icon="fa-mobile"/>
</xpath>
</field>
</record>
+1 -1
View File
@@ -6,7 +6,7 @@
<field name="model">calendar.event</field>
<field name="inherit_id" ref="calendar.view_calendar_event_search"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='user_id']" position="after">
<xpath expr="//field[@name='description']" position="after">
<field name="opportunity_id"/>
</xpath>
</field>
@@ -13,7 +13,6 @@
<label for="cal_client_secret" string="Client Secret" class="col-3 col-lg-3 o_light_label"/>
<field name="cal_client_secret" password="True" nolabel="1"/>
</div>
<a href="https://www.odoo.com/documentation/user/14.0/crm/optimize/google_calendar_credentials.html" class="oe-link" target="_blank"><i class="fa fa-fw fa-arrow-right"/>Tutorial</a>
</div>
</div>
</field>