Bug 25650: Add location and itype descriptions in ILS-DI GetRecords
[koha.git] / misc / cronjobs / longoverdue.pl
blob1f4a380babd58fbc9c1ad0eae2d6828b9d0295f8
1 #!/usr/bin/perl
2 #-----------------------------------
3 # Copyright 2008 LibLime
5 # This file is part of Koha.
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.
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.
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 #-----------------------------------
21 =head1 NAME
23 longoverdue.pl cron script to set lost statuses on overdue materials.
24 Execute without options for help.
26 =cut
28 use strict;
29 use warnings;
30 BEGIN {
31 # find Koha's Perl modules
32 # test carefully before changing this
33 use FindBin;
34 eval { require "$FindBin::Bin/../kohalib.pl" };
37 use Getopt::Long;
38 use Pod::Usage;
40 use C4::Circulation qw/LostItem MarkIssueReturned/;
41 use C4::Context;
42 use C4::Items;
43 use C4::Log;
44 use Koha::ItemTypes;
45 use Koha::Patron::Categories;
46 use Koha::Patrons;
47 use Koha::Script -cron;
49 my $lost; # key=lost value, value=num days.
50 my ($charge, $verbose, $confirm, $quiet);
51 my $endrange = 366;
52 my $mark_returned;
53 my $borrower_category = [];
54 my $skip_borrower_category = [];
55 my $itemtype = [];
56 my $skip_itemtype = [];
57 my $help=0;
58 my $man=0;
59 my $list_categories = 0;
60 my $list_itemtypes = 0;
61 my @skip_lost_values;
63 GetOptions(
64 'l|lost=s%' => \$lost,
65 'c|charge=s' => \$charge,
66 'confirm' => \$confirm,
67 'v|verbose' => \$verbose,
68 'quiet' => \$quiet,
69 'maxdays=s' => \$endrange,
70 'mark-returned' => \$mark_returned,
71 'h|help' => \$help,
72 'man|manual' => \$man,
73 'category=s' => $borrower_category,
74 'skip-category=s' => $skip_borrower_category,
75 'list-categories' => \$list_categories,
76 'itemtype=s' => $itemtype,
77 'skip-itemtype=s' => $skip_itemtype,
78 'list-itemtypes' => \$list_itemtypes,
79 'skip-lost-value=s' => \@skip_lost_values,
82 if ( $man ) {
83 pod2usage( -verbose => 2
84 -exitval => 0
88 if ( $help ) {
89 pod2usage( -verbose => 1,
90 -exitval => 0
94 if ( scalar @$borrower_category && scalar @$skip_borrower_category) {
95 pod2usage( -verbose => 1,
96 -message => "The options --category and --skip-category are mutually exclusive.\n"
97 . "Use one or the other.",
98 -exitval => 1
102 if ( scalar @$itemtype && scalar @$skip_itemtype) {
103 pod2usage( -verbose => 1,
104 -message => "The options --itemtype and --skip-itemtype are mutually exclusive.\n"
105 . "Use one or the other.",
106 -exitval => 1
110 if ( $list_categories ) {
112 my @categories = Koha::Patron::Categories->search()->get_column('categorycode');
113 print "\nBorrower Categories: " . join( " ", @categories ) . "\n\n";
114 exit 0;
117 if ( $list_itemtypes ) {
118 my @itemtypes = Koha::ItemTypes->search()->get_column('itemtype');
119 print "\nItemtypes: " . join( " ", @itemtypes ) . "\n\n";
120 exit 0;
123 =head1 SYNOPSIS
125 longoverdue.pl [ --help | -h | --man | --list-categories ]
126 longoverdue.pl --lost | -l DAYS=LOST_CODE [ --charge | -c CHARGE_CODE ] [ --verbose | -v ] [ --quiet ]
127 [ --maxdays MAX_DAYS ] [ --mark-returned ] [ --category BORROWER_CATEGORY ] ...
128 [ --skip-category BORROWER_CATEGORY ] ...
129 [ --skip-lost-value LOST_VALUE [ --skip-lost-value LOST_VALUE ] ]
130 [ --commit ]
133 WARNING: Flippant use of this script could set all or most of the items in your catalog to Lost and charge your
134 patrons for them!
136 WARNING: This script is known to be faulty. It is NOT recommended to use multiple --lost options.
137 See http://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=2883
139 =cut
141 =head1 OPTIONS
143 This script takes the following parameters :
145 =over 8
147 =item B<--lost | -l>
149 This option takes the form of n=lv, where n is num days overdue, and lv is the lost value. See warning above.
151 =item B<--charge | -c>
153 This specifies what lost value triggers Koha to charge the account for the lost item. Replacement costs are not charged if this is not specified.
155 =item B<--verbose | -v>
157 verbose.
159 =item B<--confirm>
161 confirm. without this option, the script will report the number of affected items and return without modifying any records.
163 =item B<--quiet>
165 suppress summary output.
167 =item B<--maxdays>
169 Specifies the end of the range of overdue days to deal with (defaults to 366). This value is universal to all lost num days overdue passed.
171 =item B<--mark-returned>
173 When an item is marked lost, remove it from the borrowers issued items.
174 If not provided, the value of the system preference 'MarkLostItemsAsReturned' will be used.
176 =item B<--category>
178 Act on the listed borrower category code (borrowers.categorycode).
179 Exclude all others. This may be specified multiple times to include multiple categories.
180 May not be used with B<--skip-category>
182 =item B<--skip-category>
184 Act on all available borrower category codes, except those listed.
185 This may be specified multiple times, to exclude multiple categories.
186 May not be used with B<--category>
188 =item B<--list-categories>
190 List borrower categories available for use by B<--category> or
191 B<--skip-category>, and exit.
193 =item B<--itemtype>
195 Act on the listed itemtype code.
196 Exclude all others. This may be specified multiple times to include multiple itemtypes.
197 May not be used with B<--skip-itemtype>
199 =item B<--skip-itemtype>
201 Act on all available itemtype codes, except those listed.
202 This may be specified multiple times, to exclude multiple itemtypes.
203 May not be used with B<--itemtype>
205 =item B<--skip-lost-value>
207 Act on all available lost values, except those listed.
208 This may be specified multiple times, to exclude multiple lost values.
210 =item B<--list-itemtypes>
212 List itemtypes available for use by B<--itemtype> or
213 B<--skip-itemtype>, and exit.
215 =item B<--help | -h>
217 Display short help message an exit.
219 =item B<--man | --manual >
221 Display entire manual and exit.
223 =back
225 =cut
227 =head1 Description
229 This cron script set lost values on overdue items and optionally sets charges the patron's account
230 for the item's replacement price. It is designed to be run as a nightly job. The command line options that globally
231 define this behavior for this script will likely be moved into Koha's core circulation / issuing rules code in a
232 near-term release, so this script is not intended to have a long lifetime.
235 =cut
237 =head1 Examples
239 $PERL5LIB/misc/cronjobs/longoverdue.pl --lost 30=1
240 Would set LOST=1 after 30 days (up to one year), but not charge the account.
241 This would be suitable for the Koha default LOST authorized value of 1 -> 'Lost'.
243 $PERL5LIB/misc/cronjobs/longoverdue.pl --lost 60=2 --charge 2
244 Would set LOST=2 after 60 days (up to one year), and charge the account when setting LOST=2.
245 This would be suitable for the Koha default LOST authorized value of 2 -> 'Long Overdue'
247 =cut
249 # FIXME: We need three pieces of data to operate:
250 # ~ lower bound (number of days),
251 # ~ upper bound (number of days),
252 # ~ new lost value.
253 # Right now we get only two, causing the endrange hack. This is a design-level failure.
254 # FIXME: do checks on --lost ranges to make sure they are exclusive.
255 # FIXME: do checks on --lost ranges to make sure the authorized values exist.
256 # FIXME: do checks on --lost ranges to make sure don't go past endrange.
259 unless ( scalar @skip_lost_values ) {
260 my $preference = C4::Context->preference( 'DefaultLongOverdueSkipLostStatuses' );
261 @skip_lost_values = split( ',', $preference );
264 if ( ! defined($lost) ) {
265 my $longoverdue_value = C4::Context->preference('DefaultLongOverdueLostValue');
266 my $longoverdue_days = C4::Context->preference('DefaultLongOverdueDays');
267 if(defined($longoverdue_value) and defined($longoverdue_days) and $longoverdue_value ne '' and $longoverdue_days ne '' and $longoverdue_days >= 0) {
268 $lost->{$longoverdue_days} = $longoverdue_value;
270 else {
271 pod2usage( {
272 -exitval => 1,
273 -msg => q|ERROR: No --lost (-l) option defined|,
274 } );
277 if ( ! defined($charge) ) {
278 my $charge_value = C4::Context->preference('DefaultLongOverdueChargeValue');
279 if(defined($charge_value) and $charge_value ne '') {
280 $charge = $charge_value;
283 unless ($confirm) {
284 $verbose = 1; # If you're not running it for real, then the whole point is the print output.
285 print "### TEST MODE -- NO ACTIONS TAKEN ###\n";
288 cronlogaction();
290 # In my opinion, this line is safe SQL to have outside the API. --atz
291 our $bounds_sth = C4::Context->dbh->prepare("SELECT DATE_SUB(CURDATE(), INTERVAL ? DAY)");
293 sub bounds {
294 $bounds_sth->execute(shift);
295 return $bounds_sth->fetchrow;
298 # FIXME - This sql should be inside the API.
299 sub longoverdue_sth {
300 my ( $params ) = @_;
301 my $skip_lost_values = $params->{skip_lost_values};
303 my $skip_lost_values_sql = q{};
304 if ( @$skip_lost_values ) {
305 my $values = join( ',', map { qq{'$_'} } @$skip_lost_values );
306 $skip_lost_values_sql = "AND itemlost NOT IN ( $values )"
309 my $query = "
310 SELECT items.itemnumber, borrowernumber, date_due, itemlost
311 FROM issues, items
312 WHERE items.itemnumber = issues.itemnumber
313 AND DATE_SUB(CURDATE(), INTERVAL ? DAY) > date_due
314 AND DATE_SUB(CURDATE(), INTERVAL ? DAY) <= date_due
315 AND itemlost <> ?
316 $skip_lost_values_sql
317 ORDER BY date_due
319 return C4::Context->dbh->prepare($query);
322 my $dbh = C4::Context->dbh;
324 my @available_categories = Koha::Patron::Categories->search()->get_column('categorycode');
325 $borrower_category = [ map { uc $_ } @$borrower_category ];
326 $skip_borrower_category = [ map { uc $_} @$skip_borrower_category ];
327 my %category_to_process;
328 for my $cat ( @$borrower_category ) {
329 unless ( grep { $_ eq $cat } @available_categories ) {
330 pod2usage(
331 '-exitval' => 1,
332 '-message' => "The category $cat does not exist in the database",
335 $category_to_process{$cat} = 1;
337 if ( @$skip_borrower_category ) {
338 for my $cat ( @$skip_borrower_category ) {
339 unless ( grep { $_ eq $cat } @available_categories ) {
340 pod2usage(
341 '-exitval' => 1,
342 '-message' => "The category $cat does not exist in the database",
346 %category_to_process = map { $_ => 1 } @available_categories;
347 %category_to_process = ( %category_to_process, map { $_ => 0 } @$skip_borrower_category );
350 my $filter_borrower_categories = ( scalar @$borrower_category || scalar @$skip_borrower_category );
352 my @available_itemtypes = Koha::ItemTypes->search()->get_column('itemtype');
353 $itemtype = [ map { uc $_ } @$itemtype ];
354 $skip_itemtype = [ map { uc $_} @$skip_itemtype ];
355 my %itemtype_to_process;
356 for my $it ( @$itemtype ) {
357 unless ( grep { $_ eq $it } @available_itemtypes ) {
358 pod2usage(
359 '-exitval' => 1,
360 '-message' => "The itemtype $it does not exist in the database",
363 $itemtype_to_process{$it} = 1;
365 if ( @$skip_itemtype ) {
366 for my $it ( @$skip_itemtype ) {
367 unless ( grep { $_ eq $it } @available_itemtypes ) {
368 pod2usage(
369 '-exitval' => 1,
370 '-message' => "The itemtype $it does not exist in the database",
374 %itemtype_to_process = map { $_ => 1 } @available_itemtypes;
375 %itemtype_to_process = ( %itemtype_to_process, map { $_ => 0 } @$skip_itemtype );
378 my $filter_itemtypes = ( scalar @$itemtype || scalar @$skip_itemtype );
380 my $count;
381 my @report;
382 my $total = 0;
383 my $i = 0;
385 # FIXME - The item is only marked returned if you supply --charge .
386 # We need a better way to handle this.
388 my $sth_items = longoverdue_sth({ skip_lost_values => \@skip_lost_values });
390 foreach my $startrange (sort keys %$lost) {
391 if( my $lostvalue = $lost->{$startrange} ) {
392 my ($date1) = bounds($startrange);
393 my ($date2) = bounds( $endrange);
394 # print "\nRange ", ++$i, "\nDue $startrange - $endrange days ago ($date2 to $date1), lost => $lostvalue\n" if($verbose);
395 $verbose and
396 printf "\nRange %s\nDue %3s - %3s days ago (%s to %s), lost => %s\n", ++$i,
397 $startrange, $endrange, $date2, $date1, $lostvalue;
398 $sth_items->execute($startrange, $endrange, $lostvalue);
399 $count=0;
400 ITEM: while (my $row=$sth_items->fetchrow_hashref) {
401 if( $filter_borrower_categories ) {
402 my $category = uc Koha::Patrons->find( $row->{borrowernumber} )->categorycode();
403 next ITEM unless ( $category_to_process{ $category } );
405 if ($filter_itemtypes) {
406 my $it = uc Koha::Items->find( $row->{itemnumber} )->effective_itemtype();
407 next ITEM unless ( $itemtype_to_process{$it} );
409 printf ("Due %s: item %5s from borrower %5s to lost: %s\n", $row->{date_due}, $row->{itemnumber}, $row->{borrowernumber}, $lostvalue) if($verbose);
410 if($confirm) {
411 Koha::Items->find( $row->{itemnumber} )->itemlost($lostvalue)
412 ->store;
413 if ( $charge && $charge eq $lostvalue ) {
414 LostItem( $row->{'itemnumber'}, 'cronjob', $mark_returned );
415 } elsif ( $mark_returned ) {
416 my $patron = Koha::Patrons->find( $row->{borrowernumber} );
417 MarkIssueReturned($row->{borrowernumber},$row->{itemnumber},undef,$patron->privacy)
420 $count++;
422 push @report, {
423 startrange => $startrange,
424 endrange => $endrange,
425 range => "$startrange - $endrange",
426 date1 => $date1,
427 date2 => $date2,
428 lostvalue => $lostvalue,
429 count => $count,
431 $total += $count;
433 $endrange = $startrange;
436 sub summarize {
437 my $arg = shift; # ref to array
438 my $got_items = shift || 0; # print "count" line for items
439 my @report = @$arg or return;
440 my $i = 0;
441 for my $range (@report) {
442 printf "\nRange %s\nDue %3s - %3s days ago (%s to %s), lost => %s\n", ++$i,
443 map {$range->{$_}} qw(startrange endrange date2 date1 lostvalue);
444 $got_items and printf " %4s items\n", $range->{count};
448 if (!$quiet){
449 print "\n### LONGOVERDUE SUMMARY ###";
450 summarize (\@report, 1);
451 print "\nTOTAL: $total items\n";