#!/usr/bin/perl # Copyright 2016 Jacek Ablewicz # # This file is part of Koha. # # Koha is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # Koha is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Koha; if not, see . use Modern::Perl; use C4::Context; use C4::Overdues qw/CalcFine/; use C4::Log qw( logaction ); use Koha::DateUtils qw( dt_from_string output_pref ); use Koha::Patrons; use Getopt::Long qw( GetOptions ); my ($help, $verbose, $confirm, $log, $stdout_log); GetOptions( 'h|help' => \$help, 'v|verbose' => \$verbose, 'l|log' => \$log, 'c|confirm' => \$confirm, 'p|print' => \$stdout_log ); my $usage = << 'ENDUSAGE'; Script for fixing unclosed (FU), non accruing fine records, which may still need FU -> F correction post-Bug 15675. For details, see Bug 14390 & Bug 17135. This script has the following parameters : -h --help: this message -l --log: log changes to the system logs -c --confirm: commit changes (test only mode if not present) -p --print: output affected fine records details to the STDOUT -v --verbose ENDUSAGE { if ($help) { print $usage; exit 0; } Bug_17135_fix({ 'verbose' => $verbose, 'log' => $log, 'confirm' => $confirm, 'stdout_log' => $stdout_log }); exit 0; } sub Bug_17135_fix { my $params = shift; my $verbose = $params->{'verbose'}; my $log = $params->{'log'}; my $confirm = $params->{'confirm'}; my $stdout_log = $params->{'stdout_log'}; my $control = C4::Context->preference('CircControl'); my $mode = C4::Context->preference('finesMode'); my $today = dt_from_string(); my $dbh = C4::Context->dbh; ## fetch the unclosed FU fines linked to the issues by issue_id my $acclines = getFinesForChecking(); Warn("Got ".scalar(@$acclines)." FU accountlines to check") if $verbose; my $different_dates_cnt = 0; my $not_due_not_accruning_cnt = 0; my $due_not_accruning_cnt = 0; my $forfixing = []; my $old_date_pattern; for my $fine (@$acclines) { my $datedue = dt_from_string( $fine->{date_due} ); my $due = output_pref($datedue); $fine->{current_due_date} = $due; my $due_qr = qr/$due/; ## if the dates in fine description and in the issue record match, ## this is a legit post-Bug Bug 15675 accruing overdue fine ## which does not require any correction next if ($fine->{description} =~ /$due_qr/); if( !$old_date_pattern ) { ## for extracting old due date from fine description ## not used for fixing anything, logging/debug purposes only $old_date_pattern = $due; $old_date_pattern =~ s/[A-Za-z]/\[A-Za-z\]/g; $old_date_pattern =~ s/[0-9]/\\d/g; $old_date_pattern = qr/$old_date_pattern/; } if ($fine->{description} =~ / ($old_date_pattern)$/) { my $old_date_due = $1; $fine->{old_date_due} = $old_date_due; ### Warn("'$due' vs '$old_date_due'") if $verbose; } $fine->{old_date_due} //= 'unknown'; $different_dates_cnt++; ## after the last renewal, item is no longer due = it's not accruing, ## fine still needs to be closed unless ($fine->{item_is_due}) { $fine->{log_entry} = 'item not due, fine not accruing'; $not_due_not_accruning_cnt++; push(@$forfixing, $fine); next; } my $is_not_accruing = 0; ## item got due again after the last renewal, CalcFine() needs ## to be called to establish if the fine is accruning or not { my $statement; if ( C4::Context->preference('item-level_itypes') ) { $statement = "SELECT issues.*, items.itype as itemtype, items.homebranch, items.barcode, items.itemlost, items.replacementprice FROM issues LEFT JOIN items USING (itemnumber) WHERE date_due < NOW() AND issue_id = ? "; } else { $statement = "SELECT issues.*, biblioitems.itemtype, items.itype, items.homebranch, items.barcode, items.itemlost, replacementprice FROM issues LEFT JOIN items USING (itemnumber) LEFT JOIN biblioitems USING (biblioitemnumber) WHERE date_due < NOW() AND issue_id = ? "; } my $sth = $dbh->prepare($statement); $sth->execute($fine->{issue_id}); my $overdues = $sth->fetchall_arrayref({}); last if (@$overdues != 1); my $overdue = $overdues->[0]; ### last if $overdue->{itemlost}; ## arguable my $patron = Koha::Patron->find( $overdue->{borrowernumber} ); my $branchcode = ( $control eq 'ItemHomeLibrary' ) ? $overdue->{homebranch} : ( $control eq 'PatronLibrary' ) ? $patron->branchcode : $overdue->{branchcode}; my ($amount) = CalcFine( $overdue, $patron->categorycode, $branchcode, $datedue, $today ); ### Warn("CalcFine() returned '$amount'"); last if ($amount > 0); ## accruing fine, skip closing ## If we are here: item is due again, but fine is not accruing ## yet (overdue may be in the grace period, 1st charging period ## is not over yet, all days beetwen due date and today are ## holidays etc.). Old fine record needs to be closed $is_not_accruing = 1; } if ($is_not_accruing) { $fine->{log_entry} = 'item due, fine not accruing yet'; $due_not_accruning_cnt++; push(@$forfixing, $fine); }; } if( $verbose ) { Warn( "Fine records with mismatched old vs current due dates: $different_dates_cnt" ); Warn( "Non-accruing accountlines FU records (item not due): ".$not_due_not_accruning_cnt ); Warn( "Non-accruing accountlines FU records (item due): ".$due_not_accruning_cnt ); } my $updated_cnt = 0; my $update_sql = "UPDATE accountlines SET accounttype = 'F' WHERE accounttype = 'FU' AND accountlines_id = ? LIMIT 1"; for my $fine (@$forfixing) { my $logentry = "Closing old FU fine (Bug 17135); accountlines_id=".$fine->{accountlines_id}; $logentry .= " issue_id=".$fine->{issue_id}." amount=".$fine->{amount}; $logentry .= "; due dates (old, current): '".$fine->{old_date_due}."', '".$fine->{current_due_date}."'"; $logentry .= "; reason: ".$fine->{log_entry}; print($logentry."\n") if ($stdout_log); next unless ($confirm && $mode eq 'production'); my $rows_affected = $dbh->do($update_sql, undef, $fine->{accountlines_id}); $updated_cnt += $rows_affected; logaction("FINES", "FU", $fine->{borrowernumber}, $logentry) if ($log); } # Regardless of verbose, we report at least a number here if( @$forfixing > 0 ) { if( $confirm && $mode eq 'production') { Warn( "Database update done, $updated_cnt". ( @$forfixing == $updated_cnt? "": ( "/". @$forfixing )). " fine records closed successfully." ); } else { Warn( "Dry run (test only mode), skipping ". @$forfixing. " fine records." ); } } else { Warn( "No fine records needed to be fixed" ); } } sub getFinesForChecking { my $dbh = C4::Context->dbh; my $query = "SELECT acc.*, iss.date_due, IF(iss.date_due < NOW(), 1, 0) AS item_is_due FROM accountlines acc LEFT JOIN issues iss USING (issue_id) WHERE accounttype = 'FU' AND iss.issue_id IS NOT NULL AND iss.borrowernumber = acc.borrowernumber AND iss.itemnumber = acc.itemnumber ORDER BY acc.borrowernumber, acc.issue_id "; my $sth = $dbh->prepare($query); $sth->execute(); return $sth->fetchall_arrayref({}); } sub Warn { print join("\n", @_, ''); }