Bug 15260: Modify next/prev_open_day
[koha.git] / misc / cronjobs / thirdparty / TalkingTech_itiva_outbound.pl
1 #!/usr/bin/perl
2 #
3 # Copyright (C) 2011 ByWater Solutions
4 #
5 # This file is part of Koha.
6 #
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20 use strict;
21 use warnings;
22
23 BEGIN {
24
25     # find Koha's Perl modules
26     # test carefully before changing this
27     use FindBin;
28     eval { require "$FindBin::Bin/../kohalib.pl" };
29 }
30
31 use Getopt::Long;
32 use Pod::Usage;
33
34 use Koha::Script -cron;
35 use C4::Context;
36 use C4::Items;
37 use C4::Letters;
38 use C4::Overdues;
39 use Koha::Calendar;
40 use Koha::DateUtils;
41 use Koha::Patrons;
42 use Koha::Libraries;
43
44 sub usage {
45     pod2usage( -verbose => 2 );
46     exit;
47 }
48
49 die "TalkingTechItivaPhoneNotification system preference not activated... dying\n"
50   unless ( C4::Context->preference("TalkingTechItivaPhoneNotification") );
51
52 # Database handle
53 my $dbh = C4::Context->dbh;
54
55 # Options
56 my $verbose;
57 my $language = "EN";
58 my @types;
59 my @holds_waiting_days_to_call;
60 my $library_code;
61 my $help;
62 my $outfile;
63 my $skip_patrons_with_email;
64 my $patron_branchcode;
65
66 # maps to convert I-tiva terms to Koha terms
67 my $type_module_map = {
68     'PREOVERDUE' => 'circulation',
69     'OVERDUE'    => 'circulation',
70     'RESERVE'    => 'reserves',
71 };
72
73 my $type_notice_map = {
74     'PREOVERDUE' => 'PREDUE',
75     'OVERDUE'    => 'OVERDUE',
76     'RESERVE'    => 'HOLD',
77 };
78
79 GetOptions(
80     'o|output:s'             => \$outfile,
81     'v'                      => \$verbose,
82     'lang:s'                 => \$language,
83     'type:s'                 => \@types,
84     'w|waiting-hold-day:s'   => \@holds_waiting_days_to_call,
85     'c|code|library-code:s'  => \$library_code,
86     's|skip-patrons-with-email' => \$skip_patrons_with_email,
87     'pb|patron-branchcode:s' => \$patron_branchcode,
88     'h|help'                 => \$help,
89 );
90
91 $language = uc($language);
92 $library_code ||= '';
93
94 pod2usage( -verbose => 1 ) if $help;
95
96 if ($patron_branchcode) {
97     die("Invalid branchcode '$patron_branchcode' passed in -pb --patron-branchcode parameter")
98       unless Koha::Libraries->search( { branchcode => $patron_branchcode } )->count;
99 }
100
101 # output log or STDOUT
102 my $OUT;
103 if ( defined $outfile ) {
104     open( $OUT, '>', "$outfile" ) || die("Cannot open output file");
105 } else {
106     print "No output file defined; printing to STDOUT\n"
107       if ( defined $verbose );
108     $OUT = *STDOUT || die "Couldn't duplicate STDOUT: $!";
109 }
110
111 my $format = 'V';    # format for phone notifications
112
113 foreach my $type (@types) {
114     $type = uc($type);    #just in case lower or mixed-case was supplied
115     my $module = $type_module_map->{$type};    #since the module is required to get the letter
116     my $code   = $type_notice_map->{$type};    #to get the Koha name of the notice
117
118     my @loop;
119     if ( $type eq 'OVERDUE' ) {
120         @loop = GetOverdueIssues( $patron_branchcode );
121     } elsif ( $type eq 'PREOVERDUE' ) {
122         @loop = GetPredueIssues( $patron_branchcode );
123     } elsif ( $type eq 'RESERVE' ) {
124         @loop = GetWaitingHolds( $patron_branchcode );
125     } else {
126         print "Unknown or unsupported message type $type; skipping...\n"
127           if ( defined $verbose );
128         next;
129     }
130
131     my $patrons;
132     foreach my $issues (@loop) {
133         $patrons->{$issues->{borrowernumber}} ||= Koha::Patrons->find( $issues->{borrowernumber} ) if $skip_patrons_with_email;
134         next if $skip_patrons_with_email && $patrons->{$issues->{borrowernumber}}->notice_email_address;
135
136         my $date_dt = dt_from_string ( $issues->{'date_due'} );
137         my $due_date = output_pref( { dt => $date_dt, dateonly => 1, dateformat =>'metric' } );
138
139         my $letter = C4::Letters::GetPreparedLetter(
140             module      => $module,
141             letter_code => $code,
142             lang        => 'default', # It does not sound useful to send a lang here
143             tables      => {
144                 borrowers   => $issues->{'borrowernumber'},
145                 biblio      => $issues->{'biblionumber'},
146                 biblioitems => $issues->{'biblionumber'},
147             },
148             message_transport_type => 'phone',
149         );
150
151         die "No letter found for type $type!... dying\n" unless $letter;
152
153         my $message_id = 0;
154         if ($outfile) {
155             $message_id = C4::Letters::EnqueueLetter(
156                 {   letter                 => $letter,
157                     borrowernumber         => $issues->{'borrowernumber'},
158                     message_transport_type => 'phone',
159                 }
160             );
161         }
162
163         print $OUT "\"$format\",\"$language\",\"$type\",\"$issues->{level}\",\"$issues->{cardnumber}\",\"$issues->{patron_title}\",\"$issues->{firstname}\",";
164         print $OUT "\"$issues->{surname}\",\"$issues->{phone}\",\"$issues->{email}\",\"$library_code\",";
165         print $OUT "\"$issues->{site}\",\"$issues->{site_name}\",\"$issues->{barcode}\",\"$due_date\",\"$issues->{title}\",\"$message_id\"\n";
166     }
167 }
168
169 =head1 NAME
170
171 TalkingTech_itiva_outbound.pl
172
173 =head1 SYNOPSIS
174
175   TalkingTech_itiva_outbound.pl
176   TalkingTech_itiva_outbound.pl --type=OVERDUE -w 0 -w 2 -w 6 --output=/tmp/talkingtech/outbound.csv
177   TalkingTech_itiva_outbound.pl --type=RESERVE --type=PREOVERDUE --lang=FR
178
179
180 Script to generate Spec C outbound notifications file for Talking Tech i-tiva
181 phone notification system.
182
183 =over
184
185 =item B<--help> B<-h>
186
187 Prints this help
188
189 =item B<-v>
190
191 Provide verbose log information.
192
193 =item B<--output> B<-o>
194
195 Destination for outbound notifications file (CSV format).  If no value is specified,
196 output is dumped to screen.
197
198 =item B<--lang>
199
200 Sets the language for all outbound messages.  Currently supported values are EN, FR and ES.
201 If no value is specified, EN will be used by default.
202
203 =item B<--type>
204
205 REQUIRED. Sets which messaging types are to be used.  Can be given multiple times, to
206 specify multiple types in a single output file.  Currently supported values are RESERVE, PREOVERDUE
207 and OVERDUE.  If no value is given, this script will not produce any outbound notifications.
208
209 =item B<--waiting-hold-day> B<-w>
210
211 OPTIONAL for --type=RESERVE. Sets the days after a hold has been set to waiting on which to call. Use
212 switch as many times as desired. For example, passing "-w 0 -w 2 -w 6" will cause calls to be placed
213 on the day the hold was set to waiting, 2 days after the waiting date, and 6 days after. See example above.
214 If this switch is not used with --type=RESERVE, calls will be placed every day until the waiting reserve
215 is picked up or canceled.
216
217 =item B<--library-code> B<--code> B<-c>
218
219 OPTIONAL
220 The code of the source library of the message.
221 The library code is used to group notices together for
222 consortium purposes and apply library specific settings, such as
223 prompts, to those notices.
224 This field can be blank if all messages are from a single library.
225
226 =item B<--patron-branchcode> B<--pb>
227
228 OPTIONAL
229
230 Limits the the patrons to generate notices for based on the patron's home library.
231 Items and holds from other libraries will still be included for the given patron.
232
233 =back
234
235 =cut
236
237 sub GetOverdueIssues {
238     my ( $patron_branchcode ) = @_;
239
240     my $patron_branchcode_filter = $patron_branchcode ? "AND borrowers.branchcode = '$patron_branchcode'" : q{};
241
242     my $query = "SELECT borrowers.borrowernumber, borrowers.cardnumber, borrowers.title as patron_title, borrowers.firstname, borrowers.surname,
243                 borrowers.phone, borrowers.email, borrowers.branchcode, biblio.biblionumber, biblio.title, items.barcode, issues.date_due,
244                 max(overduerules.branchcode) as rulebranch, TO_DAYS(NOW())-TO_DAYS(date_due) as daysoverdue, delay1, delay2, delay3,
245                 issues.branchcode as site, branches.branchname as site_name
246                 FROM borrowers JOIN issues USING (borrowernumber)
247                 JOIN items USING (itemnumber)
248                 JOIN biblio USING (biblionumber)
249                 JOIN branches ON (issues.branchcode = branches.branchcode)
250                 JOIN overduerules USING (categorycode)
251                 JOIN overduerules_transport_types USING ( overduerules_id )
252                 WHERE ( overduerules.branchcode = borrowers.branchcode or overduerules.branchcode = '')
253                 AND overduerules_transport_types.message_transport_type = 'phone'
254                 AND ( (TO_DAYS(NOW())-TO_DAYS(date_due) ) = delay1
255                   OR  (TO_DAYS(NOW())-TO_DAYS(date_due) ) = delay2
256                   OR  (TO_DAYS(NOW())-TO_DAYS(date_due) ) = delay3 )
257                 $patron_branchcode_filter
258                 GROUP BY items.itemnumber
259                 ";
260     my $sth = $dbh->prepare($query);
261     $sth->execute();
262     my @results;
263     while ( my $issue = $sth->fetchrow_hashref() ) {
264         if ( $issue->{'daysoverdue'} == $issue->{'delay1'} ) {
265             $issue->{'level'} = 1;
266         } elsif ( $issue->{'daysoverdue'} == $issue->{'delay2'} ) {
267             $issue->{'level'} = 2;
268         } elsif ( $issue->{'daysoverdue'} == $issue->{'delay3'} ) {
269             $issue->{'level'} = 3;
270         } else {
271
272             # this shouldn't ever happen, based our SQL criteria
273         }
274         push @results, $issue;
275     }
276     return @results;
277 }
278
279 sub GetPredueIssues {
280     my ( $patron_branchcode ) = @_;
281
282     my $patron_branchcode_filter = $patron_branchcode ? "AND borrowers.branchcode = '$patron_branchcode'" : q{};
283
284     my $query = "SELECT borrowers.borrowernumber, borrowers.cardnumber, borrowers.title as patron_title, borrowers.firstname, borrowers.surname,
285                 borrowers.phone, borrowers.email, borrowers.branchcode, biblio.biblionumber, biblio.title, items.barcode, issues.date_due,
286                 issues.branchcode as site, branches.branchname as site_name
287                 FROM borrowers JOIN issues USING (borrowernumber)
288                 JOIN items USING (itemnumber)
289                 JOIN biblio USING (biblionumber)
290                 JOIN branches ON (issues.branchcode = branches.branchcode)
291                 JOIN borrower_message_preferences USING (borrowernumber)
292                 JOIN borrower_message_transport_preferences USING (borrower_message_preference_id)
293                 JOIN message_attributes USING (message_attribute_id)
294                 WHERE ( TO_DAYS( date_due ) - TO_DAYS( NOW() ) ) = days_in_advance
295                 AND message_transport_type = 'phone'
296                 AND message_name = 'Advance_Notice'
297                 $patron_branchcode_filter
298                 ";
299     my $sth = $dbh->prepare($query);
300     $sth->execute();
301     my @results;
302     while ( my $issue = $sth->fetchrow_hashref() ) {
303         $issue->{'level'} = 1;    # only one level for Predue notifications
304         push @results, $issue;
305     }
306     return @results;
307 }
308
309 sub GetWaitingHolds {
310     my ( $patron_branchcode ) = @_;
311
312     my $patron_branchcode_filter = $patron_branchcode ? "AND borrowers.branchcode = '$patron_branchcode'" : q{};
313
314     my $query = "SELECT borrowers.borrowernumber, borrowers.cardnumber, borrowers.title as patron_title, borrowers.firstname, borrowers.surname,
315                 borrowers.phone, borrowers.email, borrowers.branchcode, biblio.biblionumber, biblio.title, items.barcode, reserves.waitingdate,
316                 reserves.branchcode AS site, branches.branchname AS site_name,
317                 TO_DAYS(NOW())-TO_DAYS(reserves.waitingdate) AS days_since_waiting
318                 FROM borrowers JOIN reserves USING (borrowernumber)
319                 JOIN items USING (itemnumber)
320                 JOIN biblio ON (biblio.biblionumber = items.biblionumber)
321                 JOIN branches ON (reserves.branchcode = branches.branchcode)
322                 JOIN borrower_message_preferences USING (borrowernumber)
323                 JOIN borrower_message_transport_preferences USING (borrower_message_preference_id)
324                 JOIN message_attributes USING (message_attribute_id)
325                 WHERE ( reserves.found = 'W' )
326                 AND message_transport_type = 'phone'
327                 AND message_name = 'Hold_Filled'
328                 $patron_branchcode_filter
329                 ";
330     my $pickupdelay = C4::Context->preference("ReservesMaxPickUpDelay");
331     my $sth         = $dbh->prepare($query);
332     $sth->execute();
333     my @results;
334     while ( my $issue = $sth->fetchrow_hashref() ) {
335         my $calendar = Koha::Calendar->new( branchcode => $issue->{'site'} );
336
337         my $waiting_date = dt_from_string( $issue->{waitingdate}, 'sql' );
338         my $pickup_date = $waiting_date->clone->add( days => $pickupdelay );
339         if ( $calendar->is_holiday($pickup_date) ) {
340             $pickup_date = $calendar->next_open_days( $pickup_date, 1 );
341         }
342
343         $issue->{'date_due'} = output_pref({dt => $pickup_date, dateformat => 'iso' });
344         $issue->{'level'} = 1;    # only one level for Hold Waiting notifications
345
346         my $days_to_subtract = 0;
347         if ( $calendar->is_holiday($waiting_date) ) {
348             my $next_open_day = $calendar->next_open_days( $waiting_date, 1 );
349             $days_to_subtract = $calendar->days_between($waiting_date, $next_open_day)->days;
350         }
351
352         $issue->{'days_since_waiting'} = $issue->{'days_since_waiting'} - $days_to_subtract;
353
354         if ( ( grep $_ eq $issue->{'days_since_waiting'}, @holds_waiting_days_to_call )
355             || !scalar(@holds_waiting_days_to_call) ) {
356             push @results, $issue;
357         }
358     }
359     return @results;
360
361 }