#!/usr/bin/perl # use lib ('/usr/local/koha/intranet/modules'); use C4::Database; use C4::Members; use C4::Circulation::Circ2; use C4::Circulation::Fines; use Date::Manip; use Data::Dumper; use HTML::Template; use Mail::Sendmail; use Mail::RFC822::Address; use strict; #levyFines(); # Do not levy real fines in testing situation. notifyOverdues(); # Todo # - Need to calculate the fine on each book; no idea how to get this information from Koha # - Need to diffentricate between the total_fines including replacement costs, # and the total fines if the books are returned in the day 29 notices (see above). # - clean up the %actions hash creation code. #Done # - preferedcont field in borrowers hash; does this do anything? # - logging # - which 'address' to send sms to? # - senders returning success or fail sub levyFines { # Look at the current overdues, and levy fines on the offenders. # arguments: # $date # $maxfine # Work out what today is as an integer value. my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) =localtime(time); $mon++; $year=$year+1900; my $date=Date_DaysSince1BC($mon,$mday,$year); my $maxfine =5; # Retrieve an array of overdues. my ($count, $overduesReference) = Getoverdues(); print "$count overdue items where found.\n\n"; my @overdues=@$overduesReference; foreach my $overdue (@overdues) { my @dates=split('-',$overdue->{'date_due'}); my $due_day=Date_DaysSince1BC($dates[1],$dates[2],$dates[0]); # Check that the item is really overdue. The output of Getoverdues() will normally # 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. if ($due_day <= $date) { my $difference=$date-$due_day; print "Itemnumber ".$overdue->{'itemnumber'}." is issued to ".$overdue->{'borrowernumber'}." is overdue by $difference days.\n"; # Calculate the cost of this overdue. # Fines vary according to borrower type, but cannot exceed the maximum fine. print $overdue->{'borrowernumber'}; my $borrower=BorType($overdue->{'borrowernumber'}); my ($amount,$type,$printout)=CalcFine($overdue->{'itemnumber'}, $borrower->{'categorycode'}, $difference); if ($amount > $maxfine){ $amount=$maxfine; } if ($amount > 0){ my $due="$dates[2]/$dates[1]/$dates[0]"; UpdateFine($overdue->{'itemnumber'}, $overdue->{'borrowernumber'}, $amount, $type, $due); print $overdue->{'borrowernumber'}." has been fined $amount for itemnumber ".$overdue->{'itemnumber'}." overdue for $difference days.\n"; } # After 28 days, the item is marked lost and the replacement charge is added as a fine if ($difference >= 28) { my $borrower=BorType($overdue->{'borrowernumber'}); if ($borrower->{'cardnumber'} ne ''){ my $cost=ReplacementCost($overdue->{'itemnumber'}); my $dbh=C4Connect(); my $env; my $accountno=C4::Circulation::Circ2::getnextacctno($env,$overdue->{'borrowernumber'},$dbh); my $item=getbibliofromitemnumber($env,$dbh,$overdue->{'itemnumber'}); if ($item->{'itemlost'} ne '1' && $item->{'itemlost'} ne '2' ){ $item->{'title'}=~ s/\'/\\'/g; my $query="Insert into accountlines (borrowernumber,itemnumber,accountno,date,amount, description,accounttype,amountoutstanding) values ($overdue->{'borrowernumber'}, $overdue->{'itemnumber'}, '$accountno',now(),'$cost','Lost item $item->{'title'} $item->{'barcode'}','L','$cost')"; my $sth=$dbh->prepare($query); $sth->execute(); $sth->finish(); $query="update items set itemlost=2 where itemnumber='$overdue->{'itemnumber'}'"; $sth=$dbh->prepare($query); $sth->execute(); $sth->finish(); } } } } } return 1; } sub notifyOverdues { # Look up the overdues for today. # Capture overdues which fall on our dates of interest. #################################################################################################### # Creating a big hash of available templates my %email; %email->{'template'}='email-8.txt'; my %sms; %sms->{'template'}='sms-8.txt'; my %fax1; %fax1->{'template'}='fax-8.html'; my %firstReminder->{'email'} = \%email; %firstReminder->{'sms'} = \%sms; %firstReminder->{'fax'} = \%fax1; my %email2; %email2->{'template'}='email-15.txt'; my %fax2; %fax2->{'template'}='fax-15.html'; my %letter2; %letter2->{'template'}='fax-15.html'; my %sms2->{'template'}='sms-15.txt'; my %secondReminder->{'email'} = \%email2; %secondReminder->{'sms'} = \%sms2; %secondReminder->{'fax'} = \%fax2; %secondReminder->{'letter'} = \%letter2; my %email3; %email3->{'template'}='email-29.txt'; my %fax3; %fax3->{'template'}='fax-29.html'; my %letter3; %letter3->{'template'}='letter-29.html'; my %finalReminder->{'email'} = \%email3; %finalReminder->{'fax'} = \%fax3; %finalReminder->{'letter'} = \%letter3; my $fines; my %actions; %actions->{'8'}=\%firstReminder; %actions->{'15'}=\%secondReminder; %actions->{'29'}=\%finalReminder; ################################################################################################################## # Work out what today is as an integer value. my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) =localtime(time); $mon++; $year=$year+1900; my $date=Date_DaysSince1BC($mon,$mday,$year); # Retrieve an array of overdues. my ($count, $overduesReference) = Getoverdues(); print "$count overdue items where found.\n\n"; my @overdues=@$overduesReference; # We're going to build a hash of arrays, containing the items requiring action. # ->borrowernumber, date, @overdues my %actionItems; foreach my $actionday (keys(%actions)) { my @items=(); %actionItems->{$actionday} = \@items; } foreach my $overdue (@overdues) { my @dates=split('-',$overdue->{'date_due'}); my $due_day=Date_DaysSince1BC($dates[1],$dates[2],$dates[0]); my $difference=$date-$due_day; # $overdue->{'fine'}=GetFine($overdue->{'itemnumber'}); # If does this item fall on a day of interest? $overdue->{'difference'}=$difference; foreach my $actiondate (keys(%actions)) { if ($actiondate == $difference) { my @items = @{%actionItems->{$actiondate}}; my %o = %$overdue; push (@items, \%o); %actionItems->{$actiondate} = \@items; } } } # We now have a hash containing overdues which need actioning, we can step through each set. # Work from earilest to latest. We only wish to send the most urgent message. my %messages; my %borritem; foreach my $actiondate (sort {$a <=> $b} (keys(%actions))) { print "\n\nThe following items are $actiondate days overdue.\n"; my @items = @{%actionItems->{$actiondate}}; foreach my $overdue (@items) { if ($overdue->{'difference'} eq $actiondate) { # Detemine which borrower is responsible for this overdue; # if the offender is a child, then the garentor is the person to notify my $borrower=responsibleBorrower($overdue); my ($method, $address) = preferedContactMethod($borrower); if ($method) { # Do we have to send something, using this method on this day? if (%actions->{$actiondate}->{$method}->{'template'}) { # If this user has one overdue, then they may have offers as well. # No point in sending a notice without mentioning all of the items. my @alloverdues; foreach my $over (@overdues) { my $responisble= responsibleBorrower($over); if ($responisble->{'borrowernumber'} eq $borrower->{'borrowernumber'}) { $over->{'borrowernumber'}=$responisble->{'borrowernumber'}; my %o = %$over; push (@alloverdues, \%o); } } my $dbh=C4Connect(); # FIXME disconnect this # Template the message my $template = HTML::Template->new(filename => 'templates/'.%actions->{$actiondate}->{$method}->{'template'}, die_on_bad_params => 0); my @bookdetails; my $total_fines = 0; foreach my $over (@alloverdues) { my %row_data; my $env; #FIXME what is this varible for? if ( my $item = getbibliofromitemnumber($env, $dbh, $over->{'itemnumber'})){ print "getting fine ($over->{'itemnumber'} $overdue->{'borrowernumber'} $over->{'borrowernumber'}\n"; my $fine = GetFine($over->{'itemnumber'},$overdue->{'borrowernumber'}); print "fine=$fine "; my $rep = ReplacementCost2($over->{'itemnumber'},$overdue->{'borrowernumber'}); if ($rep){ $rep+=0.00; } if ($fine){ $fine+=0.00; $borritem{"$over->{'itemnumber'} $over->{'borrowernumber'}"}=$fine; } else { $borritem{"$over->{'itemnumber'} $over->{'borrowernumber'}"}+=$fine; } print $borritem{"$over->{'itemnumber'} $over->{'borrowernumber'}"},"\n"; $total_fines += $borritem{"$over->{'itemnumber'} $over->{'borrowernumber'}"}; $item->{'title'}=substr($item->{'title'},0,25); my $len=length($item->{'title'}); if ($len < 25){ my $diff=25-$len; $item->{'title'}.=" " x $diff; } $row_data{'BARCODE'}=$item->{'barcode'}; $row_data{'TITLE'}=$item->{'title'}; $row_data{'DATE_DUE'}=$over->{'date_due'}; $row_data{'FINE'}=$borritem{"$over->{'itemnumber'} $over->{'borrowernumber'}"}; $row_data{'REP'}=$rep; push(@bookdetails, \%row_data); } else { print "Missing item $over->{'itemnumber'}\n"; } } $template->param(BOOKDETAILS => \@bookdetails); my $env; my %params; %params->{'borrowernumber'} = $overdue->{'borrowernumber'}; my ($count, $acctlines, $total) = &getboracctrecord($env, \%params); $template->param(FINES_TOTAL => $total_fines); $template->param(OWING => $total); my $name= "$borrower->{'firstname'} $borrower->{'surname'}"; $template->param(NAME=> $name); %messages->{$borrower->{'borrowernumber'}} = $template->output(); } else { print "No $method needs to be sent at $overdue->{'difference'} days; not sending\n"; } } else { print "This borrower has an overdue item, but no means of contact\n"; } } #end of 'if this overdue falls on an action date' } #end of 'foreach overdue' } # end of foreach actiondate # How that all of the messsages to be sent have been composed, send them. foreach my $borrowernumber (keys(%messages)) { print "$borrowernumber\n"; my $borrower=BorType($borrowernumber); my ($method, $address) = preferedContactMethod($borrower); my $result=0; if ($method eq 'email') { $result = sendEmail($address, 'lep@library.org.nz', 'Overdue Library Items', %messages->{$borrowernumber}); } elsif ($method eq 'sms') { $result = sendSMS($address, %messages->{$borrowernumber}); } elsif ($method eq 'fax') { $result = sendFax($address, %messages->{$borrowernumber}); } elsif ($method eq 'letter') { $result = printLetter($address, %messages->{$borrowernumber}); } #print %messages->{$borrowernumber}; # debug # Log the outcome of this attempt logContact($borrowernumber, $method, $address, $result, %messages->{$borrowernumber}); } return 1; } sub responsibleBorrower { # Given an overdue item, return the details of the borrower responible as a hash of database columns. my $overdue=$_[0]; if ($overdue->{'borrowernumber'}) { my $borrower=BorType($overdue->{'borrowernumber'}); # Overdue books assigned to children have notices sent to the guarantor. if ($borrower->{'categorycode'} eq 'C') { my $dbh=C4Connect(); my $query="Select borrowernumber from borrowers where borrowernumber=?"; my $sth=$dbh->prepare($query); $sth->execute($borrower->{'guarantor'}); my $tdata=$sth->fetchrow_hashref(); $sth->finish(); $dbh->disconnect(); my $guarantor=BorType($tdata->{'borrowernumber'}); $borrower = $guarantor; } return $borrower; } } sub preferedContactMethod { # Given a reference to borrower details, in the format # returned by BorType(), determine the prefered contact method, and address to use. my $borrower=$_[0]; # print "finding borrower method $borrower->{'preferredcont'} $borrower->{'emailaddress'} $borrower->{'streetaddress'}\n"; # Possible contact methods, in order of preference are: my @methods = ('email', 'sms', 'fax', 'letter'); my $method=''; my $address=''; # Does this borrower have a borrower.preferredcont set? # If so, push it to the head of our array of methods to try. # If it's a method unheard of by this system, then we'll drop though to the prefined methods above. # Note use of unshift to push onto the front of the array. if ($borrower->{'preferredcont'}) { unshift(@methods, $borrower->{'preferredcont'}); } # Cycle through the possible methods until one is accepted while ((@methods) and (!$address)) { $method=shift(@methods); if ($method eq 'email') { if (($borrower->{'emailaddress'}) and (Mail::RFC822::Address::valid($borrower->{'emailaddress'}))) { $address = $borrower->{'emailaddress'}; } } elsif ($method eq 'fax') { if ($borrower->{'faxnumber'}) { $address = $borrower->{'faxnumber'}; } } elsif ($method eq 'sms') { if ($borrower->{'textmessaging'}) { $address = $borrower->{'textmessaging'}; } } elsif ($method eq 'letter') { if ($borrower->{'streetaddress'}) { $address = mailingAddress($borrower); } } } print "$method, $address\n"; return ($method, $address); } sub logContact { # Given the details of an attempt to contact a borrower, # log them in the attempted_contacts table of the koha database. my ($borrowernumber, $method, $address, $result, $message) = @_; my $dbh=C4Connect(); # FIXME - disconnect me my $querystring = " insert into attempted_contacts (borrowernumber, method, address, result, message, date) values (?, ?, ?, ?, ?, now())"; my $sth= $dbh->prepare($querystring); $sth->execute($borrowernumber, $method, $address, $result, $message); $sth->finish(); } sub mailingAddress { # Given a hash of borrower information, such as that returned by BorType, # return a mailing address. my $borrower=$_[0]; my $address = $borrower->{'firstname'}."\n". $borrower->{'streetaddress'}."\n". $borrower->{'streetcity'}; return $address; } sub itemFine { # Given an overdue item, return the current fines on it my $overdue=$_[0]; # FIXME return 1; } sub sendEmail { # Given an email address, and a subject and message, attempt to send email. my $to=$_[0]; my $from=$_[1]; my $subject=$_[2]; my $message=$_[3]; # print "in email area"; # print "\nSending Email To: $to\n$message\n"; my %mail = ( To => $to, # CC => 'rosalie@library.org.nz', From => $from, Subject => $subject, Message => $message); if (not(sendmail %mail)) { warn "sendEmail to $to failed."; return 0; } return 1; # die "got to here"; } sub sendSMS { # Given a cell number and a message, attempt to send an SMS message. # FIXME - needs information about how to do this at HLT return 1; } sub sendFax { print "in fax \n"; # Given a fax number, and a message, attempt to send a fax. # FIXME - needs information about how to do this at HLT # This is fairly easy. # We will be past the body of the fax as HTML. # We can pass this through html2ps to generate Postscript suitable # for passing to the fax server. return 1; } sub printLetter { # Print a letter # FIXME - needs information about how to do this at HLT return 1; }