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