HUGE COMMIT : code cleaning circulation.
[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 $env;
103
104                         my $accountno=C4::Circulation::Circ2::getnextacctno($overdue->{'borrowernumber'});
105                            my $item=GetBiblioFromItemNumber($overdue->{'itemnumber'});
106                         if ($item->{'itemlost'} ne '1' && $item->{'itemlost'} ne '2' ){
107                               $item->{'title'}=~ s/\'/\\'/g;
108                               my $query="Insert into accountlines (borrowernumber,itemnumber,accountno,date,amount, description,accounttype,amountoutstanding)
109                                 values ($overdue->{'borrowernumber'}, $overdue->{'itemnumber'},
110                                         '$accountno',now(),'$cost','Lost item $item->{'title'} $item->{'barcode'}','L','$cost')";
111
112                                my $sth=$dbh->prepare($query);
113                                $sth->execute();
114                               $sth->finish();
115             
116                         $query="update items set itemlost=2 where itemnumber='$overdue->{'itemnumber'}'";
117                               $sth=$dbh->prepare($query);
118                               $sth->execute();
119                               $sth->finish();
120                             }
121                     }
122                 }
123                    }
124         }
125
126     return 1;
127     }
128
129
130
131
132
133
134 sub    notifyOverdues {
135     # Look up the overdues for today.
136     # Capture overdues which fall on our dates of interest.
137
138
139
140
141 ####################################################################################################
142 # Creating a big hash of available templates
143 my %email;
144 %email->{'template'}='email-8.txt';
145 my %sms; 
146 %sms->{'template'}='sms-8.txt';
147
148 my %fax1;
149 %fax1->{'template'}='fax-8.html';
150
151 my %firstReminder->{'email'} = \%email;
152 %firstReminder->{'sms'} = \%sms;
153 %firstReminder->{'fax'} = \%fax1;
154     
155 my %email2;
156 %email2->{'template'}='email-15.txt';
157
158 my %fax2;
159 %fax2->{'template'}='fax-15.html';
160     
161 my %letter2;
162 %letter2->{'template'}='fax-15.html';
163     
164 my %sms2->{'template'}='sms-15.txt';
165 my %secondReminder->{'email'} = \%email2;
166 %secondReminder->{'sms'} = \%sms2;
167 %secondReminder->{'fax'} = \%fax2;
168 %secondReminder->{'letter'} = \%letter2;    
169
170
171 my %email3;
172 %email3->{'template'}='email-29.txt';
173 my %fax3;
174 %fax3->{'template'}='fax-29.html';
175 my %letter3;
176 %letter3->{'template'}='letter-29.html';
177
178 my %finalReminder->{'email'} = \%email3;
179 %finalReminder->{'fax'} = \%fax3;
180 %finalReminder->{'letter'} = \%letter3;
181
182 my $fines;
183 my %actions;
184 %actions->{'8'}=\%firstReminder;
185 %actions->{'15'}=\%secondReminder;
186 %actions->{'29'}=\%finalReminder;
187
188 ##################################################################################################################
189
190
191         # Work out what today is as an integer value.
192         my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) =localtime(time);
193         $mon++; $year=$year+1900;
194         my $date=Date_DaysSince1BC($mon,$mday,$year);
195
196
197         # Retrieve an array of overdues.
198         my ($count, $overduesReference) = Getoverdues();
199         print "$count overdue items where found.\n\n";
200         my @overdues=@$overduesReference;
201
202
203     # We're going to build a hash of arrays, containing the items requiring action.
204     # ->borrowernumber, date, @overdues
205     my %actionItems;
206     foreach my $actionday (keys(%actions)) {
207         my @items=();
208         %actionItems->{$actionday} = \@items;
209         }
210     
211
212
213         foreach my $overdue (@overdues) {
214                 my @dates=split('-',$overdue->{'date_due'});
215                 my $due_day=Date_DaysSince1BC($dates[1],$dates[2],$dates[0]);
216
217                    my $difference=$date-$due_day;
218 #            $overdue->{'fine'}=GetFine($overdue->{'itemnumber'});
219         # If does this item fall on a day of interest?
220                                 $overdue->{'difference'}=$difference;
221         foreach my $actiondate (keys(%actions)) {
222             if ($actiondate == $difference) {
223                 my @items = @{%actionItems->{$actiondate}};
224
225                 my %o = %$overdue;
226                 push (@items, \%o);
227                 %actionItems->{$actiondate} = \@items;
228                 }
229             }
230         }
231
232
233
234
235     # We now have a hash containing overdues which need actioning,  we can step through each set.
236     # Work from earilest to latest. We only wish to send the most urgent message.
237     my %messages;
238         my %borritem;
239
240     foreach my $actiondate (sort {$a <=> $b} (keys(%actions))) {
241         print "\n\nThe following items are $actiondate days overdue.\n";
242         my @items = @{%actionItems->{$actiondate}};
243     
244     
245         foreach my $overdue (@items) {
246             if ($overdue->{'difference'} eq $actiondate) {
247                 # Detemine which borrower is responsible for this overdue;
248                 # if the offender is a child, then the garentor is the person to notify
249                 my $borrower=responsibleBorrower($overdue);
250
251
252                 my ($method, $address) = preferedContactMethod($borrower);
253                 if ($method) {
254     
255                     # Do we have to send something, using this method on this day?
256                     if (%actions->{$actiondate}->{$method}->{'template'}) {
257                         # If this user has one overdue, then they may have offers as well.
258                         # No point in sending a notice without mentioning all of the items.
259                         my @alloverdues;
260                         foreach my $over (@overdues) {
261                             my $responisble= responsibleBorrower($over);
262                             if ($responisble->{'borrowernumber'} eq $borrower->{'borrowernumber'}) {
263                                     $over->{'borrowernumber'}=$responisble->{'borrowernumber'};
264                                  my %o = %$over;
265                                 push (@alloverdues, \%o);
266                                 }
267                             }
268     
269                         my $dbh=C4Connect();    # FIXME disconnect this
270
271                         # Template the message
272                         my $template = HTML::Template->new(filename => 'templates/'.%actions->{$actiondate}->{$method}->{'template'}, die_on_bad_params => 0);
273
274                         my @bookdetails;
275                         my $total_fines = 0;
276                                         foreach my $over (@alloverdues) {
277                                                 my %row_data;
278                             my $env;    #FIXME what is this varible for?
279     
280                              if ( my $item = GetBiblioFromItemNumber( $over->{'itemnumber'})){
281                                 print "getting fine ($over->{'itemnumber'} $overdue->{'borrowernumber'} $over->{'borrowernumber'}\n";
282                                 my $fine = GetFine($over->{'itemnumber'},$overdue->{'borrowernumber'});
283     
284     
285                                 print "fine=$fine  ";
286
287                                   my $rep = ReplacementCost2($over->{'itemnumber'},$overdue->{'borrowernumber'});
288
289                                 if ($rep){
290                                  $rep+=0.00;
291                                 }
292                                 if ($fine){
293                                 $fine+=0.00;
294                                  $borritem{"$over->{'itemnumber'} $over->{'borrowernumber'}"}=$fine;
295                                 } else {
296                                 $borritem{"$over->{'itemnumber'} $over->{'borrowernumber'}"}+=$fine;
297                                 }
298                                   print $borritem{"$over->{'itemnumber'} $over->{'borrowernumber'}"},"\n";
299                                 $total_fines +=  $borritem{"$over->{'itemnumber'} $over->{'borrowernumber'}"};
300                             $item->{'title'}=substr($item->{'title'},0,25);
301                             my $len=length($item->{'title'});
302                             if ($len < 25){
303                                 my $diff=25-$len;
304                                 $item->{'title'}.=" " x $diff;
305                                 }
306
307                                                 $row_data{'BARCODE'}=$item->{'barcode'};
308                                                 $row_data{'TITLE'}=$item->{'title'};
309                                                 $row_data{'DATE_DUE'}=$over->{'date_due'};
310                                 $row_data{'FINE'}=$borritem{"$over->{'itemnumber'} $over->{'borrowernumber'}"};
311                             $row_data{'REP'}=$rep;
312
313                                                 push(@bookdetails, \%row_data);
314                                 } else {
315                                 print "Missing item  $over->{'itemnumber'}\n";
316                                 }
317                                                 }
318
319                                             $template->param(BOOKDETAILS => \@bookdetails);
320        
321                         my $env;
322                                 my %params;
323                                 %params->{'borrowernumber'} = $overdue->{'borrowernumber'};
324                                 my ($count, $acctlines, $total) = &getboracctrecord($env, \%params);
325                                             $template->param(FINES_TOTAL => $total_fines);
326                             $template->param(OWING => $total);
327                             my $name= "$borrower->{'firstname'} $borrower->{'surname'}";
328                             $template->param(NAME=> $name);
329     
330                         %messages->{$borrower->{'borrowernumber'}} = $template->output();
331                         }
332                     else    {
333                         print "No $method needs to be sent at $overdue->{'difference'} days; not sending\n";
334                         }
335     
336                     }
337                 else    {
338                     print "This borrower has an overdue item, but no means of contact\n";
339                     }
340
341                 } #end of 'if this overdue falls on an action date'
342
343             } #end of 'foreach overdue'
344
345         } # end of foreach actiondate
346
347
348     # How that all of the messsages to be sent have been composed, send them.
349     foreach my $borrowernumber (keys(%messages)) {
350         print "$borrowernumber\n";
351
352            my $borrower=BorType($borrowernumber);
353         my ($method, $address) = preferedContactMethod($borrower);
354
355         my $result=0;
356         if ($method eq 'email') {
357             $result = sendEmail($address, 'lep@library.org.nz', 'Overdue Library Items', %messages->{$borrowernumber});
358             }
359         elsif ($method eq 'sms') {
360             $result = sendSMS($address, %messages->{$borrowernumber});
361             }
362         elsif ($method eq 'fax') {
363             $result = sendFax($address, %messages->{$borrowernumber});
364             }
365         elsif ($method eq 'letter') {
366             $result = printLetter($address, %messages->{$borrowernumber});
367             }
368
369
370         #print %messages->{$borrowernumber};    # debug
371
372
373         # Log the outcome of this attempt
374         logContact($borrowernumber, $method, $address, $result, %messages->{$borrowernumber});
375         }
376
377
378
379     return 1;
380     }
381
382
383
384
385
386
387
388
389
390
391 sub    responsibleBorrower {
392     # Given an overdue item, return the details of the borrower responible as a hash of database columns.
393     my $overdue=$_[0];
394
395     if ($overdue->{'borrowernumber'}) {
396         my $borrower=BorType($overdue->{'borrowernumber'});
397
398
399         # Overdue books assigned to children have notices sent to the guarantor.
400            if ($borrower->{'categorycode'} eq 'C') {
401                 my $dbh=C4Connect();
402                 my $query="Select     borrowernumber from borrowers
403                         where borrowernumber=?";
404
405                 my $sth=$dbh->prepare($query);
406                 $sth->execute($borrower->{'guarantor'});
407
408                 my $tdata=$sth->fetchrow_hashref();
409                  $sth->finish();
410                  $dbh->disconnect();
411     
412             my $guarantor=BorType($tdata->{'borrowernumber'});
413             $borrower = $guarantor;
414             }
415     
416         return $borrower;
417         }
418
419     }
420
421
422
423
424
425
426
427
428
429 sub    preferedContactMethod {
430     # Given a reference to borrower details, in the format
431     # returned by BorType(), determine the prefered contact method, and address to use.
432     my $borrower=$_[0];
433 #                print "finding borrower method $borrower->{'preferredcont'} $borrower->{'emailaddress'} $borrower->{'streetaddress'}\n";
434
435     # Possible contact methods, in order of preference are:
436     my @methods = ('email', 'sms', 'fax', 'letter');
437
438     my $method='';
439     my $address='';
440
441
442     # Does this borrower have a borrower.preferredcont set?
443     # If so, push it to the head of our array of methods to try.
444     # If it's a method unheard of by this system, then we'll drop though to the prefined methods above.
445     # Note use of unshift to push onto the front of the array.
446     if ($borrower->{'preferredcont'}) {
447         unshift(@methods, $borrower->{'preferredcont'});
448         }
449
450
451     # Cycle through the possible methods until one is accepted
452     while ((@methods) and (!$address)) {
453         $method=shift(@methods);
454
455
456         if ($method eq 'email') {
457             if (($borrower->{'emailaddress'}) and (Mail::RFC822::Address::valid($borrower->{'emailaddress'}))) {
458                 $address = $borrower->{'emailaddress'};
459                 }
460             }
461         elsif ($method eq 'fax') {
462             if ($borrower->{'faxnumber'}) {
463                 $address = $borrower->{'faxnumber'};
464                 }
465             }
466         elsif ($method eq 'sms') {
467             if ($borrower->{'textmessaging'}) {
468                 $address = $borrower->{'textmessaging'};
469                 }
470             }
471         elsif ($method eq 'letter') {
472             if ($borrower->{'streetaddress'}) {
473                 $address =  mailingAddress($borrower);
474                 }
475             }
476         }
477 print "$method, $address\n";
478     return ($method, $address);
479     }
480
481
482
483
484
485
486
487
488 sub    logContact {
489     # Given the details of an attempt to contact a borrower,
490     # log them in the attempted_contacts table of the koha database.
491     my ($borrowernumber, $method, $address, $result, $message) = @_;
492
493      my $dbh=C4Connect();    # FIXME - disconnect me
494     my $querystring = "    insert into    attempted_contacts
495                         (borrowernumber, method, address, result, message, date)
496                         values (?, ?, ?, ?, ?, now())";
497     my $sth= $dbh->prepare($querystring);
498     $sth->execute($borrowernumber, $method, $address, $result, $message);
499     $sth->finish();
500     }
501
502
503
504
505
506
507
508
509 sub    mailingAddress {
510     # Given a hash of borrower information, such as that returned by BorType,
511     # return a mailing address.
512     my $borrower=$_[0];
513
514     my $address =     $borrower->{'firstname'}."\n".
515             $borrower->{'streetaddress'}."\n".
516             $borrower->{'streetcity'};
517
518     return $address;
519     }
520
521
522
523
524
525
526
527 sub itemFine {
528     # Given an overdue item, return the current fines on it
529     my $overdue=$_[0];
530     # FIXME
531     return 1;
532     }
533
534
535
536
537
538
539
540
541
542 sub    sendEmail {
543     # Given an email address, and a subject and message, attempt to send email.
544     my $to=$_[0];
545     my $from=$_[1];
546     my $subject=$_[2];
547     my $message=$_[3];
548         
549 #    print "in email area";
550
551 #    print "\nSending Email To: $to\n$message\n";
552
553     my      %mail = (           To      => $to,
554 #                                        CC => 'rosalie@library.org.nz', 
555                     From    => $from,
556                                         Subject => $subject,
557                                         Message => $message);
558
559                 
560     if (not(sendmail %mail)) {
561         warn "sendEmail to $to failed.";
562         return 0;
563         }
564     
565     return 1;
566 #    die "got to here";
567     }
568
569
570 sub    sendSMS {
571     # Given a cell number and a message, attempt to send an SMS message.
572     # FIXME - needs information about how to do this at HLT
573     return 1;
574     }
575
576
577 sub     sendFax {
578     print "in fax \n";
579     # Given a fax number, and a message, attempt to send a fax.
580     # FIXME - needs information about how to do this at HLT
581     # This is fairly easy.
582     # We will be past the body of the fax as HTML.
583     # We can pass this through html2ps to generate Postscript suitable
584     # for passing to the fax server.
585     return 1;
586     }
587
588
589 sub     printLetter {
590     # Print a letter
591     # FIXME - needs information about how to do this at HLT
592     return 1;
593     }