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