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