Bug 26704: (follow-up) Rebase for bug 29785
[koha-ffzg.git] / C4 / Circulation.pm
index 50c436e..17cab0c 100644 (file)
@@ -21,33 +21,34 @@ package C4::Circulation;
 use Modern::Perl;
 use DateTime;
 use POSIX qw( floor );
-use Koha::DateUtils;
+use YAML::XS;
+use Encode;
+
+use Koha::DateUtils qw( dt_from_string output_pref );
 use C4::Context;
-use C4::Stats;
-use C4::Reserves;
-use C4::Biblio;
-use C4::Items;
-use C4::Members;
+use C4::Stats qw( UpdateStats );
+use C4::Reserves qw( CheckReserves CanItemBeReserved MoveReserve ModReserve ModReserveMinusPriority RevertWaitingStatus IsItemOnHoldAndFound IsAvailableForItemLevelRequest );
+use C4::Biblio qw( UpdateTotalIssues );
+use C4::Items qw( ModItemTransfer ModDateLastSeen CartToShelf );
 use C4::Accounts;
 use C4::ItemCirculationAlertPreference;
 use C4::Message;
-use C4::Debug;
-use C4::Log; # logaction
-use C4::Overdues qw(CalcFine UpdateFine get_chargeable_units);
+use C4::Log qw( logaction ); # logaction
+use C4::Overdues;
 use C4::RotatingCollections qw(GetCollectionItemBranches);
-use Algorithm::CheckDigits;
+use Algorithm::CheckDigits qw( CheckDigits );
 
-use Data::Dumper;
+use Data::Dumper qw( Dumper );
 use Koha::Account;
 use Koha::AuthorisedValues;
 use Koha::Biblioitems;
-use Koha::DateUtils;
+use Koha::DateUtils qw( dt_from_string output_pref );
 use Koha::Calendar;
 use Koha::Checkouts;
 use Koha::Illrequests;
 use Koha::Items;
 use Koha::Patrons;
-use Koha::Patron::Debarments;
+use Koha::Patron::Debarments qw( DelUniqueDebarment GetDebarments AddUniqueDebarment );
 use Koha::Database;
 use Koha::Libraries;
 use Koha::Account::Lines;
@@ -56,81 +57,73 @@ use Koha::Account::Lines;
 use Koha::Account::Offsets;
 use Koha::Config::SysPrefs;
 use Koha::Charges::Fees;
-use Koha::Util::SystemPreferences;
+use Koha::Config::SysPref;
 use Koha::Checkouts::ReturnClaims;
 use Koha::SearchEngine::Indexer;
-use Carp;
-use List::MoreUtils qw( uniq any );
+use Koha::Exceptions::Checkout;
+use Koha::Plugins;
+use Carp qw( carp );
+use List::MoreUtils qw( any );
 use Scalar::Util qw( looks_like_number );
