83b8a1a4421de672980f46bc4dd03f97efba2b75
[srvgit] / 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 => 'itiva',
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 => 'itiva',
159                 }
160             );
161         }
162
163         $issues->{title} =~ s/'//g;
164         $issues->{title} =~ s/"//g;
165
166         print $OUT "\"$format\",\"$language\",\"$type\",\"$issues->{level}\",\"$issues->{cardnumber}\",\"$issues->{patron_title}\",\"$issues->{firstname}\",";
167         print $OUT "\"$issues->{surname}\",\"$issues->{phone}\",\"$issues->{email}\",\"$library_code\",";
168         print $OUT "\"$issues->{site}\",\"$issues->{site_name}\",\"$issues->{barcode}\",\"$due_date\",\"$issues->{title}\",\"$message_id\"\n";
169     }
170 }
171
172 =head1 NAME
173
174 TalkingTech_itiva_outbound.pl
175
176 =head1 SYNOPSIS
177
178   TalkingTech_itiva_outbound.pl
179   TalkingTech_itiva_outbound.pl --type=OVERDUE -w 0 -w 2 -w 6 --output=/tmp/talkingtech/outbound.csv
180   TalkingTech_itiva_outbound.pl --type=RESERVE --type=PREOVERDUE --lang=FR
181
182
183 Script to generate Spec C outbound notifications file for Talking Tech i-tiva
184 phone notification system.
185
186 =over
187
188 =item B<--help> B<-h>
189
190 Prints this help
191
192 =item B<-v>
193
194 Provide verbose log information.
195
196 =item B<--output> B<-o>
197
198 Destination for outbound notifications file (CSV format).  If no value is specified,
199 output is dumped to screen.
200
201 =item B<--lang>
202
203 Sets the language for all outbound messages.  Currently supported values are EN, FR and ES.
204 If no value is specified, EN will be used by default.
205
206 =item B<--type>
207
208 REQUIRED. Sets which messaging types are to be used.  Can be given multiple times, to
209 specify multiple types in a single output file.  Currently supported values are RESERVE, PREOVERDUE
210 and OVERDUE.  If no value is given, this script will not produce any outbound notifications.
211
212 =item B<--waiting-hold-day> B<-w>
213
214 OPTIONAL for --type=RESERVE. Sets the days after a hold has been set to waiting on which to call. Use
215 switch as many times as desired. For example, passing "-w 0 -w 2 -w 6" will cause calls to be placed
216 on the day the hold was set to waiting, 2 days after the waiting date, and 6 days after. See example above.
217 If this switch is not used with --type=RESERVE, calls will be placed every day until the waiting reserve
218 is picked up or canceled.
219
220 =item B<--library-code> B<--code> B<-c>
221
222 OPTIONAL
223 The code of the source library of the message.
224 The library code is used to group notices together for
225 consortium purposes and apply library specific settings, such as
226 prompts, to those notices.
227 This field can be blank if all messages are from a single library.
228
229 =item B<--patron-branchcode> B<--pb>
230
231 OPTIONAL
232
233 Limits the the patrons to generate notices for based on the patron's home library.
234 Items and holds from other libraries will still be included for the given patron.
235
236 =back
237
238 =cut
239
240 sub GetOverdueIssues {
241     my ( $patron_branchcode ) = @_;
242
243     my $patron_branchcode_filter = $patron_branchcode ? "AND borrowers.branchcode = '$patron_branchcode'" : q{};
244
245     my $query = "SELECT borrowers.borrowernumber, borrowers.cardnumber, borrowers.title as patron_title, borrowers.firstname, borrowers.surname,
246                 borrowers.phone, borrowers.email, borrowers.branchcode, biblio.biblionumber, biblio.title, items.barcode, issues.date_due,
247                 max(overduerules.branchcode) as rulebranch, TO_DAYS(NOW())-TO_DAYS(date_due) as daysoverdue, delay1, delay2, delay3,
248                 issues.branchcode as site, branches.branchname as site_name
249                 FROM borrowers JOIN issues USING (borrowernumber)
250                 JOIN items USING (itemnumber)
251                 JOIN biblio USING (biblionumber)
252                 JOIN branches ON (issues.branchcode = branches.branchcode)
253                 JOIN overduerules USING (categorycode)
254                 JOIN overduerules_transport_types USING ( overduerules_id )
255                 WHERE ( overduerules.branchcode = borrowers.branchcode or overduerules.branchcode = '')
256                 AND overduerules_transport_types.message_transport_type = 'itiva'
257                 AND ( (TO_DAYS(NOW())-TO_DAYS(date_due) ) = delay1
258                   OR  (TO_DAYS(NOW())-TO_DAYS(date_due) ) = delay2
259                   OR  (TO_DAYS(NOW())-TO_DAYS(date_due) ) = delay3 )
260                 $patron_branchcode_filter
261                 GROUP BY items.itemnumber
262                 ";
263     my $sth = $dbh->prepare($query);
264     $sth->execute();
265     my @results;
266     while ( my $issue = $sth->fetchrow_hashref() ) {
267         if ( $issue->{'daysoverdue'} == $issue->{'delay1'} ) {
268             $issue->{'level'} = 1;
269         } elsif ( $issue->{'daysoverdue'} == $issue->{'delay2'} ) {
270             $issue->{'level'} = 2;
271         } elsif ( $issue->{'daysoverdue'} == $issue->{'delay3'} ) {
272             $issue->{'level'} = 3;
273         } else {
274
275             # this shouldn't ever happen, based our SQL criteria
276         }
277         push @results, $issue;
278     }
279     return @results;
280 }
281
282 sub GetPredueIssues {
283     my ( $patron_branchcode ) = @_;
284
285     my $patron_branchcode_filter = $patron_branchcode ? "AND borrowers.branchcode = '$patron_branchcode'" : q{};
286
287     my $query = "SELECT borrowers.borrowernumber, borrowers.cardnumber, borrowers.title as patron_title, borrowers.firstname, borrowers.surname,
288                 borrowers.phone, borrowers.email, borrowers.branchcode, biblio.biblionumber, biblio.title, items.barcode, issues.date_due,
289                 issues.branchcode as site, branches.branchname as site_name
290                 FROM borrowers JOIN issues USING (borrowernumber)
291                 JOIN items USING (itemnumber)
292                 JOIN biblio USING (biblionumber)
293                 JOIN branches ON (issues.branchcode = branches.branchcode)
294                 JOIN borrower_message_preferences USING (borrowernumber)
295                 JOIN borrower_message_transport_preferences USING (borrower_message_preference_id)
296                 JOIN message_attributes USING (message_attribute_id)
297                 WHERE ( TO_DAYS( date_due ) - TO_DAYS( NOW() ) ) = days_in_advance
298                 AND message_transport_type = 'itiva'
299                 AND message_name = 'Advance_Notice'
300                 $patron_branchcode_filter
301                 ";
302     my $sth = $dbh->prepare($query);
303     $sth->execute();
304     my @results;
305     while ( my $issue = $sth->fetchrow_hashref() ) {
306         $issue->{'level'} = 1;    # only one level for Predue notifications
307         push @results, $issue;
308     }
309     return @results;
310 }
311
312 sub GetWaitingHolds {
313     my ( $patron_branchcode ) = @_;
314
315     my $patron_branchcode_filter = $patron_branchcode ? "AND borrowers.branchcode = '$patron_branchcode'" : q{};
316
317     my $query = "SELECT borrowers.borrowernumber, borrowers.cardnumber, borrowers.title as patron_title, borrowers.firstname, borrowers.surname, borrowers.categorycode,
318                 borrowers.phone, borrowers.email, borrowers.branchcode, biblio.biblionumber, biblio.title, items.barcode, reserves.waitingdate,
319                 reserves.branchcode AS site, branches.branchname AS site_name,
320                 TO_DAYS(NOW())-TO_DAYS(reserves.waitingdate) AS days_since_waiting
321                 FROM borrowers JOIN reserves USING (borrowernumber)
322                 JOIN items USING (itemnumber)
323                 JOIN biblio ON (biblio.biblionumber = items.biblionumber)
324                 JOIN branches ON (reserves.branchcode = branches.branchcode)
325                 JOIN borrower_message_preferences USING (borrowernumber)
326                 JOIN borrower_message_transport_preferences USING (borrower_message_preference_id)
327                 JOIN message_attributes USING (message_attribute_id)
328                 WHERE ( reserves.found = 'W' )
329                 AND message_transport_type = 'itiva'
330                 AND message_name = 'Hold_Filled'
331                 $patron_branchcode_filter
332                 ";
333     my $pickupdelay = C4::Context->preference("ReservesMaxPickUpDelay");
334     my $sth         = $dbh->prepare($query);
335     $sth->execute();
336     my @results;
337     while ( my $issue = $sth->fetchrow_hashref() ) {
338         my $item = Koha::Items->find({ barcode => $issue->{barcode} });
339         my $daysmode = Koha::CirculationRules->get_effective_daysmode(
340             {
341                 categorycode => $issue->{categorycode},
342                 itemtype     => $item->effective_itemtype,
343                 branchcode   => $issue->{site},
344             }
345         );
346
347         my $calendar = Koha::Calendar->new( branchcode => $issue->{'site'}, days_mode => $daysmode );
348
349         my $waiting_date = dt_from_string( $issue->{waitingdate}, 'sql' );
350         my $pickup_date = $waiting_date->clone->add( days => $pickupdelay );
351         if ( $calendar->is_holiday($pickup_date) ) {
352             $pickup_date = $calendar->next_open_days( $pickup_date, 1 );
353         }
354
355         $issue->{'date_due'} = output_pref({dt => $pickup_date, dateformat => 'iso' });
356         $issue->{'level'} = 1;    # only one level for Hold Waiting notifications
357
358         my $days_to_subtract = 0;
359         if ( $calendar->is_holiday($waiting_date) ) {
360             my $next_open_day = $calendar->next_open_days( $waiting_date, 1 );
361             $days_to_subtract = $calendar->days_between($waiting_date, $next_open_day)->days;
362         }
363
364         $issue->{'days_since_waiting'} = $issue->{'days_since_waiting'} - $days_to_subtract;
365
366         if ( ( grep $_ eq $issue->{'days_since_waiting'}, @holds_waiting_days_to_call )
367             || !scalar(@holds_waiting_days_to_call) ) {
368             push @results, $issue;
369         }
370     }
371     return @results;
372
373 }