Bug 24083: (follow-up) Respond to QA feedback
[koha-ffzg.git] / C4 / Circulation.pm
index 7f75e89..2fba4b9 100644 (file)
@@ -58,6 +58,7 @@ use Koha::Config::SysPrefs;
 use Koha::Charges::Fees;
 use Koha::Util::SystemPreferences;
 use Koha::Checkouts::ReturnClaims;
+use Koha::SearchEngine::Indexer;
 use Carp;
 use List::MoreUtils qw( uniq any );
 use Scalar::Util qw( looks_like_number );
@@ -322,6 +323,14 @@ sub transferbook {
     my $dotransfer      = 1;
     my $item = Koha::Items->find( { barcode => $barcode } );
 
+    Koha::Exceptions::MissingParameter->throw(
+        "Missing mandatory parameter: from_branch")
+      unless $fbr;
+
+    Koha::Exceptions::MissingParameter->throw(
+        "Missing mandatory parameter: to_branch")
+      unless $tbr;
+
     # bad barcode..
     unless ( $item ) {
         $messages->{'BadBarcode'} = $barcode;
@@ -534,10 +543,11 @@ sub TooMany {
             $checkouts = $patron->checkouts->search(
                 { 'me.branchcode' => $branch} );
         } elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
-            ; # if branch is the patron's home branch, then count all loans by patron
+            $checkouts = $patron->checkouts; # if branch is the patron's home branch, then count all loans by patron
         } else {
             $checkouts = $patron->checkouts->search(
-                { 'item.homebranch' => $branch} );
+                { 'item.homebranch' => $branch},
+                { prefetch          => 'item' } );
         }
 
         my $checkout_count = $checkouts->count;
@@ -845,6 +855,21 @@ sub CanBookBeIssued {
         }
     }
 
