Merge remote-tracking branch 'origin/new/bug_8251'
[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 = DateTime::Duration->new( days => -1 );
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 => 'day' );
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     my $datestart_temp = $start_dt->clone();
173     my $dateend_temp = $end_dt->clone();
174
175     # start and end should not be closed days
176     $datestart_temp->truncate( to => 'day' );
177     $dateend_temp->truncate( to => 'day' );
178     my $duration = $dateend_temp - $datestart_temp;
179     while ( DateTime->compare( $datestart_temp, $dateend_temp ) == -1 ) {
180         $datestart_temp->add( days => 1 );
181         if ( $self->is_holiday($datestart_temp) ) {
182             $duration->subtract( days => 1 );
183         }
184     }
185     return $duration;
186
187 }
188
189 sub hours_between {
190     my ($self, $start_dt, $end_dt) = @_;
191     my $duration = $end_dt->delta_ms($start_dt);
192     $start_dt->truncate( to => 'day' );
193     $end_dt->truncate( to => 'day' );
194     # NB this is a kludge in that it assumes all days are 24 hours
195     # However for hourly loans the logic should be expanded to
196     # take into account open/close times then it would be a duration
197     # of library open hours
198     while ( DateTime->compare( $start_dt, $end_dt ) == -1 ) {
199         $start_dt->add( days => 1 );
200         if ( $self->is_holiday($start_dt) ) {
201             $duration->subtract( hours => 24 );
202         }
203     }
204     return $duration;
205
206 }
207
208 sub _mockinit {
209     my $self = shift;
210     $self->{weekly_closed_days} = [ 1, 0, 0, 0, 0, 0, 0 ];    # Sunday only
211     $self->{day_month_closed_days} = { 6 => { 16 => 1, } };
212     my $dates = [];
213     $self->{exception_holidays} =
214       DateTime::Set->from_datetimes( dates => $dates );
215     my $special = DateTime->new(
216         year      => 2011,
217         month     => 6,
218         day       => 1,
219         time_zone => 'Europe/London',
220     );
221     push @{$dates}, $special;
222     $self->{single_holidays} = DateTime::Set->from_datetimes( dates => $dates );
223     $self->{days_mode} = 'Calendar';
224     return;
225 }
226
227 1;
228 __END__
229
230 =head1 NAME
231
232 Koha::Calendar - Object containing a branches calendar
233
234 =head1 VERSION
235
236 This documentation refers to Koha::Calendar version 0.0.1
237
238 =head1 SYNOPSIS
239
240   use Koha::Calendat
241
242   my $c = Koha::Calender->new( branchcode => 'MAIN' );
243   my $dt = DateTime->now();
244
245   # are we open
246   $open = $c->is_holiday($dt);
247   # when will item be due if loan period = $dur (a DateTime::Duration object)
248   $duedate = $c->addDate($dt,$dur,'days');
249
250
251 =head1 DESCRIPTION
252
253   Implements those features of C4::Calendar needed for Staffs Rolling Loans
254
255 =head1 METHODS
256
257 =head2 new : Create a calendar object
258
259 my $calendar = Koha::Calendar->new( branchcode => 'MAIN' );
260
261 The option branchcode is required
262
263
264 =head2 addDate
265
266     my $dt = $calendar->addDate($date, $dur, $unit)
267
268 C<$date> is a DateTime object representing the starting date of the interval.
269
270 C<$offset> is a DateTime::Duration to add to it
271
272 C<$unit> is a string value 'days' or 'hours' toflag granularity of duration
273
274 Currently unit is only used to invoke Staffs return Monday at 10 am rule this
275 parameter will be removed when issuingrules properly cope with that
276
277
278 =head2 is_holiday
279
280 $yesno = $calendar->is_holiday($dt);
281
282 passed at DateTime object returns 1 if it is a closed day
283 0 if not according to the calendar
284
285 =head2 days_between
286
287 $duration = $calendar->days_between($start_dt, $end_dt);
288
289 Passed two dates returns a DateTime::Duration object measuring the length between them
290 ignoring closed days
291
292 =head1 DIAGNOSTICS
293
294 Will croak if not passed a branchcode in new
295
296 =head1 BUGS AND LIMITATIONS
297
298 This only contains a limited subset of the functionality in C4::Calendar
299 Only enough to support Staffs Rolling loans
300
301 =head1 AUTHOR
302
303 Colin Campbell colin.campbell@ptfs-europe.com
304
305 =head1 LICENSE AND COPYRIGHT
306
307 Copyright (c) 2011 PTFS-Europe Ltd All rights reserved
308
309 This program is free software: you can redistribute it and/or modify
310 it under the terms of the GNU General Public License as published by
311 the Free Software Foundation, either version 2 of the License, or
312 (at your option) any later version.
313
314 This program is distributed in the hope that it will be useful,
315 but WITHOUT ANY WARRANTY; without even the implied warranty of
316 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
317 GNU General Public License for more details.
318
319 You should have received a copy of the GNU General Public License
320 along with this program.  If not, see <http://www.gnu.org/licenses/>.