Bug 30194: (follow-up) Remove invalid tests
[koha-ffzg.git] / C4 / Circulation.pm
index 11c3c77..c6baf48 100644 (file)
@@ -48,7 +48,7 @@ use Koha::Checkouts;
 use Koha::Illrequests;
 use Koha::Items;
 use Koha::Patrons;
-use Koha::Patron::Debarments qw( DelUniqueDebarment GetDebarments );
+use Koha::Patron::Debarments qw( DelUniqueDebarment GetDebarments AddUniqueDebarment );
 use Koha::Database;
 use Koha::Libraries;
 use Koha::Account::Lines;
@@ -61,6 +61,8 @@ use Koha::Config::SysPref;
 use Koha::Checkouts::ReturnClaims;
 use Koha::SearchEngine::Indexer;
 use Koha::Exceptions::Checkout;
+use Koha::Plugins;
+use Koha::Recalls;
 use Carp qw( carp );
 use List::MoreUtils qw( any );
 use Scalar::Util qw( looks_like_number );
@@ -165,6 +167,7 @@ sub barcodedecode {
     my ($barcode, $filter) = @_;
     my $branch = C4::Context::mybranch();
     $filter = C4::Context->preference('itemBarcodeInputFilter') unless $filter;
+    Koha::Plugins->call('item_barcode_transform',  \$barcode );
     $filter or return $barcode;     # ensure filter is defined, else return untouched barcode
        if ($filter eq 'whitespace') {
                $barcode =~ s/\s//g;
@@ -297,6 +300,14 @@ The item was reserved. The value is a reference-to-hash whose keys are fields fr
 
 The item was eligible to be transferred. Barring problems communicating with the database, the transfer should indeed have succeeded. The value should be ignored.
 
+=item C<RecallPlacedAtHoldingBranch>
+
+A recall for this item was found, and the transfer has already been completed as the item's branch matches the recall's pickup branch.
+
+=item C<RecallFound>
+
+A recall for this item was found, and the item needs to be transferred to the recall's pickup branch.
+
 =back
 
 =back
@@ -370,6 +381,21 @@ sub transferbook {
         $dotransfer = 0 unless $ignoreRs;
     }
 
+    # find recall
+    if ( C4::Context->preference('UseRecalls') ) {
+        my $recall = Koha::Recalls->find({ itemnumber => $itemnumber, status => 'in_transit' });
+        if ( defined $recall ) {
+            # do a transfer if the recall branch is different to the item holding branch
+            if ( $recall->branchcode eq $fbr ) {
+                $dotransfer = 0;
+                $messages->{'RecallPlacedAtHoldingBranch'} = 1;
+            } else {
+                $dotransfer = 1;
+                $messages->{'RecallFound'} = $recall;
+            }
+        }
+    }
+
     #actually do the transfer....
     if ($dotransfer) {
         ModItemTransfer( $itemnumber, $fbr, $tbr, $trigger );
@@ -449,47 +475,55 @@ sub TooMany {
                 $checkouts = $patron->checkouts; # if branch is the patron's home branch, then count all loans by patron
             } else {
                 $checkouts = $patron->checkouts->search(
-                    { 'item.homebranch' => $maxissueqty_rule->branchcode },
-                    { prefetch          => 'item' } );
+                    { 'item.homebranch' => $maxissueqty_rule->branchcode } );
             }
         } else {
             $checkouts = $patron->checkouts; # if rule is not branch specific then count all loans by patron
         }
+        $checkouts = $checkouts->search(undef, { prefetch => 'item' });
+
         my $sum_checkouts;
         my $rule_itemtype = $maxissueqty_rule->itemtype;
-        while ( my $c = $checkouts->next ) {
-            my $itemtype = $c->item->effective_itemtype;
-            my @types;
-            unless ( $rule_itemtype ) {
-                # matching rule has the default item type, so count only
-                # those existing loans that don't fall under a more
-                # specific rule
-                @types = Koha::CirculationRules->search(
-                    {
-                        branchcode => $maxissueqty_rule->branchcode,
-                        categorycode => [ $maxissueqty_rule->categorycode, $cat_borrower ],
-                        itemtype  => { '!=' => undef },
-                        rule_name => 'maxissueqty'
-                    }
-                )->get_column('itemtype');
 
-                next if grep {$_ eq $itemtype} @types;
-            } else {
-                my @types;
-                if ( $parent_maxissueqty_rule ) {
+        my @types;
+        unless ( $rule_itemtype ) {
+            # matching rule has the default item type, so count only
+            # those existing loans that don't fall under a more
+            # specific rule
+            @types = Koha::CirculationRules->search(
+                {
+                    branchcode => $maxissueqty_rule->branchcode,
+                    categorycode => [ $maxissueqty_rule->categorycode, $cat_borrower ],
+                    itemtype  => { '!=' => undef },
+                    rule_name => 'maxissueqty'
+                }
+            )->get_column('itemtype');
+        } else {
+            if ( $parent_maxissueqty_rule ) {
                 # if we have a parent item type then we count loans of the
                 # specific item type or its siblings or parent
-                    my $children = Koha::ItemTypes->search({ parent_type => $parent_type });
-                    @types = $children->get_column('itemtype');
-                    push @types, $parent_type;
-                } elsif ( $child_types ) {
+                my $children = Koha::ItemTypes->search({ parent_type => $parent_type });
+                @types = $children->get_column('itemtype');
+                push @types, $parent_type;
+            } elsif ( $child_types ) {
                 # If we are a parent type, we need to count all child types and our own type
-                    @types = $child_types->get_column('itemtype');
-                    push @types, $type; # And don't forget to count our own types
-                } else { push @types, $type; } # Otherwise only count the specific itemtype
+                @types = $child_types->get_column('itemtype');
+                push @types, $type; # And don't forget to count our own types
+            } else {
+                # Otherwise only count the specific itemtype
+                push @types, $type;
+            }
+        }
 
+        while ( my $c = $checkouts->next ) {
+            my $itemtype = $c->item->effective_itemtype;
+
+            unless ( $rule_itemtype ) {
+                next if grep {$_ eq $itemtype} @types;
+            } else {
                 next unless grep {$_ eq $itemtype} @types;
             }
+
             $sum_checkouts->{total}++;
             $sum_checkouts->{onsite_checkouts}++ if $c->onsite_checkout;
             $sum_checkouts->{itemtype}->{$itemtype}++;
@@ -720,6 +754,10 @@ sticky due date is invalid or due date in the past
 
 if the borrower borrows to much things
 
+=head3 RECALLED
+
+recalled by someone else
+
 =cut
 
 sub CanBookBeIssued {
@@ -835,7 +873,7 @@ sub CanBookBeIssued {
     my $no_issues_charge_guarantees = C4::Context->preference("NoIssuesChargeGuarantees");
     $no_issues_charge_guarantees = undef unless looks_like_number( $no_issues_charge_guarantees );
     if ( defined $no_issues_charge_guarantees ) {
-        my @guarantees = map { $_->guarantee } $patron->guarantee_relationships();
+        my @guarantees = map { $_->guarantee } $patron->guarantee_relationships->as_list;
         my $guarantees_non_issues_charges = 0;
         foreach my $g ( @guarantees ) {
             $guarantees_non_issues_charges += $g->account->non_issues_charges;
@@ -854,7 +892,7 @@ sub CanBookBeIssued {
     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 });
+        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;
@@ -1093,7 +1131,50 @@ sub CanBookBeIssued {
         }
     }
 
-    unless ( $ignore_reserves ) {
+    my $recall;
+    # CHECK IF ITEM HAS BEEN RECALLED BY ANOTHER PATRON
+    # Only bother doing this if UseRecalls is enabled and the item is recallable
+    # Don't look at recalls that are in transit
+    if ( C4::Context->preference('UseRecalls') and $item_object->can_be_waiting_recall ) {
+        my @recalls = $biblio->recalls({},{ order_by => { -asc => 'recalldate' } })->filter_by_current->as_list;
+
+        foreach my $r ( @recalls ) {
+            if ( $r->itemnumber and
+                $r->itemnumber == $item_object->itemnumber and
+                $r->borrowernumber == $patron->borrowernumber and
+                ( $r->waiting or $r->requested ) ) {
+                $messages{RECALLED} = $r->recall_id;
+                $recall = $r;
+                # this item is recalled by or already waiting for this borrower and the recall can be fulfilled
+                last;
+            }
+            elsif ( $r->itemnumber and
+                $r->itemnumber == $item_object->itemnumber and
+                $r->in_transit ) {
+                # recalled item is in transit
+                $issuingimpossible{RECALLED_INTRANSIT} = $r->branchcode;
+            }
+            elsif ( $r->item_level_recall and
+                $r->itemnumber == $item_object->itemnumber and
+                $r->borrowernumber != $patron->borrowernumber and
+                !$r->in_transit ) {
+                # this specific item has been recalled by a different patron
+                $needsconfirmation{RECALLED} = $r;
+                $recall = $r;
+                last;
+            }
+            elsif ( !$r->item_level_recall and
+                $r->borrowernumber != $patron->borrowernumber and
+                !$r->in_transit ) {
+                # a different patron has placed a biblio-level recall and this item is eligible to fill it
+                $needsconfirmation{RECALLED} = $r;
+                $recall = $r;
+                last;
+            }
+        }
+    }
+
+    unless ( $ignore_reserves and defined $recall ) {
         # See if the item is on reserve.
         my ( $restype, $res ) = C4::Reserves::CheckReserves( $item_object->itemnumber );
         if ($restype) {
@@ -1164,7 +1245,7 @@ sub CanBookBeIssued {
 
     ## check for high holds decreasing loan period
     if ( C4::Context->preference('decreaseLoanHighHolds') ) {
-        my $check = checkHighHolds( $item_unblessed, $patron_unblessed );
+        my $check = checkHighHolds( $item_object, $patron );
 
         if ( $check->{exceeded} ) {
             if ($override_high_holds) {
@@ -1276,9 +1357,8 @@ sub CanBookBeReturned {
 =cut
 
 sub checkHighHolds {
-    my ( $item, $borrower ) = @_;
-    my $branchcode = _GetCircControlBranch( $item, $borrower );
-    my $item_object = Koha::Items->find( $item->{itemnumber} );
+    my ( $item, $patron ) = @_;
+    my $branchcode = _GetCircControlBranch( $item->unblessed, $patron->unblessed );
 
     my $return_data = {
         exceeded    => 0,
@@ -1287,7 +1367,7 @@ sub checkHighHolds {
         due_date    => undef,
     };
 
-    my $holds = Koha::Holds->search( { biblionumber => $item->{'biblionumber'} } );
+    my $holds = Koha::Holds->search( { biblionumber => $item->biblionumber } );
 
     if ( $holds->count() ) {
         $return_data->{outstanding} = $holds->count();
@@ -1320,7 +1400,7 @@ sub checkHighHolds {
             }
 
             # Remove any items that are not holdable for this patron
-            @items = grep { CanItemBeReserved( $borrower->{borrowernumber}, $_->itemnumber, undef, { ignore_found_holds => 1 } )->{status} eq 'OK' } @items;
+            @items = grep { CanItemBeReserved( $patron , $_, undef, { ignore_found_holds => 1 } )->{status} eq 'OK' } @items;
 
             my $items_count = scalar @items;
 
@@ -1335,22 +1415,22 @@ sub checkHighHolds {
 
         my $issuedate = dt_from_string();
 
-        my $itype = $item_object->effective_itemtype;
+        my $itype = $item->effective_itemtype;
         my $daysmode = Koha::CirculationRules->get_effective_daysmode(
             {
-                categorycode => $borrower->{categorycode},
+                categorycode => $patron->categorycode,
                 itemtype     => $itype,
                 branchcode   => $branchcode,
             }
         );
         my $calendar = Koha::Calendar->new( branchcode => $branchcode, days_mode => $daysmode );
 
-        my $orig_due = C4::Circulation::CalcDateDue( $issuedate, $itype, $branchcode, $borrower );
+        my $orig_due = C4::Circulation::CalcDateDue( $issuedate, $itype, $branchcode, $patron->unblessed );
 
         my $rule = Koha::CirculationRules->get_effective_rule(
             {
-                categorycode => $borrower->{categorycode},
-                itemtype     => $item_object->effective_itemtype,
+                categorycode => $patron->categorycode,
+                itemtype     => $item->effective_itemtype,
                 branchcode   => $branchcode,
                 rule_name    => 'decreaseloanholds',
             }
@@ -1408,6 +1488,10 @@ AddIssue does the following things :
           * RESERVE PLACED ?
               - fill reserve if reserve to this patron
               - cancel reserve or not, otherwise
+          * RECALL PLACED ?
+              - fill recall if recall to this patron
+              - cancel recall or not
+              - revert recall's waiting status or not
           * TRANSFERT PENDING ?
               - complete the transfert
           * ISSUE THE BOOK
@@ -1422,6 +1506,8 @@ sub AddIssue {
     my $onsite_checkout = $params && $params->{onsite_checkout} ? 1 : 0;
     my $switch_onsite_checkout = $params && $params->{switch_onsite_checkout};
     my $auto_renew = $params && $params->{auto_renew};
+    my $cancel_recall = $params && $params->{cancel_recall};
+    my $recall_id = $params && $params->{recall_id};
     my $dbh          = C4::Context->dbh;
     my $barcodecheck = CheckValidBarcode($barcode);
 
@@ -1495,6 +1581,17 @@ sub AddIssue {
                 $item_object->discard_changes;
             }
 
+            if ( C4::Context->preference('UseRecalls') ) {
+                Koha::Recalls->move_recall(
+                    {
+                        action         => $cancel_recall,
+                        recall_id      => $recall_id,
+                        item           => $item_object,
+                        borrowernumber => $borrower->{borrowernumber},
+                    }
+                );
+            }
+
             C4::Reserves::MoveReserve( $item_object->itemnumber, $borrower->{'borrowernumber'}, $cancelreserve );
 
             # Starting process for transfer job (checking transfert and validate it if we have one)
@@ -1844,39 +1941,16 @@ sub GetBranchItemRule {
     my ( $branchcode, $itemtype ) = @_;
 
     # Search for rules!
-    my $holdallowed_rule = Koha::CirculationRules->get_effective_rule(
-        {
-            branchcode => $branchcode,
-            itemtype   => $itemtype,
-            rule_name  => 'holdallowed',
-        }
-    );
-    my $hold_fulfillment_policy_rule = Koha::CirculationRules->get_effective_rule(
-        {
-            branchcode => $branchcode,
-            itemtype   => $itemtype,
-            rule_name  => 'hold_fulfillment_policy',
-        }
-    );
-    my $returnbranch_rule = Koha::CirculationRules->get_effective_rule(
-        {
-            branchcode => $branchcode,
-            itemtype   => $itemtype,
-            rule_name  => 'returnbranch',
-        }
-    );
+    my $rules = Koha::CirculationRules->get_effective_rules({
+        branchcode => $branchcode,
+        itemtype => $itemtype,
+        rules => ['holdallowed', 'hold_fulfillment_policy', 'returnbranch']
+    });
 
     # built-in default circulation rule
-    my $rules;
-    $rules->{holdallowed} = defined $holdallowed_rule
-        ? $holdallowed_rule->rule_value
-        : 'from_any_library';
-    $rules->{hold_fulfillment_policy} = defined $hold_fulfillment_policy_rule
-        ? $hold_fulfillment_policy_rule->rule_value
-        : 'any';
-    $rules->{returnbranch} = defined $returnbranch_rule
-        ? $returnbranch_rule->rule_value
-        : 'homebranch';
+    $rules->{holdallowed} //= 'from_any_library';
+    $rules->{hold_fulfillment_policy} //= 'any';
+    $rules->{returnbranch} //= 'homebranch';
 
     return $rules;
 }
@@ -1944,6 +2018,16 @@ Value 1 if return is successful.
 
 If AutomaticItemReturn is disabled, return branch is given as value of NeedsTransfer.
 
+=item C<RecallFound>
+
+This item can fill a recall. The recall object is returned. If the recall pickup branch differs from
+the branch this item is being returned at, C<RecallNeedsTransfer> is also returned which contains this
+branchcode.
+
+=item C<TransferredRecall>
+
+This item has been transferred to this branch to fill a recall. The recall object is returned.
+
 =back
 
 C<$iteminformation> is a reference-to-hash, giving information about the
@@ -2127,29 +2211,34 @@ sub AddReturn {
     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
+            my @object_messages = @{ $updated_item->object_messages };
+            for my $message (@object_messages) {
+                $messages->{'LostItemFeeRefunded'} = 1
+                  if $message->message eq 'lost_refunded';
+                $messages->{'LostItemFeeRestored'} = 1
+                  if $message->message eq 'lost_restored';
+
+                if ( $message->message eq 'lost_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;
                     }
-                );
-                _FixOverduesOnReturn( $patron_unblessed->{borrowernumber},
-                    $item->itemnumber, undef, 'RETURNED' );
-                $messages->{'LostItemFeeCharged'} = 1;
+                    _CalculateAndUpdateFine(
+                        {
+                            issue       => $issue,
+                            item        => $item->unblessed,
+                            borrower    => $patron_unblessed,
+                            return_date => $return_date
+                        }
+                    );
+                    _FixOverduesOnReturn( $patron_unblessed->{borrowernumber},
+                        $item->itemnumber, undef, 'RETURNED' );
+                    $messages->{'LostItemFeeCharged'} = 1;
+                }
             }
         }
     }
@@ -2215,6 +2304,19 @@ sub AddReturn {
         }
     }
 
+    # find recalls...
+    if ( C4::Context->preference('UseRecalls') ) {
+        # check if this item is recallable first, which includes checking if UseRecalls syspref is enabled
+        my $recall = undef;
+        $recall = $item->check_recalls if $item->can_be_waiting_recall;
+        if ( defined $recall ) {
+            $messages->{RecallFound} = $recall;
+            if ( $recall->branchcode ne $branch ) {
+                $messages->{RecallNeedsTransfer} = $branch;
+            }
+        }
+    }
+
     # find reserves.....
     # launch the Checkreserves routine to find any holds
     my ($resfound, $resrec);
@@ -2257,6 +2359,7 @@ sub AddReturn {
                 item     => $item->unblessed,
                 borrower => $patron->unblessed,
                 branch   => $branch,
+                issue    => $issue
             });
         }
 
@@ -2273,13 +2376,23 @@ sub AddReturn {
         $request->status('RET') if $request;
     }
 
+    if ( C4::Context->preference('UseRecalls') ) {
+        # all recalls that have triggered a transfer will have an allocated itemnumber
+        my $transfer_recall = Koha::Recalls->find({ itemnumber => $item->itemnumber, status => 'in_transit' });
+        if ( $transfer_recall and $transfer_recall->branchcode eq $branch ) {
+            $messages->{TransferredRecall} = $transfer_recall;
+        }
+    }
+
     # Transfer to returnbranch if Automatic transfer set or append message NeedsTransfer
     if ( $validTransfer && !C4::RotatingCollections::isItemInAnyCollection( $item->itemnumber )
         && ( $doreturn or $messages->{'NotIssued'} )
         and !$resfound
         and ( $branch ne $returnbranch )
         and not $messages->{'WrongTransfer'}
-        and not $messages->{'WasTransfered'} )
+        and not $messages->{'WasTransfered'}
+        and not $messages->{TransferredRecall}
+        and not $messages->{RecallNeedsTransfer} )
     {
         my $BranchTransferLimitsType = C4::Context->preference("BranchTransferLimitsType") eq 'itemtype' ? 'effective_itemtype' : 'ccode';
         if  (C4::Context->preference("AutomaticItemReturn"    ) or
@@ -2357,17 +2470,6 @@ sub MarkIssueReturned {
 
     my $issue_id = $issue->issue_id;
 
-    my $anonymouspatron;
-    if ( $privacy && $privacy == 2 ) {
-        # The default of 0 will not work due to foreign key constraints
-        # The anonymisation will fail if AnonymousPatron is not a valid entry
-        # We need to check if the anonymous patron exist, Koha will fail loudly if it does not
-        # Note that a warning should appear on the about page (System information tab).
-        $anonymouspatron = C4::Context->preference('AnonymousPatron');
-        die "Fatal error: the patron ($borrowernumber) has requested their circulation history be anonymized on check-in, but the AnonymousPatron system preference is empty or not set correctly."
-            unless Koha::Patrons->find( $anonymouspatron );
-    }
-
     my $schema = Koha::Database->schema;
 
     # FIXME Improve the return value and handle it from callers
@@ -2388,7 +2490,7 @@ sub MarkIssueReturned {
 
         # anonymise patron checkout immediately if $privacy set to 2 and AnonymousPatron is set to a valid borrowernumber
         if ( $privacy && $privacy == 2) {
-            $old_checkout->borrowernumber($anonymouspatron)->store;
+            $old_checkout->anonymize;
         }
 
         # And finally delete the issue
@@ -2792,11 +2894,28 @@ sub CanBookBeRenewed {
             branchcode => $branchcode,
             issue      => $issue
         });
+        return ( 0, $auto_renew  ) if $auto_renew =~ 'auto_too_soon' && $cron;
+        # cron wants 'too_soon' over 'on_reserve' for performance and to avoid
+        # extra notices being sent. Cron also implies no override
         return ( 0, $auto_renew  ) if $auto_renew =~ 'auto_account_expired';
         return ( 0, $auto_renew  ) if $auto_renew =~ 'auto_too_late';
         return ( 0, $auto_renew  ) if $auto_renew =~ 'auto_too_much_oweing';
     }
 
+    if ( C4::Context->preference('UseRecalls') ) {
+        my $recall = undef;
+        $recall = $item->check_recalls if $item->can_be_waiting_recall;
+        if ( defined $recall ) {
+            if ( $recall->item_level_recall ) {
+                # item-level recall. check if this item is the recalled item, otherwise renewal will be allowed
+                return ( 0, 'recalled' ) if ( $recall->itemnumber == $item->itemnumber );
+            } else {
+                # biblio-level recall, so only disallow renewal if the biblio-level recall has been fulfilled by a different item
+                return ( 0, 'recalled' ) unless ( $recall->waiting );
+            }
+        }
+    }
+
     my ( $resfound, $resrec, $possible_reserves ) = 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.
@@ -2843,7 +2962,7 @@ sub CanBookBeRenewed {
                 next if IsItemOnHoldAndFound( $item->itemnumber );
                 while ( my $patron = $patrons->next ) {
                     next unless IsAvailableForItemLevelRequest($item, $patron);
-                    next unless CanItemBeReserved($patron->borrowernumber,$item->itemnumber,undef,{ignore_hold_counts=>1})->{status} eq 'OK';
+                    next unless CanItemBeReserved($patron,$item,undef,{ignore_hold_counts=>1})->{status} eq 'OK';
                     push @reservable, $item->itemnumber;
                     if (@reservable >= @borrowernumbers) {
                         $resfound = 0;
@@ -2855,12 +2974,11 @@ sub CanBookBeRenewed {
             }
         }
     }
-    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, "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";
+    if ( GetSoonestRenewDate($borrowernumber, $itemnumber) > dt_from_string() ){
+        return (0, "too_soon") unless $override_limit;
     }
 
     return ( 0, "auto_renew" ) if $auto_renew eq "ok" && !$override_limit; # 0 if auto-renewal should not succeed
@@ -3180,7 +3298,6 @@ sub GetSoonestRenewDate {
     );
 
     my $now = dt_from_string;
-    return $now unless $issuing_rule;
 
     if ( defined $issuing_rule->{norenewalbefore}
         and $issuing_rule->{norenewalbefore} ne "" )
@@ -3195,6 +3312,15 @@ sub GetSoonestRenewDate {
             $soonestrenewal->truncate( to => 'day' );
         }
         return $soonestrenewal if $now < $soonestrenewal;
+    } elsif ( $itemissue->auto_renew && $patron->autorenew_checkouts ) {
+        # Checkouts with auto-renewing fall back to due date
+        my $soonestrenewal = dt_from_string( $itemissue->date_due );
+        if ( C4::Context->preference('NoRenewalBeforePrecision') eq 'date'
+            and $issuing_rule->{lengthunit} eq 'days' )
+        {
+            $soonestrenewal->truncate( to => 'day' );
+        }
+        return $soonestrenewal;
     }
     return $now;
 }
@@ -3451,8 +3577,8 @@ B<Example>:
 
 sub SendCirculationAlert {
     my ($opts) = @_;
-    my ($type, $item, $borrower, $branch) =
-        ($opts->{type}, $opts->{item}, $opts->{borrower}, $opts->{branch});
+    my ($type, $item, $borrower, $branch, $issue) =
+        ($opts->{type}, $opts->{item}, $opts->{borrower}, $opts->{branch}, $opts->{issue});
     my %message_name = (
         CHECKIN  => 'Item_Check_in',
         CHECKOUT => 'Item_Checkout',
@@ -3462,7 +3588,23 @@ sub SendCirculationAlert {
         borrowernumber => $borrower->{borrowernumber},
         message_name   => $message_name{$type},
     });
-    my $issues_table = ( $type eq 'CHECKOUT' || $type eq 'RENEWAL' ) ? 'issues' : 'old_issues';
+
+
+    my $tables = {
+        items => $item->{itemnumber},
+        biblio      => $item->{biblionumber},
+        biblioitems => $item->{biblionumber},
+        borrowers   => $borrower,
+        branches    => $branch,
+    };
+
+    # TODO: Currently, we need to pass an issue_id as identifier for old_issues, but still an itemnumber for issues.
+    # See C4::Letters:: _parseletter_sth
+    if( $type eq 'CHECKIN' ){
+        $tables->{old_issues} = $issue->issue_id;
+    } else {
+        $tables->{issues} = $item->{itemnumber};
+    }
 
     my $schema = Koha::Database->new->schema;
     my @transports = keys %{ $borrower_preferences->{transports} };
@@ -3480,14 +3622,7 @@ sub SendCirculationAlert {
             branchcode => $branch,
             message_transport_type => $mtt,
             lang => $borrower->{lang},
-            tables => {
-                $issues_table => $item->{itemnumber},
-                'items'       => $item->{itemnumber},
-                'biblio'      => $item->{biblionumber},
-                'biblioitems' => $item->{biblionumber},
-                'borrowers'   => $borrower,
-                'branches'    => $branch,
-            }
+            tables => $tables,
         ) or next;
 
         C4::Context->dbh->do(q|LOCK TABLE message_queue READ|) unless $do_not_lock;
@@ -4249,6 +4384,8 @@ sub _CanBookBeAutoRenewed {
     my $branchcode = $params->{branchcode};
     my $issue = $params->{issue};
 
+    return "no" unless $issue->auto_renew && $patron->autorenew_checkouts;
+
     my $issuing_rule = Koha::CirculationRules->get_effective_rules(
         {
             categorycode => $patron->categorycode,
@@ -4263,80 +4400,59 @@ sub _CanBookBeAutoRenewed {
         }
     );
 
-    if ( $issue->auto_renew && $patron->autorenew_checkouts ) {
-
-        if ( $patron->category->effective_BlockExpiredPatronOpacActions and $patron->is_expired ) {
-            return 'auto_account_expired';
-        }
+    if ( $patron->category->effective_BlockExpiredPatronOpacActions and $patron->is_expired ) {
+        return '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 "auto_too_late";
-            }
+    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 "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 "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 "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 "auto_too_much_oweing";
-            }
+    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 "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 ($issue->auto_renew && $patron->autorenew_checkouts) ? "auto_too_soon" : "too_soon";
-        }
-        elsif ( $issue->auto_renew && $patron->autorenew_checkouts ) {
+        and $issuing_rule->{norenewalbefore} ne "" ) {
+        if ( GetSoonestRenewDate($patron->id, $item->id) > dt_from_string()) {
+            return "auto_too_soon";
+        } else {
             return "ok";
         }
     }
 
     # Fallback for automatic renewals:
     # If norenewalbefore is undef, don't renew before due date.
-    if ( $issue->auto_renew && $patron->autorenew_checkouts ) {
-        my $now = dt_from_string;
-        if ( $now >= dt_from_string( $issue->date_due, 'sql' ) ){
-            return "ok";
-        } else {
-            return "auto_too_soon";
-        }
+    my $now = dt_from_string;
+    if ( $now >= dt_from_string( $issue->date_due, 'sql' ) ){
+        return "ok";
+    } else {
+        return "auto_too_soon";
     }
-    return "no";
 }
 
 sub _item_denied_renewal {