Bug 19620: Allow skipping of patrons with valid emails for Talking Tech
[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 C4::Context;
35 use C4::Items;
36 use C4::Letters;
37 use C4::Overdues;
38 use Koha::Calendar;
39 use Koha::DateUtils;
40 use Koha::Patrons;
41
42 sub usage {
43     pod2usage( -verbose => 2 );
44     exit;
45 }
46
47 die "TalkingTechItivaPhoneNotification system preference not activated... dying\n"
48   unless ( C4::Context->preference("TalkingTechItivaPhoneNotification") );
49
50 # Database handle
51 my $dbh = C4::Context->dbh;
52
53 # Options
54 my $verbose;
55 my $language = "EN";
56 my @types;
57 my @holds_waiting_days_to_call;
58 my $library_code;
59 my $help;
60 my $outfile;
61 my $skip_patrons_with_email;
62
63 # maps to convert I-tiva terms to Koha terms
64 my $type_module_map = {
65     'PREOVERDUE' => 'circulation',
66     'OVERDUE'    => 'circulation',
67     'RESERVE'    => 'reserves',
68 };
69
70 my $type_notice_map = {
71     'PREOVERDUE' => 'PREDUE',
72     'OVERDUE'    => 'OVERDUE',
73     'RESERVE'    => 'HOLD',
74 };
75
76 GetOptions(
77     'o|output:s'            => \$outfile,
78     'v'                     => \$verbose,
79     'lang:s'                => \$language,
80     'type:s'                => \@types,
81     'w|waiting-hold-day:s'  => \@holds_waiting_days_to_call,
82     'c|code|library-code:s' => \$library_code,
83     's|skip-patrons-with-email' => \$skip_patrons_with_email,
84     'help|h'                => \$help,
85 );
86
87 $language = uc($language);
88 $library_code ||= '';
89
90 pod2usage( -verbose => 1 ) if $help;
91
92 # output log or STDOUT
93 my $OUT;
94 if ( defined $outfile ) {
95     open( $OUT, '>', "$outfile" ) || die("Cannot open output file");
96 } else {
97     print "No output file defined; printing to STDOUT\n"
98       if ( defined $verbose );
99     open( $OUT, '>', "&STDOUT" ) || die("Couldn't duplicate STDOUT: $!");
100 }
101
102 my $format = 'V';    # format for phone notifications
103
104 foreach my $type (@types) {
105     $type = uc($type);    #just in case lower or mixed-case was supplied
106     my $module = $type_module_map->{$type};    #since the module is required to get the letter
107     my $code   = $type_notice_map->{$type};    #to get the Koha name of the notice
108
109     my @loop;
110     if ( $type eq 'OVERDUE' ) {
111         @loop = GetOverdueIssues();
112     } elsif ( $type eq 'PREOVERDUE' ) {
113         @loop = GetPredueIssues();
114     } elsif ( $type eq 'RESERVE' ) {
115         @loop = GetWaitingHolds();
116     } else {
117         print "Unknown or unsupported message type $type; skipping...\n"
118           if ( defined $verbose );
119         next;
120     }
121
122     my $patrons;
123     foreach my $issues (@loop) {
124         $patrons->{$issues->{borrowernumber}} ||= Koha::Patrons->find( $issues->{borrowernumber} ) if $skip_patrons_with_email;
125         next if $skip_patrons_with_email && $patrons->{$issues->{borrowernumber}}->notice_email_address;
126
127         my $date_dt = dt_from_string ( $issues->{'date_due'} );
128         my $due_date = output_pref( { dt => $date_dt, dateonly => 1, dateformat =>'metric' } );
129
130         my $letter = C4::Letters::GetPreparedLetter(
131             module      => $module,
132             letter_code => $code,
133             lang        => 'default', # It does not sound useful to send a lang here
134             tables      => {
135                 borrowers   => $issues->{'borrowernumber'},
136                 biblio      => $issues->{'biblionumber'},
137                 biblioitems => $issues->{'biblionumber'},
138             },
139             message_transport_type => 'phone',
140         );
141
142         die "No letter found for type $type!... dying\n" unless $letter;
143
144         my $message_id = 0;
145         if ($outfile) {
146             $message_id = C4::Letters::EnqueueLetter(
147                 {   letter                 => $letter,
148                     borrowernumber         => $issues->{'borrowernumber'},
149                     message_transport_type => 'phone',
150                 }
151             );
152         }
153
154         print $OUT "\"$format\",\"$language\",\"$type\",\"$issues->{level}\",\"$issues->{cardnumber}\",\"$issues->{patron_title}\",\"$issues->{firstname}\",";
155         print $OUT "\"$issues->{surname}\",\"$issues->{phone}\",\"$issues->{email}\",\"$library_code\",";
156         print $OUT "\"$issues->{site}\",\"$issues->{site_name}\",\"$issues->{barcode}\",\"$due_date\",\"$issues->{title}\",\"$message_id\"\n";
157     }
158 }
159
160 =head1 NAME
161
162 TalkingTech_itiva_outbound.pl
163
164 =head1 SYNOPSIS
165
166   TalkingTech_itiva_outbound.pl
167   TalkingTech_itiva_outbound.pl --type=OVERDUE -w 0 -w 2 -w 6 --output=/tmp/talkingtech/outbound.csv
168   TalkingTech_itiva_outbound.pl --type=RESERVE --type=PREOVERDUE --lang=FR
169
170
171 Script to generate Spec C outbound notifications file for Talking Tech i-tiva
172 phone notification system.
173
174 =over
175
176 =item B<--help> B<-h>
177
178 Prints this help
179
180 =item B<-v>
181
182 Provide verbose log information.
183
184 =item B<--output> B<-o>
185
186 Destination for outbound notifications file (CSV format).  If no value is specified,
187 output is dumped to screen.
188
189 =item B<--lang>
190
191 Sets the language for all outbound messages.  Currently supported values are EN, FR and ES.
192 If no value is specified, EN will be used by default.
193
194 =item B<--type>
195
196 REQUIRED. Sets which messaging types are to be used.  Can be given multiple times, to
197 specify multiple types in a single output file.  Currently supported values are RESERVE, PREOVERDUE
198 and OVERDUE.  If no value is given, this script will not produce any outbound notifications.
199
200 =item B<--waiting-hold-day> B<-w>
201
202 OPTIONAL for --type=RESERVE. Sets the days after a hold has been set to waiting on which to call. Use
203 switch as many times as desired. For example, passing "-w 0 -w 2 -w 6" will cause calls to be placed
204 on the day the hold was set to waiting, 2 days after the waiting date, and 6 days after. See example above.
205 If this switch is not used with --type=RESERVE, calls will be placed every day until the waiting reserve
206 is picked up or canceled.
207
208 =item B<--library-code> B<--code> B<-c>
209
210 OPTIONAL
211 The code of the source library of the message.
212 The library code is used to group notices together for
213 consortium purposes and apply library specific settings, such as
214 prompts, to those notices.
215 This field can be blank if all messages are from a single library.
216
217 =back
218
219 =cut
220
221 sub GetOverdueIssues {
222     my $query = "SELECT borrowers.borrowernumber, borrowers.cardnumber, borrowers.title as patron_title, borrowers.firstname, borrowers.surname,
223                 borrowers.phone, borrowers.email, borrowers.branchcode, biblio.biblionumber, biblio.title, items.barcode, issues.date_due,
224                 max(overduerules.branchcode) as rulebranch, TO_DAYS(NOW())-TO_DAYS(date_due) as daysoverdue, delay1, delay2, delay3,
225                 issues.branchcode as site, branches.branchname as site_name
226                 FROM borrowers JOIN issues USING (borrowernumber)
227                 JOIN items USING (itemnumber)
228                 JOIN biblio USING (biblionumber)
229                 JOIN branches ON (issues.branchcode = branches.branchcode)
230                 JOIN overduerules USING (categorycode)
231                 JOIN overduerules_transport_types USING ( overduerules_id )
232                 WHERE ( overduerules.branchcode = borrowers.branchcode or overduerules.branchcode = '')
233                 AND overduerules_transport_types.message_transport_type = 'phone'
234                 AND ( (TO_DAYS(NOW())-TO_DAYS(date_due) ) = delay1
235                   OR  (TO_DAYS(NOW())-TO_DAYS(date_due) ) = delay2
236                   OR  (TO_DAYS(NOW())-TO_DAYS(date_due) ) = delay3 )
237                 GROUP BY items.itemnumber
238                 ";
239     my $sth = $dbh->prepare($query);
240     $sth->execute();
241     my @results;
242     while ( my $issue = $sth->fetchrow_hashref() ) {
243         if ( $issue->{'daysoverdue'} == $issue->{'delay1'} ) {
244             $issue->{'level'} = 1;
245         } elsif ( $issue->{'daysoverdue'} == $issue->{'delay2'} ) {
246             $issue->{'level'} = 2;
247         } elsif ( $issue->{'daysoverdue'} == $issue->{'delay3'} ) {
248             $issue->{'level'} = 3;
249         } else {
250
251             # this shouldn't ever happen, based our SQL criteria
252         }
253         push @results, $issue;
254     }
255     return @results;
256 }
257
258 sub GetPredueIssues {
259     my $query = "SELECT borrowers.borrowernumber, borrowers.cardnumber, borrowers.title as patron_title, borrowers.firstname, borrowers.surname,
260                 borrowers.phone, borrowers.email, borrowers.branchcode, biblio.biblionumber, biblio.title, items.barcode, issues.date_due,
261                 issues.branchcode as site, branches.branchname as site_name
262                 FROM borrowers JOIN issues USING (borrowernumber)
263                 JOIN items USING (itemnumber)
264                 JOIN biblio USING (biblionumber)
265                 JOIN branches ON (issues.branchcode = branches.branchcode)
266                 JOIN borrower_message_preferences USING (borrowernumber)
267                 JOIN borrower_message_transport_preferences USING (borrower_message_preference_id)
268                 JOIN message_attributes USING (message_attribute_id)
269                 WHERE ( TO_DAYS( date_due ) - TO_DAYS( NOW() ) ) = days_in_advance
270                 AND message_transport_type = 'phone'
271                 AND message_name = 'Advance_Notice'
272                 ";
273     my $sth = $dbh->prepare($query);
274     $sth->execute();
275     my @results;
276     while ( my $issue = $sth->fetchrow_hashref() ) {
277         $issue->{'level'} = 1;    # only one level for Predue notifications
278         push @results, $issue;
279     }
280     return @results;
281 }
282
283 sub GetWaitingHolds {
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, reserves.waitingdate,
286                 reserves.branchcode AS site, branches.branchname AS site_name,
287                 TO_DAYS(NOW())-TO_DAYS(reserves.waitingdate) AS days_since_waiting
288                 FROM borrowers JOIN reserves USING (borrowernumber)
289                 JOIN items USING (itemnumber)
290                 JOIN biblio ON (biblio.biblionumber = items.biblionumber)
291                 JOIN branches ON (reserves.branchcode = branches.branchcode)
292                 JOIN borrower_message_preferences USING (borrowernumber)
293                 JOIN borrower_message_transport_preferences USING (borrower_message_preference_id)
294                 JOIN message_attributes USING (message_attribute_id)
295                 WHERE ( reserves.found = 'W' )
296                 AND message_transport_type = 'phone'
297                 AND message_name = 'Hold_Filled'
298                 ";
299     my $pickupdelay = C4::Context->preference("ReservesMaxPickUpDelay");
300     my $sth         = $dbh->prepare($query);
301     $sth->execute();
302     my @results;
303     while ( my $issue = $sth->fetchrow_hashref() ) {
304         my $calendar = Koha::Calendar->new( branchcode => $issue->{'site'} );
305
306         my $waiting_date = dt_from_string( $issue->{waitingdate}, 'sql' );
307         my $pickup_date = $waiting_date->clone->add( days => $pickupdelay );
308         if ( $calendar->is_holiday($pickup_date) ) {
309             $pickup_date = $calendar->next_open_day( $pickup_date );
310         }
311
312         $issue->{'date_due'} = output_pref({dt => $pickup_date, dateformat => 'iso' });
313         $issue->{'level'} = 1;    # only one level for Hold Waiting notifications
314
315         my $days_to_subtract = 0;
316         if ( $calendar->is_holiday($waiting_date) ) {
317             my $next_open_day = $calendar->next_open_day( $waiting_date );
318             $days_to_subtract = $calendar->days_between($waiting_date, $next_open_day)->days;
319         }
320
321         $issue->{'days_since_waiting'} = $issue->{'days_since_waiting'} - $days_to_subtract;
322
323         if ( ( grep $_ eq $issue->{'days_since_waiting'}, @holds_waiting_days_to_call )
324             || !scalar(@holds_waiting_days_to_call) ) {
325             push @results, $issue;
326         }
327     }
328     return @results;
329
330 }