Still Code Cleaning.
[koha.git] / misc / notifys / fines.pl
1 #!/usr/bin/perl
2
3 # This file is part of Koha.
4 #
5 # Koha is free software; you can redistribute it and/or modify it under the
6 # terms of the GNU General Public License as published by the Free Software
7 # Foundation; either version 2 of the License, or (at your option) any later
8 # version.
9 #
10 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License along with
15 # Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
16 # Suite 330, Boston, MA  02111-1307 USA
17
18
19 use C4::Members;
20 use C4::Circulation;
21 use C4::Overdues;
22 use Date::Manip;
23
24 use Mail::Sendmail;
25 use Mail::RFC822::Address;
26 use C4::Biblio;
27 use strict;
28
29
30 #levyFines();    # Do not levy real fines in testing situation.
31 notifyOverdues();
32
33
34
35 # Todo
36 #     - Need to calculate the fine on each book; no idea how to get this information from Koha
37 #    - Need to diffentricate between the total_fines including replacement costs,
38 #    and the total fines if the books are returned in the day 29 notices (see above).
39 #    - clean up the %actions hash creation code.
40
41 #Done
42 #     - preferedcont field in borrowers hash; does this do anything?
43 #    - logging
44 #    - which 'address' to send sms to?
45 #    - senders returning success or fail
46
47
48
49 sub levyFines {
50     # Look at the current overdues, and levy fines on the offenders.
51     # arguments:
52     #    $date
53     #     $maxfine
54     
55     # Work out what today is as an integer value.
56     my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) =localtime(time);
57     $mon++; $year=$year+1900;
58     my $date=Date_DaysSince1BC($mon,$mday,$year);
59     my $maxfine =5;
60
61
62     # Retrieve an array of overdues.
63     my ($count, $overduesReference) = Getoverdues();
64     print "$count overdue items where found.\n\n";
65     my @overdues=@$overduesReference;
66
67     foreach my $overdue (@overdues) {
68           my @dates=split('-',$overdue->{'date_due'});
69           my $due_day=Date_DaysSince1BC($dates[1],$dates[2],$dates[0]);
70
71
72         # Check that the item is really overdue. The output of Getoverdues() will normally
73         # always be overdue items. However, if you are running this script with a value of $date other than the current time, this check is needed.
74         if ($due_day <= $date) {
75              my $difference=$date-$due_day;
76             print "Itemnumber ".$overdue->{'itemnumber'}." is issued to ".$overdue->{'borrowernumber'}." is overdue by $difference days.\n";
77
78             # Calculate the cost of this overdue.
79             # Fines vary according to borrower type, but cannot exceed the maximum fine.
80             print $overdue->{'borrowernumber'};
81             my $borrower=BorType($overdue->{'borrowernumber'});
82
83                 my ($amount,$type,$printout)=CalcFine($overdue->{'itemnumber'}, $borrower->{'categorycode'}, $difference);
84             if ($amount > $maxfine){
85                       $amount=$maxfine;
86                     }
87
88             if ($amount > 0){
89                 my $due="$dates[2]/$dates[1]/$dates[0]";
90                       UpdateFine($overdue->{'itemnumber'}, $overdue->{'borrowernumber'}, $amount, $type, $due);
91                 print $overdue->{'borrowernumber'}." has been fined $amount for itemnumber ".$overdue->{'itemnumber'}." overdue for $difference days.\n";
92                 }
93
94     
95
96             # After 28 days, the item is marked lost and the replacement charge is added as a fine
97             if ($difference >= 28) {
98                       my $borrower=BorType($overdue->{'borrowernumber'});
99                       if ($borrower->{'cardnumber'} ne ''){
100                            my $cost=ReplacementCost($overdue->{'itemnumber'});
101                         my $dbh=C4Connect();
102                         my $accountno=C4::Circulation::getnextacctno($overdue->{'borrowernumber'});
103                            my $item=GetBiblioFromItemNumber($overdue->{'itemnumber'});
104                         if ($item->{'itemlost'} ne '1' && $item->{'itemlost'} ne '2' ){
105                               $item->{'title'}=~ s/\'/\\'/g;
106                               my $query="Insert into accountlines (borrowernumber,itemnumber,accountno,date,amount, description,accounttype,amountoutstanding)
107                                 values ($overdue->{'borrowernumber'}, $overdue->{'itemnumber'},
108                                         '$accountno',now(),'$cost','Lost item $item->{'title'} $item->{'barcode'}','L','$cost')";
109
110                                my $sth=$dbh->prepare($query);
111                                $sth->execute();
112                               $sth->finish();
113             
114                         $query="update items set itemlost=2 where itemnumber='$overdue->{'itemnumber'}'";
115                               $sth=$dbh->prepare($query);
116                               $sth->execute();
117                               $sth->finish();
118                             }
119                     }
120                 }
121                    }
122         }
123
124     return 1;
125     }
126
127
128
129
130
131
132 sub    notifyOverdues {
133     # Look up the overdues for today.
134     # Capture overdues which fall on our dates of interest.
135
136
137
138
139 ####################################################################################################
140 # Creating a big hash of available templates
141 my %email;
142 %email->{'template'}='email-8.txt';
143 my %sms; 
144 %sms->{'template'}='sms-8.txt';
145
146 my %fax1;
147 %fax1->{'template'}='fax-8.html';
148
149 my %firstReminder->{'email'} = \%email;
150 %firstReminder->{'sms'} = \%sms;
151 %firstReminder->{'fax'} = \%fax1;
152     
153 my %email2;
154 %email2->{'template'}='email-15.txt';
155
156 my %fax2;
157 %fax2->{'template'}='fax-15.html';
158     
159 my %letter2;
160 %letter2->{'template'}='fax-15.html';
161     
162 my %sms2->{'template'}='sms-15.txt';
163 my %secondReminder->{'email'} = \%email2;
164 %secondReminder->{'sms'} = \%sms2;
165 %secondReminder->{'fax'} = \%fax2;
166 %secondReminder->{'letter'} = \%letter2;    
167
168
169 my %email3;
170 %email3->{'template'}='email-29.txt';
171 my %fax3;
172 %fax3->{'template'}='fax-29.html';
173 my %letter3;
174 %letter3->{'template'}='letter-29.html';
175
176 my %finalReminder->{'email'} = \%email3;
177 %finalReminder->{'fax'} = \%fax3;
178 %finalReminder->{'letter'} = \%letter3;
179
180 my $fines;
181 my %actions;
182 %actions->{'8'}=\%firstReminder;
183 %actions->{'15'}=\%secondReminder;
184 %actions->{'29'}=\%finalReminder;
185
186 ##################################################################################################################
187
188
189         # Work out what today is as an integer value.
190         my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) =localtime(time);
191         $mon++; $year=$year+1900;
192         my $date=Date_DaysSince1BC($mon,$mday,$year);
193
194
195         # Retrieve an array of overdues.
196         my ($count, $overduesReference) = Getoverdues();
197         print "$count overdue items where found.\n\n";
198         my @overdues=@$overduesReference;
199
200
201     # We're going to build a hash of arrays, containing the items requiring action.
202     # ->borrowernumber, date, @overdues
203     my %actionItems;
204     foreach my $actionday (keys(%actions)) {
205         my @items=();
206         %actionItems->{$actionday} = \@items;
207         }
208     
209
210
211         foreach my $overdue (@overdues) {
212                 my @dates=split('-',$overdue->{'date_due'});
213                 my $due_day=Date_DaysSince1BC($dates[1],$dates[2],$dates[0]);
214
215                    my $difference=$date-$due_day;
216 #            $overdue->{'fine'}=GetFine($overdue->{'itemnumber'});
217         # If does this item fall on a day of interest?
218                                 $overdue->{'difference'}=$difference;
219         foreach my $actiondate (keys(%actions)) {
220             if ($actiondate == $difference) {
221                 my @items = @{%actionItems->{$actiondate}};
222
223                 my %o = %$overdue;
224                 push (@items, \%o);
225                 %actionItems->{$actiondate} = \@items;
226                 }
227             }
228         }
229
230
231
232
233     # We now have a hash containing overdues which need actioning,  we can step through each set.
234     # Work from earilest to latest. We only wish to send the most urgent message.
235     my %messages;
236         my %borritem;
237
238     foreach my $actiondate (sort {$a <=> $b} (keys(%actions))) {
239         print "\n\nThe following items are $actiondate days overdue.\n";
240         my @items = @{%actionItems->{$actiondate}};
241     
242     
243         foreach my $overdue (@items) {
244             if ($overdue->{'difference'} eq $actiondate) {
245                 # Detemine which borrower is responsible for this overdue;
246                 # if the offender is a child, then the garentor is the person to notify
247                 my $borrower=responsibleBorrower($overdue);
248
249
250                 my ($method, $address) = preferedContactMethod($borrower);
251                 if ($method) {
252     
253                     # Do we have to send something, using this method on this day?
254                     if (%actions->{$actiondate}->{$method}->{'template'}) {
255                         # If this user has one overdue, then they may have offers as well.
256                         # No point in sending a notice without mentioning all of the items.
257                         my @alloverdues;
258                         foreach my $over (@overdues) {
259                             my $responisble= responsibleBorrower($over);
260                             if ($responisble->{'borrowernumber'} eq $borrower->{'borrowernumber'}) {
261                                     $over->{'borrowernumber'}=$responisble->{'borrowernumber'};
262                                  my %o = %$over;
263                                 push (@alloverdues, \%o);
264                                 }
265                             }
266     
267                         my $dbh=C4Connect();    # FIXME disconnect this
268
269                         # Template the message
270                         my $template = HTML::Template->new(filename => 'templates/'.%actions->{$actiondate}->{$method}->{'template'}, die_on_bad_params => 0);
271
272                         my @bookdetails;
273                         my $total_fines = 0;
274                         foreach my $over (@alloverdues) {
275                             my %row_data;    
276                              if ( my $item = GetBiblioFromItemNumber( $over->{'itemnumber'})){
277                                 print "getting fine ($over->{'itemnumber'} $overdue->{'borrowernumber'} $over->{'borrowernumber'}\n";
278                                 my $fine = GetFine($over->{'itemnumber'},$overdue->{'borrowernumber'});
279     
280     
281                                 print "fine=$fine  ";
282
283                                   my $rep = ReplacementCost2($over->{'itemnumber'},$overdue->{'borrowernumber'});
284
285                                 if ($rep){
286                                  $rep+=0.00;
287                                 }
288                                 if ($fine){
289                                 $fine+=0.00;
290                                  $borritem{"$over->{'itemnumber'} $over->{'borrowernumber'}"}=$fine;
291                                 } else {
292                                 $borritem{"$over->{'itemnumber'} $over->{'borrowernumber'}"}+=$fine;
293                                 }
294                                   print $borritem{"$over->{'itemnumber'} $over->{'borrowernumber'}"},"\n";
295                                 $total_fines +=  $borritem{"$over->{'itemnumber'} $over->{'borrowernumber'}"};
296                             $item->{'title'}=substr($item->{'title'},0,25);
297                             my $len=length($item->{'title'});
298                             if ($len < 25){
299                                 my $diff=25-$len;
300                                 $item->{'title'}.=" " x $diff;
301                                 }
302
303                                                 $row_data{'BARCODE'}=$item->{'barcode'};
304                                                 $row_data{'TITLE'}=$item->{'title'};
305                                                 $row_data{'DATE_DUE'}=$over->{'date_due'};
306                                 $row_data{'FINE'}=$borritem{"$over->{'itemnumber'} $over->{'borrowernumber'}"};
307                             $row_data{'REP'}=$rep;
308
309                                                 push(@bookdetails, \%row_data);
310                                 } else {
311                                 print "Missing item  $over->{'itemnumber'}\n";
312                                 }
313                                                 }
314
315                                 $template->param(BOOKDETAILS => \@bookdetails);
316                                 my %params;
317                                 %params->{'borrowernumber'} = $overdue->{'borrowernumber'};
318                                 my ($total, $acctlines, $count) = &GetMemberAccountRecords($overdue->{'borrowernumber'});
319                                             $template->param(FINES_TOTAL => $total_fines);
320                             $template->param(OWING => $total);
321                             my $name= "$borrower->{'firstname'} $borrower->{'surname'}";
322                             $template->param(NAME=> $name);
323     
324                         %messages->{$borrower->{'borrowernumber'}} = $template->output();
325                         }
326                     else    {
327                         print "No $method needs to be sent at $overdue->{'difference'} days; not sending\n";
328                         }
329     
330                     }
331                 else    {
332                     print "This borrower has an overdue item, but no means of contact\n";
333                     }
334
335                 } #end of 'if this overdue falls on an action date'
336
337             } #end of 'foreach overdue'
338
339         } # end of foreach actiondate
340
341
342     # How that all of the messsages to be sent have been composed, send them.
343     foreach my $borrowernumber (keys(%messages)) {
344         print "$borrowernumber\n";
345
346            my $borrower=BorType($borrowernumber);
347         my ($method, $address) = preferedContactMethod($borrower);
348
349         my $result=0;
350         if ($method eq 'email') {
351             $result = sendEmail($address, 'lep@library.org.nz', 'Overdue Library Items', %messages->{$borrowernumber});
352             }
353         elsif ($method eq 'sms') {
354             $result = sendSMS($address, %messages->{$borrowernumber});
355             }
356         elsif ($method eq 'fax') {
357             $result = sendFax($address, %messages->{$borrowernumber});
358             }
359         elsif ($method eq 'letter') {
360             $result = printLetter($address, %messages->{$borrowernumber});
361             }
362
363
364         #print %messages->{$borrowernumber};    # debug
365
366
367         # Log the outcome of this attempt
368         logContact($borrowernumber, $method, $address, $result, %messages->{$borrowernumber});
369         }
370
371
372
373     return 1;
374     }
375
376
377
378
379
380
381
382
383
384
385 sub    responsibleBorrower {
386     # Given an overdue item, return the details of the borrower responible as a hash of database columns.
387     my $overdue=$_[0];
388
389     if ($overdue->{'borrowernumber'}) {
390         my $borrower=BorType($overdue->{'borrowernumber'});
391
392
393         # Overdue books assigned to children have notices sent to the guarantor.
394            if ($borrower->{'categorycode'} eq 'C') {
395                 my $dbh=C4Connect();
396                 my $query="Select     borrowernumber from borrowers
397                         where borrowernumber=?";
398
399                 my $sth=$dbh->prepare($query);
400                 $sth->execute($borrower->{'guarantor'});
401
402                 my $tdata=$sth->fetchrow_hashref();
403                  $sth->finish();
404                  $dbh->disconnect();
405     
406             my $guarantor=BorType($tdata->{'borrowernumber'});
407             $borrower = $guarantor;
408             }
409     
410         return $borrower;
411         }
412
413     }
414
415
416
417
418
419
420
421
422
423 sub    preferedContactMethod {
424     # Given a reference to borrower details, in the format
425     # returned by BorType(), determine the prefered contact method, and address to use.
426     my $borrower=$_[0];
427 #                print "finding borrower method $borrower->{'preferredcont'} $borrower->{'emailaddress'} $borrower->{'streetaddress'}\n";
428
429     # Possible contact methods, in order of preference are:
430     my @methods = ('email', 'sms', 'fax', 'letter');
431
432     my $method='';
433     my $address='';
434
435
436     # Does this borrower have a borrower.preferredcont set?
437     # If so, push it to the head of our array of methods to try.
438     # If it's a method unheard of by this system, then we'll drop though to the prefined methods above.
439     # Note use of unshift to push onto the front of the array.
440     if ($borrower->{'preferredcont'}) {
441         unshift(@methods, $borrower->{'preferredcont'});
442         }
443
444
445     # Cycle through the possible methods until one is accepted
446     while ((@methods) and (!$address)) {
447         $method=shift(@methods);
448
449
450         if ($method eq 'email') {
451             if (($borrower->{'emailaddress'}) and (Mail::RFC822::Address::valid($borrower->{'emailaddress'}))) {
452                 $address = $borrower->{'emailaddress'};
453                 }
454             }
455         elsif ($method eq 'fax') {
456             if ($borrower->{'faxnumber'}) {
457                 $address = $borrower->{'faxnumber'};
458                 }
459             }
460         elsif ($method eq 'sms') {
461             if ($borrower->{'textmessaging'}) {
462                 $address = $borrower->{'textmessaging'};
463                 }
464             }
465         elsif ($method eq 'letter') {
466             if ($borrower->{'streetaddress'}) {
467                 $address =  mailingAddress($borrower);
468                 }
469             }
470         }
471 print "$method, $address\n";
472     return ($method, $address);
473     }
474
475
476
477
478
479
480
481
482 sub    logContact {
483     # Given the details of an attempt to contact a borrower,
484     # log them in the attempted_contacts table of the koha database.
485     my ($borrowernumber, $method, $address, $result, $message) = @_;
486
487      my $dbh=C4Connect();    # FIXME - disconnect me
488     my $querystring = "    insert into    attempted_contacts
489                         (borrowernumber, method, address, result, message, date)
490                         values (?, ?, ?, ?, ?, now())";
491     my $sth= $dbh->prepare($querystring);
492     $sth->execute($borrowernumber, $method, $address, $result, $message);
493     $sth->finish();
494     }
495
496
497
498
499
500
501
502
503 sub    mailingAddress {
504     # Given a hash of borrower information, such as that returned by BorType,
505     # return a mailing address.
506     my $borrower=$_[0];
507
508     my $address =     $borrower->{'firstname'}."\n".
509             $borrower->{'streetaddress'}."\n".
510             $borrower->{'streetcity'};
511
512     return $address;
513     }
514
515
516
517
518
519
520
521 sub itemFine {
522     # Given an overdue item, return the current fines on it
523     my $overdue=$_[0];
524     # FIXME
525     return 1;
526     }
527
528
529
530
531
532
533
534
535
536 sub    sendEmail {
537     # Given an email address, and a subject and message, attempt to send email.
538     my $to=$_[0];
539     my $from=$_[1];
540     my $subject=$_[2];
541     my $message=$_[3];
542         
543 #    print "in email area";
544
545 #    print "\nSending Email To: $to\n$message\n";
546
547     my      %mail = (           To      => $to,
548 #                                        CC => 'rosalie@library.org.nz', 
549                     From    => $from,
550                                         Subject => $subject,
551                                         Message => $message);
552
553                 
554     if (not(sendmail %mail)) {
555         warn "sendEmail to $to failed.";
556         return 0;
557         }
558     
559     return 1;
560 #    die "got to here";
561     }
562
563
564 sub    sendSMS {
565     # Given a cell number and a message, attempt to send an SMS message.
566     # FIXME - needs information about how to do this at HLT
567     return 1;
568     }
569
570
571 sub     sendFax {
572     print "in fax \n";
573     # Given a fax number, and a message, attempt to send a fax.
574     # FIXME - needs information about how to do this at HLT
575     # This is fairly easy.
576     # We will be past the body of the fax as HTML.
577     # We can pass this through html2ps to generate Postscript suitable
578     # for passing to the fax server.
579     return 1;
580     }
581
582
583 sub     printLetter {
584     # Print a letter
585     # FIXME - needs information about how to do this at HLT
586     return 1;
587     }