mirror of
https://github.com/bringout/oca-ocb-technical.git
synced 2026-04-22 02:52:06 +02:00
Initial commit: Technical packages
This commit is contained in:
commit
3473fa71a0
873 changed files with 297766 additions and 0 deletions
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-inherit="mail.Activity" t-inherit-mode="extension">
|
||||
<xpath expr="//button[hasclass('o_Activity_editButton')]" position="attributes">
|
||||
<attribute name="t-if">!activityView.activity.calendar_event_id</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//button[hasclass('o_Activity_editButton')]" position="after">
|
||||
<t t-if="activityView.activity.calendar_event_id">
|
||||
<button class="o_Activity_toolButton o_Activity_editButton btn btn-link pt-0" t-on-click="activityView.onClickEdit">
|
||||
<i class="fa fa-calendar"/> Reschedule
|
||||
</button>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-inherit="mail.ActivityMenuView" t-inherit-mode="extension">
|
||||
<xpath expr="//*[hasclass('o_ActivityMenuView_activityGroupTitle')]" position="after">
|
||||
<div t-if="activityGroupView.activityGroup.type == 'meeting'">
|
||||
<t t-set="is_next_meeting" t-value="true"/>
|
||||
<t t-foreach="activityGroupView.activityGroup.meetings" t-as="meeting" t-key="meeting.localId">
|
||||
<div class="d-flex">
|
||||
<span class="flex-grow-1" t-att-class="!meeting.allday and is_next_meeting ? 'o_meeting_filter o_meeting_bold' : 'o_meeting_filter'" t-att-data-res_model="activityGroupView.activityGroup.irModel.model" t-att-data-res_id="meeting.id" t-att-data-model_name="activityGroupView.activityGroup.irModel.name" t-att-title="meeting.name">
|
||||
<span><t t-esc="meeting.name"/></span>
|
||||
</span>
|
||||
<span t-if="meeting.formattedStart">
|
||||
<t t-if="meeting.allday">All Day</t>
|
||||
<t t-else=''>
|
||||
<t t-set="is_next_meeting" t-value="false"/>
|
||||
<t t-esc="meeting.formattedStart"/>
|
||||
</t>
|
||||
</span>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 605 B |
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
|
|
@ -0,0 +1,85 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import BasicModel from 'web.BasicModel';
|
||||
import fieldRegistry from 'web.field_registry';
|
||||
import relationalFields from 'web.relational_fields';
|
||||
|
||||
const FieldMany2ManyTagsAvatar = relationalFields.FieldMany2ManyTagsAvatar;
|
||||
|
||||
BasicModel.include({
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} record
|
||||
* @param {string} fieldName
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_fetchSpecialAttendeeStatus: function (record, fieldName) {
|
||||
var context = record.getContext({fieldName: fieldName});
|
||||
var attendeeIDs = record.data[fieldName] ? this.localData[record.data[fieldName]].res_ids : [];
|
||||
var meetingID = _.isNumber(record.res_id) ? record.res_id : false;
|
||||
return this._rpc({
|
||||
model: 'res.partner',
|
||||
method: 'get_attendee_detail',
|
||||
args: [attendeeIDs, [meetingID]],
|
||||
context: context,
|
||||
}).then(function (result) {
|
||||
return result;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const Many2ManyAttendee = FieldMany2ManyTagsAvatar.extend({
|
||||
// as this widget is model dependant (rpc on res.partner), use it in another
|
||||
// context probably won't work
|
||||
// supportedFieldTypes: ['many2many'],
|
||||
specialData: "_fetchSpecialAttendeeStatus",
|
||||
className: 'o_field_many2manytags avatar',
|
||||
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.className += this.nodeOptions.block ? ' d-block' : '';
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_renderTags: function () {
|
||||
this._super.apply(this, arguments);
|
||||
const avatars = this.el.querySelectorAll('.o_m2m_avatar');
|
||||
for (const avatar of avatars) {
|
||||
const partner_id = parseInt(avatar.dataset["id"]);
|
||||
const partner_data = this.record.specialData.partner_ids.find(partner => partner.id === partner_id);
|
||||
if (partner_data) {
|
||||
avatar.classList.add('o_attendee_border', "o_attendee_border_" + partner_data.status);
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_getRenderTagsContext: function () {
|
||||
let result = this._super.apply(this, arguments);
|
||||
result.attendeesData = this.record.specialData.partner_ids;
|
||||
// Sort attendees to have the organizer on top.
|
||||
// partner_ids are sorted by default according to their id/display_name in the "elements" FieldMany2ManyTag
|
||||
// This method sort them to put the organizer on top
|
||||
const organizer = result.attendeesData.find(item => item.is_organizer);
|
||||
if (organizer) {
|
||||
const org_id = organizer.id
|
||||
// sort elements according to the partner id
|
||||
result.elements.sort((a, b) => {
|
||||
const a_org = a.id === org_id;
|
||||
return a_org ? -1 : 1;
|
||||
});
|
||||
}
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
fieldRegistry.add('many2manyattendee', Many2ManyAttendee);
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { ConnectionLostError } from "@web/core/network/rpc_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
export const calendarNotificationService = {
|
||||
dependencies: ["action", "bus_service", "notification", "rpc"],
|
||||
|
||||
start(env, { action, bus_service, notification, rpc }) {
|
||||
let calendarNotifTimeouts = {};
|
||||
let nextCalendarNotifTimeout = null;
|
||||
const displayedNotifications = new Set();
|
||||
|
||||
bus_service.addEventListener('notification', ({ detail: notifications }) => {
|
||||
for (const { payload, type } of notifications) {
|
||||
if (type === "calendar.alarm") {
|
||||
displayCalendarNotification(payload);
|
||||
}
|
||||
}
|
||||
});
|
||||
bus_service.start();
|
||||
|
||||
/**
|
||||
* Displays the Calendar notification on user's screen
|
||||
*/
|
||||
function displayCalendarNotification(notifications) {
|
||||
let lastNotifTimer = 0;
|
||||
|
||||
// Clear previously set timeouts and destroy currently displayed calendar notifications
|
||||
browser.clearTimeout(nextCalendarNotifTimeout);
|
||||
Object.values(calendarNotifTimeouts).forEach((notif) => browser.clearTimeout(notif));
|
||||
calendarNotifTimeouts = {};
|
||||
|
||||
// For each notification, set a timeout to display it
|
||||
notifications.forEach(function (notif) {
|
||||
const key = notif.event_id + "," + notif.alarm_id;
|
||||
if (displayedNotifications.has(key)) {
|
||||
return;
|
||||
}
|
||||
calendarNotifTimeouts[key] = browser.setTimeout(function () {
|
||||
const notificationRemove = notification.add(notif.message, {
|
||||
title: notif.title,
|
||||
type: "warning",
|
||||
sticky: true,
|
||||
onClose: () => {
|
||||
displayedNotifications.delete(key);
|
||||
},
|
||||
buttons: [
|
||||
{
|
||||
name: env._t("OK"),
|
||||
primary: true,
|
||||
onClick: async () => {
|
||||
await rpc("/calendar/notify_ack");
|
||||
notificationRemove();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: env._t("Details"),
|
||||
onClick: async () => {
|
||||
await action.doAction({
|
||||
type: 'ir.actions.act_window',
|
||||
res_model: 'calendar.event',
|
||||
res_id: notif.event_id,
|
||||
views: [[false, 'form']],
|
||||
}
|
||||
);
|
||||
notificationRemove();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: env._t("Snooze"),
|
||||
onClick: () => {
|
||||
notificationRemove();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
displayedNotifications.add(key);
|
||||
}, notif.timer * 1000);
|
||||
lastNotifTimer = Math.max(lastNotifTimer, notif.timer);
|
||||
});
|
||||
|
||||
// Set a timeout to get the next notifications when the last one has been displayed
|
||||
if (lastNotifTimer > 0) {
|
||||
nextCalendarNotifTimeout = browser.setTimeout(
|
||||
getNextCalendarNotif,
|
||||
lastNotifTimer * 1000
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function getNextCalendarNotif() {
|
||||
try {
|
||||
const result = await rpc("/calendar/notify", {}, { silent: true });
|
||||
displayCalendarNotification(result);
|
||||
} catch (error) {
|
||||
if (!(error instanceof ConnectionLostError)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("services").add("calendarNotification", calendarNotificationService);
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerPatch } from '@mail/model/model_core';
|
||||
import { attr } from '@mail/model/model_field';
|
||||
|
||||
registerPatch({
|
||||
name: 'Activity',
|
||||
modelMethods: {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
convertData(data) {
|
||||
const res = this._super(data);
|
||||
if ('calendar_event_id' in data) {
|
||||
res.calendar_event_id = data.calendar_event_id[0];
|
||||
}
|
||||
return res;
|
||||
},
|
||||
},
|
||||
recordMethods: {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async deleteServerRecord() {
|
||||
if (!this.calendar_event_id){
|
||||
await this._super();
|
||||
} else {
|
||||
await this.messaging.rpc({
|
||||
model: 'mail.activity',
|
||||
method: 'unlink_w_meeting',
|
||||
args: [[this.id]],
|
||||
});
|
||||
if (!this.exists()) {
|
||||
return;
|
||||
}
|
||||
this.delete();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* In case the activity is linked to a meeting, we want to open the
|
||||
* calendar view instead.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
async edit() {
|
||||
if (!this.calendar_event_id){
|
||||
await this._super();
|
||||
} else {
|
||||
const action = await this.messaging.rpc({
|
||||
model: 'mail.activity',
|
||||
method: 'action_create_calendar_event',
|
||||
args: [[this.id]],
|
||||
});
|
||||
this.env.services.action.doAction(action);
|
||||
}
|
||||
},
|
||||
},
|
||||
fields: {
|
||||
calendar_event_id: attr({
|
||||
default: false,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerPatch } from '@mail/model/model_core';
|
||||
import { many } from '@mail/model/model_field';
|
||||
|
||||
registerPatch({
|
||||
name: 'ActivityGroup',
|
||||
modelMethods: {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
convertData(data) {
|
||||
const data2 = this._super(data);
|
||||
data2.meetings = data.meetings;
|
||||
return data2;
|
||||
},
|
||||
},
|
||||
recordMethods: {
|
||||
_onChangeMeetings() {
|
||||
if (this.type === 'meeting' && this.meetings.length === 0) {
|
||||
this.delete();
|
||||
}
|
||||
},
|
||||
},
|
||||
fields: {
|
||||
meetings: many('calendar.event'),
|
||||
},
|
||||
onChanges: [
|
||||
{
|
||||
dependencies: ['meetings', 'type'],
|
||||
methodName: '_onChangeMeetings',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerPatch } from '@mail/model/model_core';
|
||||
|
||||
registerPatch({
|
||||
name: 'ActivityGroupView',
|
||||
recordMethods: {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
onClickFilterButton(ev) {
|
||||
const $el = $(ev.currentTarget);
|
||||
const data = _.extend({}, $el.data());
|
||||
if (data.res_model === "calendar.event" && data.filter === "my") {
|
||||
this.activityMenuViewOwner.update({ isOpen: false });
|
||||
this.env.services['action'].doAction('calendar.action_calendar_event', {
|
||||
additionalContext: {
|
||||
default_mode: 'day',
|
||||
search_default_mymeetings: 1,
|
||||
},
|
||||
clearBreadcrumbs: true,
|
||||
});
|
||||
} else {
|
||||
this._super.apply(this, arguments);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerPatch } from '@mail/model/model_core';
|
||||
|
||||
import Dialog from 'web.Dialog';
|
||||
|
||||
registerPatch({
|
||||
name: 'ActivityView',
|
||||
recordMethods: {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async onClickCancel(ev) {
|
||||
if (this.activity.calendar_event_id) {
|
||||
await new Promise(resolve => {
|
||||
Dialog.confirm(
|
||||
this,
|
||||
this.env._t("The activity is linked to a meeting. Deleting it will remove the meeting as well. Do you want to proceed?"),
|
||||
{ confirm_callback: resolve },
|
||||
);
|
||||
});
|
||||
}
|
||||
if (!this.exists()) {
|
||||
return;
|
||||
}
|
||||
await this._super();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerModel } from '@mail/model/model_core';
|
||||
import { attr } from '@mail/model/model_field';
|
||||
import { clear } from '@mail/model/model_field_command';
|
||||
|
||||
import fieldUtils from 'web.field_utils';
|
||||
import { getLangTimeFormat } from 'web.time';
|
||||
|
||||
registerModel({
|
||||
name: 'calendar.event',
|
||||
fields: {
|
||||
allday: attr(),
|
||||
attendee_status: attr(),
|
||||
formattedStart: attr({
|
||||
compute() {
|
||||
if (!this.start) {
|
||||
return clear();
|
||||
}
|
||||
return moment(fieldUtils.parse.datetime(this.start, false, { isUTC: true })).local().format(getLangTimeFormat());
|
||||
},
|
||||
}),
|
||||
id: attr({
|
||||
identifying: true,
|
||||
}),
|
||||
name: attr(),
|
||||
start: attr(),
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
.o_calendar_invitation {
|
||||
@extend .o_status;
|
||||
|
||||
&.accepted {
|
||||
background-color: map-get($theme-colors, 'success');
|
||||
}
|
||||
|
||||
&.tentative {
|
||||
background-color: $o-main-color-muted;
|
||||
}
|
||||
|
||||
&.declined {
|
||||
background-color: map-get($theme-colors, 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
.o_add_favorite_calendar {
|
||||
margin-top: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.o_calendar_invitation_page {
|
||||
flex: 0 0 auto;
|
||||
width: 50%;
|
||||
margin: 30px auto 0;
|
||||
@include o-webclient-padding($top: 10px, $bottom: 10px);
|
||||
background-color: $o-view-background-color;
|
||||
|
||||
.o_logo {
|
||||
width: 15%;
|
||||
}
|
||||
.o_event_title {
|
||||
margin-left: 20%;
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
.o_event_table {
|
||||
clear: both;
|
||||
margin: 15px 0 0;
|
||||
|
||||
th {
|
||||
padding-right: 15px;
|
||||
}
|
||||
ul {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.o_accepted {
|
||||
@extend .text-success;
|
||||
}
|
||||
.o_declined {
|
||||
@extend .text-danger;
|
||||
}
|
||||
}
|
||||
.o_meeting_filter {
|
||||
@include o-text-overflow();
|
||||
min-width: 0;
|
||||
color: grey;
|
||||
vertical-align: top;
|
||||
&.o_meeting_bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.o_cw_body .o_field_copy {
|
||||
max-width: calc(100% - 6rem);
|
||||
width: unset !important;
|
||||
}
|
||||
|
||||
.o_cw_body .o_clipboard_button {
|
||||
padding-top: 0px !important;
|
||||
}
|
||||
|
||||
.o_calendar_attendees {
|
||||
max-width:80% !important;
|
||||
}
|
||||
|
||||
.o_attendee_border {
|
||||
border-width: 2px;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.o_attendee_border_accepted {
|
||||
border-color: map-get($theme-colors, 'success');
|
||||
}
|
||||
.o_attendee_border_declined {
|
||||
border-color: map-get($theme-colors, 'danger');
|
||||
}
|
||||
.o_attendee_border_tentative {
|
||||
border-color: map-get($theme-colors, 'light');
|
||||
}
|
||||
|
||||
@for $i from 1 through length($o-colors-complete) {
|
||||
$color: nth($o-colors-complete, $i);
|
||||
|
||||
.o_cw_popover_link.o_calendar_color_#{$i - 1} {
|
||||
&.o_attendee_status_tentative {
|
||||
color: color-contrast($color);
|
||||
background: repeating-linear-gradient(
|
||||
45deg,
|
||||
$color,
|
||||
$color 10px,
|
||||
rgba($color, 0.7) 10px,
|
||||
rgba($color, 0.7) 20px
|
||||
) !important;
|
||||
}
|
||||
|
||||
&.o_attendee_status_needsAction {
|
||||
background-color: rgba($o-view-background-color, 0.9) !important;
|
||||
}
|
||||
|
||||
&.o_attendee_status_declined {
|
||||
text-decoration: line-through;
|
||||
background-color: rgba($o-view-background-color, 0.9) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.o_calendar_renderer {
|
||||
.fc-event.o_calendar_color_#{$i - 1} {
|
||||
&.o_attendee_status_needsAction,
|
||||
&.o_attendee_status_tentative,
|
||||
&.o_attendee_status_declined,
|
||||
&.o_attendee_status_alone {
|
||||
border-width: 2px 2px 2px !important;
|
||||
|
||||
&.o_cw_custom_highlight {
|
||||
background-color: $color;
|
||||
}
|
||||
}
|
||||
|
||||
&.o_attendee_status_tentative {
|
||||
color: color-contrast($color) !important;
|
||||
.fc-bg {
|
||||
background: repeating-linear-gradient(
|
||||
45deg,
|
||||
$color,
|
||||
$color 10px,
|
||||
rgba(white, 0.4) 10px,
|
||||
rgba(white, 0.4) 20px
|
||||
) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.o_attendee_status_alone,
|
||||
&.o_attendee_status_needsAction {
|
||||
background-color: rgba($o-view-background-color, 0.9) !important;
|
||||
color: $o-black !important;
|
||||
.fc-bg {
|
||||
background-color: rgba($o-view-background-color, 0.9) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.o_attendee_status_declined {
|
||||
text-decoration: line-through;
|
||||
background-color: rgba($o-view-background-color, 0.9) !important;
|
||||
color: $o-black !important;
|
||||
.fc-bg {
|
||||
background-color: rgba($o-view-background-color, 0.9) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { Dialog } from "@web/core/dialog/dialog";
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class AskRecurrenceUpdatePolicyDialog extends Component {
|
||||
setup() {
|
||||
this.possibleValues = {
|
||||
self_only: {
|
||||
checked: true,
|
||||
label: this.env._t("This event"),
|
||||
},
|
||||
future_events: {
|
||||
checked: false,
|
||||
label: this.env._t("This and following events"),
|
||||
},
|
||||
all_events: {
|
||||
checked: false,
|
||||
label: this.env._t("All events"),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
get selected() {
|
||||
return Object.entries(this.possibleValues).find(state => state[1].checked)[0];
|
||||
}
|
||||
|
||||
set selected(val) {
|
||||
this.possibleValues[this.selected].checked = false;
|
||||
this.possibleValues[val].checked = true;
|
||||
}
|
||||
|
||||
confirm() {
|
||||
this.props.confirm(this.selected);
|
||||
this.props.close();
|
||||
}
|
||||
}
|
||||
AskRecurrenceUpdatePolicyDialog.template = "calendar.AskRecurrenceUpdatePolicyDialog";
|
||||
AskRecurrenceUpdatePolicyDialog.components = {
|
||||
Dialog,
|
||||
};
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
<t t-name="calendar.AskRecurrenceUpdatePolicyDialog" owl="1">
|
||||
<Dialog size="'sm'" title="env._t('Edit Recurrent event')">
|
||||
<t t-foreach="Object.entries(possibleValues)" t-as="value" t-key="value[0]">
|
||||
<t t-set="name" t-value="value[0]"/>
|
||||
<t t-set="state" t-value="value[1]"/>
|
||||
<div class="form-check o_radio_item">
|
||||
<input name="recurrence-update" type="radio" class="form-check-input o_radio_input" t-att-checked="state.checked" t-att-value="name" t-att-id="name" t-on-click="(ev) => this.selected = ev.target.value"/>
|
||||
<label class="form-check-label o_form_label" t-att-for="name" t-esc="state.label"/>
|
||||
</div>
|
||||
</t>
|
||||
<t t-set-slot="footer" owl="1">
|
||||
<button class="btn btn-primary" t-on-click="confirm">
|
||||
Confirm
|
||||
</button>
|
||||
</t>
|
||||
</Dialog>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { AskRecurrenceUpdatePolicyDialog } from "@calendar/views/ask_recurrence_update_policy_dialog";
|
||||
|
||||
export function askRecurrenceUpdatePolicy(dialogService) {
|
||||
return new Promise((resolve) => {
|
||||
dialogService.add(AskRecurrenceUpdatePolicyDialog, {
|
||||
confirm: resolve,
|
||||
}, {
|
||||
onClose: resolve.bind(null, false),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function useAskRecurrenceUpdatePolicy() {
|
||||
const dialogService = useService("dialog");
|
||||
return askRecurrenceUpdatePolicy.bind(null, dialogService);
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { CalendarController } from "@web/views/calendar/calendar_controller";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { onWillStart } from "@odoo/owl";
|
||||
|
||||
export class AttendeeCalendarController extends CalendarController {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.actionService = useService("action");
|
||||
this.user = useService("user");
|
||||
this.orm = useService("orm");
|
||||
onWillStart(async () => {
|
||||
this.isSystemUser = await this.user.hasGroup('base.group_system');
|
||||
});
|
||||
}
|
||||
|
||||
onClickAddButton() {
|
||||
this.actionService.doAction({
|
||||
type: 'ir.actions.act_window',
|
||||
res_model: 'calendar.event',
|
||||
views: [[false, 'form']],
|
||||
}, {
|
||||
additionalContext: this.props.context,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*
|
||||
* If the event is deleted by the organizer, the event is deleted, otherwise it is declined.
|
||||
*/
|
||||
deleteRecord(record) {
|
||||
if (this.user.partnerId === record.attendeeId && this.user.partnerId === record.rawRecord.partner_id[0]) {
|
||||
super.deleteRecord(...arguments);
|
||||
} else {
|
||||
// Decline event
|
||||
this.orm.call(
|
||||
"calendar.attendee",
|
||||
"do_decline",
|
||||
[record.calendarAttendeeId],
|
||||
).then(this.model.load.bind(this.model));
|
||||
}
|
||||
}
|
||||
|
||||
configureCalendarProviderSync(providerName) {
|
||||
this.actionService.doAction({
|
||||
name: this.env._t('Connect your Calendar'),
|
||||
type: 'ir.actions.act_window',
|
||||
res_model: 'calendar.provider.config',
|
||||
views: [[false, "form"]],
|
||||
view_mode: "form",
|
||||
target: 'new',
|
||||
context: {
|
||||
'default_external_calendar_provider': providerName,
|
||||
'dialog_size': 'medium',
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
AttendeeCalendarController.template = "calendar.AttendeeCalendarController";
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
<t t-name="calendar.AttendeeCalendarController" t-inherit="web.CalendarController" t-inherit-mode="primary" owl="1">
|
||||
<DatePicker position="after">
|
||||
<div id="calendar_sync_wrapper" t-if="isSystemUser">
|
||||
<div id="calendar_sync_title" class="o_calendar_sync text-center">
|
||||
<span class="text-primary fs-6">Synchronize with:</span>
|
||||
</div>
|
||||
<div id="calendar_sync" class="container inline btn-group justify-content-evenly align-items-center">
|
||||
<div id="google_calendar_sync" class="o_calendar_sync" t-if="isSystemUser">
|
||||
<button type="button" id="google_sync_activate" class="btn btn-muted" t-on-click="() => this.configureCalendarProviderSync('google')">
|
||||
<b><i class='fa fa-plug'/> Google</b>
|
||||
</button>
|
||||
</div>
|
||||
<div id="microsoft_calendar_sync" class="o_calendar_sync" t-if="isSystemUser">
|
||||
<button type="button" id="microsoft_sync_activate" class="btn btn-muted" t-on-click="() => this.configureCalendarProviderSync('microsoft')">
|
||||
<b><i class='fa fa-plug'/> Outlook</b>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DatePicker>
|
||||
</t>
|
||||
|
||||
<t t-name="calendar.AttendeeCalendarController.controlButtons" t-inherit="web.CalendarController.controlButtons" owl="1">
|
||||
<xpath expr="//span[hasclass('o_calendar_navigation_buttons')]" position="before">
|
||||
<span class="o_calendar_create_buttons">
|
||||
<button class="btn btn-primary o-calendar-button-new me-1" t-on-click="onClickAddButton">New</button>
|
||||
</span>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,178 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { CalendarModel } from "@web/views/calendar/calendar_model";
|
||||
import { askRecurrenceUpdatePolicy } from "@calendar/views/ask_recurrence_update_policy_hook";
|
||||
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
|
||||
export class AttendeeCalendarModel extends CalendarModel {
|
||||
setup(params, { dialog }) {
|
||||
super.setup(...arguments);
|
||||
this.dialog = dialog;
|
||||
}
|
||||
|
||||
get attendees() {
|
||||
return this.data.attendees;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*
|
||||
* Upon updating a record with recurrence, we need to ask how it will affect recurrent events.
|
||||
*/
|
||||
async updateRecord(record) {
|
||||
const rec = this.records[record.id];
|
||||
if (rec.rawRecord.recurrency) {
|
||||
const recurrenceUpdate = await askRecurrenceUpdatePolicy(this.dialog);
|
||||
if (!recurrenceUpdate) {
|
||||
return this.notify();
|
||||
}
|
||||
record.recurrenceUpdate = recurrenceUpdate;
|
||||
}
|
||||
return await super.updateRecord(...arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
buildRawRecord(partialRecord, options = {}) {
|
||||
const result = super.buildRawRecord(...arguments);
|
||||
if (partialRecord.recurrenceUpdate) {
|
||||
result.recurrence_update = partialRecord.recurrenceUpdate;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async updateData(data) {
|
||||
await super.updateData(...arguments);
|
||||
await this.updateAttendeeData(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Split the events to display an event for each attendee with the correct status.
|
||||
* If the all filter is activated, we don't display an event for each attendee and keep
|
||||
* the previous behavior to display a single event.
|
||||
*/
|
||||
async updateAttendeeData(data) {
|
||||
const attendeeFilters = data.filterSections.partner_ids;
|
||||
let isEveryoneFilterActive = false;
|
||||
let attendeeIds = [];
|
||||
const eventIds = Object.keys(data.records).map(id => Number.parseInt(id));
|
||||
if (attendeeFilters) {
|
||||
const allFilter = attendeeFilters.filters.find(filter => filter.type === "all")
|
||||
isEveryoneFilterActive = allFilter && allFilter.active || false;
|
||||
attendeeIds = attendeeFilters.filters.filter(filter => filter.type !== "all" && filter.value).map(filter => filter.value);
|
||||
}
|
||||
data.attendees = await this.orm.call(
|
||||
"res.partner",
|
||||
"get_attendee_detail",
|
||||
[attendeeIds, eventIds],
|
||||
);
|
||||
const currentPartnerId = this.user.partnerId;
|
||||
if (!isEveryoneFilterActive) {
|
||||
const activeAttendeeIds = new Set(attendeeFilters.filters
|
||||
.filter(filter => filter.type !== "all" && filter.value && filter.active)
|
||||
.map(filter => filter.value)
|
||||
);
|
||||
// Duplicate records per attendee
|
||||
const newRecords = {};
|
||||
let duplicatedRecordIdx = -1;
|
||||
for (const event of Object.values(data.records)) {
|
||||
const eventData = event.rawRecord;
|
||||
const attendees = eventData.partner_ids && eventData.partner_ids.length ? eventData.partner_ids : [eventData.partner_id[0]];
|
||||
let duplicatedRecords = 0;
|
||||
for (const attendee of attendees) {
|
||||
if (!activeAttendeeIds.has(attendee)) {
|
||||
continue;
|
||||
}
|
||||
// Records will share the same rawRecord.
|
||||
const record = { ...event };
|
||||
const attendeeInfo = data.attendees.find(a => (
|
||||
a.id === attendee &&
|
||||
a.event_id === event.id
|
||||
));
|
||||
record.attendeeId = attendee;
|
||||
// Colors are linked to the partner_id but in this case we want it linked
|
||||
// to attendeeId
|
||||
record.colorIndex = attendee;
|
||||
if (attendeeInfo) {
|
||||
record.attendeeStatus = attendeeInfo.status;
|
||||
record.isAlone = attendeeInfo.is_alone;
|
||||
record.isCurrentPartner = attendeeInfo.id === currentPartnerId;
|
||||
record.calendarAttendeeId = attendeeInfo.attendee_id;
|
||||
}
|
||||
const recordId = duplicatedRecords ? duplicatedRecordIdx-- : record.id;
|
||||
// Index in the records
|
||||
record._recordId = recordId;
|
||||
newRecords[recordId] = record;
|
||||
duplicatedRecords++;
|
||||
}
|
||||
}
|
||||
data.records = newRecords;
|
||||
} else {
|
||||
for (const event of Object.values(data.records)) {
|
||||
const eventData = event.rawRecord;
|
||||
event.attendeeId = eventData.partner_id && eventData.partner_id[0]
|
||||
const attendeeInfo = data.attendees.find(a => (
|
||||
a.id === currentPartnerId &&
|
||||
a.event_id === event.id
|
||||
));
|
||||
if (attendeeInfo) {
|
||||
event.isAlone = attendeeInfo.is_alone;
|
||||
event.calendarAttendeeId = attendeeInfo.attendee_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Archives a record, ask for the recurrence update policy in case of recurrent event.
|
||||
*/
|
||||
async archiveRecord(record) {
|
||||
let recurrenceUpdate = false;
|
||||
if (record.rawRecord.recurrency) {
|
||||
recurrenceUpdate = await askRecurrenceUpdatePolicy(this.dialog);
|
||||
if (!recurrenceUpdate) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const confirm = await new Promise((resolve) => {
|
||||
this.dialog.add(ConfirmationDialog, {
|
||||
body: this.env._t("Are you sure you want to delete this record ?"),
|
||||
confirm: resolve.bind(null, true),
|
||||
}, {
|
||||
onClose: resolve.bind(null, false),
|
||||
});
|
||||
})
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
await this._archiveRecord(record.id, recurrenceUpdate);
|
||||
}
|
||||
|
||||
async _archiveRecord(id, recurrenceUpdate) {
|
||||
if (!recurrenceUpdate && recurrenceUpdate !== "self_only") {
|
||||
await this.orm.call(
|
||||
this.resModel,
|
||||
"action_archive",
|
||||
[[id]],
|
||||
);
|
||||
} else {
|
||||
await this.orm.call(
|
||||
this.resModel,
|
||||
"action_mass_archive",
|
||||
[[id], recurrenceUpdate],
|
||||
);
|
||||
}
|
||||
await this.load();
|
||||
}
|
||||
}
|
||||
AttendeeCalendarModel.services = [...CalendarModel.services, "dialog", "orm"];
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { CalendarRenderer } from "@web/views/calendar/calendar_renderer";
|
||||
import { AttendeeCalendarCommonRenderer } from "@calendar/views/attendee_calendar/common/attendee_calendar_common_renderer";
|
||||
import { AttendeeCalendarYearRenderer } from "@calendar/views/attendee_calendar/year/attendee_calendar_year_renderer";
|
||||
|
||||
export class AttendeeCalendarRenderer extends CalendarRenderer {}
|
||||
AttendeeCalendarRenderer.components = {
|
||||
...CalendarRenderer.components,
|
||||
day: AttendeeCalendarCommonRenderer,
|
||||
week: AttendeeCalendarCommonRenderer,
|
||||
month: AttendeeCalendarCommonRenderer,
|
||||
year: AttendeeCalendarYearRenderer,
|
||||
};
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { calendarView } from "@web/views/calendar/calendar_view";
|
||||
import { AttendeeCalendarController } from "@calendar/views/attendee_calendar/attendee_calendar_controller";
|
||||
import { AttendeeCalendarModel } from "@calendar/views/attendee_calendar/attendee_calendar_model";
|
||||
import { AttendeeCalendarRenderer } from "@calendar/views/attendee_calendar/attendee_calendar_renderer";
|
||||
|
||||
export const attendeeCalendarView = {
|
||||
...calendarView,
|
||||
Controller: AttendeeCalendarController,
|
||||
Model: AttendeeCalendarModel,
|
||||
Renderer: AttendeeCalendarRenderer,
|
||||
buttonTemplate: "calendar.AttendeeCalendarController.controlButtons",
|
||||
};
|
||||
|
||||
registry.category("views").add("attendee_calendar", attendeeCalendarView);
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { CalendarCommonPopover } from "@web/views/calendar/calendar_common/calendar_common_popover";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { useAskRecurrenceUpdatePolicy } from "@calendar/views/ask_recurrence_update_policy_hook";
|
||||
import { Dropdown } from "@web/core/dropdown/dropdown";
|
||||
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
|
||||
|
||||
export class AttendeeCalendarCommonPopover extends CalendarCommonPopover {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.user = useService("user");
|
||||
this.orm = useService("orm");
|
||||
this.askRecurrenceUpdatePolicy = useAskRecurrenceUpdatePolicy();
|
||||
// Show status dropdown if user is in attendees list
|
||||
if (this.isCurrentUserAttendee) {
|
||||
this.statusColors = {
|
||||
accepted: "text-success",
|
||||
declined: "text-danger",
|
||||
tentative: "text-muted",
|
||||
needsAction: "text-dark",
|
||||
};
|
||||
this.statusInfo = {};
|
||||
for (const selection of this.props.model.fields.attendee_status.selection) {
|
||||
this.statusInfo[selection[0]] = {
|
||||
text: selection[1],
|
||||
color: this.statusColors[selection[0]],
|
||||
};
|
||||
}
|
||||
this.selectedStatusInfo = this.statusInfo[this.props.record.attendeeStatus];
|
||||
}
|
||||
}
|
||||
|
||||
get isCurrentUserAttendee() {
|
||||
return this.props.record.rawRecord.partner_ids.includes(this.user.partnerId);
|
||||
}
|
||||
|
||||
get isCurrentUserOrganizer() {
|
||||
return this.props.record.rawRecord.partner_id[0] === this.user.partnerId;
|
||||
}
|
||||
|
||||
get isEventPrivate() {
|
||||
return this.props.record.rawRecord.privacy === "private";
|
||||
}
|
||||
|
||||
get displayAttendeeAnswerChoice() {
|
||||
return (
|
||||
this.props.record.rawRecord.partner_ids.some((partner) => partner !== this.user.partnerId) &&
|
||||
this.props.record.isCurrentPartner
|
||||
);
|
||||
}
|
||||
|
||||
get isEventDetailsVisible() {
|
||||
return this.isEventPrivate ? this.isCurrentUserAttendee : true;
|
||||
}
|
||||
|
||||
get isEventArchivable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
get isEventDeletable() {
|
||||
return super.isEventDeletable && (this.isCurrentUserAttendee || this.isCurrentUserOrganizer) && !this.isEventArchivable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
get isEventEditable() {
|
||||
return this.isEventPrivate ? this.isCurrentUserAttendee || this.isCurrentUserOrganizer : super.isEventEditable;
|
||||
}
|
||||
|
||||
async changeAttendeeStatus(selectedStatus) {
|
||||
const record = this.props.record;
|
||||
if (record.attendeeStatus === selectedStatus) {
|
||||
return this.props.close();
|
||||
}
|
||||
let recurrenceUpdate = false;
|
||||
if (record.rawRecord.recurrency) {
|
||||
recurrenceUpdate = await this.askRecurrenceUpdatePolicy();
|
||||
if (!recurrenceUpdate) {
|
||||
return this.props.close();
|
||||
}
|
||||
}
|
||||
await this.env.services.orm.call(
|
||||
this.props.model.resModel,
|
||||
"change_attendee_status",
|
||||
[[record.id], selectedStatus, recurrenceUpdate],
|
||||
);
|
||||
await this.props.model.load();
|
||||
this.props.close();
|
||||
}
|
||||
|
||||
async onClickArchive() {
|
||||
this.props.close();
|
||||
await this.props.model.archiveRecord(this.props.record);
|
||||
}
|
||||
}
|
||||
AttendeeCalendarCommonPopover.components = {
|
||||
...CalendarCommonPopover.components,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
};
|
||||
AttendeeCalendarCommonPopover.subTemplates = {
|
||||
...CalendarCommonPopover.subTemplates,
|
||||
body: "calendar.AttendeeCalendarCommonPopover.body",
|
||||
footer: "calendar.AttendeeCalendarCommonPopover.footer",
|
||||
};
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
<t t-name="calendar.AttendeeCalendarCommonPopover.body" t-inherit="web.CalendarCommonPopover.body" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//ul[hasclass('o_cw_popover_fields_secondary')]" position="attributes">
|
||||
<attribute name="t-if">isEventDetailsVisible</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="calendar.AttendeeCalendarCommonPopover.footer" t-inherit="web.CalendarCommonPopover.footer" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//t[@t-if='isEventDeletable']" position="after">
|
||||
<a t-if="isEventArchivable and isEventDetailsVisible" href="#" class="btn btn-secondary o_cw_popover_archive_g" t-on-click="onClickArchive">Delete</a>
|
||||
<div t-if="displayAttendeeAnswerChoice" class="d-inline-block">
|
||||
<Dropdown togglerClass="'btn btn-secondary'">
|
||||
<t t-set-slot="toggler">
|
||||
<i t-attf-class="fa fa-circle o-calendar-attendee-status-icon #{selectedStatusInfo.color}"/> <span class="o-calendar-attendee-status-text" t-esc="selectedStatusInfo.text"></span>
|
||||
</t>
|
||||
<DropdownItem onSelected="() => this.changeAttendeeStatus('accepted')">
|
||||
<i class="fa fa-circle text-success"/> Accept
|
||||
</DropdownItem>
|
||||
<DropdownItem onSelected="() => this.changeAttendeeStatus('declined')">
|
||||
<i class="fa fa-circle text-danger"/> Decline
|
||||
</DropdownItem>
|
||||
<DropdownItem onSelected="() => this.changeAttendeeStatus('tentative')">
|
||||
<i class="fa fa-circle text-muted"/> Uncertain
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { CalendarCommonRenderer } from "@web/views/calendar/calendar_common/calendar_common_renderer";
|
||||
import { AttendeeCalendarCommonPopover } from "@calendar/views/attendee_calendar/common/attendee_calendar_common_popover";
|
||||
|
||||
export class AttendeeCalendarCommonRenderer extends CalendarCommonRenderer {
|
||||
/**
|
||||
* @override
|
||||
*
|
||||
* Give a new key to our fc records to be able to iterate through in templates
|
||||
*/
|
||||
convertRecordToEvent(record) {
|
||||
return {
|
||||
...super.convertRecordToEvent(record),
|
||||
id: record._recordId || record.id,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
onEventRender(info) {
|
||||
super.onEventRender(...arguments);
|
||||
const { el, event } = info;
|
||||
const record = this.props.model.records[event.id];
|
||||
|
||||
if (record) {
|
||||
if (record.rawRecord.is_highlighted) {
|
||||
el.classList.add("o_event_highlight");
|
||||
}
|
||||
if (record.isAlone) {
|
||||
el.classList.add("o_attendee_status_alone");
|
||||
} else {
|
||||
el.classList.add(`o_attendee_status_${record.attendeeStatus}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*
|
||||
* Allow slots to be selected over multiple days
|
||||
*/
|
||||
isSelectionAllowed(event) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
AttendeeCalendarCommonRenderer.eventTemplate = "calendar.AttendeeCalendarCommonRenderer.event";
|
||||
AttendeeCalendarCommonRenderer.components = {
|
||||
...CalendarCommonRenderer.components,
|
||||
Popover: AttendeeCalendarCommonPopover,
|
||||
};
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
<t t-name="calendar.AttendeeCalendarCommonRenderer.event" owl="1">
|
||||
<div class="fc-content">
|
||||
<div t-attf-class="#{attendeeStatus != 'accepted' ? 'fw-normal' : ''} o_event_title me-2">
|
||||
<span t-if="isAlone" class="fa fa-exclamation-circle"/>
|
||||
<t t-esc="title"/>
|
||||
</div>
|
||||
<span t-if="!isTimeHidden" class="fc-time" t-esc="startTime" />
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { CalendarYearPopover } from "@web/views/calendar/calendar_year/calendar_year_popover";
|
||||
|
||||
export class AttendeeCalendarYearPopover extends CalendarYearPopover {
|
||||
getRecordClass(record) {
|
||||
const classes = [super.getRecordClass(record)];
|
||||
if (record.isAlone) {
|
||||
classes.push("o_attendee_status_alone");
|
||||
} else {
|
||||
classes.push(`o_attendee_status_${record.attendeeStatus}`);
|
||||
}
|
||||
return classes.join(" ");
|
||||
}
|
||||
}
|
||||
AttendeeCalendarYearPopover.subTemplates = {
|
||||
...CalendarYearPopover.subTemplates,
|
||||
body: "calendar.AttendeeCalendarYearPopover.body",
|
||||
};
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
<t t-name="calendar.AttendeeCalendarYearPopover.body" t-inherit="web.CalendarYearPopover.body" t-inherit-mode="primary" owl="1">
|
||||
<!-- Since we duplicate the event this is needed to avoid duplicated key errors -->
|
||||
<xpath expr="//t[@t-foreach='recordGroup.records']" position="attributes">
|
||||
<attribute name="t-key">record._recordId</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { CalendarYearRenderer } from "@web/views/calendar/calendar_year/calendar_year_renderer";
|
||||
import { AttendeeCalendarYearPopover } from "@calendar/views/attendee_calendar/year/attendee_calendar_year_popover";
|
||||
|
||||
export class AttendeeCalendarYearRenderer extends CalendarYearRenderer {}
|
||||
AttendeeCalendarYearRenderer.components = {
|
||||
...CalendarYearRenderer,
|
||||
Popover: AttendeeCalendarYearPopover,
|
||||
};
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { FormController } from "@web/views/form/form_controller";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
const { onWillStart } = owl;
|
||||
|
||||
export class CalendarFormController extends FormController {
|
||||
setup() {
|
||||
super.setup();
|
||||
const ormService = useService("orm");
|
||||
|
||||
onWillStart(async () => {
|
||||
this.discussVideocallLocation = await ormService.call(
|
||||
"calendar.event",
|
||||
"get_discuss_videocall_location",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async beforeExecuteActionButton(clickParams) {
|
||||
const action = clickParams.name;
|
||||
if (action == 'clear_videocall_location' || action === 'set_discuss_videocall_location') {
|
||||
let newVal = false;
|
||||
let videoCallSource = 'custom'
|
||||
let changes = {};
|
||||
if (action === 'set_discuss_videocall_location') {
|
||||
newVal = this.discussVideocallLocation;
|
||||
videoCallSource = 'discuss';
|
||||
changes.access_token = this.discussVideocallLocation.split('/').pop();
|
||||
}
|
||||
changes = Object.assign(changes, {
|
||||
videocall_location: newVal,
|
||||
videocall_source: videoCallSource,
|
||||
});
|
||||
this.model.root.update(changes);
|
||||
return false; // no continue
|
||||
}
|
||||
return super.beforeExecuteActionButton(...arguments);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { formView } from "@web/views/form/form_view";
|
||||
import { CalendarFormController } from "@calendar/views/calendar_form/calendar_form_controller";
|
||||
|
||||
export const CalendarFormView = {
|
||||
...formView,
|
||||
Controller: CalendarFormController,
|
||||
};
|
||||
|
||||
registry.category("views").add("calendar_form", CalendarFormView);
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { FormRenderer } from "@web/views/form/form_renderer";
|
||||
import { session } from "@web/session";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
const providerData = {
|
||||
'google': {
|
||||
'restart_sync_method': 'restart_google_synchronization',
|
||||
'sync_route': '/google_calendar/sync_data'
|
||||
},
|
||||
'microsoft': {
|
||||
'restart_sync_method': 'restart_microsoft_synchronization',
|
||||
'sync_route': '/microsoft_calendar/sync_data'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class CalendarProviderConfigFormRenderer extends FormRenderer {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.orm = useService('orm');
|
||||
this.rpc = useService('rpc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate the external sync for the first time, after installing the
|
||||
* relevant submodule if necessary.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
async onConnect(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopImmediatePropagation();
|
||||
if (!await this.props.record.save({stayInEdition: true})) {
|
||||
return; // handled by view
|
||||
}
|
||||
await this.orm.call(
|
||||
this.props.record.resModel,
|
||||
'action_calendar_prepare_external_provider_sync',
|
||||
[this.props.record.resId]
|
||||
)
|
||||
// See google/microsoft_calendar for the origin of this shortened version
|
||||
const { restart_sync_method, sync_route } = providerData[this.props.record.data.external_calendar_provider];
|
||||
await this.orm.call(
|
||||
'res.users',
|
||||
restart_sync_method,
|
||||
[[session.uid]]
|
||||
);
|
||||
const response = await this.rpc(
|
||||
sync_route, {
|
||||
model: 'calendar.event',
|
||||
fromurl: window.location.href,
|
||||
}
|
||||
);
|
||||
await this._beforeLeaveContext();
|
||||
if (response.status === "need_auth") {
|
||||
window.location.assign(response.url);
|
||||
} else if (response.status === "need_refresh") {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Hook to perform additional work before redirecting to external url or reloading.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
async _beforeLeaveContext() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { formView } from "@web/views/form/form_view";
|
||||
import { CalendarProviderConfigFormRenderer } from "./calendar_provider_config_form_renderer";
|
||||
|
||||
|
||||
export const CalendarProviderConfigFormView = {
|
||||
...formView,
|
||||
Renderer: CalendarProviderConfigFormRenderer,
|
||||
};
|
||||
|
||||
registry.category("views").add("calendar_provider_config_form", CalendarProviderConfigFormView);
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { Many2ManyTagsAvatarField } from "@web/views/fields/many2many_tags_avatar/many2many_tags_avatar_field";
|
||||
|
||||
export class Many2ManyAttendee extends Many2ManyTagsAvatarField {
|
||||
get tags() {
|
||||
const { partner_ids: partnerIds } = this.props.record.preloadedData;
|
||||
const tags = super.tags.map((tag) => {
|
||||
const partner = partnerIds.find((partner) => tag.resId === partner.id);
|
||||
if (partner) {
|
||||
tag.className = `o_attendee_border o_attendee_border_${partner.status}`;
|
||||
}
|
||||
return tag;
|
||||
});
|
||||
|
||||
const organizer = partnerIds.find((partner) => partner.is_organizer);
|
||||
if (organizer) {
|
||||
const orgId = organizer.id;
|
||||
// sort elements according to the partner id
|
||||
tags.sort((a, b) => {
|
||||
const a_org = a.resId === orgId;
|
||||
return a_org ? -1 : 1;
|
||||
});
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
}
|
||||
Many2ManyAttendee.additionalClasses = ["o_field_many2many_tags_avatar"];
|
||||
Many2ManyAttendee.legacySpecialData = "_fetchSpecialAttendeeStatus";
|
||||
|
||||
registry.category("fields").add("many2manyattendee", Many2ManyAttendee);
|
||||
|
||||
export function preloadMany2ManyAttendee(orm, record, fieldName) {
|
||||
const context = record.getFieldContext(fieldName);
|
||||
return orm.call(
|
||||
"res.partner",
|
||||
"get_attendee_detail",
|
||||
[record.data[fieldName].records.map(rec => rec.resId), [record.resId || false]],
|
||||
{
|
||||
context,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
registry.category("preloadedData").add("many2manyattendee", {
|
||||
loadOnTypes: ["many2many"],
|
||||
preload: preloadMany2ManyAttendee,
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue