1# --
2# Copyright (C) 2001-2020 OTRS AG, https://otrs.com/
3# --
4# This software comes with ABSOLUTELY NO WARRANTY. For details, see
5# the enclosed file COPYING for license information (GPL). If you
6# did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt.
7# --
8
9package Kernel::System::Calendar::Appointment;
10
11use strict;
12use warnings;
13
14use Digest::MD5;
15
16use vars qw(@ISA);
17
18use Kernel::System::VariableCheck qw(:all);
19use Kernel::System::EventHandler;
20
21our @ObjectDependencies = (
22    'Kernel::Config',
23    'Kernel::System::Cache',
24    'Kernel::System::Calendar',
25    'Kernel::System::DB',
26    'Kernel::System::Daemon::SchedulerDB',
27    'Kernel::System::DateTime',
28    'Kernel::System::Group',
29    'Kernel::System::Log',
30    'Kernel::System::Main',
31    'Kernel::System::Scheduler',
32);
33
34=head1 NAME
35
36Kernel::System::Calendar::Appointment - calendar appointment lib
37
38=head1 DESCRIPTION
39
40All appointment functions.
41
42=head1 PUBLIC INTERFACE
43
44=head2 new()
45
46create an object. Do not use it directly, instead use:
47
48    use Kernel::System::ObjectManager;
49    local $Kernel::OM = Kernel::System::ObjectManager->new();
50    my $AppointmentObject = $Kernel::OM->Get('Kernel::System::Calendar::Appointment');
51
52=cut
53
54sub new {
55    my ( $Type, %Param ) = @_;
56
57    # allocate new hash for object
58    my $Self = {%Param};
59    bless( $Self, $Type );
60
61    @ISA = qw(
62        Kernel::System::EventHandler
63    );
64
65    # init of event handler
66    $Self->EventHandlerInit(
67        Config => 'AppointmentCalendar::EventModulePost',
68    );
69
70    $Self->{CacheType} = 'Appointment';
71    $Self->{CacheTTL}  = 60 * 60 * 24 * 20;
72
73    return $Self;
74}
75
76=head2 AppointmentCreate()
77
78creates a new appointment.
79
80    my $AppointmentID = $AppointmentObject->AppointmentCreate(
81        ParentID              => 1,                                       # (optional) valid ParentID for recurring appointments
82        CalendarID            => 1,                                       # (required) valid CalendarID
83        UniqueID              => 'jwioji-fwjio',                          # (optional) provide desired UniqueID; if there is already existing Appointment
84                                                                          #            with same UniqueID, system will delete it
85        Title                 => 'Webinar',                               # (required) Title
86        Description           => 'How to use Process tickets...',         # (optional) Description
87        Location              => 'Straubing',                             # (optional) Location
88        StartTime             => '2016-01-01 16:00:00',                   # (required)
89        EndTime               => '2016-01-01 17:00:00',                   # (required)
90        AllDay                => 0,                                       # (optional) default 0
91        TeamID                => [ 1 ],                                   # (optional) must be an array reference if supplied
92        ResourceID            => [ 1, 3 ],                                # (optional) must be an array reference if supplied
93        Recurring             => 1,                                       # (optional) flag the appointment as recurring (parent only!)
94        RecurringRaw          => 1,                                       # (optional) skip loop for recurring appointments (do not create occurrences!)
95        RecurrenceType        => 'Daily',                                 # (required if Recurring) Possible "Daily", "Weekly", "Monthly", "Yearly",
96                                                                          #           "CustomWeekly", "CustomMonthly", "CustomYearly"
97
98        RecurrenceFrequency   => [1, 3, 5],                               # (required if Custom Recurring) Recurrence pattern
99                                                                          #           for CustomWeekly: 1-Mon, 2-Tue,..., 7-Sun
100                                                                          #           for CustomMonthly: 1-1st, 2-2nd,.., 31th
101                                                                          #           for CustomYearly: 1-Jan, 2-Feb,..., 12-Dec
102                                                                          # ...
103        RecurrenceCount       => 1,                                       # (optional) How many Appointments to create
104        RecurrenceInterval    => 2,                                       # (optional) Repeating interval (default 1)
105        RecurrenceUntil       => '2016-01-10 00:00:00',                   # (optional) Until date
106        RecurrenceID          => '2016-01-10 00:00:00',                   # (optional) Expected start time for this occurrence
107        RecurrenceExclude     => [                                        # (optional) Which specific occurrences to exclude
108            '2016-01-10 00:00:00',
109            '2016-01-11 00:00:00',
110        ],
111        NotificationTime      => '2016-01-01 17:00:00',                   # (optional) Point of time to execute the notification event
112        NotificationTemplate  => 'Custom',                                # (optional) Template to be used for notification point of time
113        NotificationCustom    => 'relative',                              # (optional) Type of the custom template notification point of time
114                                                                          #            Possible "relative", "datetime"
115        NotificationCustomRelativeUnitCount   => '12',                    # (optional) minutes, hours or days count for custom template
116        NotificationCustomRelativeUnit        => 'minutes',               # (optional) minutes, hours or days unit for custom template
117        NotificationCustomRelativePointOfTime => 'beforestart',           # (optional) Point of execute for custom templates
118                                                                          #            Possible "beforestart", "afterstart", "beforeend", "afterend"
119        NotificationCustomDateTime => '2016-01-01 17:00:00',              # (optional) Notification date time for custom template
120        TicketAppointmentRuleID    => '9bb20ea035e7a9930652a9d82d00c725', # (optional) Ticket appointment rule ID (for ticket appointments only!)
121        UserID                     => 1,                                  # (required) UserID
122    );
123
124returns parent AppointmentID if successful
125
126Events:
127    AppointmentCreate
128
129=cut
130
131sub AppointmentCreate {
132    my ( $Self, %Param ) = @_;
133
134    # check needed stuff
135    for my $Needed (qw(CalendarID Title StartTime EndTime UserID)) {
136        if ( !$Param{$Needed} ) {
137            $Kernel::OM->Get('Kernel::System::Log')->Log(
138                Priority => 'error',
139                Message  => "Need $Needed!",
140            );
141            return;
142        }
143    }
144
145    # prepare possible notification params
146    $Self->_AppointmentNotificationPrepare(
147        Data => \%Param,
148    );
149
150    # if Recurring is provided, additional parameters must be present
151    if ( $Param{Recurring} ) {
152
153        my @RecurrenceTypes = (
154            "Daily",       "Weekly",       "Monthly",       "Yearly",
155            "CustomDaily", "CustomWeekly", "CustomMonthly", "CustomYearly"
156        );
157
158        if (
159            !$Param{RecurrenceType}
160            || !grep { $_ eq $Param{RecurrenceType} } @RecurrenceTypes
161            )
162        {
163            $Kernel::OM->Get('Kernel::System::Log')->Log(
164                Priority => 'error',
165                Message  => "RecurrenceType invalid!",
166            );
167            return;
168        }
169
170        if (
171            (
172                $Param{RecurrenceType} eq 'CustomWeekly'
173                || $Param{RecurrenceType} eq 'CustomMonthly'
174                || $Param{RecurrenceType} eq 'CustomYearly'
175            )
176            && !$Param{RecurrenceFrequency}
177            )
178        {
179            $Kernel::OM->Get('Kernel::System::Log')->Log(
180                Priority => 'error',
181                Message  => "RecurrenceFrequency needed!",
182            );
183            return;
184        }
185    }
186
187    $Param{RecurrenceInterval} ||= 1;
188
189    if ( $Param{UniqueID} && !$Param{ParentID} ) {
190        my %Appointment = $Self->AppointmentGet(
191            UniqueID   => $Param{UniqueID},
192            CalendarID => $Param{CalendarID},
193        );
194
195        # delete existing appointment with same UniqueID
196        if ( %Appointment && $Appointment{AppointmentID} ) {
197            $Self->AppointmentDelete(
198                AppointmentID => $Appointment{AppointmentID},
199                UserID        => $Param{UserID},
200            );
201        }
202    }
203
204    # check ParentID
205    if ( $Param{ParentID} && !IsInteger( $Param{ParentID} ) ) {
206        $Kernel::OM->Get('Kernel::System::Log')->Log(
207            Priority => 'error',
208            Message  => "ParentID must be a number!",
209        );
210        return;
211    }
212
213    # Check StartTime.
214    my $StartTimeObject = $Kernel::OM->Create(
215        'Kernel::System::DateTime',
216        ObjectParams => {
217            String => $Param{StartTime},
218        },
219    );
220    if ( !$StartTimeObject ) {
221        $Kernel::OM->Get('Kernel::System::Log')->Log(
222            Priority => 'error',
223            Message  => "Invalid StartTime!",
224        );
225        return;
226    }
227
228    # check UniqueID
229    if ( !$Param{UniqueID} ) {
230        $Param{UniqueID} = $Self->GetUniqueID(
231            CalendarID => $Param{CalendarID},
232            StartTime  => $Param{StartTime},
233            UserID     => $Param{UserID},
234        );
235    }
236
237    # Check EndTime.
238    my $EndTimeObject = $Kernel::OM->Create(
239        'Kernel::System::DateTime',
240        ObjectParams => {
241            String => $Param{EndTime},
242        },
243    );
244    if ( !$EndTimeObject ) {
245        $Kernel::OM->Get('Kernel::System::Log')->Log(
246            Priority => 'error',
247            Message  => "Invalid EndTime!",
248        );
249        return;
250    }
251
252    # check if array refs
253    my %Arrays;
254    for my $Parameter (
255        qw(TeamID ResourceID RecurrenceFrequency RecurrenceExclude)
256        )
257    {
258        if ( $Param{$Parameter} && @{ $Param{$Parameter} // [] } ) {
259            if ( !IsArrayRefWithData( $Param{$Parameter} ) ) {
260                $Kernel::OM->Get('Kernel::System::Log')->Log(
261                    Priority => 'error',
262                    Message  => "$Parameter not ARRAYREF!",
263                );
264                return;
265            }
266
267            my @Array = @{ $Param{$Parameter} };
268
269            # remove undefined values
270            @Array = grep { defined $_ } @Array;
271
272            $Arrays{$Parameter} = join( ',', @Array ) if @Array;
273        }
274    }
275
276    # check if numbers
277    for my $Parameter (
278        qw(Recurring RecurrenceCount RecurrenceInterval)
279        )
280    {
281        if ( $Param{$Parameter} && !IsInteger( $Param{$Parameter} ) ) {
282            $Kernel::OM->Get('Kernel::System::Log')->Log(
283                Priority => 'error',
284                Message  => "$Parameter must be a number!",
285            );
286            return;
287        }
288    }
289
290    # check RecurrenceUntil
291    if ( $Param{RecurrenceUntil} ) {
292
293        # Usually hour, minute and second = 0. In this case, take time from StartTime.
294        $Param{RecurrenceUntil} = $Self->_TimeCheck(
295            OriginalTime => $Param{StartTime},
296            Time         => $Param{RecurrenceUntil},
297        );
298
299        my $RecurrenceUntilObject = $Kernel::OM->Create(
300            'Kernel::System::DateTime',
301            ObjectParams => {
302                String => $Param{RecurrenceUntil},
303            },
304        );
305
306        if (
307            !$RecurrenceUntilObject
308            || $StartTimeObject > $RecurrenceUntilObject
309            )
310        {
311            $Kernel::OM->Get('Kernel::System::Log')->Log(
312                Priority => 'error',
313                Message  => "Invalid RecurrenceUntil!",
314            );
315            return;
316        }
317    }
318
319    # get db object
320    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
321    my @Bind;
322
323    # parent ID supplied
324    my $ParentIDCol = my $ParentIDVal = '';
325    if ( $Param{ParentID} ) {
326        $ParentIDCol = 'parent_id,';
327        $ParentIDVal = '?,';
328        push @Bind, \$Param{ParentID};
329
330        # turn off all recurring fields
331        delete $Param{Recurring};
332        delete $Param{RecurrenceType};
333        delete $Param{RecurrenceFrequency};
334        delete $Param{RecurrenceCount};
335        delete $Param{RecurrenceInterval};
336        delete $Param{RecurrenceUntil};
337    }
338
339    push @Bind, \$Param{CalendarID}, \$Param{UniqueID}, \$Param{Title}, \$Param{Description},
340        \$Param{Location}, \$Param{StartTime},   \$Param{EndTime},   \$Param{AllDay},
341        \$Arrays{TeamID},  \$Arrays{ResourceID}, \$Param{Recurring}, \$Param{RecurrenceType},
342        \$Arrays{RecurrenceFrequency}, \$Param{RecurrenceCount},      \$Param{RecurrenceInterval},
343        \$Param{RecurrenceUntil},      \$Param{RecurrenceID},         \$Arrays{RecurrenceExclude},
344        \$Param{NotificationDate},     \$Param{NotificationTemplate}, \$Param{NotificationCustom},
345        \$Param{NotificationCustomRelativeUnitCount},   \$Param{NotificationCustomRelativeUnit},
346        \$Param{NotificationCustomRelativePointOfTime}, \$Param{NotificationCustomDateTime},
347        \$Param{TicketAppointmentRuleID},               \$Param{UserID}, \$Param{UserID};
348
349    my $SQL = "
350        INSERT INTO calendar_appointment
351            ($ParentIDCol calendar_id, unique_id, title, description, location, start_time,
352            end_time, all_day, team_id, resource_id, recurring, recur_type, recur_freq, recur_count,
353            recur_interval, recur_until, recur_id, recur_exclude, notify_time, notify_template,
354            notify_custom, notify_custom_unit_count, notify_custom_unit, notify_custom_unit_point,
355            notify_custom_date, ticket_appointment_rule_id, create_time, create_by, change_time,
356            change_by)
357        VALUES ($ParentIDVal ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
358            ?, ?, current_timestamp, ?, current_timestamp, ?)
359    ";
360
361    # create db record
362    return if !$DBObject->Do(
363        SQL  => $SQL,
364        Bind => \@Bind,
365    );
366
367    my $AppointmentID;
368
369    # return parent id for appointment occurrences
370    if ( $Param{ParentID} ) {
371        $AppointmentID = $Param{ParentID};
372    }
373
374    # get appointment id for parent appointment
375    else {
376        return if !$DBObject->Prepare(
377            SQL => '
378                SELECT id FROM calendar_appointment
379                WHERE unique_id=? AND parent_id IS NULL
380            ',
381            Bind  => [ \$Param{UniqueID} ],
382            Limit => 1,
383        );
384
385        while ( my @Row = $DBObject->FetchrowArray() ) {
386            $AppointmentID = $Row[0] || '';
387        }
388
389        # return if there is not appointment created
390        if ( !$AppointmentID ) {
391            $Kernel::OM->Get('Kernel::System::Log')->Log(
392                Priority => 'error',
393                Message  => 'Can\'t get AppointmentID from INSERT!',
394            );
395            return;
396        }
397    }
398
399    # add recurring appointments
400    if ( $Param{Recurring} && !$Param{RecurringRaw} ) {
401        return if !$Self->_AppointmentRecurringCreate(
402            ParentID    => $AppointmentID,
403            Appointment => \%Param,
404        );
405    }
406
407    my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache');
408
409    # clean up list methods cache
410    $CacheObject->CleanUp(
411        Type => $Self->{CacheType} . 'List' . $Param{CalendarID},
412    );
413    $CacheObject->CleanUp(
414        Type => $Self->{CacheType} . 'Days' . $Param{UserID},
415    );
416
417    # fire event
418    $Self->EventHandler(
419        Event => 'AppointmentCreate',
420        Data  => {
421            AppointmentID => $AppointmentID,
422            CalendarID    => $Param{CalendarID},
423        },
424        UserID => $Param{UserID},
425    );
426
427    return $AppointmentID;
428}
429
430=head2 AppointmentList()
431
432get a hash of Appointments.
433
434    my @Appointments = $AppointmentObject->AppointmentList(
435        CalendarID          => 1,                                       # (required) Valid CalendarID
436        Title               => '*',                                     # (optional) Filter by title, wildcard supported
437        Description         => '*',                                     # (optional) Filter by description, wildcard supported
438        Location            => '*',                                     # (optional) Filter by location, wildcard supported
439        StartTime           => '2016-01-01 00:00:00',                   # (optional) Filter by start date
440        EndTime             => '2016-02-01 00:00:00',                   # (optional) Filter by end date
441        TeamID              => 1,                                       # (optional) Filter by team
442        ResourceID          => 2,                                       # (optional) Filter by resource
443        Result              => 'HASH',                                  # (optional), HASH|ARRAY
444    );
445
446returns an array of hashes with select Appointment data or simple array of AppointmentIDs:
447
448Result => 'HASH':
449
450    @Appointments = [
451        {
452            AppointmentID => 1,
453            CalendarID    => 1,
454            UniqueID      => '20160101T160000-71E386@localhost',
455            Title         => 'Webinar',
456            Description   => 'How to use Process tickets...',
457            Location      => 'Straubing',
458            StartTime     => '2016-01-01 16:00:00',
459            EndTime       => '2016-01-01 17:00:00',
460            AllDay        => 0,
461            Recurring     => 1,                                           # for recurring (parent) appointments only
462        },
463        {
464            AppointmentID => 2,
465            ParentID      => 1,                                           # for recurred (child) appointments only
466            CalendarID    => 1,
467            UniqueID      => '20160101T180000-A78B57@localhost',
468            Title         => 'Webinar',
469            Description   => 'How to use Process tickets...',
470            Location      => 'Straubing',
471            StartTime     => '2016-01-02 16:00:00',
472            EndTime       => '2016-01-02 17:00:00',
473            TeamID        => [ 1 ],
474            ResourceID    => [ 1, 3 ],
475            AllDay        => 0,
476        },
477        {
478            AppointmentID                         => 3,
479            CalendarID                            => 1,
480            UniqueID                              => '20160101T180000-A78B57@localhost',
481            Title                                 => 'Webinar',
482            Description                           => 'How to use Process tickets...',
483            Location                              => 'Straubing',
484            StartTime                             => '2016-01-02 16:00:00',
485            EndTime                               => '2016-01-02 17:00:00',
486            TimezoneID                            => 1,
487            TeamID                                => [ 1 ],
488            ResourceID                            => [ 1, 3 ],
489            NotificationDate                      => '2016-01-02 16:10:00',
490            NotificationTemplate                  => 'Custom',
491            NotificationCustom                    => 'relative',
492            NotificationCustomRelativeUnitCount   => '10',
493            NotificationCustomRelativeUnit        => 'minutes',
494            NotificationCustomRelativePointOfTime => 'afterstart',
495            NotificationCustomDateTime            => '2016-01-02 16:00:00',
496            TicketAppointmentRuleID               => '9bb20ea035e7a9930652a9d82d00c725',    # for ticket appointments only!
497        },
498        ...
499    ];
500
501Result => 'ARRAY':
502
503    @Appointments = [ 1, 2, ... ]
504
505=cut
506
507sub AppointmentList {
508    my ( $Self, %Param ) = @_;
509
510    # check needed stuff
511    for my $Needed (qw(CalendarID)) {
512        if ( !$Param{$Needed} ) {
513            $Kernel::OM->Get('Kernel::System::Log')->Log(
514                Priority => 'error',
515                Message  => "Need $Needed!",
516            );
517            return;
518        }
519    }
520
521    # output array of hashes by default
522    $Param{Result} = $Param{Result} || 'HASH';
523
524    # cache keys
525    my $CacheType        = $Self->{CacheType} . 'List' . $Param{CalendarID};
526    my $CacheKeyTitle    = $Param{Title} || 'any';
527    my $CacheKeyDesc     = $Param{Description} || 'any';
528    my $CacheKeyLocation = $Param{Location} || 'any';
529    my $CacheKeyStart    = $Param{StartTime} || 'any';
530    my $CacheKeyEnd      = $Param{EndTime} || 'any';
531    my $CacheKeyTeam     = $Param{TeamID} || 'any';
532    my $CacheKeyResource = $Param{ResourceID} || 'any';
533
534    if ( defined $Param{Title} && $Param{Title} =~ /^[\*]+$/ ) {
535        $CacheKeyTitle = 'any';
536    }
537    if ( defined $Param{Description} && $Param{Description} =~ /^[\*]+$/ ) {
538        $CacheKeyDesc = 'any';
539    }
540    if ( defined $Param{Location} && $Param{Location} =~ /^[\*]+$/ ) {
541        $CacheKeyLocation = 'any';
542    }
543
544    my $CacheKey
545        = "$CacheKeyTitle-$CacheKeyDesc-$CacheKeyLocation-$CacheKeyStart-$CacheKeyEnd-$CacheKeyTeam-$CacheKeyResource-$Param{Result}";
546
547    # check cache
548    my $Data = $Kernel::OM->Get('Kernel::System::Cache')->Get(
549        Type => $CacheType,
550        Key  => $CacheKey,
551    );
552
553    if ( ref $Data eq 'ARRAY' ) {
554        return @{$Data};
555    }
556
557    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
558
559    # check time
560    if ( $Param{StartTime} ) {
561        my $StartTimeObject = $Kernel::OM->Create(
562            'Kernel::System::DateTime',
563            ObjectParams => {
564                String => $Param{StartTime},
565            },
566        );
567        if ( !$StartTimeObject ) {
568            $Kernel::OM->Get('Kernel::System::Log')->Log(
569                Priority => 'error',
570                Message  => "StartTime invalid!",
571            );
572            return;
573        }
574    }
575    if ( $Param{EndTime} ) {
576        my $EndTimeObject = $Kernel::OM->Create(
577            'Kernel::System::DateTime',
578            ObjectParams => {
579                String => $Param{EndTime},
580            },
581        );
582        if ( !$EndTimeObject ) {
583            $Kernel::OM->Get('Kernel::System::Log')->Log(
584                Priority => 'error',
585                Message  => "EndTime invalid!",
586            );
587            return;
588        }
589    }
590
591    my $SQL = '
592        SELECT id, parent_id, calendar_id, unique_id, title, description, location, start_time,
593            end_time, team_id, resource_id, all_day, recurring, notify_time, notify_template,
594            notify_custom, notify_custom_unit_count, notify_custom_unit, notify_custom_unit_point,
595            notify_custom_date, ticket_appointment_rule_id
596        FROM calendar_appointment
597        WHERE calendar_id=?
598    ';
599
600    my @Bind;
601
602    push @Bind, \$Param{CalendarID};
603
604    # Filter title, description and location fields by using QueryCondition method, which will
605    #   return backend specific SQL statements in order to provide case insensitive match and
606    #   wildcard support.
607    FILTER:
608    for my $Filter (qw(Title Description Location)) {
609        next FILTER if !$Param{$Filter};
610        $SQL .= ' AND ' . $DBObject->QueryCondition(
611            Key          => lc $Filter,
612            Value        => $Param{$Filter},
613            SearchPrefix => '*',
614            SearchSuffix => '*',
615        );
616    }
617
618    if ( $Param{StartTime} && $Param{EndTime} ) {
619
620        $SQL .= ' AND (
621            (start_time >= ? AND start_time < ?) OR
622            (end_time > ? AND end_time <= ?) OR
623            (start_time <= ? AND end_time >= ?)
624        ) ';
625        push @Bind, \$Param{StartTime}, \$Param{EndTime}, \$Param{StartTime}, \$Param{EndTime}, \$Param{StartTime},
626            \$Param{EndTime};
627    }
628    elsif ( $Param{StartTime} && !$Param{EndTime} ) {
629
630        $SQL .= ' AND end_time >= ? ';
631        push @Bind, \$Param{StartTime};
632    }
633    elsif ( !$Param{StartTime} && $Param{EndTime} ) {
634
635        $SQL .= ' AND start_time <= ? ';
636        push @Bind, \$Param{EndTime};
637    }
638
639    $SQL .= ' ORDER BY id ASC';
640
641    # db query
642    return if !$DBObject->Prepare(
643        SQL  => $SQL,
644        Bind => \@Bind,
645    );
646
647    my @Result;
648
649    ROW:
650    while ( my @Row = $DBObject->FetchrowArray() ) {
651
652        # team id
653        my @TeamID = split( ',', $Row[9] // '' );
654        if ( $Param{TeamID} ) {
655            next ROW if !grep { $_ == $Param{TeamID} } @TeamID;
656        }
657
658        # resource id
659        $Row[10] = $Row[10] ? $Row[10] : 0;
660        my @ResourceID = $Row[10] =~ /,/ ? split( ',', $Row[10] ) : ( $Row[10] );
661        if ( $Param{ResourceID} ) {
662            next ROW if !grep { $_ == $Param{ResourceID} } @ResourceID;
663        }
664
665        my %Appointment = (
666            AppointmentID                         => $Row[0],
667            ParentID                              => $Row[1],
668            CalendarID                            => $Row[2],
669            UniqueID                              => $Row[3],
670            Title                                 => $Row[4],
671            Description                           => $Row[5],
672            Location                              => $Row[6],
673            StartTime                             => $Row[7],
674            EndTime                               => $Row[8],
675            TeamID                                => \@TeamID,
676            ResourceID                            => \@ResourceID,
677            AllDay                                => $Row[11],
678            Recurring                             => $Row[12],
679            NotificationDate                      => $Row[13] || '',
680            NotificationTemplate                  => $Row[14],
681            NotificationCustom                    => $Row[15],
682            NotificationCustomRelativeUnitCount   => $Row[16],
683            NotificationCustomRelativeUnit        => $Row[17],
684            NotificationCustomRelativePointOfTime => $Row[18],
685            NotificationCustomDateTime            => $Row[19] || '',
686            TicketAppointmentRuleID               => $Row[20],
687        );
688        push @Result, \%Appointment;
689    }
690
691    # if Result was ARRAY, output only IDs
692    if ( $Param{Result} eq 'ARRAY' ) {
693        my @ResultList;
694        for my $Appointment (@Result) {
695            push @ResultList, $Appointment->{AppointmentID};
696        }
697        @Result = @ResultList;
698    }
699
700    # cache
701    $Kernel::OM->Get('Kernel::System::Cache')->Set(
702        Type  => $CacheType,
703        Key   => $CacheKey,
704        Value => \@Result,
705        TTL   => $Self->{CacheTTL},
706    );
707
708    return @Result;
709}
710
711=head2 AppointmentDays()
712
713get a hash of days with Appointments in all user calendars.
714
715    my %AppointmentDays = $AppointmentObject->AppointmentDays(
716        StartTime           => '2016-01-01 00:00:00',                   # (optional) Filter by start date
717        EndTime             => '2016-02-01 00:00:00',                   # (optional) Filter by end date
718        UserID              => 1,                                       # (required) Valid UserID
719    );
720
721returns a hash with days as keys and number of Appointments as values:
722
723    %AppointmentDays = {
724        '2016-01-01' => 1,
725        '2016-01-13' => 2,
726        '2016-01-30' => 1,
727    };
728
729=cut
730
731sub AppointmentDays {
732    my ( $Self, %Param ) = @_;
733
734    # check needed stuff
735    for my $Needed (qw(UserID)) {
736        if ( !$Param{$Needed} ) {
737            $Kernel::OM->Get('Kernel::System::Log')->Log(
738                Priority => 'error',
739                Message  => "Need $Needed!",
740            );
741            return;
742        }
743    }
744
745    # cache keys
746    my $CacheType     = $Self->{CacheType} . 'Days' . $Param{UserID};
747    my $CacheKeyStart = $Param{StartTime} || 'any';
748    my $CacheKeyEnd   = $Param{EndTime} || 'any';
749
750    # check time
751    if ( $Param{StartTime} ) {
752        my $StartTimeObject = $Kernel::OM->Create(
753            'Kernel::System::DateTime',
754            ObjectParams => {
755                String => $Param{StartTime},
756            },
757        );
758        if ( !$StartTimeObject ) {
759            $Kernel::OM->Get('Kernel::System::Log')->Log(
760                Priority => 'error',
761                Message  => "StartTime invalid!",
762            );
763            return;
764        }
765    }
766    if ( $Param{EndTime} ) {
767        my $EndTimeObject = $Kernel::OM->Create(
768            'Kernel::System::DateTime',
769            ObjectParams => {
770                String => $Param{EndTime},
771            },
772        );
773        if ( !$EndTimeObject ) {
774            $Kernel::OM->Get('Kernel::System::Log')->Log(
775                Priority => 'error',
776                Message  => "EndTime invalid!",
777            );
778            return;
779        }
780    }
781
782    my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache');
783
784    # check cache
785    my $Data = $CacheObject->Get(
786        Type => $CacheType,
787        Key  => "$CacheKeyStart-$CacheKeyEnd",
788    );
789
790    if ( ref $Data eq 'HASH' ) {
791        return %{$Data};
792    }
793
794    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
795
796    # get user groups
797    my %GroupList = $Kernel::OM->Get('Kernel::System::Group')->PermissionUserGet(
798        UserID => $Param{UserID},
799        Type   => 'ro',
800    );
801    my @GroupIDs = sort keys %GroupList;
802
803    my $SQL = "
804        SELECT ca.start_time, ca.end_time
805        FROM calendar_appointment ca
806        JOIN calendar c ON ca.calendar_id = c.id
807        WHERE c.group_id IN ( ${\(join ', ', @GroupIDs)} )
808    ";
809
810    my @Bind;
811
812    if ( $Param{StartTime} && $Param{EndTime} ) {
813
814        $SQL .= 'AND (
815            (ca.start_time >= ? AND ca.start_time < ?) OR
816            (ca.end_time > ? AND ca.end_time <= ?) OR
817            (ca.start_time <= ? AND ca.end_time >= ?)
818        ) ';
819        push @Bind, \$Param{StartTime}, \$Param{EndTime}, \$Param{StartTime}, \$Param{EndTime}, \$Param{StartTime},
820            \$Param{EndTime};
821    }
822    elsif ( $Param{StartTime} && !$Param{EndTime} ) {
823
824        $SQL .= 'AND ca.end_time >= ? ';
825        push @Bind, \$Param{StartTime};
826    }
827    elsif ( !$Param{StartTime} && $Param{EndTime} ) {
828
829        $SQL .= 'AND ca.start_time <= ? ';
830        push @Bind, \$Param{EndTime};
831    }
832
833    $SQL .= 'ORDER BY ca.id ASC';
834
835    # db query
836    return if !$DBObject->Prepare(
837        SQL  => $SQL,
838        Bind => \@Bind,
839    );
840
841    my %Result;
842
843    while ( my @Row = $DBObject->FetchrowArray() ) {
844
845        my ( $StartTime, $EndTime, $StartTimeSystem, $EndTimeSystem );
846
847        # StartTime
848        if ( $Param{StartTime} ) {
849            $StartTime = $Row[0] lt $Param{StartTime} ? $Param{StartTime} : $Row[0];
850        }
851        else {
852            $StartTime = $Row[0];
853        }
854
855        # EndTime
856        if ( $Param{EndTime} ) {
857            $EndTime = $Row[1] gt $Param{EndTime} ? $Param{EndTime} : $Row[1];
858        }
859        else {
860            $EndTime = $Row[1];
861        }
862
863        # Get system times.
864        my $StartTimeObject = $Kernel::OM->Create(
865            'Kernel::System::DateTime',
866            ObjectParams => {
867                String => $StartTime,
868            },
869        );
870        my $EndTimeObject = $Kernel::OM->Create(
871            'Kernel::System::DateTime',
872            ObjectParams => {
873                String => $EndTime,
874            },
875        );
876
877        for (
878            my $LoopTimeObject = $StartTimeObject->Clone();
879            $LoopTimeObject < $EndTimeObject;
880            $LoopTimeObject->Add( Days => 1 )
881            )
882        {
883            my $LoopTime = $LoopTimeObject->ToString();
884
885            $LoopTime =~ s/\s.*?$//gsm;
886
887            if ( $Result{$LoopTime} ) {
888                $Result{$LoopTime}++;
889            }
890            else {
891                $Result{$LoopTime} = 1;
892            }
893        }
894    }
895
896    # cache
897    $CacheObject->Set(
898        Type  => $CacheType,
899        Key   => "$CacheKeyStart-$CacheKeyEnd",
900        Value => \%Result,
901        TTL   => $Self->{CacheTTL},
902    );
903
904    return %Result;
905}
906
907=head2 AppointmentGet()
908
909Get appointment data.
910
911    my %Appointment = $AppointmentObject->AppointmentGet(
912        AppointmentID => 1,                                  # (required)
913                                                             # or
914        UniqueID      => '20160101T160000-71E386@localhost', # (required) will return only parent for recurring appointments
915        CalendarID    => 1,                                  # (required)
916    );
917
918Returns a hash:
919
920    %Appointment = (
921        AppointmentID       => 2,
922        ParentID            => 1,                                  # only for recurred (child) appointments
923        CalendarID          => 1,
924        UniqueID            => '20160101T160000-71E386@localhost',
925        Title               => 'Webinar',
926        Description         => 'How to use Process tickets...',
927        Location            => 'Straubing',
928        StartTime           => '2016-01-01 16:00:00',
929        EndTime             => '2016-01-01 17:00:00',
930        AllDay              => 0,
931        TeamID              => [ 1 ],
932        ResourceID          => [ 1, 3 ],
933        Recurring           => 1,
934        RecurrenceType      => 'Daily',
935        RecurrenceFrequency => 1,
936        RecurrenceCount     => 1,
937        RecurrenceInterval  => 2,
938        RecurrenceUntil     => '2016-01-10 00:00:00',
939        RecurrenceID        => '2016-01-10 00:00:00',
940        RecurrenceExclude   => [
941            '2016-01-10 00:00:00',
942            '2016-01-11 00:00:00',
943        ],
944        NotificationTime                  => '2016-01-01 17:0:00',
945        NotificationTemplate              => 'Custom',
946        NotificationCustomUnitCount       => '12',
947        NotificationCustomUnit            => 'minutes',
948        NotificationCustomUnitPointOfTime => 'beforestart',
949
950        TicketAppointmentRuleID => '9bb20ea035e7a9930652a9d82d00c725',  # for ticket appointments only!
951        CreateTime              => '2016-01-01 00:00:00',
952        CreateBy                => 2,
953        ChangeTime              => '2016-01-01 00:00:00',
954        ChangeBy                => 2,
955    );
956
957=cut
958
959sub AppointmentGet {
960    my ( $Self, %Param ) = @_;
961
962    # check needed stuff
963    if (
964        !$Param{AppointmentID}
965        && !( $Param{UniqueID} && $Param{CalendarID} )
966        )
967    {
968        $Kernel::OM->Get('Kernel::System::Log')->Log(
969            Priority => 'error',
970            Message  => "Need AppointmentID or UniqueID and CalendarID!",
971        );
972        return;
973    }
974
975    my $Data;
976
977    my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache');
978
979    if ( $Param{AppointmentID} ) {
980
981        # check cache
982        $Data = $CacheObject->Get(
983            Type => $Self->{CacheType},
984            Key  => $Param{AppointmentID},
985        );
986    }
987
988    if ( ref $Data eq 'HASH' ) {
989        return %{$Data};
990    }
991
992    # needed objects
993    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
994
995    my @Bind;
996    my $SQL = '
997        SELECT id, parent_id, calendar_id, unique_id, title, description, location, start_time,
998            end_time, all_day, team_id, resource_id, recurring, recur_type, recur_freq, recur_count,
999            recur_interval, recur_until, recur_id, recur_exclude, notify_time, notify_template,
1000            notify_custom, notify_custom_unit_count, notify_custom_unit, notify_custom_unit_point,
1001            notify_custom_date, ticket_appointment_rule_id, create_time, create_by, change_time,
1002            change_by
1003        FROM calendar_appointment
1004        WHERE
1005    ';
1006
1007    if ( $Param{AppointmentID} ) {
1008        $SQL .= 'id=? ';
1009        push @Bind, \$Param{AppointmentID};
1010    }
1011    else {
1012        $SQL .= 'unique_id=? AND calendar_id=? AND parent_id IS NULL ';
1013        push @Bind, \$Param{UniqueID}, \$Param{CalendarID};
1014    }
1015
1016    # db query
1017    return if !$DBObject->Prepare(
1018        SQL   => $SQL,
1019        Bind  => \@Bind,
1020        Limit => 1,
1021    );
1022
1023    my %Result;
1024
1025    while ( my @Row = $DBObject->FetchrowArray() ) {
1026
1027        # team id
1028        my @TeamID = split( ',', $Row[10] // '' );
1029
1030        # resource id
1031        my @ResourceID = split( ',', $Row[11] // '0' );
1032
1033        # recurrence frequency
1034        my @RecurrenceFrequency = $Row[14] ? split( ',', $Row[14] ) : undef;
1035
1036        # recurrence exclude
1037        my @RecurrenceExclude = $Row[19] ? split( ',', $Row[19] ) : undef;
1038
1039        $Result{AppointmentID}                         = $Row[0];
1040        $Result{ParentID}                              = $Row[1];
1041        $Result{CalendarID}                            = $Row[2];
1042        $Result{UniqueID}                              = $Row[3];
1043        $Result{Title}                                 = $Row[4];
1044        $Result{Description}                           = $Row[5];
1045        $Result{Location}                              = $Row[6];
1046        $Result{StartTime}                             = $Row[7];
1047        $Result{EndTime}                               = $Row[8];
1048        $Result{AllDay}                                = $Row[9];
1049        $Result{TeamID}                                = \@TeamID;
1050        $Result{ResourceID}                            = \@ResourceID;
1051        $Result{Recurring}                             = $Row[12];
1052        $Result{RecurrenceType}                        = $Row[13];
1053        $Result{RecurrenceFrequency}                   = \@RecurrenceFrequency;
1054        $Result{RecurrenceCount}                       = $Row[15];
1055        $Result{RecurrenceInterval}                    = $Row[16];
1056        $Result{RecurrenceUntil}                       = $Row[17];
1057        $Result{RecurrenceID}                          = $Row[18];
1058        $Result{RecurrenceExclude}                     = \@RecurrenceExclude;
1059        $Result{NotificationDate}                      = $Row[20] || '';
1060        $Result{NotificationTemplate}                  = $Row[21] || '';
1061        $Result{NotificationCustom}                    = $Row[22] || '';
1062        $Result{NotificationCustomRelativeUnitCount}   = $Row[23] || 0;
1063        $Result{NotificationCustomRelativeUnit}        = $Row[24] || '';
1064        $Result{NotificationCustomRelativePointOfTime} = $Row[25] || '';
1065        $Result{NotificationCustomDateTime}            = $Row[26] || '';
1066        $Result{TicketAppointmentRuleID}               = $Row[27];
1067        $Result{CreateTime}                            = $Row[28];
1068        $Result{CreateBy}                              = $Row[29];
1069        $Result{ChangeTime}                            = $Row[30];
1070        $Result{ChangeBy}                              = $Row[31];
1071    }
1072
1073    if ( $Param{AppointmentID} ) {
1074
1075        # cache
1076        $CacheObject->Set(
1077            Type  => $Self->{CacheType},
1078            Key   => $Param{AppointmentID},
1079            Value => \%Result,
1080            TTL   => $Self->{CacheTTL},
1081        );
1082    }
1083
1084    return %Result;
1085}
1086
1087=head2 AppointmentUpdate()
1088
1089updates an existing appointment.
1090
1091    my $Success = $AppointmentObject->AppointmentUpdate(
1092        AppointmentID         => 2,                                       # (required)
1093        CalendarID            => 1,                                       # (required) Valid CalendarID
1094        Title                 => 'Webinar',                               # (required) Title
1095        Description           => 'How to use Process tickets...',         # (optional) Description
1096        Location              => 'Straubing',                             # (optional) Location
1097        StartTime             => '2016-01-01 16:00:00',                   # (required)
1098        EndTime               => '2016-01-01 17:00:00',                   # (required)
1099        AllDay                => 0,                                       # (optional) Default 0
1100        Team                  => 1,                                       # (optional)
1101        ResourceID            => [ 1, 3 ],                                # (optional) must be an array reference if supplied
1102        Recurring             => 1,                                       # (optional) flag the appointment as recurring (parent only!)
1103
1104        RecurrenceType        => 'Daily',                                 # (required if Recurring) Possible "Daily", "Weekly", "Monthly", "Yearly",
1105                                                                          #           "CustomWeekly", "CustomMonthly", "CustomYearly"
1106
1107        RecurrenceFrequency   => 1,                                       # (required if Custom Recurring) Recurrence pattern
1108                                                                          #           for CustomWeekly: 1-Mon, 2-Tue,..., 7-Sun
1109                                                                          #           for CustomMonthly: 1-Jan, 2-Feb,..., 12-Dec
1110                                                                          # ...
1111        RecurrenceCount       => 1,                                       # (optional) How many Appointments to create
1112        RecurrenceInterval    => 2,                                       # (optional) Repeating interval (default 1)
1113        RecurrenceUntil       => '2016-01-10 00:00:00',                   # (optional) Until date
1114        NotificationTime      => '2016-01-01 17:00:00',                   # (optional) Point of time to execute the notification event
1115        NotificationTemplate  => 'Custom',                                # (optional) Template to be used for notification point of time
1116        NotificationCustom    => 'relative',                              # (optional) Type of the custom template notification point of time
1117                                                                          #            Possible "relative", "datetime"
1118        NotificationCustomRelativeUnitCount   => '12',                    # (optional) minutes, hours or days count for custom template
1119        NotificationCustomRelativeUnit        => 'minutes',               # (optional) minutes, hours or days unit for custom template
1120        NotificationCustomRelativePointOfTime => 'beforestart',           # (optional) Point of execute for custom templates
1121                                                                          #            Possible "beforestart", "afterstart", "beforeend", "afterend"
1122        NotificationCustomDateTime => '2016-01-01 17:00:00',              # (optional) Notification date time for custom template
1123        TicketAppointmentRuleID    => '9bb20ea035e7a9930652a9d82d00c725', # (optional) Ticket appointment rule ID (for ticket appointments only!)
1124        UserID                     => 1,                                  # (required) UserID
1125    );
1126
1127returns 1 if successful:
1128    $Success = 1;
1129
1130Events:
1131    AppointmentUpdate
1132
1133=cut
1134
1135sub AppointmentUpdate {
1136    my ( $Self, %Param ) = @_;
1137
1138    # check needed stuff
1139    for my $Needed (qw(AppointmentID CalendarID Title StartTime EndTime UserID)) {
1140        if ( !$Param{$Needed} ) {
1141            $Kernel::OM->Get('Kernel::System::Log')->Log(
1142                Priority => 'error',
1143                Message  => "Need $Needed!",
1144            );
1145            return;
1146        }
1147    }
1148
1149    # prepare possible notification params
1150    my $Success = $Self->_AppointmentNotificationPrepare(
1151        Data => \%Param,
1152    );
1153
1154    # if Recurring is provided, additional parameters must be present
1155    if ( $Param{Recurring} ) {
1156
1157        my @RecurrenceTypes = (
1158            "Daily",       "Weekly",       "Monthly",       "Yearly",
1159            "CustomDaily", "CustomWeekly", "CustomMonthly", "CustomYearly"
1160        );
1161
1162        if (
1163            !$Param{RecurrenceType}
1164            || !grep { $_ eq $Param{RecurrenceType} } @RecurrenceTypes
1165            )
1166        {
1167            $Kernel::OM->Get('Kernel::System::Log')->Log(
1168                Priority => 'error',
1169                Message  => "RecurrenceType invalid!",
1170            );
1171            return;
1172        }
1173
1174        if (
1175            (
1176                $Param{RecurrenceType} eq 'CustomWeekly'
1177                || $Param{RecurrenceType} eq 'CustomMonthly'
1178                || $Param{RecurrenceType} eq 'CustomYearly'
1179            )
1180            && !$Param{RecurrenceFrequency}
1181            )
1182        {
1183            $Kernel::OM->Get('Kernel::System::Log')->Log(
1184                Priority => 'error',
1185                Message  => "RecurrenceFrequency needed!",
1186            );
1187            return;
1188        }
1189    }
1190
1191    $Param{RecurrenceInterval} ||= 1;
1192
1193    # Check StartTime.
1194    my $StartTimeObject = $Kernel::OM->Create(
1195        'Kernel::System::DateTime',
1196        ObjectParams => {
1197            String => $Param{StartTime},
1198        },
1199    );
1200    if ( !$StartTimeObject ) {
1201        $Kernel::OM->Get('Kernel::System::Log')->Log(
1202            Priority => 'error',
1203            Message  => "StartTime invalid!",
1204        );
1205        return;
1206    }
1207
1208    # Check EndTime.
1209    my $EndTimeObject = $Kernel::OM->Create(
1210        'Kernel::System::DateTime',
1211        ObjectParams => {
1212            String => $Param{EndTime},
1213        },
1214    );
1215    if ( !$EndTimeObject ) {
1216        $Kernel::OM->Get('Kernel::System::Log')->Log(
1217            Priority => 'error',
1218            Message  => "EndTime invalid!",
1219        );
1220        return;
1221    }
1222
1223    # needed objects
1224    my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache');
1225
1226    # check if array refs
1227    my %Arrays;
1228    for my $Parameter (
1229        qw(TeamID ResourceID RecurrenceFrequency)
1230        )
1231    {
1232        if ( $Param{$Parameter} && @{ $Param{$Parameter} // [] } ) {
1233            if ( !IsArrayRefWithData( $Param{$Parameter} ) ) {
1234                $Kernel::OM->Get('Kernel::System::Log')->Log(
1235                    Priority => 'error',
1236                    Message  => "$Parameter not ARRAYREF!",
1237                );
1238                return;
1239            }
1240
1241            my @Array = @{ $Param{$Parameter} };
1242
1243            # remove undefined values
1244            @Array = grep { defined $_ } @Array;
1245
1246            $Arrays{$Parameter} = join( ',', @Array ) if @Array;
1247        }
1248    }
1249
1250    # check if numbers
1251    for my $Parameter (
1252        qw(Recurring RecurrenceCount RecurrenceInterval)
1253        )
1254    {
1255        if ( $Param{$Parameter} && !IsInteger( $Param{$Parameter} ) ) {
1256            $Kernel::OM->Get('Kernel::System::Log')->Log(
1257                Priority => 'error',
1258                Message  => "$Parameter must be a number!",
1259            );
1260            return;
1261        }
1262    }
1263
1264    # check RecurrenceUntil
1265    if ( $Param{RecurrenceUntil} ) {
1266
1267        # Usually hour, minute and second = 0. In this case, take time from StartTime.
1268        $Param{RecurrenceUntil} = $Self->_TimeCheck(
1269            OriginalTime => $Param{StartTime},
1270            Time         => $Param{RecurrenceUntil},
1271        );
1272
1273        my $RecurrenceUntilObject = $Kernel::OM->Create(
1274            'Kernel::System::DateTime',
1275            ObjectParams => {
1276                String => $Param{RecurrenceUntil},
1277            },
1278        );
1279
1280        if (
1281            !$RecurrenceUntilObject
1282            || $StartTimeObject > $RecurrenceUntilObject
1283            )
1284        {
1285            $Kernel::OM->Get('Kernel::System::Log')->Log(
1286                Priority => 'error',
1287                Message  => "RecurrenceUntil invalid!",
1288            );
1289            return;
1290        }
1291    }
1292
1293    # get previous CalendarID
1294    my $PreviousCalendarID = $Self->_AppointmentGetCalendarID(
1295        AppointmentID => $Param{AppointmentID},
1296    );
1297
1298    # set recurrence exclude list
1299    my @RecurrenceExclude = @{ $Param{RecurrenceExclude} // [] };
1300
1301    # get RecurrenceID
1302    my $RecurrenceID = $Self->_AppointmentGetRecurrenceID(
1303        AppointmentID => $Param{AppointmentID},
1304    );
1305
1306    # use exclude list to flag the recurring occurrence as updated
1307    if ($RecurrenceID) {
1308        @RecurrenceExclude = ($RecurrenceID);
1309    }
1310
1311    # reset exclude list if recurrence is turned off
1312    elsif ( !$Param{Recurring} ) {
1313        @RecurrenceExclude = ();
1314    }
1315
1316    # remove undefined values
1317    @RecurrenceExclude = grep { defined $_ } @RecurrenceExclude;
1318
1319    # serialize data
1320    my $RecurrenceExclude = join( ',', @RecurrenceExclude ) || undef;
1321
1322    # delete existing recurred appointments
1323    my $DeleteSuccess = $Self->_AppointmentRecurringDelete(
1324        ParentID => $Param{AppointmentID},
1325    );
1326
1327    if ( !$DeleteSuccess ) {
1328        $Kernel::OM->Get('Kernel::System::Log')->Log(
1329            Priority => 'error',
1330            Message  => "Unable to delete recurring Appointment!",
1331        );
1332        return;
1333    }
1334
1335    # update appointment
1336    my $SQL = '
1337        UPDATE calendar_appointment
1338        SET
1339            calendar_id=?, title=?, description=?, location=?, start_time=?, end_time=?, all_day=?,
1340            team_id=?, resource_id=?, recurring=?, recur_type=?, recur_freq=?, recur_count=?,
1341            recur_interval=?, recur_until=?, recur_exclude=?, notify_time=?, notify_template=?,
1342            notify_custom=?, notify_custom_unit_count=?, notify_custom_unit=?,
1343            notify_custom_unit_point=?, notify_custom_date=?, ticket_appointment_rule_id=?,
1344            change_time=current_timestamp, change_by=?
1345        WHERE id=?
1346    ';
1347
1348    # update db record
1349    return if !$Kernel::OM->Get('Kernel::System::DB')->Do(
1350        SQL  => $SQL,
1351        Bind => [
1352            \$Param{CalendarID}, \$Param{Title},   \$Param{Description}, \$Param{Location},
1353            \$Param{StartTime},  \$Param{EndTime}, \$Param{AllDay},      \$Arrays{TeamID},
1354            \$Arrays{ResourceID},          \$Param{Recurring},       \$Param{RecurrenceType},
1355            \$Arrays{RecurrenceFrequency}, \$Param{RecurrenceCount}, \$Param{RecurrenceInterval},
1356            \$Param{RecurrenceUntil}, \$RecurrenceExclude, \$Param{NotificationDate},
1357            \$Param{NotificationTemplate},                  \$Param{NotificationCustom},
1358            \$Param{NotificationCustomRelativeUnitCount},   \$Param{NotificationCustomRelativeUnit},
1359            \$Param{NotificationCustomRelativePointOfTime}, \$Param{NotificationCustomDateTime},
1360            \$Param{TicketAppointmentRuleID},               \$Param{UserID}, \$Param{AppointmentID},
1361        ],
1362    );
1363
1364    # add recurred appointments again
1365    if ( $Param{Recurring} ) {
1366        return if !$Self->_AppointmentRecurringCreate(
1367            ParentID    => $Param{AppointmentID},
1368            Appointment => \%Param,
1369        );
1370    }
1371
1372    # delete cache
1373    $CacheObject->Delete(
1374        Type => $Self->{CacheType},
1375        Key  => $Param{AppointmentID},
1376    );
1377
1378    # clean up list methods cache
1379    my @CalendarIDs = ( $Param{CalendarID} );
1380    push @CalendarIDs, $PreviousCalendarID if $PreviousCalendarID ne $Param{CalendarID};
1381    for my $CalendarID (@CalendarIDs) {
1382        $CacheObject->CleanUp(
1383            Type => $Self->{CacheType} . 'List' . $CalendarID,
1384        );
1385    }
1386    $CacheObject->CleanUp(
1387        Type => $Self->{CacheType} . 'Days' . $Param{UserID},
1388    );
1389
1390    # fire event
1391    $Self->EventHandler(
1392        Event => 'AppointmentUpdate',
1393        Data  => {
1394            AppointmentID => $Param{AppointmentID},
1395            CalendarID    => $Param{CalendarID},
1396        },
1397        UserID => $Param{UserID},
1398    );
1399
1400    return 1;
1401}
1402
1403=head2 AppointmentDelete()
1404
1405deletes an existing appointment.
1406
1407    my $Success = $AppointmentObject->AppointmentDelete(
1408        AppointmentID   => 1,                              # (required)
1409        UserID          => 1,                              # (required)
1410    );
1411
1412returns 1 if successful:
1413    $Success = 1;
1414
1415Events:
1416    AppointmentDelete
1417
1418=cut
1419
1420sub AppointmentDelete {
1421    my ( $Self, %Param ) = @_;
1422
1423    # check needed stuff
1424    for my $Needed (qw(AppointmentID UserID)) {
1425        if ( !$Param{$Needed} ) {
1426            $Kernel::OM->Get('Kernel::System::Log')->Log(
1427                Priority => 'error',
1428                Message  => "Need $Needed!",
1429            );
1430            return;
1431        }
1432    }
1433
1434    # needed objects
1435    my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache');
1436
1437    # get CalendarID
1438    my $CalendarID = $Self->_AppointmentGetCalendarID(
1439        AppointmentID => $Param{AppointmentID},
1440    );
1441
1442    # check user's permissions for this calendar
1443    my $Permission = $Kernel::OM->Get('Kernel::System::Calendar')->CalendarPermissionGet(
1444        CalendarID => $CalendarID,
1445        UserID     => $Param{UserID},
1446    );
1447
1448    my @RequiredPermissions = ( 'create', 'rw' );
1449
1450    if ( !grep { $Permission eq $_ } @RequiredPermissions ) {
1451        $Kernel::OM->Get('Kernel::System::Log')->Log(
1452            Priority => 'error',
1453            Message  => "User($Param{UserID}) has no permission to delete Appointment($Param{AppointmentID})!",
1454        );
1455        return;
1456    }
1457
1458    my %Appointment = $Self->AppointmentGet(
1459        AppointmentID => $Param{AppointmentID},
1460    );
1461
1462    # save exclusion info to parent appointment
1463    if ( $Appointment{ParentID} && $Appointment{RecurrenceID} ) {
1464        $Self->_AppointmentRecurringExclude(
1465            ParentID     => $Appointment{ParentID},
1466            RecurrenceID => $Appointment{RecurrenceID},
1467        );
1468    }
1469
1470    # delete recurring appointments
1471    my $DeleteRecurringSuccess = $Self->_AppointmentRecurringDelete(
1472        ParentID => $Param{AppointmentID},
1473    );
1474
1475    if ( !$DeleteRecurringSuccess ) {
1476        $Kernel::OM->Get('Kernel::System::Log')->Log(
1477            Priority => 'error',
1478            Message  => 'Recurring appointments couldn\'t be deleted!',
1479        );
1480        return;
1481    }
1482
1483    # delete appointment
1484    my $SQL = '
1485        DELETE FROM calendar_appointment
1486        WHERE id=?
1487    ';
1488
1489    # delete db record
1490    return if !$Kernel::OM->Get('Kernel::System::DB')->Do(
1491        SQL  => $SQL,
1492        Bind => [
1493            \$Param{AppointmentID},
1494        ],
1495    );
1496
1497    # Fire event.
1498    $Self->EventHandler(
1499        Event => 'AppointmentDelete',
1500        Data  => {
1501            AppointmentID => $Param{AppointmentID},
1502            CalendarID    => $CalendarID,
1503        },
1504        UserID => $Param{UserID},
1505    );
1506
1507    # delete cache
1508    $CacheObject->Delete(
1509        Type => $Self->{CacheType},
1510        Key  => $Param{AppointmentID},
1511    );
1512
1513    # clean up list methods cache
1514    $CacheObject->CleanUp(
1515        Type => $Self->{CacheType} . 'List' . $CalendarID,
1516    );
1517    $CacheObject->CleanUp(
1518        Type => $Self->{CacheType} . 'Days' . $Param{UserID},
1519    );
1520
1521    return 1;
1522}
1523
1524=head2 AppointmentDeleteOccurrence()
1525
1526deletes a single recurring appointment occurrence.
1527
1528    my $Success = $AppointmentObject->AppointmentDeleteOccurrence(
1529        UniqueID     => '20160101T160000-71E386@localhost',    # (required)
1530        RecurrenceID => '2016-01-10 00:00:00',                 # (required)
1531        UserID       => 1,                                     # (required)
1532    );
1533
1534returns 1 if successful:
1535    $Success = 1;
1536
1537=cut
1538
1539sub AppointmentDeleteOccurrence {
1540    my ( $Self, %Param ) = @_;
1541
1542    # check needed stuff
1543    for my $Needed (qw(UniqueID CalendarID RecurrenceID UserID)) {
1544        if ( !$Param{$Needed} ) {
1545            $Kernel::OM->Get('Kernel::System::Log')->Log(
1546                Priority => 'error',
1547                Message  => "Need $Needed!",
1548            );
1549            return;
1550        }
1551    }
1552
1553    # get db object
1554    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
1555
1556    # db query
1557    return if !$DBObject->Prepare(
1558        SQL => '
1559            SELECT id FROM calendar_appointment
1560            WHERE unique_id=? AND calendar_id=? AND recur_id=?',
1561        Bind  => [ \$Param{UniqueID}, \$Param{CalendarID}, \$Param{RecurrenceID} ],
1562        Limit => 1,
1563    );
1564
1565    my %Appointment;
1566
1567    # get additional info
1568    while ( my @Row = $DBObject->FetchrowArray() ) {
1569        $Appointment{AppointmentID} = $Row[0];
1570    }
1571    return if !%Appointment;
1572
1573    # delete db record
1574    return if !$DBObject->Do(
1575        SQL   => 'DELETE FROM calendar_appointment WHERE id=?',
1576        Bind  => [ \$Appointment{AppointmentID} ],
1577        Limit => 1,
1578    );
1579
1580    # get cache object
1581    my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache');
1582
1583    # delete cache
1584    $CacheObject->Delete(
1585        Type => $Self->{CacheType},
1586        Key  => $Appointment{AppointmentID},
1587    );
1588
1589    # clean up list methods cache
1590    $CacheObject->CleanUp(
1591        Type => $Self->{CacheType} . 'List' . $Param{CalendarID},
1592    );
1593    $CacheObject->CleanUp(
1594        Type => $Self->{CacheType} . 'Days' . $Param{UserID},
1595    );
1596
1597    return 1;
1598}
1599
1600=head2 GetUniqueID()
1601
1602Returns UniqueID containing appointment start time, random hash and system C<FQDN>.
1603
1604    my $UniqueID = $AppointmentObject->GetUniqueID(
1605        CalendarID => 1,                        # (required)
1606        StartTime  => '2016-01-01 00:00:00',    # (required)
1607        UserID     => 1,                        # (required)
1608    );
1609
1610    $UniqueID = '20160101T000000-B9909D@localhost';
1611
1612=cut
1613
1614sub GetUniqueID {
1615    my ( $Self, %Param ) = @_;
1616
1617    # check needed stuff
1618    for my $Needed (qw(CalendarID StartTime UserID)) {
1619        if ( !$Param{$Needed} ) {
1620            $Kernel::OM->Get('Kernel::System::Log')->Log(
1621                Priority => 'error',
1622                Message  => "Need $Needed!",
1623            );
1624            return;
1625        }
1626    }
1627
1628    # calculate a hash
1629    my $RandomString = $Kernel::OM->Get('Kernel::System::Main')->GenerateRandomString( Length => 32 );
1630    my $String       = "$Param{CalendarID}-$RandomString-$Param{UserID}";
1631    my $Digest       = unpack( 'N', Digest::MD5->new()->add($String)->digest() );
1632    my $DigestHex    = sprintf( '%x', $Digest );
1633    my $Hash         = uc( sprintf( "%.6s", $DigestHex ) );
1634
1635    # Prepare start timestamp for UniqueID.
1636    my $StartTimeObject = $Kernel::OM->Create(
1637        'Kernel::System::DateTime',
1638        ObjectParams => {
1639            String => $Param{StartTime},
1640        },
1641    );
1642    return if !$StartTimeObject;
1643    my $StartTimeStrg = $StartTimeObject->ToString();
1644    $StartTimeStrg =~ s/[-:]//g;
1645    $StartTimeStrg =~ s/\s/T/;
1646
1647    # get system FQDN
1648    my $FQDN = $Kernel::OM->Get('Kernel::Config')->Get('FQDN');
1649
1650    # return UniqueID
1651    return "$StartTimeStrg-$Hash\@$FQDN";
1652}
1653
1654=head2 AppointmentUpcomingGet()
1655
1656Get appointment data for upcoming appointment start or end.
1657
1658    my @UpcomingAppointments = $AppointmentObject->AppointmentUpcomingGet(
1659        Timestamp => '2016-08-02 03:59:00', # get appointments for the related notification timestamp
1660    );
1661
1662Returns appointment data of AppointmentGet().
1663
1664=cut
1665
1666sub AppointmentUpcomingGet {
1667    my ( $Self, %Param ) = @_;
1668
1669    # get current timestamp
1670    my $CurrentTimestamp;
1671
1672    # create needed sql query based on the current or a given timestamp
1673    my $SQL = 'SELECT id, parent_id, calendar_id, unique_id FROM calendar_appointment ';
1674
1675    if ( $Param{Timestamp} ) {
1676        $CurrentTimestamp = $Param{Timestamp};
1677        $SQL .= "WHERE notify_time = ? ";
1678    }
1679    else {
1680        $CurrentTimestamp = $Kernel::OM->Create('Kernel::System::DateTime')->ToString();
1681        $SQL .= "WHERE notify_time >= ? ";
1682    }
1683
1684    $SQL .= 'ORDER BY notify_time ASC';
1685
1686    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
1687
1688    # db query
1689    return if !$DBObject->Prepare(
1690        SQL  => $SQL,
1691        Bind => [ \$CurrentTimestamp ],
1692    );
1693
1694    my @ResultRaw;
1695
1696    while ( my @Row = $DBObject->FetchrowArray() ) {
1697
1698        my %UpcomingAppointment;
1699
1700        $UpcomingAppointment{AppointmentID} = $Row[0];
1701        $UpcomingAppointment{ParentID}      = $Row[1];
1702        $UpcomingAppointment{CalendarID}    = $Row[2];
1703        $UpcomingAppointment{UniqueID}      = $Row[3];
1704
1705        push @ResultRaw, \%UpcomingAppointment;
1706    }
1707
1708    my @Results;
1709
1710    APPOINTMENTDATA:
1711    for my $AppointmentData (@ResultRaw) {
1712
1713        next APPOINTMENTDATA if !IsHashRefWithData($AppointmentData);
1714        next APPOINTMENTDATA if !$AppointmentData->{CalendarID};
1715        next APPOINTMENTDATA if !$AppointmentData->{AppointmentID};
1716
1717        my %Appointment = $Self->AppointmentGet( %{$AppointmentData} );
1718
1719        push @Results, \%Appointment;
1720    }
1721
1722    return @Results;
1723}
1724
1725=head2 AppointmentFutureTasksDelete()
1726
1727Delete all calendar appointment future tasks.
1728
1729    my $Success = $AppointmentObject->AppointmentFutureTasksDelete();
1730
1731returns:
1732
1733    True if future task deletion was successful, otherwise false.
1734
1735=cut
1736
1737sub AppointmentFutureTasksDelete {
1738    my ( $Self, %Param ) = @_;
1739
1740    # get a local scheduler db object
1741    my $SchedulerObject = $Kernel::OM->Get('Kernel::System::Scheduler');
1742
1743    # get a list of already stored future tasks
1744    my @FutureTaskList = $SchedulerObject->FutureTaskList(
1745        Type => 'CalendarAppointment',
1746    );
1747
1748    # flush obsolete future tasks
1749    if ( IsArrayRefWithData( \@FutureTaskList ) ) {
1750
1751        FUTURETASK:
1752        for my $FutureTask (@FutureTaskList) {
1753
1754            next FUTURETASK if !$FutureTask;
1755            next FUTURETASK if !IsHashRefWithData($FutureTask);
1756
1757            my $Success = $SchedulerObject->FutureTaskDelete(
1758                TaskID => $FutureTask->{TaskID},
1759            );
1760
1761            if ( !$Success ) {
1762                $Kernel::OM->Get('Kernel::System::Log')->Log(
1763                    Priority => 'error',
1764                    Message  => "Could not delete future task with id $FutureTask->{TaskID}!",
1765                );
1766                return;
1767            }
1768        }
1769    }
1770
1771    return 1;
1772}
1773
1774=head2 AppointmentFutureTasksUpdate()
1775
1776Update OTRS daemon future task list for upcoming appointments.
1777
1778    my $Success = $AppointmentObject->AppointmentFutureTasksUpdate();
1779
1780returns:
1781
1782    True if future task update was successful, otherwise false.
1783
1784=cut
1785
1786sub AppointmentFutureTasksUpdate {
1787    my ( $Self, %Param ) = @_;
1788
1789    # get appointment data for upcoming appointments
1790    my @UpcomingAppointments = $Self->AppointmentUpcomingGet();
1791
1792    # check for no upcoming appointments
1793    if ( !IsArrayRefWithData( \@UpcomingAppointments ) ) {
1794
1795        # flush obsolete future tasks
1796        my $Success = $Self->AppointmentFutureTasksDelete();
1797
1798        if ( !$Success ) {
1799            $Kernel::OM->Get('Kernel::System::Log')->Log(
1800                Priority => 'error',
1801                Message  => 'Could not delete appointment future tasks!',
1802            );
1803            return;
1804        }
1805
1806        return 1;
1807    }
1808
1809    # get a local scheduler db object
1810    my $SchedulerObject = $Kernel::OM->Get('Kernel::System::Scheduler');
1811
1812    # get a list of already stored future tasks
1813    my @FutureTaskList = $SchedulerObject->FutureTaskList(
1814        Type => 'CalendarAppointment',
1815    );
1816
1817    # check for invalid task count (just one task max allowed)
1818    if ( scalar @FutureTaskList > 1 ) {
1819
1820        # flush obsolete future tasks
1821        my $Success = $Self->AppointmentFutureTasksDelete();
1822
1823        if ( !$Success ) {
1824            $Kernel::OM->Get('Kernel::System::Log')->Log(
1825                Priority => 'error',
1826                Message  => 'Could not delete appointment future tasks!',
1827            );
1828            return;
1829        }
1830    }
1831
1832    # check if it is needed to update the future task list
1833    if ( IsArrayRefWithData( \@FutureTaskList ) ) {
1834        my $UpdateNeeded = 0;
1835
1836        FUTURETASK:
1837        for my $FutureTask (@FutureTaskList) {
1838
1839            if (
1840                !IsHashRefWithData($FutureTask)
1841                || !$FutureTask->{TaskID}
1842                || !$FutureTask->{ExecutionTime}
1843                )
1844            {
1845                $UpdateNeeded = 1;
1846                last FUTURETASK;
1847            }
1848
1849            # get the stored future task
1850            my %FutureTaskData = $Kernel::OM->Get('Kernel::System::Daemon::SchedulerDB')->FutureTaskGet(
1851                TaskID => $FutureTask->{TaskID},
1852            );
1853
1854            if ( !IsHashRefWithData( \%FutureTaskData ) ) {
1855                $UpdateNeeded = 1;
1856                last FUTURETASK;
1857            }
1858
1859            # Get date time objects of stored and upcoming times to compare.
1860            my $FutureTaskTimeObject = $Kernel::OM->Create(
1861                'Kernel::System::DateTime',
1862                ObjectParams => {
1863                    String => $FutureTaskData{Data}->{NotifyTime},
1864                },
1865            );
1866            my $UpcomingAppointmentTimeObject = $Kernel::OM->Create(
1867                'Kernel::System::DateTime',
1868                ObjectParams => {
1869                    String => $UpcomingAppointments[0]->{NotificationDate},
1870                },
1871            );
1872
1873            # Do nothing if the upcoming notification time equals the stored value.
1874            if ( $UpcomingAppointmentTimeObject != $FutureTaskTimeObject ) {
1875                $UpdateNeeded = 1;
1876                last FUTURETASK;
1877            }
1878        }
1879
1880        if ($UpdateNeeded) {
1881
1882            # flush obsolete future tasks
1883            my $Success = $Self->AppointmentFutureTasksDelete();
1884
1885            if ( !$Success ) {
1886                $Kernel::OM->Get('Kernel::System::Log')->Log(
1887                    Priority => 'error',
1888                    Message  => 'Could not delete appointment future tasks!',
1889                );
1890                return;
1891            }
1892        }
1893        else {
1894            return 1;
1895        }
1896    }
1897
1898    # schedule new future tasks for notification actions
1899    my $TaskID = $SchedulerObject->TaskAdd(
1900        ExecutionTime => $UpcomingAppointments[0]->{NotificationDate},
1901        Name          => 'AppointmentNotification',
1902        Type          => 'CalendarAppointment',
1903        Data          => {
1904            NotifyTime => $UpcomingAppointments[0]->{NotificationDate},
1905        },
1906    );
1907
1908    if ( !$TaskID ) {
1909        $Kernel::OM->Get('Kernel::System::Log')->Log(
1910            Priority => 'error',
1911            Message =>
1912                "Could not schedule future task for AppointmentID $UpcomingAppointments[0]->{AppointmentID}!",
1913        );
1914        return;
1915    }
1916
1917    return 1;
1918}
1919
1920=head2 _AppointmentNotificationPrepare()
1921
1922Prepare appointment notification data.
1923
1924    my $Success = $AppointmentObject->_AppointmentNotificationPrepare();
1925
1926returns:
1927
1928    True if preparation was successful, otherwise false.
1929
1930=cut
1931
1932sub _AppointmentNotificationPrepare {
1933    my ( $Self, %Param ) = @_;
1934
1935    # check needed stuff
1936    for my $Needed (qw(Data)) {
1937        if ( !$Param{$Needed} ) {
1938            $Kernel::OM->Get('Kernel::System::Log')->Log(
1939                Priority => 'error',
1940                Message  => "Need $Needed!",
1941            );
1942            return;
1943        }
1944    }
1945
1946    # reset notification data if needed
1947    if ( !$Param{Data}->{NotificationTemplate} ) {
1948
1949        for my $PossibleParam (
1950            qw(
1951            NotificationDate NotificationTemplate NotificationCustom NotificationCustomRelativeUnitCount
1952            NotificationCustomRelativeUnit NotificationCustomRelativePointOfTime NotificationCustomDateTime
1953            )
1954            )
1955        {
1956            $Param{Data}->{$PossibleParam} = undef;
1957        }
1958    }
1959
1960    # prepare possible notification params
1961    for my $PossibleParam (
1962        qw(
1963        NotificationTemplate NotificationCustom NotificationCustomRelativeUnit
1964        NotificationCustomRelativePointOfTime
1965        )
1966        )
1967    {
1968        $Param{Data}->{$PossibleParam} ||= '';
1969    }
1970
1971    # special check for relative unit count as it can be zero
1972    # (empty and negative values will be treated as zero to avoid errors)
1973    if (
1974        !IsNumber( $Param{Data}->{NotificationCustomRelativeUnitCount} )
1975        || $Param{Data}->{NotificationCustomRelativeUnitCount} <= 0
1976        )
1977    {
1978        $Param{Data}->{NotificationCustomRelativeUnitCount} = 0;
1979    }
1980
1981    # set empty datetime strings to undef
1982    for my $PossibleParam (qw(NotificationDate NotificationCustomDateTime)) {
1983        $Param{Data}->{$PossibleParam} ||= undef;
1984    }
1985
1986    return if !$Param{Data}->{NotificationTemplate};
1987
1988    #
1989    # template Start
1990    #
1991    if ( $Param{Data}->{NotificationTemplate} eq 'Start' ) {
1992
1993        # setup the appointment start date as notification date
1994        $Param{Data}->{NotificationDate} = $Param{Data}->{StartTime};
1995    }
1996
1997    #
1998    # template time before start
1999    #
2000    elsif (
2001        $Param{Data}->{NotificationTemplate} ne 'Custom'
2002        && IsNumber( $Param{Data}->{NotificationTemplate} )
2003        && $Param{Data}->{NotificationTemplate} > 0
2004        )
2005    {
2006
2007        return if !IsNumber( $Param{Data}->{NotificationTemplate} );
2008
2009        # offset template (before start datetime) used
2010        my $Offset = $Param{Data}->{NotificationTemplate};
2011
2012        # Get date time object of appointment start time.
2013        my $StartTimeObject = $Kernel::OM->Create(
2014            'Kernel::System::DateTime',
2015            ObjectParams => {
2016                String => $Param{Data}->{StartTime},
2017            },
2018        );
2019
2020        # Subtract offset in seconds for new notification date time.
2021        $StartTimeObject->Subtract(
2022            Seconds => $Offset,
2023        );
2024
2025        $Param{Data}->{NotificationDate} = $StartTimeObject->ToString();
2026    }
2027
2028    #
2029    # template Custom
2030    #
2031    else {
2032
2033        # Compute date of custom relative input.
2034        if ( $Param{Data}->{NotificationCustom} eq 'relative' ) {
2035
2036            my $CustomUnitCount = $Param{Data}->{NotificationCustomRelativeUnitCount};
2037            my $CustomUnit      = $Param{Data}->{NotificationCustomRelativeUnit};
2038            my $CustomUnitPoint = $Param{Data}->{NotificationCustomRelativePointOfTime};
2039
2040            # setup the count to compute for the offset
2041            my %UnitOffsetCompute = (
2042                minutes => 60,
2043                hours   => 3600,
2044                days    => 86400,
2045            );
2046
2047            my $NotificationLocalTimeObject;
2048
2049            # Compute from start time.
2050            if ( $CustomUnitPoint eq 'beforestart' || $CustomUnitPoint eq 'afterstart' ) {
2051                $NotificationLocalTimeObject = $Kernel::OM->Create(
2052                    'Kernel::System::DateTime',
2053                    ObjectParams => {
2054                        String => $Param{Data}->{StartTime},
2055                    },
2056                );
2057            }
2058
2059            # Compute from end time.
2060            elsif ( $CustomUnitPoint eq 'beforeend' || $CustomUnitPoint eq 'afterend' ) {
2061                $NotificationLocalTimeObject = $Kernel::OM->Create(
2062                    'Kernel::System::DateTime',
2063                    ObjectParams => {
2064                        String => $Param{Data}->{EndTime},
2065                    },
2066                );
2067            }
2068
2069            # Not supported point of time.
2070            else {
2071                return;
2072            }
2073
2074            # compute the offset to be used
2075            my $Offset = ( $CustomUnitCount * $UnitOffsetCompute{$CustomUnit} );
2076
2077            # save the newly computed notification datetime string
2078            if ( $CustomUnitPoint eq 'beforestart' || $CustomUnitPoint eq 'beforeend' ) {
2079                $NotificationLocalTimeObject->Subtract(
2080                    Seconds => $Offset,
2081                );
2082                $Param{Data}->{NotificationDate} = $NotificationLocalTimeObject->ToString();
2083            }
2084            else {
2085                $NotificationLocalTimeObject->Add(
2086                    Seconds => $Offset,
2087                );
2088                $Param{Data}->{NotificationDate} = $NotificationLocalTimeObject->ToString();
2089            }
2090        }
2091
2092        # Compute date of custom date/time input.
2093        elsif ( $Param{Data}->{NotificationCustom} eq 'datetime' ) {
2094
2095            $Param{Data}->{NotificationCustom} = 'datetime';
2096
2097            # validation
2098            if ( !IsStringWithData( $Param{Data}->{NotificationCustomDateTime} ) ) {
2099                return;
2100            }
2101
2102            # save the given date time values as notification datetime string (i.e. 2016-06-28 02:00:00)
2103            $Param{Data}->{NotificationDate} = $Param{Data}->{NotificationCustomDateTime};
2104        }
2105    }
2106
2107    if ( !IsStringWithData( $Param{Data}->{NotificationDate} ) ) {
2108        $Param{Data}->{NotificationDate} = undef;
2109    }
2110
2111    if ( !IsStringWithData( $Param{Data}->{NotificationCustomDateTime} ) ) {
2112        $Param{Data}->{NotificationCustomDateTime} = undef;
2113    }
2114
2115    return 1;
2116}
2117
2118=head2 AppointmentNotification()
2119
2120Will be triggered by the OTRS daemon to fire events for appointments,
2121that reaches it's reminder (notification) time.
2122
2123    my $Success = $AppointmentObject->AppointmentNotification();
2124
2125returns:
2126
2127    True if notify action was successful, otherwise false.
2128
2129=cut
2130
2131sub AppointmentNotification {
2132    my ( $Self, %Param ) = @_;
2133
2134    # check needed stuff
2135    for my $Needed (qw(NotifyTime)) {
2136        if ( !$Param{$Needed} ) {
2137            $Kernel::OM->Get('Kernel::System::Log')->Log(
2138                Priority => 'error',
2139                Message  => "Need $Needed!",
2140            );
2141            return;
2142        }
2143    }
2144
2145    # get appointments for the related notification timestamp
2146    my @UpcomingAppointments = $Self->AppointmentUpcomingGet(
2147        Timestamp => $Param{NotifyTime},
2148    );
2149
2150    return if !IsArrayRefWithData( \@UpcomingAppointments );
2151
2152    # sleep at least 1 second to make sure the timestamp doesn't
2153    # equals the last one for update upcoming future tasks
2154    sleep 1;
2155
2156    UPCOMINGAPPOINTMENT:
2157    for my $UpcomingAppointment (@UpcomingAppointments) {
2158
2159        next UPCOMINGAPPOINTMENT if !$UpcomingAppointment;
2160        next UPCOMINGAPPOINTMENT if !IsHashRefWithData($UpcomingAppointment);
2161        next UPCOMINGAPPOINTMENT if !$UpcomingAppointment->{AppointmentID};
2162
2163        # fire event
2164        $Self->EventHandler(
2165            Event => 'AppointmentNotification',
2166            Data  => {
2167                AppointmentID => $UpcomingAppointment->{AppointmentID},
2168                CalendarID    => $UpcomingAppointment->{CaledarID},
2169            },
2170            UserID => 1,
2171        );
2172    }
2173
2174    return 1;
2175}
2176
2177=begin Internal:
2178
2179=cut
2180
2181sub _AppointmentRecurringCreate {
2182    my ( $Self, %Param ) = @_;
2183
2184    # check needed stuff
2185    for my $Needed (qw(ParentID Appointment)) {
2186        if ( !$Param{$Needed} ) {
2187            $Kernel::OM->Get('Kernel::System::Log')->Log(
2188                Priority => 'error',
2189                Message  => "Need $Needed!",
2190            );
2191            return;
2192        }
2193    }
2194
2195    my $StartTimeObject = $Kernel::OM->Create(
2196        'Kernel::System::DateTime',
2197        ObjectParams => {
2198            String => $Param{Appointment}->{StartTime},
2199        },
2200    );
2201    my $EndTimeObject = $Kernel::OM->Create(
2202        'Kernel::System::DateTime',
2203        ObjectParams => {
2204            String => $Param{Appointment}->{EndTime},
2205        },
2206    );
2207
2208    my @RecurrenceExclude = @{ $Param{Appointment}->{RecurrenceExclude} // [] };
2209
2210    # remove undefined values
2211    @RecurrenceExclude = grep { defined $_ } @RecurrenceExclude;
2212
2213    # reset the parameter for occurrences
2214    $Param{Appointment}->{RecurrenceExclude} = undef;
2215
2216    my $OriginalStartTimeObject = $StartTimeObject->Clone();
2217    my $OriginalEndTimeObject   = $EndTimeObject->Clone();
2218    my $Step                    = 0;
2219
2220    # until ...
2221    if ( $Param{Appointment}->{RecurrenceUntil} ) {
2222        my $RecurrenceUntilObject = $Kernel::OM->Create(
2223            'Kernel::System::DateTime',
2224            ObjectParams => {
2225                String => $Param{Appointment}->{RecurrenceUntil},
2226            },
2227        );
2228
2229        UNTIL_TIME:
2230        while ( $StartTimeObject <= $RecurrenceUntilObject ) {
2231            $Step += $Param{Appointment}->{RecurrenceInterval};
2232
2233            # calculate recurring times
2234            $StartTimeObject = $Self->_CalculateRecurrenceTime(
2235                Appointment  => $Param{Appointment},
2236                Step         => $Step,
2237                OriginalTime => $OriginalStartTimeObject,
2238                CurrentTime  => $StartTimeObject,
2239            );
2240
2241            last UNTIL_TIME if !$StartTimeObject;
2242            last UNTIL_TIME if $StartTimeObject > $RecurrenceUntilObject;
2243
2244            $EndTimeObject = $StartTimeObject->Clone();
2245            $EndTimeObject->Add(
2246                Seconds =>
2247                    $OriginalEndTimeObject->Delta( DateTimeObject => $OriginalStartTimeObject )->{AbsoluteSeconds},
2248            );
2249
2250            my $StartTime = $StartTimeObject->ToString();
2251            my $EndTime   = $EndTimeObject->ToString();
2252
2253            # Bugfix: on some systems with older perl version system might calculate timezone difference.
2254            $StartTime = $Self->_TimeCheck(
2255                OriginalTime => $Param{Appointment}->{StartTime},
2256                Time         => $StartTime,
2257            );
2258            $EndTime = $Self->_TimeCheck(
2259                OriginalTime => $Param{Appointment}->{EndTime},
2260                Time         => $EndTime,
2261            );
2262
2263            # skip excluded appointments
2264            next UNTIL_TIME if grep { $StartTime eq $_ } @RecurrenceExclude;
2265
2266            $Self->AppointmentCreate(
2267                %{ $Param{Appointment} },
2268                ParentID     => $Param{ParentID},
2269                StartTime    => $StartTime,
2270                EndTime      => $EndTime,
2271                RecurrenceID => $StartTime,
2272            );
2273        }
2274    }
2275
2276    # for ... time(s)
2277    elsif ( $Param{Appointment}->{RecurrenceCount} ) {
2278
2279        COUNT:
2280        for ( 1 .. $Param{Appointment}->{RecurrenceCount} - 1 ) {
2281            $Step += $Param{Appointment}->{RecurrenceInterval};
2282
2283            # calculate recurring times
2284            $StartTimeObject = $Self->_CalculateRecurrenceTime(
2285                Appointment  => $Param{Appointment},
2286                Step         => $Step,
2287                OriginalTime => $OriginalStartTimeObject,
2288                CurrentTime  => $StartTimeObject,
2289            );
2290
2291            last COUNT if !$StartTimeObject;
2292
2293            $EndTimeObject = $StartTimeObject->Clone();
2294            $EndTimeObject->Add(
2295                Seconds =>
2296                    $OriginalEndTimeObject->Delta( DateTimeObject => $OriginalStartTimeObject )->{AbsoluteSeconds},
2297            );
2298
2299            my $StartTime = $StartTimeObject->ToString();
2300            my $EndTime   = $EndTimeObject->ToString();
2301
2302            # Bugfix: on some systems with older perl version system might calculate timezone difference.
2303            $StartTime = $Self->_TimeCheck(
2304                OriginalTime => $Param{Appointment}->{StartTime},
2305                Time         => $StartTime,
2306            );
2307            $EndTime = $Self->_TimeCheck(
2308                OriginalTime => $Param{Appointment}->{EndTime},
2309                Time         => $EndTime,
2310            );
2311
2312            # skip excluded appointments
2313            next COUNT if grep { $StartTime eq $_ } @RecurrenceExclude;
2314
2315            $Self->AppointmentCreate(
2316                %{ $Param{Appointment} },
2317                ParentID     => $Param{ParentID},
2318                StartTime    => $StartTime,
2319                EndTime      => $EndTime,
2320                RecurrenceID => $StartTime,
2321            );
2322        }
2323    }
2324
2325    return 1;
2326}
2327
2328sub _AppointmentRecurringDelete {
2329    my ( $Self, %Param ) = @_;
2330
2331    # check needed stuff
2332    for my $Needed (qw(ParentID)) {
2333        if ( !$Param{$Needed} ) {
2334            $Kernel::OM->Get('Kernel::System::Log')->Log(
2335                Priority => 'error',
2336                Message  => "Need $Needed!",
2337            );
2338            return;
2339        }
2340    }
2341
2342    # delete recurring appointments
2343    my $SQL = '
2344        DELETE FROM calendar_appointment
2345        WHERE parent_id=?
2346    ';
2347
2348    # delete db record
2349    return if !$Kernel::OM->Get('Kernel::System::DB')->Do(
2350        SQL  => $SQL,
2351        Bind => [
2352            \$Param{ParentID},
2353        ],
2354    );
2355
2356    return 1;
2357}
2358
2359sub _AppointmentRecurringExclude {
2360    my ( $Self, %Param ) = @_;
2361
2362    # check needed stuff
2363    for my $Needed (qw(ParentID RecurrenceID)) {
2364        if ( !$Param{$Needed} ) {
2365            $Kernel::OM->Get('Kernel::System::Log')->Log(
2366                Priority => 'error',
2367                Message  => "Need $Needed!",
2368            );
2369            return;
2370        }
2371    }
2372
2373    my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache');
2374    my $DBObject    = $Kernel::OM->Get('Kernel::System::DB');
2375
2376    # db query
2377    return if !$DBObject->Prepare(
2378        SQL  => 'SELECT recur_exclude FROM calendar_appointment WHERE id=?',
2379        Bind => [ \$Param{ParentID} ],
2380    );
2381
2382    # get existing exclusions
2383    my @RecurrenceExclude;
2384    while ( my @Row = $DBObject->FetchrowArray() ) {
2385        @RecurrenceExclude = split( ',', $Row[0] ) if $Row[0];
2386    }
2387    push @RecurrenceExclude, $Param{RecurrenceID};
2388    @RecurrenceExclude = sort @RecurrenceExclude;
2389
2390    # join into string
2391    my $RecurrenceExclude;
2392    if (@RecurrenceExclude) {
2393        $RecurrenceExclude = join( ',', @RecurrenceExclude );
2394    }
2395
2396    # update db record
2397    return if !$DBObject->Do(
2398        SQL  => 'UPDATE calendar_appointment SET recur_exclude=? WHERE id=?',
2399        Bind => [ \$RecurrenceExclude, \$Param{ParentID} ],
2400    );
2401
2402    # delete cache
2403    $CacheObject->Delete(
2404        Type => $Self->{CacheType},
2405        Key  => $Param{ParentID},
2406    );
2407
2408    return 1;
2409}
2410
2411sub _AppointmentGetCalendarID {
2412    my ( $Self, %Param ) = @_;
2413
2414    # check needed stuff
2415    for my $Needed (qw(AppointmentID)) {
2416        if ( !defined $Param{$Needed} ) {
2417            $Kernel::OM->Get('Kernel::System::Log')->Log(
2418                Priority => 'error',
2419                Message  => "Need $Needed!",
2420            );
2421            return;
2422        }
2423    }
2424
2425    # sql query
2426    my $SQL  = 'SELECT calendar_id FROM calendar_appointment WHERE id=?';
2427    my @Bind = ( \$Param{AppointmentID} );
2428
2429    # get database object
2430    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
2431
2432    # start query
2433    return if !$DBObject->Prepare(
2434        SQL   => $SQL,
2435        Bind  => \@Bind,
2436        Limit => 1,
2437    );
2438
2439    my $CalendarID;
2440    while ( my @Row = $DBObject->FetchrowArray() ) {
2441        $CalendarID = $Row[0];
2442    }
2443
2444    return $CalendarID;
2445}
2446
2447sub _AppointmentGetRecurrenceID {
2448    my ( $Self, %Param ) = @_;
2449
2450    # check needed stuff
2451    for my $Needed (qw(AppointmentID)) {
2452        if ( !defined $Param{$Needed} ) {
2453            $Kernel::OM->Get('Kernel::System::Log')->Log(
2454                Priority => 'error',
2455                Message  => "Need $Needed!",
2456            );
2457            return;
2458        }
2459    }
2460
2461    # sql query
2462    my $SQL  = 'SELECT recur_id FROM calendar_appointment WHERE id=?';
2463    my @Bind = ( \$Param{AppointmentID} );
2464
2465    # get database object
2466    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
2467
2468    # start query
2469    return if !$DBObject->Prepare(
2470        SQL   => $SQL,
2471        Bind  => \@Bind,
2472        Limit => 1,
2473    );
2474
2475    my $RecurrenceID;
2476    while ( my @Row = $DBObject->FetchrowArray() ) {
2477        $RecurrenceID = $Row[0];
2478    }
2479
2480    return $RecurrenceID;
2481}
2482
2483sub _CalculateRecurrenceTime {
2484    my ( $Self, %Param ) = @_;
2485
2486    # check needed stuff
2487    for my $Needed (qw(Appointment Step OriginalTime CurrentTime)) {
2488        if ( !defined $Param{$Needed} ) {
2489            $Kernel::OM->Get('Kernel::System::Log')->Log(
2490                Priority => 'error',
2491                Message  => "Need $Needed!",
2492            );
2493            return;
2494        }
2495    }
2496
2497    my $OriginalTimeObject = $Param{OriginalTime};
2498
2499    # We will modify this object throughout the function.
2500    my $CurrentTimeObject = $Param{CurrentTime};
2501
2502    if ( $Param{Appointment}->{RecurrenceType} eq 'Daily' ) {
2503
2504        # Add one day.
2505        $CurrentTimeObject->Add(
2506            Days => 1,
2507        );
2508    }
2509    elsif ( $Param{Appointment}->{RecurrenceType} eq 'Weekly' ) {
2510
2511        # Add 7 days.
2512        $CurrentTimeObject->Add(
2513            Days => 7,
2514        );
2515    }
2516    elsif ( $Param{Appointment}->{RecurrenceType} eq 'Monthly' ) {
2517        my $TempTimeObject = $OriginalTimeObject->Clone();
2518
2519        # Remember start day.
2520        my $StartDay = $TempTimeObject->Get()->{Day};
2521
2522        # Add months based on current step.
2523        $TempTimeObject->Add(
2524            Months => $Param{Step},
2525        );
2526
2527        # Get end day.
2528        my $EndDay = $TempTimeObject->Get()->{Day};
2529
2530        # Check if month doesn't have enough days (for example: January 31 + 1 month = March 1).
2531        if ( $StartDay != $EndDay ) {
2532            $TempTimeObject->Subtract(
2533                Days => $EndDay,
2534            );
2535        }
2536
2537        $CurrentTimeObject = $TempTimeObject->Clone();
2538    }
2539    elsif ( $Param{Appointment}->{RecurrenceType} eq 'Yearly' ) {
2540        my $TempTimeObject = $OriginalTimeObject->Clone();
2541
2542        # Remember start day.
2543        my $StartDay = $TempTimeObject->Get()->{Day};
2544
2545        # Add years based on current step.
2546        $TempTimeObject->Add(
2547            Years => $Param{Step},
2548        );
2549
2550        # Get end day.
2551        my $EndDay = $TempTimeObject->Get()->{Day};
2552
2553        # Check if month doesn't have enough days (for example: January 31 + 1 month = March 1).
2554        if ( $StartDay != $EndDay ) {
2555            $TempTimeObject->Subtract(
2556                Days => $EndDay,
2557            );
2558        }
2559
2560        $CurrentTimeObject = $TempTimeObject->Clone();
2561    }
2562    elsif ( $Param{Appointment}->{RecurrenceType} eq 'CustomDaily' ) {
2563
2564        # Add number of days.
2565        $CurrentTimeObject->Add(
2566            Days => $Param{Appointment}->{RecurrenceInterval},
2567        );
2568    }
2569    elsif ( $Param{Appointment}->{RecurrenceType} eq 'CustomWeekly' ) {
2570
2571        # this block covers following use case:
2572        # each n-th Monday and Friday
2573
2574        my $Found;
2575
2576        # loop up to 7*n times (7 days in week * frequency)
2577        LOOP:
2578        for ( my $Counter = 0; $Counter < 7 * $Param{Appointment}->{RecurrenceInterval}; $Counter++ ) {
2579
2580            # Add one day.
2581            $CurrentTimeObject->Add(
2582                Days => 1,
2583            );
2584
2585            my $CWDiff = $Self->_CWDiff(
2586                CurrentTime  => $CurrentTimeObject,
2587                OriginalTime => $OriginalTimeObject,
2588            );
2589
2590            next LOOP if $CWDiff % $Param{Appointment}->{RecurrenceInterval};
2591
2592            my $WeekDay = $CurrentTimeObject->Get()->{DayOfWeek};
2593
2594            # check if SystemTime match requirements
2595            if ( grep { $WeekDay == $_ } @{ $Param{Appointment}->{RecurrenceFrequency} } ) {
2596                $Found = 1;
2597                last LOOP;
2598            }
2599        }
2600
2601        return if !$Found;
2602    }
2603
2604    elsif ( $Param{Appointment}->{RecurrenceType} eq 'CustomMonthly' ) {
2605
2606        # Occurs every 2nd month on 5th, 10th and 15th day
2607        my $Found;
2608
2609        # loop through each day (max one year), and check if day matches.
2610        DAY:
2611        for ( my $Counter = 0; $Counter < 31 * 366; $Counter++ ) {
2612
2613            # Add one day.
2614            $CurrentTimeObject->Add(
2615                Days => 1,
2616            );
2617
2618            # Skip month if needed
2619            next DAY
2620                if ( $CurrentTimeObject->Get()->{Month} - $OriginalTimeObject->Get()->{Month} )
2621                % $Param{Appointment}->{RecurrenceInterval};
2622
2623            # next day if this day should be skipped
2624            next DAY
2625                if !grep { $CurrentTimeObject->Get()->{Day} == $_ } @{ $Param{Appointment}->{RecurrenceFrequency} };
2626
2627            $Found = 1;
2628            last DAY;
2629        }
2630        return if !$Found;
2631    }
2632    elsif ( $Param{Appointment}->{RecurrenceType} eq 'CustomYearly' ) {
2633
2634        # this block covers following use case:
2635        # Occurs each 3th year, January 18th and March 18th
2636        my $Found;
2637
2638        my $RecurrenceUntilObject;
2639        if ( $Param{Appointment}->{RecurrenceUntil} ) {
2640            $RecurrenceUntilObject = $Kernel::OM->Create(
2641                'Kernel::System::DateTime',
2642                ObjectParams => {
2643                    String => $Param{Appointment}->{RecurrenceUntil},
2644                },
2645            );
2646        }
2647
2648        my $NextDayObject = $CurrentTimeObject->Clone();
2649        $NextDayObject->Add(
2650            Days => 1,
2651        );
2652
2653        MONTH:
2654        for ( my $Counter = 1;; $Counter++ ) {
2655            my $TempTimeObject = $OriginalTimeObject->Clone();
2656
2657            # remember start day
2658            my $StartDay = $TempTimeObject->Get()->{Day};
2659
2660            $TempTimeObject->Add(
2661                Months => $Counter,
2662            );
2663
2664            # get end day
2665            my $EndDay = $TempTimeObject->Get()->{Day};
2666
2667            # check if month doesn't have enough days (for example: january 31 + 1 month = march 01)
2668            if ( $StartDay != $EndDay ) {
2669                $TempTimeObject->Subtract(
2670                    Days => $EndDay,
2671                );
2672            }
2673
2674            $CurrentTimeObject = $TempTimeObject->Clone();
2675
2676            # skip this time, since it was already checked
2677            next MONTH if $CurrentTimeObject < $NextDayObject;
2678
2679            # check loop conditions (according to Until / )
2680            if ($RecurrenceUntilObject) {
2681                last MONTH if $CurrentTimeObject > $RecurrenceUntilObject;
2682            }
2683            else {
2684                last MONTH
2685                    if $Counter
2686                    > 12 * $Param{Appointment}->{RecurrenceInterval} * $Param{Appointment}->{RecurrenceCount};
2687            }
2688
2689            # check if year is OK
2690            next MONTH
2691                if ( $CurrentTimeObject->Get()->{Year} - $OriginalTimeObject->Get()->{Year} )
2692                % $Param{Appointment}->{RecurrenceInterval};
2693
2694            # next month if this month should be skipped
2695            next MONTH
2696                if !grep { $CurrentTimeObject->Get()->{Month} == $_ } @{ $Param{Appointment}->{RecurrenceFrequency} };
2697
2698            $Found = 1;
2699            last MONTH;
2700        }
2701        return if !$Found;
2702    }
2703    else {
2704        return;
2705    }
2706
2707    return $CurrentTimeObject;
2708}
2709
2710=head2 _TimeCheck()
2711
2712Check if Time and OriginalTime have same hour, minute and second value, and return timestamp with
2713values (hour, minute and second) as in Time.
2714
2715    my $Result = $Self->_TimeCheck(
2716        OriginalTime => '2016-01-01 00:01:00',     # (required)
2717        Time         => '2016-02-01 00:02:00',     # (required)
2718    );
2719
2720Returns:
2721    $Result = '2016-02-01 00:01:00';
2722
2723=cut
2724
2725sub _TimeCheck {
2726    my ( $Self, %Param ) = @_;
2727
2728    for my $Needed (qw(OriginalTime Time)) {
2729        if ( !defined $Param{$Needed} ) {
2730            $Kernel::OM->Get('Kernel::System::Log')->Log(
2731                Priority => 'error',
2732                Message  => "Need $Needed!",
2733            );
2734            return;
2735        }
2736    }
2737
2738    my $Result = '';
2739
2740    $Param{OriginalTime} =~ /(.*?)\s(.*?)$/;
2741    my $OriginalDate = $1;
2742    my $OriginalTime = $2;
2743
2744    $Param{Time} =~ /(.*?)\s(.*?)$/;
2745    my $Date = $1;
2746
2747    $Result = "$Date $OriginalTime";
2748    return $Result;
2749}
2750
2751=head2 _CWDiff()
2752
2753Returns how many calendar weeks has passed between two unix times.
2754
2755    my $CWDiff = $Self->_CWDiff(
2756        CurrentTime  => $CurrentTimeObject,     (required) Date time object with current time
2757        OriginalTime => $OriginalTimeObject,    (required) Date time object with original time
2758    );
2759
2760returns:
2761    $CWDiff = 5;
2762
2763=cut
2764
2765sub _CWDiff {
2766    my ( $Self, %Param ) = @_;
2767
2768    for my $Needed (qw(CurrentTime OriginalTime)) {
2769        if ( !defined $Param{$Needed} ) {
2770            $Kernel::OM->Get('Kernel::System::Log')->Log(
2771                Priority => 'error',
2772                Message  => "Need $Needed!",
2773            );
2774            return;
2775        }
2776    }
2777
2778    my $OriginalTimeObject = $Param{OriginalTime};
2779    my $CurrentTimeObject  = $Param{CurrentTime};
2780
2781    my $StartYear = $OriginalTimeObject->Get()->{Year};
2782    my $EndYear   = $CurrentTimeObject->Get()->{Year};
2783
2784    my $Result = $CurrentTimeObject->{CPANDateTimeObject}->week_number()
2785        - $OriginalTimeObject->{CPANDateTimeObject}->week_number();
2786
2787    # If date is end of the year and date CW starts with 1, we need to include additional year.
2788    if ( $Result < 0 && $CurrentTimeObject->Get()->{Day} == 31 && $CurrentTimeObject->Get()->{Month} == 12 ) {
2789        $EndYear++;
2790    }
2791
2792    for my $Year ( $StartYear .. $EndYear - 1 ) {
2793        my $CW  = 0;
2794        my $Day = 31;
2795
2796        while ( $CW < 50 ) {
2797
2798            # To get how many CW's are in this year, we set temporary date to 31-dec.
2799            my $DateTimeObject = $Kernel::OM->Create(
2800                'Kernel::System::DateTime',
2801                ObjectParams => {
2802                    String => "$Year-12-$Day 23:59:00",
2803                },
2804            );
2805
2806            $CW = $DateTimeObject->{CPANDateTimeObject}->week_number();
2807            $Day--;
2808        }
2809
2810        $Result += $CW;
2811    }
2812
2813    return $Result;
2814}
2815
28161;
2817
2818=end Internal:
2819
2820=head1 TERMS AND CONDITIONS
2821
2822This software is part of the OTRS project (L<https://otrs.org/>).
2823
2824This software comes with ABSOLUTELY NO WARRANTY. For details, see
2825the enclosed file COPYING for license information (GPL). If you
2826did not receive this file, see L<https://www.gnu.org/licenses/gpl-3.0.txt>.
2827
2828=cut
2829