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