bdd4b8f06a85e69146b8ae8209c0718f9042def7
[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, $base_date, $add_duration, $unit ) = @_;
87     if ( ref $add_duration ne 'DateTime::Duration' ) {
88         $add_duration = DateTime::Duration->new( days => $add_duration );
89     }
90     my $days_mode = $self->{days_mode};
91     Readonly::Scalar my $return_by_hour => 10;
92     my $day_dur = DateTime::Duration->new( days => 1 );
93     if ( $add_duration->is_negative() ) {
94         $day_dur->inverse();
95     }
96     if ( $days_mode eq 'Datedue' ) {
97
98         my $dt = $base_date + $add_duration;
99         while ( $self->is_holiday($dt) ) {
100
101             # TODOP if hours set to 10 am
102             $dt->add_duration($day_dur);
103             if ( $unit eq 'hours' ) {
104                 $dt->set_hour($return_by_hour);    # Staffs specific
105             }
106         }
107         return $dt;
108     } elsif ( $days_mode eq 'Calendar' ) {
109         if ( $unit eq 'hours' ) {
110             $base_date->add_duration($add_duration);
111             while ( $self->is_holiday($base_date) ) {
112                 $base_date->add_duration($day_dur);
113
114             }
115
116         } else {
117             my $days = $add_duration->in_units('days');
118             while ($days) {
119                 $base_date->add_duration($day_dur);
120                 if ( $self->is_holiday($base_date) ) {
121                     next;
122                 } else {
123                     --$days;
124                 }
125             }
126         }
127         if ( $unit eq 'hours' ) {
128             my $dt = $base_date->clone()->subtract( days => 1 );
129             if ( $self->is_holiday($dt) ) {
130                 $base_date->set_hour($return_by_hour);    # Staffs specific
131             }
132         }
133         return $base_date;
134     } else {    # Days
135         return $base_date + $add_duration;
136     }
137 }
138
139 sub is_holiday {
140     my ( $self, $dt ) = @_;
141     my $dow = $dt->day_of_week;
142     if ( $dow == 7 ) {
143         $dow = 0;
144     }
145     if ( $self->{weekly_closed_days}->[$dow] == 1 ) {
146         return 1;
147     }
148     $dt->truncate( to => 'days' );
149     my $day   = $dt->day;
150     my $month = $dt->month;
151     if ( exists $self->{day_month_closed_days}->{$month}->{$day} ) {
152         return 1;
153     }
154     if ( $self->{exception_holidays}->contains($dt) ) {
155         return 1;
156     }
157     if ( $self->{single_holidays}->contains($dt) ) {
158         return 1;
159     }
160
161     # damn have to go to work after all
162     return 0;
163 }
164
165 sub days_between {
166     my $self     = shift;
167     my $start_dt = shift;
168     my $end_dt   = shift;
169     $start_dt->truncate( to => 'hours' );
170     $end_dt->truncate( to => 'hours' );
171
172     # start and end should not be closed days
173     my $duration = $end_dt - $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 _mockinit {
187     my $self = shift;
188     $self->{weekly_closed_days} = [ 1, 0, 0, 0, 0, 0, 0 ];    # Sunday only
189     $self->{day_month_closed_days} = { 6 => { 16 => 1, } };
190     my $dates = [];
191     $self->{exception_holidays} =
192       DateTime::Set->from_datetimes( dates => $dates );
193     my $special = DateTime->new(
194         year      => 2011,
195         month     => 6,
196         day       => 1,
197         time_zone => 'Europe/London',
198     );
199     push @{$dates}, $special;
200     $self->{single_holidays} = DateTime::Set->from_datetimes( dates => $dates );
201     $self->{days_mode} = 'Calendar';
202     return;
203 }
204
205 1;
206 __END__
207
208 =head1 NAME
209
210 Koha::Calendar - Object containing a branches calendar
211
212 =head1 VERSION
213
214 This documentation refers to Koha::Calendar version 0.0.1
215
216 =head1 SYNOPSIS
217
218   use Koha::Calendat
219
220   my $c = Koha::Calender->new( branchcode => 'MAIN' );
221   my $dt = DateTime->now();
222
223   # are we open
224   $open = $c->is_holiday($dt);
225   # when will item be due if loan period = $dur (a DateTime::Duration object)
226   $duedate = $c->addDate($dt,$dur,'days');
227
228
229 =head1 DESCRIPTION
230
231   Implements those features of C4::Calendar needed for Staffs Rolling Loans
232
233 =head1 METHODS
234
235 =head2 new : Create a calendar object
236
237 my $calendar = Koha::Calendar->new( branchcode => 'MAIN' );
238
239 The option branchcode is required
240
241
242 =head2 addDate
243
244     my $dt = $calendar->addDate($date, $dur, $unit)
245
246 C<$date> is a DateTime object representing the starting date of the interval.
247
248 C<$offset> is a DateTime::Duration to add to it
249
250 C<$unit> is a string value 'days' or 'hours' toflag granularity of duration
251
252 Currently unit is only used to invoke Staffs return Monday at 10 am rule this
253 parameter will be removed when issuingrules properly cope with that
254
255
256 =head2 is_holiday
257
258 $yesno = $calendar->is_holiday($dt);
259
260 passed at DateTime object returns 1 if it is a closed day
261 0 if not according to the calendar
262
263 =head2 days_between
264
265 $duration = $calendar->days_between($start_dt, $end_dt);
266
267 Passed two dates returns a DateTime::Duration object measuring the length between them
268 ignoring closed days
269
270 =head1 DIAGNOSTICS
271
272 Will croak if not passed a branchcode in new
273
274 =head1 BUGS AND LIMITATIONS
275
276 This only contains a limited subset of the functionality in C4::Calendar
277 Only enough to support Staffs Rolling loans
278
279 =head1 AUTHOR
280
281 Colin Campbell colin.campbell@ptfs-europe.com
282
283 =head1 LICENSE AND COPYRIGHT
284
285 Copyright (c) 2011 PTFS-Europe Ltd All rights reserved
286
287 This program is free software: you can redistribute it and/or modify
288 it under the terms of the GNU General Public License as published by
289 the Free Software Foundation, either version 2 of the License, or
290 (at your option) any later version.
291
292 This program is distributed in the hope that it will be useful,
293 but WITHOUT ANY WARRANTY; without even the implied warranty of
294 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
295 GNU General Public License for more details.
296
297 You should have received a copy of the GNU General Public License
298 along with this program.  If not, see <http://www.gnu.org/licenses/>.