-use Try::Tiny;
-use Date::Calc qw(
-  Today
-  Today_and_Now
-  Add_Delta_YM
-  Add_Delta_DHMS
-  Date_to_Days
-  Day_of_Week
-  Add_Delta_Days
-);
-use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
-
+use Date::Calc qw( Date_to_Days );
+our (@ISA, @EXPORT_OK);
 BEGIN {
-       require Exporter;
-       @ISA    = qw(Exporter);
-
-       # FIXME subs that should probably be elsewhere
-       push @EXPORT, qw(
-               &barcodedecode
-        &LostItem
-        &ReturnLostItem
-        &GetPendingOnSiteCheckouts
-       );
-
-       # subs to deal with issuing a book
-       push @EXPORT, qw(
-               &CanBookBeIssued
-               &CanBookBeRenewed
-               &AddIssue
-               &AddRenewal
-               &GetRenewCount
-        &GetSoonestRenewDate
-        &GetLatestAutoRenewDate
-               &GetIssuingCharges
-        &GetBranchBorrowerCircRule
-        &GetBranchItemRule
-               &GetOpenIssue
-        &CheckIfIssuedToPatron
-        &IsItemIssued
-        GetTopIssues
-       );
-
-       # subs to deal with returns
-       push @EXPORT, qw(
-               &AddReturn
-        &MarkIssueReturned
-       );
-
-       # subs to deal with transfers
-       push @EXPORT, qw(
-               &transferbook
-               &GetTransfers
-               &GetTransfersFromTo
-               &updateWrongTransfer
-               &DeleteTransfer
-                &IsBranchTransferAllowed
-                &CreateBranchTransferLimit
-                &DeleteBranchTransferLimits
-        &TransferSlip
-       );
-
-    # subs to deal with offline circulation
-    push @EXPORT, qw(
-      &GetOfflineOperations
-      &GetOfflineOperation
-      &AddOfflineOperation
-      &DeleteOfflineOperation
-      &ProcessOfflineOperation
+
+    require Exporter;
+    @ISA = qw(Exporter);
+
+    # FIXME subs that should probably be elsewhere
+    push @EXPORT_OK, qw(
+      barcodedecode
+      LostItem
+      ReturnLostItem
+      GetPendingOnSiteCheckouts
+
+      CanBookBeIssued
+      checkHighHolds
+      CanBookBeRenewed
+      AddIssue
+      GetLoanLength
+      GetHardDueDate
+      AddRenewal
+      GetRenewCount
+      GetSoonestRenewDate
+      GetLatestAutoRenewDate
+      GetIssuingCharges
+      AddIssuingCharge
+      GetBranchBorrowerCircRule
+      GetBranchItemRule
+      GetBiblioIssues
+      GetOpenIssue
+      GetUpcomingDueIssues
+      CheckIfIssuedToPatron
+      IsItemIssued
+      GetAgeRestriction
+      GetTopIssues
+
+      AddReturn
+      MarkIssueReturned
+
+      transferbook
+      TooMany
+      GetTransfers
+      GetTransfersFromTo
+      updateWrongTransfer
+      CalcDateDue
+      CheckValidBarcode
+      IsBranchTransferAllowed
+      CreateBranchTransferLimit
+      DeleteBranchTransferLimits
+      TransferSlip
+
+      GetOfflineOperations
+      GetOfflineOperation
+      AddOfflineOperation
+      DeleteOfflineOperation
+      ProcessOfflineOperation
+      ProcessOfflinePayment
     );
+    push @EXPORT_OK, '_GetCircControlBranch';    # This is wrong!
 }
 
 =head1 NAME
@@ -173,13 +166,14 @@ 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;
        } elsif ($filter eq 'cuecat') {
                chomp($barcode);
            my @fields = split( /\./, $barcode );
-           my @results = map( decode($_), @fields[ 1 .. $#fields ] );
+           my @results = map( C4::Circulation::_decode($_), @fields[ 1 .. $#fields ] );
            ($#results == 2) and return $results[2];
        } elsif ($filter eq 'T-prefix') {
                if ($barcode =~ /^[Tt](\d)/) {
@@ -210,9 +204,9 @@ sub barcodedecode {
     return $barcode;    # return barcode, modified or not
 }
 
-=head2 decode
+=head2 _decode
 
-  $str = &decode($chunk);
+  $str = &_decode($chunk);
 
 Decodes a segment of a string emitted by a CueCat barcode scanner and
 returns it.
@@ -222,7 +216,7 @@ or Javascript based decoding on the client side.
 
 =cut
 
-sub decode {
+sub _decode {
     my ($encoded) = @_;
     my $seq =
       'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+-';
@@ -372,10 +366,10 @@ sub transferbook {
     # That'll save a database query.
     my ( $resfound, $resrec, undef ) =
       CheckReserves( $itemnumber );
-    if ( $resfound and not $ignoreRs ) {
+    if ( $resfound ) {
         $resrec->{'ResFound'} = $resfound;
         $messages->{'ResFound'} = $resrec;
-        $dotransfer = 1;
+        $dotransfer = 0 unless $ignoreRs;
     }
 
     #actually do the transfer....
@@ -383,7 +377,7 @@ sub transferbook {
         ModItemTransfer( $itemnumber, $fbr, $tbr, $trigger );
 
         # don't need to update MARC anymore, we do it in batch now
-        $messages->{'WasTransfered'} = 1;
+        $messages->{'WasTransfered'} = $tbr;
 
     }
     ModDateLastSeen( $itemnumber );
@@ -716,6 +710,10 @@ issued to someone else.
 
 reserved for someone else.
 
+=head3 TRANSFERRED
+
+reserved and being transferred for someone else.
+
 =head3 INVALID_DATE
 
 sticky due date is invalid or due date in the past
@@ -794,7 +792,7 @@ sub CanBookBeIssued {
     #
     if ( $patron->category->category_type eq 'X' && (  $item_object->barcode  )) {
        # stats only borrower -- add entry to statistics table, and return issuingimpossible{STATS} = 1  .
-        &UpdateStats({
+        C4::Stats::UpdateStats({
                      branch => C4::Context->userenv->{'branch'},
                      type => 'localuse',
                      itemnumber => $item_object->itemnumber,
@@ -839,8 +837,8 @@ 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_non_issues_charges;
+        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;
         }
@@ -1128,6 +1126,28 @@ sub CanBookBeIssued {
                     $needsconfirmation{'resreservedate'} = $res->{reservedate};
                     $needsconfirmation{'reserve_id'} = $res->{reserve_id};
                 }
+                elsif ( $restype eq "Transferred" ) {
+                    # The item is determined hold being transferred for someone else.
+                    $needsconfirmation{TRANSFERRED} = 1;
+                    $needsconfirmation{'resfirstname'} = $patron->firstname;
+                    $needsconfirmation{'ressurname'} = $patron->surname;
+                    $needsconfirmation{'rescardnumber'} = $patron->cardnumber;
+                    $needsconfirmation{'resborrowernumber'} = $patron->borrowernumber;
+                    $needsconfirmation{'resbranchcode'} = $patron->branchcode;
+                    $needsconfirmation{'resreservedate'} = $res->{reservedate};
+                    $needsconfirmation{'reserve_id'} = $res->{reserve_id};
+                }
+                elsif ( $restype eq "Processing" ) {
+                    # The item is determined hold being processed for someone else.
+                    $needsconfirmation{PROCESSING} = 1;
+                    $needsconfirmation{'resfirstname'} = $patron->firstname;
+                    $needsconfirmation{'ressurname'} = $patron->surname;
+                    $needsconfirmation{'rescardnumber'} = $patron->cardnumber;
+                    $needsconfirmation{'resborrowernumber'} = $patron->borrowernumber;
+                    $needsconfirmation{'resbranchcode'} = $patron->branchcode;
+                    $needsconfirmation{'resreservedate'} = $res->{reservedate};
+                    $needsconfirmation{'reserve_id'} = $res->{reserve_id};
+                }
             }
         }
     }
@@ -1146,7 +1166,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) {
@@ -1258,9 +1278,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,
@@ -1269,7 +1288,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();
@@ -1302,7 +1321,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;
 
@@ -1317,22 +1336,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',
             }
@@ -1345,7 +1364,7 @@ sub checkHighHolds {
         } else {
             $duration = C4::Context->preference('decreaseLoanHighHoldsDuration');
         }
-        my $reduced_datedue = $calendar->addDate( $issuedate, $duration );
+        my $reduced_datedue = $calendar->addDuration( $issuedate, $duration );
         $reduced_datedue->set_hour($orig_due->hour);
         $reduced_datedue->set_minute($orig_due->minute);
         $reduced_datedue->truncate( to => 'minute' );
@@ -1473,6 +1492,8 @@ sub AddIssue {
                 my ( $allowed, $message ) = CanBookBeReturned( $item_unblessed, C4::Context->userenv->{branch} );
                 return unless $allowed;
                 AddReturn( $item_object->barcode, C4::Context->userenv->{'branch'} );
+                # AddReturn certainly has side-effects, like onloan => undef
+                $item_object->discard_changes;
             }
 
             C4::Reserves::MoveReserve( $item_object->itemnumber, $borrower->{'borrowernumber'}, $cancelreserve );
@@ -1545,6 +1566,7 @@ sub AddIssue {
                 )->store;
             }
             $issue->discard_changes;
+            C4::Auth::track_login_daily( $borrower->{userid} );
             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.
@@ -1610,7 +1632,7 @@ sub AddIssue {
             }
 
             # Record the fact that this book was issued.
-            &UpdateStats(
+            C4::Stats::UpdateStats(
                 {
                     branch => C4::Context->userenv->{'branch'},
                     type => ( $onsite_checkout ? 'onsite_checkout' : 'issue' ),
@@ -1798,9 +1820,10 @@ branch and item type, regardless of patron category.
 The return value is a hashref containing the following keys:
 
 holdallowed => Hold policy for this branch and itemtype. Possible values:
-  0: No holds allowed.
-  1: Holds allowed only by patrons that have the same homebranch as the item.
-  2: Holds allowed from any patron.
+  not_allowed:           No holds allowed.
+  from_home_library:     Holds allowed only by patrons that have the same homebranch as the item.
+  from_any_library:      Holds allowed from any patron.
+  from_local_hold_group: Holds allowed from libraries in hold group
 
 returnbranch => branch to which to return item.  Possible values:
   noreturn: do not return, let item remain where checked in (floating collections)
@@ -1825,22 +1848,22 @@ sub GetBranchItemRule {
     my $holdallowed_rule = Koha::CirculationRules->get_effective_rule(
         {
             branchcode => $branchcode,
-            itemtype => $itemtype,
-            rule_name => 'holdallowed',
+            itemtype   => $itemtype,
+            rule_name  => 'holdallowed',
         }
     );
     my $hold_fulfillment_policy_rule = Koha::CirculationRules->get_effective_rule(
         {
             branchcode => $branchcode,
-            itemtype => $itemtype,
-            rule_name => 'hold_fulfillment_policy',
+            itemtype   => $itemtype,
+            rule_name  => 'hold_fulfillment_policy',
         }
     );
     my $returnbranch_rule = Koha::CirculationRules->get_effective_rule(
         {
             branchcode => $branchcode,
-            itemtype => $itemtype,
-            rule_name => 'returnbranch',
+            itemtype   => $itemtype,
+            rule_name  => 'returnbranch',
         }
     );
 
@@ -1848,7 +1871,7 @@ sub GetBranchItemRule {
     my $rules;
     $rules->{holdallowed} = defined $holdallowed_rule
         ? $holdallowed_rule->rule_value
-        : 2;
+        : 'from_any_library';
     $rules->{hold_fulfillment_policy} = defined $hold_fulfillment_policy_rule
         ? $hold_fulfillment_policy_rule->rule_value
         : 'any';
@@ -1986,13 +2009,16 @@ sub AddReturn {
     my $borrowernumber = $patron ? $patron->borrowernumber : undef;    # we don't know if we had a borrower or not
     my $patron_unblessed = $patron ? $patron->unblessed : {};
 
-    my $update_loc_rules = get_yaml_pref_hash('UpdateItemLocationOnCheckin');
+    my $update_loc_rules = Koha::Config::SysPrefs->find('UpdateItemLocationOnCheckin')->get_yaml_pref_hash();
     map { $update_loc_rules->{$_} = $update_loc_rules->{$_}[0] } keys %$update_loc_rules; #We can only move to one location so we flatten the arrays
     if ($update_loc_rules) {
         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 ( defined $item->location && $item->location ne $update_loc_rules->{_ALL_}) {
+            if (
+                ( defined $item->location && $item->location ne $update_loc_rules->{_ALL_}) ||
+                (!defined $item->location && $update_loc_rules->{_ALL_} ne "")
+               ) {
                 $messages->{'ItemLocationUpdated'} = { from => $item->location, to => $update_loc_rules->{_ALL_} };
                 $item->location($update_loc_rules->{_ALL_})->store({skip_record_index=>1});
             }
@@ -2014,7 +2040,7 @@ sub AddReturn {
     if ($yaml) {
         $yaml = "$yaml\n\n";  # YAML is anal on ending \n. Surplus does not hurt
         my $rules;
-        eval { $rules = YAML::Load($yaml); };
+        eval { $rules = YAML::XS::Load(Encode::encode_utf8($yaml)); };
         if ($@) {
             warn "Unable to parse UpdateNotForLoanStatusOnCheckin syspref : $@";
         }
@@ -2102,51 +2128,68 @@ 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;
+                }
             }
         }
     }
 
     # check if we have a transfer for this document
-    my ($datesent,$frombranch,$tobranch) = GetTransfers( $item->itemnumber );
+    my $transfer = $item->get_transfer;
 
     # 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
-        # either way we should not now generate a new transfer
+    if ($transfer) {
         $validTransfer = 0;
-        if ( $tobranch eq $branch ) {
-            my $sth = C4::Context->dbh->prepare(
-                "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;
+        if ( $transfer->in_transit ) {
+            if ( $transfer->tobranch eq $branch ) {
+                $transfer->receive;
+                $messages->{'TransferArrived'} = $transfer->frombranch;
+                # validTransfer=1 allows us returning the item back if the reserve is cancelled
+                $validTransfer = 1 if $transfer->reason eq 'Reserve';
+            }
+            else {
+                $messages->{'WrongTransfer'}     = $transfer->tobranch;
+                $messages->{'WrongTransferItem'} = $item->itemnumber;
+                $messages->{'TransferTrigger'}   = $transfer->reason;
+            }
+        }
+        else {
+            if ( $transfer->tobranch eq $branch ) {
+                $transfer->receive;
+                $messages->{'TransferArrived'} = $transfer->frombranch;
+                # validTransfer=1 allows us returning the item back if the reserve is cancelled
+                $validTransfer = 1 if $transfer->reason eq 'Reserve';
+            }
+            else {
+                $messages->{'WasTransfered'}   = $transfer->tobranch;
+                $messages->{'TransferTrigger'} = $transfer->reason;
+            }
         }
     }
 
@@ -2195,7 +2238,7 @@ sub AddReturn {
     }
 
     # Record the fact that this book was returned.
-    UpdateStats({
+    C4::Stats::UpdateStats({
         branch         => $branch,
         type           => $stat_type,
         itemnumber     => $itemnumber,
@@ -2220,6 +2263,7 @@ sub AddReturn {
                 item     => $item->unblessed,
                 borrower => $patron->unblessed,
                 branch   => $branch,
+                issue    => $issue
             });
         }
 
@@ -2237,16 +2281,21 @@ sub AddReturn {
     }
 
     # Transfer to returnbranch if Automatic transfer set or append message NeedsTransfer
-    if ($validTransfer && !$is_in_rotating_collection && ($doreturn or $messages->{'NotIssued'}) and !$resfound and ($branch ne $returnbranch) ){
+    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'} )
+    {
         my $BranchTransferLimitsType = C4::Context->preference("BranchTransferLimitsType") eq 'itemtype' ? 'effective_itemtype' : 'ccode';
         if  (C4::Context->preference("AutomaticItemReturn"    ) or
             (C4::Context->preference("UseBranchTransferLimits") and
              ! IsBranchTransferAllowed($branch, $returnbranch, $item->$BranchTransferLimitsType )
            )) {
-            $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, { skip_record_index => 1 });
-            $messages->{'WasTransfered'} = 1;
+            $messages->{'WasTransfered'} = $returnbranch;
+            $messages->{'TransferTrigger'} = $transfer_trigger;
         } else {
             $messages->{'NeedsTransfer'} = $returnbranch;
             $messages->{'TransferTrigger'} = $transfer_trigger;
@@ -2466,7 +2515,7 @@ sub _calculate_new_debar_dt {
                 branchcode => $branchcode,
                 days_mode  => 'Calendar'
             );
-            $new_debar_dt = $calendar->addDate( $return_date, $suspension_days );
+            $new_debar_dt = $calendar->addDuration( $return_date, $suspension_days );
         }
         else {
             $new_debar_dt = $return_date->clone()->add_duration($suspension_days);
@@ -2569,7 +2618,7 @@ sub _FixOverduesOnReturn {
                     }
                 );
 
-                $credit->apply({ debits => [ $accountline ], offset_type => 'Forgiven' });
+                $credit->apply({ debits => [ $accountline ] });
 
                 if (C4::Context->preference("FinesLog")) {
                     &logaction("FINES", 'MODIFY',$borrowernumber,"Overdue forgiven: item $item");
@@ -2699,8 +2748,6 @@ already renewed the loan. $error will contain the reason the renewal can not pro
 sub CanBookBeRenewed {
     my ( $borrowernumber, $itemnumber, $override_limit, $cron ) = @_;
 
-    my $dbh    = C4::Context->dbh;
-    my $renews = 1;
     my $auto_renew = "no";
 
     my $item      = Koha::Items->find($itemnumber)      or return ( 0, 'no_item' );
@@ -2720,10 +2767,7 @@ sub CanBookBeRenewed {
                 branchcode   => $branchcode,
                 rules => [
                     'renewalsallowed',
-                    'no_auto_renewal_after',
-                    'no_auto_renewal_after_hard_limit',
                     'lengthunit',
-                    'norenewalbefore',
                     'unseen_renewals_allowed'
                 ]
             }
@@ -2749,82 +2793,21 @@ sub CanBookBeRenewed {
             return ( 0, 'overdue');
         }
 
-        if ( $issue->auto_renew && $patron->autorenew_checkouts ) {
-
-            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() )
-            {
-                $auto_renew = ($issue->auto_renew && $patron->autorenew_checkouts) ? "auto_too_soon" : "too_soon";
-            }
-            elsif ( $issue->auto_renew && $patron->autorenew_checkouts ) {
-                $auto_renew = "ok";
-            }
-        }
-
-        # Fallback for automatic renewals:
-        # If norenewalbefore is undef, don't renew before due date.
-        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 = "ok";
-            } else {
-                $auto_renew = "auto_too_soon";
-            }
-        }
+        $auto_renew = _CanBookBeAutoRenewed({
+            patron     => $patron,
+            item       => $item,
+            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';
     }
 
-    my ( $resfound, $resrec, undef ) = C4::Reserves::CheckReserves($itemnumber);
+    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.
     if ( $resfound && $resrec->{non_priority} ) {
@@ -2840,9 +2823,7 @@ sub CanBookBeRenewed {
     if ( $resfound
         && C4::Context->preference('AllowRenewalIfOtherItemsAvailable') )
     {
-        my $schema = Koha::Database->new()->schema();
-
-        my $item_holds = $schema->resultset('Reserve')->search( { itemnumber => $itemnumber, found => undef } )->count();
+        my $item_holds = Koha::Holds->search( { itemnumber => $itemnumber, found => undef } )->count();
         if ($item_holds) {
             # There is an item level hold on this item, no other item can fill the hold
             $resfound = 1;
@@ -2850,60 +2831,45 @@ sub CanBookBeRenewed {
         else {
 
             # Get all other items that could possibly fill reserves
-            my @itemnumbers = $schema->resultset('Item')->search(
-                {
-                    biblionumber => $resrec->{biblionumber},
-                    onloan       => undef,
-                    notforloan   => 0,
-                    -not         => { itemnumber => $itemnumber }
-                },
-                { columns => 'itemnumber' }
-            )->get_column('itemnumber')->all();
+            my $items = Koha::Items->search({
+                biblionumber => $resrec->{biblionumber},
+                onloan       => undef,
+                notforloan   => 0,
+                -not         => { itemnumber => $itemnumber }
+            });
 
             # Get all other reserves that could have been filled by this item
-            my @borrowernumbers;
-            while (1) {
-                my ( $reserve_found, $reserve, undef ) =
-                  C4::Reserves::CheckReserves( $itemnumber, undef, undef, \@borrowernumbers );
-
-                if ($reserve_found) {
-                    push( @borrowernumbers, $reserve->{borrowernumber} );
-                }
-                else {
-                    last;
-                }
-            }
+            my @borrowernumbers = map { $_->{borrowernumber} } @$possible_reserves;
+            my $patrons = Koha::Patrons->search({
+                borrowernumber => { -in => \@borrowernumbers }
+            });
 
             # If the count of the union of the lists of reservable items for each borrower
             # is equal or greater than the number of borrowers, we know that all reserves
             # can be filled with available items. We can get the union of the sets simply
             # by pushing all the elements onto an array and removing the duplicates.
             my @reservable;
-            my %patrons;
-            ITEM: foreach my $itemnumber (@itemnumbers) {
-                my $item = Koha::Items->find( $itemnumber );
-                next if IsItemOnHoldAndFound( $itemnumber );
-                for my $borrowernumber (@borrowernumbers) {
-                    my $patron = $patrons{$borrowernumber} //= Koha::Patrons->find( $borrowernumber );
+            ITEM: while ( my $item = $items->next ) {
+                next if IsItemOnHoldAndFound( $item->itemnumber );
+                while ( my $patron = $patrons->next ) {
                     next unless IsAvailableForItemLevelRequest($item, $patron);
-                    next unless CanItemBeReserved($borrowernumber,$itemnumber);
-
-                    push @reservable, $itemnumber;
+                    next unless CanItemBeReserved($patron,$item,undef,{ignore_hold_counts=>1})->{status} eq 'OK';
+                    push @reservable, $item->itemnumber;
                     if (@reservable >= @borrowernumbers) {
                         $resfound = 0;
                         last ITEM;
                     }
                     last;
                 }
+                $patrons->reset;
             }
         }
     }
-    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
@@ -3030,12 +2996,16 @@ sub AddRenewal {
         # 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 = ?, unseen_renewals = ?, lastreneweddate = ?
-                                WHERE borrowernumber=?
-                                AND itemnumber=?"
-        );
+        my $sth = $dbh->prepare("UPDATE issues SET date_due = ?, renewals = ?, unseen_renewals = ?, lastreneweddate = ? WHERE issue_id = ?");
 
-        $sth->execute( $datedue->strftime('%Y-%m-%d %H:%M'), $renews, $unseen_renewals, $lastreneweddate, $borrowernumber, $itemnumber );
+        eval{
+            $sth->execute( $datedue->strftime('%Y-%m-%d %H:%M'), $renews, $unseen_renewals, $lastreneweddate, $issue->issue_id );
+        };
+        if( $sth->err ){
+            Koha::Exceptions::Checkout::FailedRenewal->throw(
+                error => 'Update of issue# ' . $issue->issue_id . ' failed with error: ' . $sth->errstr
+            );
+        }
 
         # Update the renewal count on the item, and tell zebra to reindex
         $renews = ( $item_object->renewals || 0 ) + 1;
@@ -3090,7 +3060,7 @@ sub AddRenewal {
         }
 
         # Add the renewal to stats
-        UpdateStats(
+        C4::Stats::UpdateStats(
             {
                 branch         => $item_object->renewal_branchcode({branch => $branch}),
                 type           => 'renew',
@@ -3219,7 +3189,6 @@ sub GetSoonestRenewDate {
     );
 
     my $now = dt_from_string;
-    return $now unless $issuing_rule;
 
     if ( defined $issuing_rule->{norenewalbefore}
         and $issuing_rule->{norenewalbefore} ne "" )
@@ -3234,6 +3203,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;
 }
@@ -3345,19 +3323,19 @@ sub GetIssuingCharges {
     if ( my $item_data = $sth->fetchrow_hashref ) {
         $item_type = $item_data->{itemtype};
         $charge    = $item_data->{rentalcharge};
-        # FIXME This should follow CircControl
-        my $branch = C4::Context::mybranch();
-        my $patron = Koha::Patrons->find( $borrowernumber );
-        my $discount = Koha::CirculationRules->get_effective_rule({
-            categorycode => $patron->categorycode,
-            branchcode   => $branch,
-            itemtype     => $item_type,
-            rule_name    => 'rentaldiscount'
-        });
-        if ($discount) {
-            $charge = ( $charge * ( 100 - $discount->rule_value ) ) / 100;
-        }
         if ($charge) {
+            # FIXME This should follow CircControl
+            my $branch = C4::Context::mybranch();
+            my $patron = Koha::Patrons->find( $borrowernumber );
+            my $discount = Koha::CirculationRules->get_effective_rule({
+                categorycode => $patron->categorycode,
+                branchcode   => $branch,
+                itemtype     => $item_type,
+                rule_name    => 'rentaldiscount'
+            });
+            if ($discount) {
+                $charge = ( $charge * ( 100 - $discount->rule_value ) ) / 100;
+            }
             $charge = sprintf '%.2f', $charge; # ensure no fractions of a penny returned
         }
     }
@@ -3406,10 +3384,13 @@ sub GetTransfers {
         SELECT datesent,
                frombranch,
                tobranch,
-               branchtransfer_id
+               branchtransfer_id,
+               daterequested,
+               reason
         FROM branchtransfers
         WHERE itemnumber = ?
           AND datearrived IS NULL
+          AND datecancelled IS NULL
         ';
     my $sth = $dbh->prepare($query);
     $sth->execute($itemnumber);
@@ -3434,6 +3415,8 @@ sub GetTransfersFromTo {
         FROM   branchtransfers
         WHERE  frombranch=?
           AND  tobranch=?
+          AND datecancelled IS NULL
+          AND datesent IS NOT NULL
           AND datearrived IS NULL
     ";
     my $sth = $dbh->prepare($query);
@@ -3446,24 +3429,6 @@ sub GetTransfersFromTo {
     return (@gettransfers);
 }
 
-=head2 DeleteTransfer
-
-  &DeleteTransfer($itemnumber);
-
-=cut
-
-sub DeleteTransfer {
-    my ($itemnumber) = @_;
-    return unless $itemnumber;
-    my $dbh          = C4::Context->dbh;
-    my $sth          = $dbh->prepare(
-        "DELETE FROM branchtransfers
-         WHERE itemnumber=?
-         AND datearrived IS NULL "
-    );
-    return $sth->execute($itemnumber);
-}
-
 =head2 SendCirculationAlert
 
 Send out a C<check-in> or C<checkout> alert using the messaging system.
@@ -3503,8 +3468,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',
@@ -3514,7 +3479,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} };
@@ -3532,17 +3513,9 @@ 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;
 
-        $schema->storage->txn_begin;
         C4::Context->dbh->do(q|LOCK TABLE message_queue READ|) unless $do_not_lock;
         C4::Context->dbh->do(q|LOCK TABLE message_queue WRITE|) unless $do_not_lock;
         my $message = C4::Message->find_last_message($borrower, $type, $mtt);
@@ -3554,7 +3527,6 @@ sub SendCirculationAlert {
             $message->update;
         }
         C4::Context->dbh->do(q|UNLOCK TABLES|) unless $do_not_lock;
-        $schema->storage->txn_commit;
     }
 
     return;
@@ -3570,19 +3542,24 @@ This function validate the line of brachtransfer but with the wrong destination
 
 sub updateWrongTransfer {
        my ( $itemNumber,$waitingAtLibrary,$FromLibrary ) = @_;
-       my $dbh = C4::Context->dbh;     
-# first step validate the actual line of transfert .
-       my $sth =
-               $dbh->prepare(
-                       "update branchtransfers set datearrived = now(),tobranch=?,comments='wrongtransfer' where itemnumber= ? AND datearrived IS NULL"
-               );
-               $sth->execute($FromLibrary,$itemNumber);
-
-# second step create a new line of branchtransfer to the right location .
-       ModItemTransfer($itemNumber, $FromLibrary, $waitingAtLibrary);
-
-#third step changing holdingbranch of item
-    my $item = Koha::Items->find($itemNumber)->holdingbranch($FromLibrary)->store;
+
+    # first step: cancel the original transfer
+    my $item = Koha::Items->find($itemNumber);
+    my $transfer = $item->get_transfer;
+    $transfer->set({ datecancelled => dt_from_string, cancellation_reason => 'WrongTransfer' })->store();
+
+    # second step: create a new transfer to the right location
+    my $new_transfer = $item->request_transfer(
+        {
+            to            => $transfer->to_library,
+            reason        => $transfer->reason,
+            comment       => $transfer->comments,
+            ignore_limits => 1,
+            enqueue       => 1
+        }
+    );
+
+    return $new_transfer;
 }
 
 =head2 CalcDateDue
@@ -3651,7 +3628,7 @@ sub CalcDateDue {
             $dur = DateTime::Duration->new( days => $loanlength->{$length_key});
         }
         my $calendar = Koha::Calendar->new( branchcode => $branch, days_mode => $daysmode );
-        $datedue = $calendar->addDate( $datedue, $dur, $loanlength->{lengthunit} );
+        $datedue = $calendar->addDuration( $datedue, $dur, $loanlength->{lengthunit} );
         if ($loanlength->{lengthunit} eq 'days') {
             $datedue->set_hour(23);
             $datedue->set_minute(59);
@@ -3855,14 +3832,15 @@ sub LostItem{
             #warn " $issues->{'borrowernumber'}  /  $itemnumber ";
         }
 
-        MarkIssueReturned($borrowernumber,$itemnumber,undef,$patron->privacy) if $mark_returned;
+        MarkIssueReturned($borrowernumber,$itemnumber,undef,$patron->privacy,$params) if $mark_returned;
     }
 
-    #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({ skip_record_index => $params->{skip_record_index} });
+    # When an item is marked as lost, we should automatically cancel its outstanding transfers.
+    my $item = Koha::Items->find($itemnumber);
+    my $transfers = $item->get_transfers;
+    while (my $transfer = $transfers->next) {
+        $transfer->cancel({ reason => 'ItemLost', force => 1 });
     }
-    my $transferdeleted = DeleteTransfer($itemnumber);
 }
 
 sub GetOfflineOperations {
@@ -4290,6 +4268,84 @@ sub _CalculateAndUpdateFine {
     }
 }
 
+sub _CanBookBeAutoRenewed {
+    my ( $params ) = @_;
+    my $patron = $params->{patron};
+    my $item = $params->{item};
+    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,
+            itemtype     => $item->effective_itemtype,
+            branchcode   => $branchcode,
+            rules => [
+                'no_auto_renewal_after',
+                'no_auto_renewal_after_hard_limit',
+                'lengthunit',
+                'norenewalbefore',
+            ]
+        }
+    );
+
+    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_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 ( defined $issuing_rule->{norenewalbefore}
+        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.
+    my $now = dt_from_string;
+    if ( $now >= dt_from_string( $issue->date_due, 'sql' ) ){
+        return "ok";
+    } else {
+        return "auto_too_soon";
+    }
+}
+
 sub _item_denied_renewal {
     my ($params) = @_;