+    # Check the debt of this patrons guarantors *and* the guarantees of those guarantors
+    my $no_issues_charge_guarantors = C4::Context->preference("NoIssuesChargeGuarantorsWithGuarantees");
+    $no_issues_charge_guarantors = undef unless looks_like_number( $no_issues_charge_guarantors );
+    if ( defined $no_issues_charge_guarantors ) {
+        my $guarantors_non_issues_charges += $patron->relationships_debt({ include_guarantors => 1, only_this_guarantor => 0, include_this_patron => 1 });
+
+        if ( $guarantors_non_issues_charges > $no_issues_charge_guarantors && !$inprocess && !$allowfineoverride) {
+            $issuingimpossible{DEBT_GUARANTORS} = $guarantors_non_issues_charges;
+        } elsif ( $guarantors_non_issues_charges > $no_issues_charge_guarantors && !$inprocess && $allowfineoverride) {
+            $needsconfirmation{DEBT_GUARANTORS} = $guarantors_non_issues_charges;
+        } elsif ( $allfinesneedoverride && $guarantors_non_issues_charges > 0 && $guarantors_non_issues_charges <= $no_issues_charge_guarantors && !$inprocess ) {
+            $needsconfirmation{DEBT_GUARANTORS} = $guarantors_non_issues_charges;
+        }
+    }
+
     if ( C4::Context->preference("IssuingInProcess") ) {
         if ( $non_issues_charges > $amountlimit && !$inprocess && !$allowfineoverride) {
             $issuingimpossible{DEBT} = $non_issues_charges;
@@ -889,6 +914,13 @@ sub CanBookBeIssued {
         }
     }
 
+    # Additional Materials Check
+    if ( C4::Context->preference("CircConfirmItemParts")
+        && $item_object->materials )
+    {
+        $needsconfirmation{ADDITIONAL_MATERIALS} = $item_object->materials;
+    }
+
     #
     # CHECK IF BOOK ALREADY ISSUED TO THIS BORROWER
     #
@@ -1298,16 +1330,30 @@ sub checkHighHolds {
 
         my $orig_due = C4::Circulation::CalcDateDue( $issuedate, $itype, $branchcode, $borrower );
 
-        my $decreaseLoanHighHoldsDuration = C4::Context->preference('decreaseLoanHighHoldsDuration');
+        my $rule = Koha::CirculationRules->get_effective_rule(
+            {
+                categorycode => $borrower->{categorycode},
+                itemtype     => $item_object->effective_itemtype,
+                branchcode   => $branchcode,
+                rule_name    => 'decreaseloanholds',
+            }
+        );
 
-        my $reduced_datedue = $calendar->addDate( $issuedate, $decreaseLoanHighHoldsDuration );
+        my $duration;
+        if ( defined($rule) && $rule->rule_value ne '' ){
+            # overrides decreaseLoanHighHoldsDuration syspref
+            $duration = $rule->rule_value;
+        } else {
+            $duration = C4::Context->preference('decreaseLoanHighHoldsDuration');
+        }
+        my $reduced_datedue = $calendar->addDate( $issuedate, $duration );
         $reduced_datedue->set_hour($orig_due->hour);
         $reduced_datedue->set_minute($orig_due->minute);
         $reduced_datedue->truncate( to => 'minute' );
 
         if ( DateTime->compare( $reduced_datedue, $orig_due ) == -1 ) {
             $return_data->{exceeded} = 1;
-            $return_data->{duration} = $decreaseLoanHighHoldsDuration;
+            $return_data->{duration} = $duration;
             $return_data->{due_date} = $reduced_datedue;
         }
     }
@@ -1433,18 +1479,22 @@ sub AddIssue {
             C4::Reserves::MoveReserve( $item_object->itemnumber, $borrower->{'borrowernumber'}, $cancelreserve );
 
             # Starting process for transfer job (checking transfert and validate it if we have one)
-            my ($datesent) = GetTransfers( $item_object->itemnumber );
-            if ($datesent) {
+            if ( my $transfer = $item_object->get_transfer ) {
                 # updating line of branchtranfert to finish it, and changing the to branch value, implement a comment for visibility of this case (maybe for stats ....)
-                my $sth = $dbh->prepare(
-                    "UPDATE branchtransfers 
-                        SET datearrived = now(),
-                        tobranch = ?,
-                        comments = 'Forced branchtransfer'
-                    WHERE itemnumber= ? AND datearrived IS NULL"
-                );
-                $sth->execute( C4::Context->userenv->{'branch'},
-                    $item_object->itemnumber );
+                $transfer->set(
+                    {
+                        datearrived => dt_from_string,
+                        tobranch    => C4::Context->userenv->{branch},
+                        comments    => 'Forced branchtransfer'
+                    }
+                )->store;
+                if ( $transfer->reason && $transfer->reason eq 'Reserve' ) {
+                    my $hold = $item_object->holds->search( { found => 'T' } )->next;
+                    if ( $hold ) { # Is this really needed?
+                        $hold->set( { found => undef } )->store;
+                        C4::Reserves::ModReserveMinusPriority($item_object->itemnumber, $hold->reserve_id);
+                    }
+                }
             }
 
             # If automatic renewal wasn't selected while issuing, set the value according to the issuing rule.
@@ -1461,14 +1511,6 @@ sub AddIssue {
                 $auto_renew = $rule->rule_value if $rule;
             }
 
-            # Record in the database the fact that the book was issued.
-            unless ($datedue) {
-                my $itype = $item_object->effective_itemtype;
-                $datedue = CalcDateDue( $issuedate, $itype, $branchcode, $borrower );
-
-            }
-            $datedue->truncate( to => 'minute' );
-
             my $issue_attributes = {
                 borrowernumber  => $borrower->{'borrowernumber'},
                 issuedate       => $issuedate->strftime('%Y-%m-%d %H:%M:%S'),
@@ -1478,6 +1520,19 @@ sub AddIssue {
                 auto_renew      => $auto_renew ? 1 : 0,
             };
 
+            # Get ID of logged in user.  if called from a batch job,
+            # no user session exists and C4::Context->userenv() returns
+            # the scalar '0'. Only do this if the syspref says so
+            if ( C4::Context->preference('RecordStaffUserOnCheckout') ) {
+                my $userenv = C4::Context->userenv();
+                my $usernumber = (ref($userenv) eq 'HASH') ? $userenv->{'number'} : undef;
+                if ($usernumber) {
+                    $issue_attributes->{issuer_id} = $usernumber;
+                }
+            }
+
+            # In the case that the borrower has an on-site checkout
+            # and SwitchOnSiteCheckouts is enabled this converts it to a regular checkout
             $issue = Koha::Checkouts->find( { itemnumber => $item_object->itemnumber } );
             if ($issue) {
                 $issue->set($issue_attributes)->store;
@@ -1490,6 +1545,7 @@ sub AddIssue {
                     }
                 )->store;
             }
+            $issue->discard_changes;
             if ( $item_object->location && $item_object->location eq 'CART'
                 && ( !$item_object->permanent_location || $item_object->permanent_location ne 'CART' ) ) {
             ## Item was moved to cart via UpdateItemLocationOnCheckin, anything issued should be taken off the cart.
@@ -1500,41 +1556,43 @@ sub AddIssue {
                 UpdateTotalIssues( $item_object->biblionumber, 1 );
             }
 
-            ## If item was lost, it has now been found, reverse any list item charges if necessary.
-            if ( $item_object->itemlost ) {
-                my $refund = 1;
-                my $no_refund_after_days = C4::Context->preference('NoRefundOnLostReturnedItemsAge');
-                if ($no_refund_after_days) {
-                    my $today = dt_from_string();
-                    my $lost_age_in_days =
-                      dt_from_string( $item_object->itemlost_on )
-                      ->delta_days($today)
-                      ->in_units('days');
-
-                    $refund = 0 unless ( $lost_age_in_days < $no_refund_after_days );
-                }
-
-                if (
-                    $refund && Koha::CirculationRules->get_lostreturn_policy(
-                        {
-                            return_branch => C4::Context->userenv->{branch},
-                            item          => $item_object
-                        }
-                    )
-                  )
-                {
-                    _FixAccountForLostAndFound( $item_object->itemnumber, undef,
-                        $item_object->barcode );
-                }
-            }
+            # Record if item was lost
+            my $was_lost = $item_object->itemlost;
 
             $item_object->issues( ( $item_object->issues || 0 ) + 1);
             $item_object->holdingbranch(C4::Context->userenv->{'branch'});
             $item_object->itemlost(0);
             $item_object->onloan($datedue->ymd());
             $item_object->datelastborrowed( dt_from_string()->ymd() );
+            $item_object->datelastseen( dt_from_string()->ymd() );
             $item_object->store({log_action => 0});
-            ModDateLastSeen( $item_object->itemnumber );
+
+            # If the item was lost, it has now been found, charge the overdue if necessary
+            if ($was_lost) {
+                if ( $item_object->{_charge} ) {
+                    $actualissue //= Koha::Old::Checkouts->search(
+                        { itemnumber => $item_unblessed->{itemnumber} },
+                        {
+                            order_by => { '-desc' => 'returndate' },
+                            rows     => 1
+                        }
+                    )->single;
+                    unless ( exists( $borrower->{branchcode} ) ) {
+                        my $patron = $actualissue->patron;
+                        $borrower = $patron->unblessed;
+                    }
+                    _CalculateAndUpdateFine(
+                        {
+                            issue       => $actualissue,
+                            item        => $item_unblessed,
+                            borrower    => $borrower,
+                            return_date => $issuedate
+                        }
+                    );
+                    _FixOverduesOnReturn( $borrower->{borrowernumber},
+                        $item_object->itemnumber, undef, 'RENEWED' );
+                }
+            }
 
             # If it costs to borrow this book, charge it to the patron's account.
             my ( $charge, $itemtype ) = GetIssuingCharges( $item_object->itemnumber, $borrower->{'borrowernumber'} );
@@ -1614,50 +1672,6 @@ Get loan length for an itemtype, a borrower type and a branch
 sub GetLoanLength {
     my ( $categorycode, $itemtype, $branchcode ) = @_;
 
-    # Set search precedences
-    my @params = (
-        {
-            categorycode => $categorycode,
-            itemtype     => $itemtype,
-            branchcode   => $branchcode,
-        },
-        {
-            categorycode => $categorycode,
-            itemtype     => undef,
-            branchcode   => $branchcode,
-        },
-        {
-            categorycode => undef,
-            itemtype     => $itemtype,
-            branchcode   => $branchcode,
-        },
-        {
-            categorycode => undef,
-            itemtype     => undef,
-            branchcode   => $branchcode,
-        },
-        {
-            categorycode => $categorycode,
-            itemtype     => $itemtype,
-            branchcode   => undef,
-        },
-        {
-            categorycode => $categorycode,
-            itemtype     => undef,
-            branchcode   => undef,
-        },
-        {
-            categorycode => undef,
-            itemtype     => $itemtype,
-            branchcode   => undef,
-        },
-        {
-            categorycode => undef,
-            itemtype     => undef,
-            branchcode   => undef,
-        },
-    );
-
     # Initialize default values
     my $rules = {
         issuelength   => 0,
@@ -1665,21 +1679,20 @@ sub GetLoanLength {
         lengthunit    => 'days',
     };
 
-    # Search for rules!
-    foreach my $rule_name (qw( issuelength renewalperiod lengthunit )) {
-        foreach my $params (@params) {
-            my $rule = Koha::CirculationRules->search(
-                {
-                    rule_name => $rule_name,
-                    %$params,
-                }
-            )->next();
+    my $found = Koha::CirculationRules->get_effective_rules( {
+        branchcode => $branchcode,
+        categorycode => $categorycode,
+        itemtype => $itemtype,
+        rules => [
+            'issuelength',
+            'renewalperiod',
+            'lengthunit'
+        ],
+    } );
 
-            if ($rule) {
-                $rules->{$rule_name} = $rule->rule_value;
-                last;
-            }
-        }
+    # Search for rules!
+    foreach my $rule_name (keys %$found) {
+        $rules->{$rule_name} = $found->{$rule_name};
     }
 
     return $rules;
@@ -1952,7 +1965,7 @@ sub AddReturn {
                 . Dumper($issue->unblessed) . "\n";
     } else {
         $messages->{'NotIssued'} = $barcode;
-        $item->onloan(undef)->store if defined $item->onloan;
+        $item->onloan(undef)->store({skip_record_index=>1}) if defined $item->onloan;
 
         # even though item is not on loan, it may still be transferred;  therefore, get current branch info
         $doreturn = 0;
@@ -1980,9 +1993,9 @@ sub AddReturn {
         if (defined $update_loc_rules->{_ALL_}) {
             if ($update_loc_rules->{_ALL_} eq '_PERM_') { $update_loc_rules->{_ALL_} = $item->permanent_location; }
             if ($update_loc_rules->{_ALL_} eq '_BLANK_') { $update_loc_rules->{_ALL_} = ''; }
-            if ( $item->location ne $update_loc_rules->{_ALL_}) {
+            if ( defined $item->location && $item->location ne $update_loc_rules->{_ALL_}) {
                 $messages->{'ItemLocationUpdated'} = { from => $item->location, to => $update_loc_rules->{_ALL_} };
-                $item->location($update_loc_rules->{_ALL_})->store;
+                $item->location($update_loc_rules->{_ALL_})->store({skip_record_index=>1});
             }
         }
         else {
@@ -1991,7 +2004,7 @@ sub AddReturn {
                 if ( $update_loc_rules->{$key} eq '_BLANK_') { $update_loc_rules->{$key} = '' ;}
                 if ( ($item->location eq $key && $item->location ne $update_loc_rules->{$key}) || ($key eq '_BLANK_' && $item->location eq '' && $update_loc_rules->{$key} ne '') ) {
                     $messages->{'ItemLocationUpdated'} = { from => $item->location, to => $update_loc_rules->{$key} };
-                    $item->location($update_loc_rules->{$key})->store;
+                    $item->location($update_loc_rules->{$key})->store({skip_record_index=>1});
                     last;
                 }
             }
@@ -2010,7 +2023,7 @@ sub AddReturn {
             foreach my $key ( keys %$rules ) {
                 if ( $item->notforloan eq $key ) {
                     $messages->{'NotForLoanStatusUpdated'} = { from => $item->notforloan, to => $rules->{$key} };
-                    $item->notforloan($rules->{$key})->store({ log_action => 0 });
+                    $item->notforloan($rules->{$key})->store({ log_action => 0, skip_record_index => 1 });
                     last;
                 }
             }
@@ -2025,6 +2038,8 @@ sub AddReturn {
             Rightbranch => $message
         };
         $doreturn = 0;
+        my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
+        $indexer->index_records( $item->biblionumber, "specialUpdate", "biblioserver" );
         return ( $doreturn, $messages, $issue, $patron_unblessed);
     }
 
@@ -2044,7 +2059,7 @@ sub AddReturn {
 
         if ($patron) {
             eval {
-                MarkIssueReturned( $borrowernumber, $item->itemnumber, $return_date, $patron->privacy );
+                MarkIssueReturned( $borrowernumber, $item->itemnumber, $return_date, $patron->privacy, { skip_record_index => 1} );
             };
             unless ( $@ ) {
                 if (
@@ -2060,31 +2075,65 @@ sub AddReturn {
             } else {
                 carp "The checkin for the following issue failed, Please go to the about page, section 'data corrupted' to know how to fix this problem ($@)" . Dumper( $issue->unblessed );
 
+                my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
+                $indexer->index_records( $item->biblionumber, "specialUpdate", "biblioserver" );
+
                 return ( 0, { WasReturned => 0, DataCorrupted => 1 }, $issue, $patron_unblessed );
             }
 
             # FIXME is the "= 1" right?  This could be the borrower hash.
             $messages->{'WasReturned'} = 1;
 
+        } else {
+            $item->onloan(undef)->store({ log_action => 0 , skip_record_index => 1 });
         }
-
-        $item->onloan(undef)->store({ log_action => 0 });
     }
 
     # the holdingbranch is updated if the document is returned to another location.
     # this is always done regardless of whether the item was on loan or not
-    my $item_holding_branch = $item->holdingbranch;
     if ($item->holdingbranch ne $branch) {
-        $item->holdingbranch($branch)->store;
+        $item->holdingbranch($branch)->store({ skip_record_index => 1 });
     }
 
+    my $item_was_lost = $item->itemlost;
     my $leave_item_lost = C4::Context->preference("BlockReturnOfLostItems") ? 1 : 0;
-    ModDateLastSeen( $item->itemnumber, $leave_item_lost );
+    my $updated_item = ModDateLastSeen( $item->itemnumber, $leave_item_lost, { skip_record_index => 1 } ); # will unset itemlost if needed
+
+    # fix up the accounts.....
+    if ($item_was_lost) {
+        $messages->{'WasLost'} = 1;
+        unless ( C4::Context->preference("BlockReturnOfLostItems") ) {
+            $messages->{'LostItemFeeRefunded'} = $updated_item->{_refunded};
+            $messages->{'LostItemFeeRestored'} = $updated_item->{_restored};
+
+            if ( $updated_item->{_charge} ) {
+                $issue //= Koha::Old::Checkouts->search(
+                    { itemnumber => $item->itemnumber },
+                    { order_by   => { '-desc' => 'returndate' }, rows => 1 } )
+                  ->single;
+                unless ( exists( $patron_unblessed->{branchcode} ) ) {
+                    my $patron = $issue->patron;
+                    $patron_unblessed = $patron->unblessed;
+                }
+                _CalculateAndUpdateFine(
+                    {
+                        issue       => $issue,
+                        item        => $item->unblessed,
+                        borrower    => $patron_unblessed,
+                        return_date => $return_date
+                    }
+                );
+                _FixOverduesOnReturn( $patron_unblessed->{borrowernumber},
+                    $item->itemnumber, undef, 'RETURNED' );
+                $messages->{'LostItemFeeCharged'} = 1;
+            }
+        }
+    }
 
     # check if we have a transfer for this document
     my ($datesent,$frombranch,$tobranch) = GetTransfers( $item->itemnumber );
 
-    # if we have a transfer to do, we update the line of transfers with the datearrived
+    # if we have a transfer to complete, we update the line of transfers with the datearrived
     my $is_in_rotating_collection = C4::RotatingCollections::isItemInAnyCollection( $item->itemnumber );
     if ($datesent) {
         # At this point we will either fill the transfer or it is a wrong transfer
@@ -2095,45 +2144,13 @@ sub AddReturn {
                 "UPDATE branchtransfers SET datearrived = now() WHERE itemnumber= ? AND datearrived IS NULL"
             );
             $sth->execute( $item->itemnumber );
+            $messages->{'TransferArrived'} = $frombranch;
         } else {
             $messages->{'WrongTransfer'}     = $tobranch;
             $messages->{'WrongTransferItem'} = $item->itemnumber;
         }
     }
 
-    # fix up the accounts.....
-    if ( $item->itemlost ) {
-        $messages->{'WasLost'} = 1;
-        unless ( C4::Context->preference("BlockReturnOfLostItems") ) {
-            my $refund = 1;
-            my $no_refund_after_days = C4::Context->preference('NoRefundOnLostReturnedItemsAge');
-            if ($no_refund_after_days) {
-                my $today = dt_from_string();
-                my $lost_age_in_days =
-                  dt_from_string( $item->itemlost_on )
-                  ->delta_days($today)
-                  ->in_units('days');
-
-                $refund = 0 unless ( $lost_age_in_days < $no_refund_after_days );
-            }
-
-            if (
-                $refund &&
-                Koha::CirculationRules->get_lostreturn_policy(
-                    {
-                        return_branch => C4::Context->userenv->{branch},
-                        item          => $item,
-                    }
-                  )
-              )
-            {
-                _FixAccountForLostAndFound( $item->itemnumber,
-                    $borrowernumber, $barcode );
-                $messages->{'LostItemFeeRefunded'} = 1;
-            }
-        }
-    }
-
     # fix up the overdues in accounts...
     if ($borrowernumber) {
         my $fix = _FixOverduesOnReturn( $borrowernumber, $item->itemnumber, $exemptfine, 'RETURNED' );
@@ -2229,7 +2246,7 @@ sub AddReturn {
            )) {
             $debug and warn sprintf "about to call ModItemTransfer(%s, %s, %s, %s)", $item->itemnumber,$branch, $returnbranch, $transfer_trigger;
             $debug and warn "item: " . Dumper($item->unblessed);
-            ModItemTransfer($item->itemnumber, $branch, $returnbranch, $transfer_trigger);
+            ModItemTransfer($item->itemnumber, $branch, $returnbranch, $transfer_trigger, { skip_record_index => 1 });
             $messages->{'WasTransfered'} = 1;
         } else {
             $messages->{'NeedsTransfer'} = $returnbranch;
@@ -2250,6 +2267,9 @@ sub AddReturn {
         }
     }
 
+    my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
+    $indexer->index_records( $item->biblionumber, "specialUpdate", "biblioserver" );
+
     if ( $doreturn and $issue ) {
         my $checkin = Koha::Old::Checkouts->find($issue->id);
 
@@ -2266,7 +2286,7 @@ sub AddReturn {
 
 =head2 MarkIssueReturned
 
-  MarkIssueReturned($borrowernumber, $itemnumber, $returndate, $privacy);
+  MarkIssueReturned($borrowernumber, $itemnumber, $returndate, $privacy, [$params] );
 
 Unconditionally marks an issue as being returned by
 moving the C<issues> row to C<old_issues> and
@@ -2282,10 +2302,12 @@ Ideally, this function would be internal to C<C4::Circulation>,
 not exported, but it is currently used in misc/cronjobs/longoverdue.pl
 and offline_circ/process_koc.pl.
 
+The last optional parameter allos passing skip_record_index to the item store call.
+
 =cut
 
 sub MarkIssueReturned {
-    my ( $borrowernumber, $itemnumber, $returndate, $privacy ) = @_;
+    my ( $borrowernumber, $itemnumber, $returndate, $privacy, $params ) = @_;
 
     # Retrieve the issue
     my $issue = Koha::Checkouts->find( { itemnumber => $itemnumber } ) or return;
@@ -2331,7 +2353,7 @@ sub MarkIssueReturned {
         # And finally delete the issue
         $issue->delete;
 
-        $issue->item->onloan(undef)->store({ log_action => 0 });
+        $issue->item->onloan(undef)->store({ log_action => 0, skip_record_index => $params->{skip_record_index} });
 
         if ( C4::Context->preference('StoreLastBorrower') ) {
             my $item = Koha::Items->find( $itemnumber );
@@ -2404,7 +2426,7 @@ sub _calculate_new_debar_dt {
 
     # grace period is measured in the same units as the loan
     my $grace =
-      DateTime::Duration->new( $unit => $issuing_rule->{firstremind} );
+      DateTime::Duration->new( $unit => $issuing_rule->{firstremind} // 0);
 
     my $deltadays = DateTime::Duration->new(
         days => $chargeable_units
@@ -2534,6 +2556,7 @@ sub _FixOverduesOnReturn {
             my $amountoutstanding = $accountline->amountoutstanding;
             if ( $accountline->amount == 0 && $payments->count == 0 ) {
                 $accountline->delete;
+                return 0; # no warning, we've just removed a zero value fine (backdated return)
             } elsif ($exemptfine && ($amountoutstanding != 0)) {
                 my $account = Koha::Account->new({patron_id => $borrowernumber});
                 my $credit = $account->add_credit(
@@ -2552,107 +2575,14 @@ sub _FixOverduesOnReturn {
                 if (C4::Context->preference("FinesLog")) {
                     &logaction("FINES", 'MODIFY',$borrowernumber,"Overdue forgiven: item $item");
                 }
-
-                $accountline->status('FORGIVEN');
-                $accountline->store();
-            } else {
-                $accountline->status($status);
-                $accountline->store();
-
             }
-        }
-    );
-
-    return $result;
-}
-
-=head2 _FixAccountForLostAndFound
-
-  &_FixAccountForLostAndFound($itemnumber, [$borrowernumber, $barcode]);
 
-Finds the most recent lost item charge for this item and refunds the borrower
-appropriatly, taking into account any payments or writeoffs already applied
-against the charge.
-
-Internal function, not exported, called only by AddReturn.
-
-=cut
-
-sub _FixAccountForLostAndFound {
-    my $itemnumber     = shift or return;
-    my $borrowernumber = @_ ? shift : undef;
-    my $item_id        = @_ ? shift : $itemnumber;  # Send the barcode if you want that logged in the description
-
-    my $credit;
-
-    # check for charge made for lost book
-    my $accountlines = Koha::Account::Lines->search(
-        {
-            itemnumber      => $itemnumber,
-            debit_type_code => 'LOST',
-            status          => [ undef, { '<>' => 'FOUND' } ]
-        },
-        {
-            order_by => { -desc => [ 'date', 'accountlines_id' ] }
+            $accountline->status($status);
+            return $accountline->store();
         }
     );
 
-    return unless $accountlines->count > 0;
-    my $accountline     = $accountlines->next;
-    my $total_to_refund = 0;
-
-    return unless $accountline->borrowernumber;
-    my $patron = Koha::Patrons->find( $accountline->borrowernumber );
-    return unless $patron; # Patron has been deleted, nobody to credit the return to
-
-    my $account = $patron->account;
-
-    # Use cases
-    if ( $accountline->amount > $accountline->amountoutstanding ) {
-        # some amount has been cancelled. collect the offsets that are not writeoffs
-        # this works because the only way to subtract from this kind of a debt is
-        # using the UI buttons 'Pay' and 'Write off'
-        my $credits_offsets = Koha::Account::Offsets->search({
-            debit_id  => $accountline->id,
-            credit_id => { '!=' => undef }, # it is not the debit itself
-            type      => { '!=' => 'Writeoff' },
-            amount    => { '<'  => 0 } # credits are negative on the DB
-        });
-
-        $total_to_refund = ( $credits_offsets->count > 0 )
-                            ? $credits_offsets->total * -1 # credits are negative on the DB
-                            : 0;
-    }
-
-    my $credit_total = $accountline->amountoutstanding + $total_to_refund;
-
-    if ( $credit_total > 0 ) {
-        my $branchcode = C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
-        $credit = $account->add_credit(
-            {
-                amount      => $credit_total,
-                description => 'Item found ' . $item_id,
-                type        => 'LOST_FOUND',
-                interface   => C4::Context->interface,
-                library_id  => $branchcode,
-                item_id     => $itemnumber
-            }
-        );
-
-        $credit->apply( { debits => [ $accountline ] } );
-    }
-
-    # Update the account status
-    $accountline->discard_changes->status('FOUND');
-    $accountline->store;
-
-    $accountline->item->paidfor('')->store({ log_action => 0 });
-
-    if ( defined $account and C4::Context->preference('AccountAutoReconcile') ) {
-        $account->reconcile_balance;
-    }
-
-    return ($credit) ? $credit->id : undef;
+    return $result;
 }
 
 =head2 _GetCircControlBranch
@@ -2816,11 +2746,11 @@ already renewed the loan. $error will contain the reason the renewal can not pro
 =cut
 
 sub CanBookBeRenewed {
-    my ( $borrowernumber, $itemnumber, $override_limit ) = @_;
+    my ( $borrowernumber, $itemnumber, $override_limit, $cron ) = @_;
 
     my $dbh    = C4::Context->dbh;
     my $renews = 1;
-    my $auto_renew = 0;
+    my $auto_renew = "no";
 
     my $item      = Koha::Items->find($itemnumber)      or return ( 0, 'no_item' );
     my $issue = $item->checkout or return ( 0, 'no_checkout' );
@@ -2843,6 +2773,7 @@ sub CanBookBeRenewed {
                     'no_auto_renewal_after_hard_limit',
                     'lengthunit',
                     'norenewalbefore',
+                    'unseen_renewals_allowed'
                 ]
             }
         );
@@ -2918,28 +2849,36 @@ sub CanBookBeRenewed {
 
             if ( $soonestrenewal > dt_from_string() )
             {
-                return ( 0, "auto_too_soon" ) if $issue->auto_renew && $patron->autorenew_checkouts;
-                return ( 0, "too_soon" );
+                $auto_renew = ($issue->auto_renew && $patron->autorenew_checkouts) ? "auto_too_soon" : "too_soon";
             }
             elsif ( $issue->auto_renew && $patron->autorenew_checkouts ) {
-                $auto_renew = 1;
+                $auto_renew = "ok";
             }
         }
 
         # Fallback for automatic renewals:
         # If norenewalbefore is undef, don't renew before due date.
-        if ( $issue->auto_renew && !$auto_renew && $patron->autorenew_checkouts ) {
+        if ( $issue->auto_renew && $auto_renew eq "no" && $patron->autorenew_checkouts ) {
             my $now = dt_from_string;
             if ( $now >= dt_from_string( $issue->date_due, 'sql' ) ){
-                $auto_renew = 1;
+                $auto_renew = "ok";
             } else {
-                return ( 0, "auto_too_soon" );
+                $auto_renew = "auto_too_soon";
             }
         }
     }
 
     my ( $resfound, $resrec, undef ) = C4::Reserves::CheckReserves($itemnumber);
 
+    # If next hold is non priority, then check if any hold with priority (non_priority = 0) exists for the same biblionumber.
+    if ( $resfound && $resrec->{non_priority} ) {
+        $resfound = Koha::Holds->search(
+            { biblionumber => $resrec->{biblionumber}, non_priority => 0 } )
+          ->count > 0;
+    }
+
+
+
     # This item can fill one or more unfilled reserve, can those unfilled reserves
     # all be filled by other available items?
     if ( $resfound
@@ -3003,15 +2942,134 @@ sub CanBookBeRenewed {
             }
         }
     }
-    return ( 0, "on_reserve" ) if $resfound;    # '' when no hold was found
-    return ( 0, "auto_renew" ) if $auto_renew && !$override_limit; # 0 if auto-renewal should not succeed
+    if( $cron ) { #The cron wants to return 'too_soon' over 'on_reserve'
+        return ( 0, $auto_renew  ) if $auto_renew =~ 'too_soon';#$auto_renew ne "no" && $auto_renew ne "ok";
+        return ( 0, "on_reserve" ) if $resfound;    # '' when no hold was found
+    } else { # For other purposes we want 'on_reserve' before 'too_soon'
+        return ( 0, "on_reserve" ) if $resfound;    # '' when no hold was found
+        return ( 0, $auto_renew  ) if $auto_renew =~ 'too_soon';#$auto_renew ne "no" && $auto_renew ne "ok";
+    }
+
+    return ( 0, "auto_renew" ) if $auto_renew eq "ok" && !$override_limit; # 0 if auto-renewal should not succeed
+
+    return ( 1, undef ) if $override_limit;
+
+    my $branchcode = _GetCircControlBranch( $item->unblessed, $patron->unblessed );
+    my $issuing_rule = Koha::CirculationRules->get_effective_rules(
+        {
+            categorycode => $patron->categorycode,
+            itemtype     => $item->effective_itemtype,
+            branchcode   => $branchcode,
+            rules => [
+                'renewalsallowed',
+                'no_auto_renewal_after',
+                'no_auto_renewal_after_hard_limit',
+                'lengthunit',
+                'norenewalbefore',
+                'unseen_renewals_allowed'
+            ]
+        }
+    );
+
+    return ( 0, "too_many" )
+      if not $issuing_rule->{renewalsallowed} or $issuing_rule->{renewalsallowed} <= $issue->renewals;
+
+    return ( 0, "too_unseen" )
+      if C4::Context->preference('UnseenRenewals') &&
+        $issuing_rule->{unseen_renewals_allowed} &&
+        $issuing_rule->{unseen_renewals_allowed} <= $issue->unseen_renewals;
+
+    my $overduesblockrenewing = C4::Context->preference('OverduesBlockRenewing');
+    my $restrictionblockrenewing = C4::Context->preference('RestrictionBlockRenewing');
+    $patron         = Koha::Patrons->find($borrowernumber); # FIXME Is this really useful?
+    my $restricted  = $patron->is_debarred;
+    my $hasoverdues = $patron->has_overdues;
+
+    if ( $restricted and $restrictionblockrenewing ) {
+        return ( 0, 'restriction');
+    } elsif ( ($hasoverdues and $overduesblockrenewing eq 'block') || ($issue->is_overdue and $overduesblockrenewing eq 'blockitem') ) {
+        return ( 0, 'overdue');
+    }
+
+    if ( $issue->auto_renew ) {
+
+        if ( $patron->category->effective_BlockExpiredPatronOpacActions and $patron->is_expired ) {
+            return ( 0, 'auto_account_expired' );
+        }
+
+        if ( defined $issuing_rule->{no_auto_renewal_after}
+                and $issuing_rule->{no_auto_renewal_after} ne "" ) {
+            # Get issue_date and add no_auto_renewal_after
+            # If this is greater than today, it's too late for renewal.
+            my $maximum_renewal_date = dt_from_string($issue->issuedate, 'sql');
+            $maximum_renewal_date->add(
+                $issuing_rule->{lengthunit} => $issuing_rule->{no_auto_renewal_after}
+            );
+            my $now = dt_from_string;
+            if ( $now >= $maximum_renewal_date ) {
+                return ( 0, "auto_too_late" );
+            }
+        }
+        if ( defined $issuing_rule->{no_auto_renewal_after_hard_limit}
+                      and $issuing_rule->{no_auto_renewal_after_hard_limit} ne "" ) {
+            # If no_auto_renewal_after_hard_limit is >= today, it's also too late for renewal
+            if ( dt_from_string >= dt_from_string( $issuing_rule->{no_auto_renewal_after_hard_limit} ) ) {
+                return ( 0, "auto_too_late" );
+            }
+        }
+
+        if ( C4::Context->preference('OPACFineNoRenewalsBlockAutoRenew') ) {
+            my $fine_no_renewals = C4::Context->preference("OPACFineNoRenewals");
+            my $amountoutstanding =
+              C4::Context->preference("OPACFineNoRenewalsIncludeCredit")
+              ? $patron->account->balance
+              : $patron->account->outstanding_debits->total_outstanding;
+            if ( $amountoutstanding and $amountoutstanding > $fine_no_renewals ) {
+                return ( 0, "auto_too_much_oweing" );
+            }
+        }
+    }
+
+    if ( defined $issuing_rule->{norenewalbefore}
+        and $issuing_rule->{norenewalbefore} ne "" )
+    {
+
+        # Calculate soonest renewal by subtracting 'No renewal before' from due date
+        my $soonestrenewal = dt_from_string( $issue->date_due, 'sql' )->subtract(
+            $issuing_rule->{lengthunit} => $issuing_rule->{norenewalbefore} );
+
+        # Depending on syspref reset the exact time, only check the date
+        if ( C4::Context->preference('NoRenewalBeforePrecision') eq 'date'
+            and $issuing_rule->{lengthunit} eq 'days' )
+        {
+            $soonestrenewal->truncate( to => 'day' );
+        }
+
+        if ( $soonestrenewal > dt_from_string() )
+        {
+            return ( 0, "auto_too_soon" ) if $issue->auto_renew;
+            return ( 0, "too_soon" );
+        }
+        elsif ( $issue->auto_renew ) {
+            return ( 0, "auto_renew" );
+        }
+    }
+
+    # Fallback for automatic renewals:
+    # If norenewalbefore is undef, don't renew before due date.
+    if ( $issue->auto_renew ) {
+        my $now = dt_from_string;
+        return ( 0, "auto_renew" )
+          if $now >= dt_from_string( $issue->date_due, 'sql' );
+        return ( 0, "auto_too_soon" );
+    }
 
     return ( 1, undef );
 }
 
 =head2 AddRenewal
 
-  &AddRenewal($borrowernumber, $itemnumber, $branch, [$datedue], [$lastreneweddate]);
+  &AddRenewal($borrowernumber, $itemnumber, $branch, [$datedue], [$lastreneweddate], [$seen]);
 
 Renews a loan.
 
@@ -3036,6 +3094,10 @@ syspref)
 If C<$datedue> is the empty string, C<&AddRenewal> will calculate the due date automatically
 from the book's item type.
 
+C<$seen> is a boolean flag indicating if the item was seen or not during the renewal. This
+informs the incrementing of the unseen_renewals column. If this flag is not supplied, we
+fallback to a true value
+
 =cut
 
 sub AddRenewal {
@@ -3045,6 +3107,10 @@ sub AddRenewal {
     my $datedue         = shift;
     my $lastreneweddate = shift || dt_from_string();
     my $skipfinecalc    = shift;
+    my $seen            = shift;
+
+    # Fallback on a 'seen' renewal
+    $seen = defined $seen && $seen == 0 ? 0 : 1;
 
     my $item_object   = Koha::Items->find($itemnumber) or return;
     my $biblio = $item_object->biblio;
@@ -3097,15 +3163,35 @@ sub AddRenewal {
             }
         );
 
+        # Increment the unseen renewals, if appropriate
+        # We only do so if the syspref is enabled and
+        # a maximum value has been set in the circ rules
+        my $unseen_renewals = $issue->unseen_renewals;
+        if (C4::Context->preference('UnseenRenewals')) {
+            my $rule = Koha::CirculationRules->get_effective_rule(
+                {   categorycode => $patron->categorycode,
+                    itemtype     => $item_object->effective_itemtype,
+                    branchcode   => $circ_library->branchcode,
+                    rule_name    => 'unseen_renewals_allowed'
+                }
+            );
+            if (!$seen && $rule && $rule->rule_value) {
+                $unseen_renewals++;
+            } else {
+                # If the renewal is seen, unseen should revert to 0
+                $unseen_renewals = 0;
+            }
+        }
+
         # Update the issues record to have the new due date, and a new count
         # of how many times it has been renewed.
         my $renews = ( $issue->renewals || 0 ) + 1;
-        my $sth = $dbh->prepare("UPDATE issues SET date_due = ?, renewals = ?, lastreneweddate = ?
+        my $sth = $dbh->prepare("UPDATE issues SET date_due = ?, renewals = ?, unseen_renewals = ?, lastreneweddate = ?
                                 WHERE borrowernumber=?
                                 AND itemnumber=?"
         );
 
-        $sth->execute( $datedue->strftime('%Y-%m-%d %H:%M'), $renews, $lastreneweddate, $borrowernumber, $itemnumber );
+        $sth->execute( $datedue->strftime('%Y-%m-%d %H:%M'), $renews, $unseen_renewals, $lastreneweddate, $borrowernumber, $itemnumber );
 
         # Update the renewal count on the item, and tell zebra to reindex
         $renews = ( $item_object->renewals || 0 ) + 1;
@@ -3192,8 +3278,11 @@ sub GetRenewCount {
     my ( $bornum, $itemno ) = @_;
     my $dbh           = C4::Context->dbh;
     my $renewcount    = 0;
+    my $unseencount    = 0;
     my $renewsallowed = 0;
+    my $unseenallowed = 0;
     my $renewsleft    = 0;
+    my $unseenleft    = 0;
 
     my $patron = Koha::Patrons->find( $bornum );
     my $item   = Koha::Items->find($itemno);
@@ -3212,22 +3301,34 @@ sub GetRenewCount {
     $sth->execute( $bornum, $itemno );
     my $data = $sth->fetchrow_hashref;
     $renewcount = $data->{'renewals'} if $data->{'renewals'};
+    $unseencount = $data->{'unseen_renewals'} if $data->{'unseen_renewals'};
     # $item and $borrower should be calculated
     my $branchcode = _GetCircControlBranch($item->unblessed, $patron->unblessed);
 
-    my $rule = Koha::CirculationRules->get_effective_rule(
+    my $rules = Koha::CirculationRules->get_effective_rules(
         {
             categorycode => $patron->categorycode,
             itemtype     => $item->effective_itemtype,
             branchcode   => $branchcode,
-            rule_name    => 'renewalsallowed',
+            rules        => [ 'renewalsallowed', 'unseen_renewals_allowed' ]
         }
     );
-
-    $renewsallowed = $rule ? $rule->rule_value : 0;
+    $renewsallowed = $rules ? $rules->{renewalsallowed} : 0;
+    $unseenallowed = $rules->{unseen_renewals_allowed} ?
+        $rules->{unseen_renewals_allowed} :
+        0;
     $renewsleft    = $renewsallowed - $renewcount;
+    $unseenleft    = $unseenallowed - $unseencount;
     if($renewsleft < 0){ $renewsleft = 0; }
-    return ( $renewcount, $renewsallowed, $renewsleft );
+    if($unseenleft < 0){ $unseenleft = 0; }
+    return (
+        $renewcount,
+        $renewsallowed,
+        $renewsleft,
+        $unseencount,
+        $unseenallowed,
+        $unseenleft
+    );
 }
 
 =head2 GetSoonestRenewDate
@@ -3886,9 +3987,23 @@ sub ReturnLostItem{
     MarkIssueReturned( $borrowernumber, $itemnum );
 }
 
+=head2 LostItem
+
+  LostItem( $itemnumber, $mark_lost_from, $force_mark_returned, [$params] );
+
+The final optional parameter, C<$params>, expected to contain
+'skip_record_index' key, which relayed down to Koha::Item/store,
+there it prevents calling of ModZebra index_records,
+which takes most of the time in batch adds/deletes: index_records better
+to be called later in C<additem.pl> after the whole loop.
+
+$params:
+    skip_record_index => 1|0
+
+=cut
 
 sub LostItem{
-    my ($itemnumber, $mark_lost_from, $force_mark_returned) = @_;
+    my ($itemnumber, $mark_lost_from, $force_mark_returned, $params) = @_;
 
     unless ( $mark_lost_from ) {
         # Temporary check to avoid regressions
@@ -3939,7 +4054,7 @@ sub LostItem{
 
     #When item is marked lost automatically cancel its outstanding transfers and set items holdingbranch to the transfer source branch (frombranch)
     if (my ( $datesent,$frombranch,$tobranch ) = GetTransfers($itemnumber)) {
-        Koha::Items->find($itemnumber)->holdingbranch($frombranch)->store;
+        Koha::Items->find($itemnumber)->holdingbranch($frombranch)->store({ skip_record_index => $params->{skip_record_index} });
     }
     my $transferdeleted = DeleteTransfer($itemnumber);
 }