Bug 31854: (QA follow-up) Terminology: loans > checkouts
[srvgit] / misc / cronjobs / staticfines.pl
1 #!/usr/bin/perl
2
3 #  This script loops through each overdue item, determines the fine,
4 #  and updates the total amount of fines due by each user.  It relies on
5 #  the existence of /tmp/fines, which is created by ???
6 # Doesn't really rely on it, it relies on being able to write to /tmp/
7 # It creates the fines file
8 #
9 #  This script is meant to be run nightly out of cron.
10
11 # Copyright 2011-2012 BibLibre
12 #
13 # This file is part of Koha.
14 #
15 # Koha is free software; you can redistribute it and/or modify it
16 # under the terms of the GNU General Public License as published by
17 # the Free Software Foundation; either version 3 of the License, or
18 # (at your option) any later version.
19 #
20 # Koha is distributed in the hope that it will be useful, but
21 # WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 # GNU General Public License for more details.
24 #
25 # You should have received a copy of the GNU General Public License
26 # along with Koha; if not, see <http://www.gnu.org/licenses>.
27
28 use Modern::Perl;
29
30 use Date::Calc qw( Date_to_Days );
31
32 use Koha::Script -cron;
33 use C4::Context;
34 use C4::Overdues qw( CalcFine checkoverdues GetFine Getoverdues );
35 use C4::Calendar qw();    # don't need any exports from Calendar
36 use C4::Log qw( cronlogaction );
37 use Getopt::Long qw( GetOptions );
38 use List::MoreUtils qw( none );
39 use Koha::DateUtils qw( dt_from_string );
40 use Koha::Patrons;
41
42 my $help    = 0;
43 my $verbose = 0;
44 my @pcategories;
45 my @categories;
46 my %catamounts;
47 my @libraries;
48 my $delay;
49 my $useborrowerlibrary;
50 my $borrowernumberlimit;
51 my $borrowersalreadyapplied; # hashref of borrowers for whom we already applied the fine, so it's only applied once
52 my $debug = 0;
53 my $bigdebug = 0;
54
55 my $command_line_options = join(" ",@ARGV);
56
57 GetOptions(
58     'h|help'      => \$help,
59     'v|verbose'   => \$verbose,
60     'c|category:s'=> \@pcategories,
61     'l|library:s' => \@libraries,
62     'd|delay:i'   => \$delay,
63     'u|use-borrower-library' => \$useborrowerlibrary,
64     'b|borrower:i' => \$borrowernumberlimit
65 );
66 my $usage = << 'ENDUSAGE';
67
68 This script calculates and charges overdue fines to patron accounts.
69
70 If the Koha System Preference 'finesMode' is set to 'production', the fines are charged to the patron accounts.
71
72 Please note that the fines won't be applied on a holiday.
73
74 This script has the following parameters :
75     -h --help: this message
76     -v --verbose
77     -c --category borrower_category,amount (repeatable)
78     -l --library (repeatable)
79     -d --delay
80     -u --use-borrower-library: use borrower's library, regardless of the CircControl syspref
81     -b --borrower borrowernumber: only for one given borrower
82
83 ENDUSAGE
84 die $usage if $help;
85
86 cronlogaction({ info => $command_line_options });
87
88 my $dbh = C4::Context->dbh;
89
90 # Processing categories
91 foreach (@pcategories) {
92     my ($category, $amount) = split(',', $_);
93     push @categories, $category;
94     $catamounts{$category} = $amount;
95 }
96
97 use vars qw(@borrower_fields @item_fields @other_fields);
98 use vars qw($fldir $libname $control $mode $delim $dbname $today $today_iso $today_days);
99 use vars qw($filename);
100
101 CHECK {
102     @borrower_fields = qw(cardnumber categorycode surname firstname email phone address citystate);
103     @item_fields     = qw(itemnumber barcode date_due);
104     @other_fields    = qw(type days_overdue fine);
105     $libname         = C4::Context->preference('LibraryName');
106     $control         = C4::Context->preference('CircControl');
107     $mode            = C4::Context->preference('finesMode');
108     $dbname          = C4::Context->config('database');
109     $delim           = "\t";                                                                          # ?  C4::Context->preference('delimiter') || "\t";
110
111 }
112
113 INIT {
114     $debug and print "Each line will contain the following fields:\n",
115       "From borrowers : ", join( ', ', @borrower_fields ), "\n",
116       "From items : ",     join( ', ', @item_fields ),     "\n",
117       "Per overdue: ",     join( ', ', @other_fields ),    "\n",
118       "Delimiter: '$delim'\n";
119 }
120 $debug and (defined $borrowernumberlimit) and print "--borrower limitation: borrower $borrowernumberlimit\n";
121 my ($numOverdueItems, $data);
122 if (defined $borrowernumberlimit) {
123     ($numOverdueItems, $data) = checkoverdues($borrowernumberlimit);
124 } else {
125     $data = Getoverdues();
126     $numOverdueItems = scalar @$data;
127 }
128 my $overdueItemsCounted = 0;
129 my %calendars           = ();
130 $today      = dt_from_string;
131 $today_iso  = $today->ymd;
132 my ($tyear, $tmonth, $tday) = split( /-/, $today_iso );
133 $today_days = Date_to_Days( $tyear, $tmonth, $tday );
134
135 for ( my $i = 0 ; $i < scalar(@$data) ; $i++ ) {
136     next if $data->[$i]->{'itemlost'};
137     my ( $datedue, $datedue_days );
138     eval {
139         $datedue = dt_from_string( $data->[$i]->{'date_due'} );
140         my $datedue_iso = output_pref( { dt => $datedue, dateonly => 1, dateformat => 'iso' } );
141         $datedue_days = Date_to_Days( split( /-/, $datedue_iso ) );
142     };
143     if ($@) {
144     warn "Error on date for borrower " . $data->[$i]->{'borrowernumber'} .  ": $@date_due: " . $data->[$i]->{'date_due'} . "\ndatedue_days: " . $datedue_days . "\nSkipping";
145     next;
146     }
147     my $due_str = output_pref( { dt => $datedue, dateonly => 1 } );
148     unless ( defined $data->[$i]->{'borrowernumber'} ) {
149         print STDERR "ERROR in Getoverdues line $i: issues.borrowernumber IS NULL.  Repair 'issues' table now!  Skipping record.\n";
150         next;    # Note: this doesn't solve everything.  After NULL borrowernumber, multiple issues w/ real borrowernumbers can pile up.
151     }
152     my $patron = Koha::Patrons->find( $data->[$i]->{'borrowernumber'} );
153
154     # Skipping borrowers that are not in @categories
155     $bigdebug and warn "Skipping borrower from category " . $patron->categorycode if none { $patron->categorycode eq $_ } @categories;
156     next if none { $patron->categorycode eq $_ } @categories;
157
158     my $branchcode =
159         ( $useborrowerlibrary )           ? $patron->branchcode
160       : ( $control eq 'ItemHomeLibrary' ) ? $data->[$i]->{homebranch}
161       : ( $control eq 'PatronLibrary' )   ? $patron->branchcode
162       :                                     $data->[$i]->{branchcode};
163     # In final case, CircControl must be PickupLibrary. (branchcode comes from issues table here).
164
165     # Skipping branchcodes that are not in @libraries
166     $bigdebug and warn "Skipping library $branchcode" if none { $branchcode eq $_ } @libraries;
167     next if none { $branchcode eq $_ } @libraries;
168
169     my $calendar;
170     unless ( defined( $calendars{$branchcode} ) ) {
171         $calendars{$branchcode} = C4::Calendar->new( branchcode => $branchcode );
172     }
173     $calendar = $calendars{$branchcode};
174     my $isHoliday = $calendar->isHoliday( $tday, $tmonth, $tyear );
175
176     # Reassing datedue_days if -delay specified in commandline
177     $bigdebug and warn "Using commandline supplied delay : $delay" if ($delay);
178     $datedue_days += $delay if ($delay);
179
180     ( $datedue_days <= $today_days ) or next;    # or it's not overdue, right?
181
182     $overdueItemsCounted++;
183     my ( $amount, $unitcounttotal, $unitcount ) = CalcFine(
184         $data->[$i],
185         $patron->categorycode,
186         $branchcode,
187         $datedue,
188         $today,
189     );
190
191     # Reassign fine's amount if specified in command-line
192     $amount = $catamounts{$patron->categorycode} if (defined $catamounts{$patron->categorycode});
193
194     # We check if there is already a fine for the given borrower
195     my $fine = GetFine(undef, $data->[$i]->{'borrowernumber'});
196     if ($fine > 0) {
197         $debug and warn "There is already a fine for borrower " . $data->[$i]->{'borrowernumber'} . ". Nothing to do here. Skipping this borrower";
198         next;
199     }
200
201     # Don't update the fine if today is a holiday.
202     # This ensures that dropbox mode will remove the correct amount of fine.
203     if ( $mode eq 'production' and !$borrowersalreadyapplied->{$data->[$i]->{'borrowernumber'}}) {
204         # If we're on a holiday, warn the user (if debug) that no fine will be applied
205         if($isHoliday) {
206             $debug and warn "Today is a holiday. The fine for borrower " . $data->[$i]->{'borrowernumber'} . " will not be applied";
207         } else {
208             $debug and warn "Creating fine for borrower " . $data->[$i]->{'borrowernumber'} . " with amount : $amount";
209
210             # We mark this borrower as already processed
211             $borrowersalreadyapplied->{$data->[$i]->{'borrowernumber'}} = 1;
212
213             my $borrowernumber = $data->[$i]->{'borrowernumber'};
214             my $itemnumber     = $data->[$i]->{'itemnumber'};
215
216             # And we create the fine
217             my $sth4 = $dbh->prepare( "SELECT title FROM biblio LEFT JOIN items ON biblio.biblionumber=items.biblionumber WHERE items.itemnumber=?" );
218             $sth4->execute($itemnumber);
219             my $title = $sth4->fetchrow;
220
221             my $desc        = "staticfine";
222             my $query       = "INSERT INTO accountlines
223                         (borrowernumber,itemnumber,date,amount,description,debit_type_code,status,amountoutstanding)
224                                 VALUES (?,?,now(),?,?,'OVERDUE','RETURNED',?)";
225             my $sth2 = $dbh->prepare($query);
226             $bigdebug and warn "query: $query\nw/ args: $borrowernumber, $itemnumber, $amount, $desc, $amount\n";
227             $sth2->execute( $borrowernumber, $itemnumber, $amount, $desc, $amount );
228
229         }
230     }
231 }
232
233 if ($verbose) {
234     print <<EOM;
235 Fines assessment -- $today_iso
236 Number of Overdue Items:
237      counted $overdueItemsCounted
238     reported $numOverdueItems
239
240 EOM
241 }
242
243 cronlogaction({ action => 'End', info => "COMPLETED" });