Bug 7864: Reintroduce list of subscribers to a serial alert message
[koha.git] / Koha / Calendar.pm
1 package Koha::Calendar;
2 use strict;
3 use warnings;
4 use 5.010;
5
6 use DateTime;
7 use DateTime::Set;
8 use DateTime::Duration;
9 use C4::Context;
10 use Carp;
11 use Readonly;
12
13 sub new {
14     my ( $classname, %options ) = @_;
15     my $self = {};
16     bless $self, $classname;
17     for my $o_name ( keys %options ) {
18         my $o = lc $o_name;
19         $self->{$o} = $options{$o_name};
20     }
21     if ( exists $options{TEST_MODE} ) {
22         $self->_mockinit();
23         return $self;
24     }
25     if ( !defined $self->{branchcode} ) {
26         croak 'No branchcode argument passed to Koha::Calendar->new';
27     }
28     $self->_init();
29     return $self;
30 }
31
32 sub _init {
33     my $self       = shift;
34     my $branch     = $self->{branchcode};
35     my $dbh        = C4::Context->dbh();
36     my $repeat_sth = $dbh->prepare(
37 'SELECT * from repeatable_holidays WHERE branchcode = ? AND ISNULL(weekday) = ?'
38     );
39     $repeat_sth->execute( $branch, 0 );
40     $self->{weekly_closed_days} = [ 0, 0, 0, 0, 0, 0, 0 ];
41     Readonly::Scalar my $sunday => 7;
42     while ( my $tuple = $repeat_sth->fetchrow_hashref ) {
43         $self->{weekly_closed_days}->[ $tuple->{weekday} ] = 1;
44     }
45     $repeat_sth->execute( $branch, 1 );
46     $self->{day_month_closed_days} = {};
47     while ( my $tuple = $repeat_sth->fetchrow_hashref ) {
48         $self->{day_month_closed_days}->{ $tuple->{day} }->{ $tuple->{month} } =
49           1;
50     }
51     my $special = $dbh->prepare(
52 'SELECT day, month, year, title, description FROM special_holidays WHERE ( branchcode = ? ) AND (isexception = ?)'
53     );
54     $special->execute( $branch, 1 );
55     my $dates = [];
56     while ( my ( $day, $month, $year, $title, $description ) =
57         $special->fetchrow ) {
58         push @{$dates},
59           DateTime->new(
60             day       => $day,
61             month     => $month,
62             year      => $year,
63             time_zone => C4::Context->tz()
64           )->truncate( to => 'day' );
65     }
66     $self->{exception_holidays} =
67       DateTime::Set->from_datetimes( dates => $dates );
68     $special->execute( $branch, 1 );
69     $dates = [];
70     while ( my ( $day, $month, $year, $title, $description ) =
71         $special->fetchrow ) {
72         push @{$dates},
73           DateTime->new(
74             day       => $day,
75             month     => $month,
76             year      => $year,
77             time_zone => C4::Context->tz()
78           )->truncate( to => 'day' );
79     }
80     $self->{single_holidays} = DateTime::Set->from_datetimes( dates => $dates );
81     $self->{days_mode} = C4::Context->preference('useDaysMode');
82     return;
83 }
84
85 sub addDate {
86     my ( $self, $startdate, $add_duration, $unit ) = @_;
87     my $base_date = $startdate->clone();
88     if ( ref $add_duration ne 'DateTime::Duration' ) {
89         $add_duration = DateTime::Duration->new( days => $add_duration );
90     }
91     $unit ||= q{};    # default days ?
92     my $days_mode = $self->{days_mode};
93     Readonly::Scalar my $return_by_hour => 10;
94     my $day_dur = DateTime::Duration->new( days => 1 );
95     if ( $add_duration->is_negative() ) {
96         $day_dur->inverse();
97     }
98     if ( $days_mode eq 'Datedue' ) {
99
100         my $dt = $base_date + $add_duration;
101         while ( $self->is_holiday($dt) ) {
102
103             # TODOP if hours set to 10 am
104             $dt->add_duration($day_dur);
105             if ( $unit eq 'hours' ) {
106                 $dt->set_hour($return_by_hour);    # Staffs specific
107             }
108         }
109         return $dt;
110     } elsif ( $days_mode eq 'Calendar' ) {
111         if ( $unit eq 'hours' ) {
112             $base_date->add_duration($add_duration);
113             while ( $self->is_holiday($base_date) ) {
114                 $base_date->add_duration($day_dur);
115
116             }
117
118         } else {
119             my $days = abs $add_duration->in_units('days');
120             while ($days) {
121                 $base_date->add_duration($day_dur);
122                 if ( $self->is_holiday($base_date) ) {
123                     next;
124                 } else {
125                     --$days;
126                 }
127             }
128         }
129         if ( $unit eq 'hours' ) {
130             my $dt = $base_date->clone()->subtract( days => 1 );
131             if ( $self->is_holiday($dt) ) {
132                 $base_date->set_hour($return_by_hour);    # Staffs specific
133             }
134         }
135         return $base_date;
136     } else {    # Days
137         return $base_date + $add_duration;
138     }
139 }
140
141 sub is_holiday {
142     my ( $self, $dt ) = @_;
143     my $dow = $dt->day_of_week;
144     if ( $dow == 7 ) {
145         $dow = 0;
146     }
147     if ( $self->{weekly_closed_days}->[$dow] == 1 ) {
148         return 1;
149     }
150     $dt->truncate( to => 'days' );
151     my $day   = $dt->day;
152     my $month = $dt->month;
153     if ( exists $self->{day_month_closed_days}->{$month}->{$day} ) {
154         return 1;
155     }
156     if ( $self->{exception_holidays}->contains($dt) ) {
157         return 1;
158     }
159     if ( $self->{single_holidays}->contains($dt) ) {
160         return 1;
161     }
162
163     # damn have to go to work after all
164     return 0;
165 }
166
167 sub days_between {
168     my $self     = shift;
169     my $start_dt = shift;
170     my $end_dt   = shift;
171
172     # start and end should not be closed days
173     my $duration = $end_dt->delta_days($start_dt);
174     $start_dt->truncate( to => 'days' );
175     $end_dt->truncate( to => 'days' );
176     while ( DateTime->compare( $start_dt, $end_dt ) == -1 ) {
177         $start_dt->add( days => 1 );
178         if ( $self->is_holiday($start_dt) ) {
179             $duration->subtract( days => 1 );
180         }
181     }
182     return $duration;
183
184 }
185
186 sub hours_between {
187     my ($self, $start_dt, $end_dt) = @_;
188     my $duration = $end_dt->delta_ms($start_dt);
189     $start_dt->truncate( to => 'days' );
190     $end_dt->truncate( to => 'days' );
191     # NB this is a kludge in that it assumes all days are 24 hours
192     # However for hourly loans the logic should be expanded to
193     # take into account open/close times then it would be a duration
194     # of library open hours
195     while ( DateTime->compare( $start_dt, $end_dt ) == -1 ) {
196         $start_dt->add( days => 1 );
197         if ( $self->is_holiday($start_dt) ) {
198             $duration->subtract( hours => 24 );
199         }
200     }
201     return $duration;
202
203 }
204
205 sub _mockinit {
206     my $self = shift;
207     $self->{weekly_closed_days} = [ 1, 0, 0, 0, 0, 0, 0 ];    # Sunday only
208     $self->{day_month_closed_days} = { 6 => { 16 => 1, } };
209     my $dates = [];
210     $self->{exception_holidays} =
211       DateTime::Set->from_datetimes( dates => $dates );
212     my $special = DateTime->new(
213         year      => 2011,
214         month     => 6,
215         day       => 1,
216         time_zone => 'Europe/London',
217     );
218     push @{$dates}, $special;
219     $self->{single_holidays} = DateTime::Set->from_datetimes( dates => $dates );
220     $self->{days_mode} = 'Calendar';
221     return;
222 }
223
224 1;
225 __END__
226
227 =head1 NAME
228
229 Koha::Calendar - Object containing a branches calendar
230
231 =head1 VERSION
232
233 This documentation refers to Koha::Calendar version 0.0.1
234
235 =head1 SYNOPSIS
236
237   use Koha::Calendat
238
239   my $c = Koha::Calender->new( branchcode => 'MAIN' );
240   my $dt = DateTime->now();
241
242   # are we open
243   $open = $c->is_holiday($dt);
244   # when will item be due if loan period = $dur (a DateTime::Duration object)
245   $duedate = $c->addDate($dt,$dur,'days');
246
247
248 =head1 DESCRIPTION
249
250   Implements those features of C4::Calendar needed for Staffs Rolling Loans
251
252 =head1 METHODS
253
254 =head2 new : Create a calendar object
255
256 my $calendar = Koha::Calendar->new( branchcode => 'MAIN' );
257
258 The option branchcode is required
259
260
261 =head2 addDate
262
263     my $dt = $calendar->addDate($date, $dur, $unit)
264
265 C<$date> is a DateTime object representing the starting date of the interval.
266
267 C<$offset> is a DateTime::Duration to add to it
268
269 C<$unit> is a string value 'days' or 'hours' toflag granularity of duration
270
271 Currently unit is only used to invoke Staffs return Monday at 10 am rule this
272 parameter will be removed when issuingrules properly cope with that
273
274
275 =head2 is_holiday
276
277 $yesno = $calendar->is_holiday($dt);
278
279 passed at DateTime object returns 1 if it is a closed day
280 0 if not according to the calendar
281
282 =head2 days_between
283
284 $duration = $calendar->days_between($start_dt, $end_dt);
285
286 Passed two dates returns a DateTime::Duration object measuring the length between them
287 ignoring closed days
288
289 =head1 DIAGNOSTICS
290
291 Will croak if not passed a branchcode in new
292
293 =head1 BUGS AND LIMITATIONS
294
295 This only contains a limited subset of the functionality in C4::Calendar
296 Only enough to support Staffs Rolling loans
297
298 =head1 AUTHOR
299
300 Colin Campbell colin.campbell@ptfs-europe.com
301
302 =head1 LICENSE AND COPYRIGHT
303
304 Copyright (c) 2011 PTFS-Europe Ltd All rights reserved
305
306 This program is free software: you can redistribute it and/or modify
307 it under the terms of the GNU General Public License as published by
308 the Free Software Foundation, either version 2 of the License, or
309 (at your option) any later version.
310
311 This program is distributed in the hope that it will be useful,
312 but WITHOUT ANY WARRANTY; without even the implied warranty of
313 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
314 GNU General Public License for more details.
315
316 You should have received a copy of the GNU General Public License
317 along with this program.  If not, see <http://www.gnu.org/licenses/>.