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 #-----------------------------------
23 longoverdue.pl cron script to set lost statuses on overdue materials.
24 Execute without options for help.
31 use Getopt::Long qw( GetOptions );
32 use Pod::Usage qw( pod2usage );
34 use C4::Circulation qw( LostItem MarkIssueReturned );
36 use C4::Log qw( cronlogaction );
38 use Koha::Patron::Categories;
40 use Koha::Script -cron;
42 my $lost; # key=lost value, value=num days.
43 my ($charge, $verbose, $confirm, $quiet);
46 my $borrower_category = [];
47 my $skip_borrower_category = [];
49 my $skip_itemtype = [];
52 my $list_categories = 0;
53 my $list_itemtypes = 0;
57 'l|lost=s%' => \$lost,
58 'c|charge=s' => \$charge,
59 'confirm' => \$confirm,
60 'v|verbose' => \$verbose,
62 'maxdays=s' => \$endrange,
63 'mark-returned' => \$mark_returned,
65 'man|manual' => \$man,
66 'category=s' => $borrower_category,
67 'skip-category=s' => $skip_borrower_category,
68 'list-categories' => \$list_categories,
69 'itemtype=s' => $itemtype,
70 'skip-itemtype=s' => $skip_itemtype,
71 'list-itemtypes' => \$list_itemtypes,
72 'skip-lost-value=s' => \@skip_lost_values,
76 pod2usage( -verbose => 2
82 pod2usage( -verbose => 1,
87 if ( scalar @$borrower_category && scalar @$skip_borrower_category) {
88 pod2usage( -verbose => 1,
89 -message => "The options --category and --skip-category are mutually exclusive.\n"
90 . "Use one or the other.",
95 if ( scalar @$itemtype && scalar @$skip_itemtype) {
96 pod2usage( -verbose => 1,
97 -message => "The options --itemtype and --skip-itemtype are mutually exclusive.\n"
98 . "Use one or the other.",
103 if ( $list_categories ) {
105 my @categories = Koha::Patron::Categories->search()->get_column('categorycode');
106 print "\nBorrower Categories: " . join( " ", @categories ) . "\n\n";
110 if ( $list_itemtypes ) {
111 my @itemtypes = Koha::ItemTypes->search()->get_column('itemtype');
112 print "\nItemtypes: " . join( " ", @itemtypes ) . "\n\n";
118 longoverdue.pl [ --help | -h | --man | --list-categories ]
119 longoverdue.pl --lost | -l DAYS=LOST_CODE [ --charge | -c CHARGE_CODE ] [ --verbose | -v ] [ --quiet ]
120 [ --maxdays MAX_DAYS ] [ --mark-returned ] [ --category BORROWER_CATEGORY ] ...
121 [ --skip-category BORROWER_CATEGORY ] ...
122 [ --skip-lost-value LOST_VALUE [ --skip-lost-value LOST_VALUE ] ]
126 WARNING: Flippant use of this script could set all or most of the items in your catalog to Lost and charge your
129 WARNING: This script is known to be faulty. It is NOT recommended to use multiple --lost options.
130 See http://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=2883
136 This script takes the following parameters :
142 This option takes the form of n=lv, where n is num days overdue, and lv is the lost value. See warning above.
144 =item B<--charge | -c>
146 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.
148 =item B<--verbose | -v>
154 confirm. without this option, the script will report the number of affected items and return without modifying any records.
158 suppress summary output.
162 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.
164 =item B<--mark-returned>
166 When an item is marked lost, remove it from the borrowers issued items.
167 If not provided, the value of the system preference 'MarkLostItemsAsReturned' will be used.
171 Act on the listed borrower category code (borrowers.categorycode).
172 Exclude all others. This may be specified multiple times to include multiple categories.
173 May not be used with B<--skip-category>
175 =item B<--skip-category>
177 Act on all available borrower category codes, except those listed.
178 This may be specified multiple times, to exclude multiple categories.
179 May not be used with B<--category>
181 =item B<--list-categories>
183 List borrower categories available for use by B<--category> or
184 B<--skip-category>, and exit.
188 Act on the listed itemtype code.
189 Exclude all others. This may be specified multiple times to include multiple itemtypes.
190 May not be used with B<--skip-itemtype>
192 =item B<--skip-itemtype>
194 Act on all available itemtype codes, except those listed.
195 This may be specified multiple times, to exclude multiple itemtypes.
196 May not be used with B<--itemtype>
198 =item B<--skip-lost-value>
200 Act on all available lost values, except those listed.
201 This may be specified multiple times, to exclude multiple lost values.
203 =item B<--list-itemtypes>
205 List itemtypes available for use by B<--itemtype> or
206 B<--skip-itemtype>, and exit.
210 Display short help message an exit.
212 =item B<--man | --manual >
214 Display entire manual and exit.
222 This cron script set lost values on overdue items and optionally sets charges the patron's account
223 for the item's replacement price. It is designed to be run as a nightly job. The command line options that globally
224 define this behavior for this script will likely be moved into Koha's core circulation / issuing rules code in a
225 near-term release, so this script is not intended to have a long lifetime.
232 $PERL5LIB/misc/cronjobs/longoverdue.pl --lost 30=1
233 Would set LOST=1 after 30 days (up to one year), but not charge the account.
234 This would be suitable for the Koha default LOST authorized value of 1 -> 'Lost'.
236 $PERL5LIB/misc/cronjobs/longoverdue.pl --lost 60=2 --charge 2
237 Would set LOST=2 after 60 days (up to one year), and charge the account when setting LOST=2.
238 This would be suitable for the Koha default LOST authorized value of 2 -> 'Long Overdue'
242 # FIXME: We need three pieces of data to operate:
243 # ~ lower bound (number of days),
244 # ~ upper bound (number of days),
246 # Right now we get only two, causing the endrange hack. This is a design-level failure.
247 # FIXME: do checks on --lost ranges to make sure they are exclusive.
248 # FIXME: do checks on --lost ranges to make sure the authorized values exist.
249 # FIXME: do checks on --lost ranges to make sure don't go past endrange.
252 unless ( scalar @skip_lost_values ) {
253 my $preference = C4::Context->preference( 'DefaultLongOverdueSkipLostStatuses' );
254 @skip_lost_values = split( ',', $preference );
257 if ( ! defined($lost) ) {
258 my $longoverdue_value = C4::Context->preference('DefaultLongOverdueLostValue');
259 my $longoverdue_days = C4::Context->preference('DefaultLongOverdueDays');
260 if(defined($longoverdue_value) and defined($longoverdue_days) and $longoverdue_value ne '' and $longoverdue_days ne '' and $longoverdue_days >= 0) {
261 $lost->{$longoverdue_days} = $longoverdue_value;
266 -msg => q|ERROR: No --lost (-l) option or system preferences DefaultLongOverdueLostValue/DefaultLongOverdueDays defined|,
270 if ( ! defined($charge) ) {
271 my $charge_value = C4::Context->preference('DefaultLongOverdueChargeValue');
272 if(defined($charge_value) and $charge_value ne '') {
273 $charge = $charge_value;
277 $verbose = 1; # If you're not running it for real, then the whole point is the print output.
278 print "### TEST MODE -- NO ACTIONS TAKEN ###\n";
283 # In my opinion, this line is safe SQL to have outside the API. --atz
284 our $bounds_sth = C4::Context->dbh->prepare("SELECT DATE_SUB(CURDATE(), INTERVAL ? DAY)");
287 $bounds_sth->execute(shift);
288 return $bounds_sth->fetchrow;
291 # FIXME - This sql should be inside the API.
292 sub longoverdue_sth {
294 my $skip_lost_values = $params->{skip_lost_values};
296 my $skip_lost_values_sql = q{};
297 if ( @$skip_lost_values ) {
298 my $values = join( ',', map { qq{'$_'} } @$skip_lost_values );
299 $skip_lost_values_sql = "AND itemlost NOT IN ( $values )"
303 SELECT items.itemnumber, borrowernumber, date_due, itemlost
305 WHERE items.itemnumber = issues.itemnumber
306 AND DATE_SUB(CURDATE(), INTERVAL ? DAY) > date_due
307 AND DATE_SUB(CURDATE(), INTERVAL ? DAY) <= date_due
309 $skip_lost_values_sql
312 return C4::Context->dbh->prepare($query);
315 my $dbh = C4::Context->dbh;
317 my @available_categories = Koha::Patron::Categories->search()->get_column('categorycode');
318 $borrower_category = [ map { uc $_ } @$borrower_category ];
319 $skip_borrower_category = [ map { uc $_} @$skip_borrower_category ];
320 my %category_to_process;
321 for my $cat ( @$borrower_category ) {
322 unless ( grep { $_ eq $cat } @available_categories ) {
325 '-message' => "The category $cat does not exist in the database",
328 $category_to_process{$cat} = 1;
330 if ( @$skip_borrower_category ) {
331 for my $cat ( @$skip_borrower_category ) {
332 unless ( grep { $_ eq $cat } @available_categories ) {
335 '-message' => "The category $cat does not exist in the database",
339 %category_to_process = map { $_ => 1 } @available_categories;
340 %category_to_process = ( %category_to_process, map { $_ => 0 } @$skip_borrower_category );
343 my $filter_borrower_categories = ( scalar @$borrower_category || scalar @$skip_borrower_category );
345 my @available_itemtypes = Koha::ItemTypes->search()->get_column('itemtype');
346 $itemtype = [ map { uc $_ } @$itemtype ];
347 $skip_itemtype = [ map { uc $_} @$skip_itemtype ];
348 my %itemtype_to_process;
349 for my $it ( @$itemtype ) {
350 unless ( grep { $_ eq $it } @available_itemtypes ) {
353 '-message' => "The itemtype $it does not exist in the database",
356 $itemtype_to_process{$it} = 1;
358 if ( @$skip_itemtype ) {
359 for my $it ( @$skip_itemtype ) {
360 unless ( grep { $_ eq $it } @available_itemtypes ) {
363 '-message' => "The itemtype $it does not exist in the database",
367 %itemtype_to_process = map { $_ => 1 } @available_itemtypes;
368 %itemtype_to_process = ( %itemtype_to_process, map { $_ => 0 } @$skip_itemtype );
371 my $filter_itemtypes = ( scalar @$itemtype || scalar @$skip_itemtype );
378 # FIXME - The item is only marked returned if you supply --charge .
379 # We need a better way to handle this.
381 my $sth_items = longoverdue_sth({ skip_lost_values => \@skip_lost_values });
383 foreach my $startrange (sort keys %$lost) {
384 if( my $lostvalue = $lost->{$startrange} ) {
385 my ($date1) = bounds($startrange);
386 my ($date2) = bounds( $endrange);
387 # print "\nRange ", ++$i, "\nDue $startrange - $endrange days ago ($date2 to $date1), lost => $lostvalue\n" if($verbose);
389 printf "\nRange %s\nDue %3s - %3s days ago (%s to %s), lost => %s\n", ++$i,
390 $startrange, $endrange, $date2, $date1, $lostvalue;
391 $sth_items->execute($startrange, $endrange, $lostvalue);
393 ITEM: while (my $row=$sth_items->fetchrow_hashref) {
394 if( $filter_borrower_categories ) {
395 my $category = uc Koha::Patrons->find( $row->{borrowernumber} )->categorycode();
396 next ITEM unless ( $category_to_process{ $category } );
398 if ($filter_itemtypes) {
399 my $it = uc Koha::Items->find( $row->{itemnumber} )->effective_itemtype();
400 next ITEM unless ( $itemtype_to_process{$it} );
402 printf ("Due %s: item %5s from borrower %5s to lost: %s\n", $row->{date_due}, $row->{itemnumber}, $row->{borrowernumber}, $lostvalue) if($verbose);
404 Koha::Items->find( $row->{itemnumber} )->itemlost($lostvalue)
406 if ( $charge && $charge eq $lostvalue ) {
407 LostItem( $row->{'itemnumber'}, 'cronjob', $mark_returned );
408 } elsif ( $mark_returned ) {
409 my $patron = Koha::Patrons->find( $row->{borrowernumber} );
410 MarkIssueReturned($row->{borrowernumber},$row->{itemnumber},undef,$patron->privacy)
416 startrange => $startrange,
417 endrange => $endrange,
418 range => "$startrange - $endrange",
421 lostvalue => $lostvalue,
426 $endrange = $startrange;
430 my $arg = shift; # ref to array
431 my $got_items = shift || 0; # print "count" line for items
432 my @report = @$arg or return;
434 for my $range (@report) {
435 printf "\nRange %s\nDue %3s - %3s days ago (%s to %s), lost => %s\n", ++$i,
436 map {$range->{$_}} qw(startrange endrange date2 date1 lostvalue);
437 $got_items and printf " %4s items\n", $range->{count};
442 print "\n### LONGOVERDUE SUMMARY ###";
443 summarize (\@report, 1);
444 print "\nTOTAL: $total items\n";