Bug 31963: Only show hold fee msg on OPAC if patron will be charged
[koha-ffzg.git] / C4 / Reserves.pm
index 503771e..d96d6ce 100644 (file)
@@ -24,34 +24,30 @@ package C4::Reserves;
 use Modern::Perl;
 
 use C4::Accounts;
 use Modern::Perl;
 
 use C4::Accounts;
-use C4::Biblio;
-use C4::Circulation;
+use C4::Biblio qw( GetMarcFromKohaField );
+use C4::Circulation qw( CheckIfIssuedToPatron GetAgeRestriction GetBranchItemRule );
 use C4::Context;
 use C4::Context;
-use C4::Items;
+use C4::Items qw( CartToShelf get_hostitemnumbers_of );
 use C4::Letters;
 use C4::Letters;
-use C4::Log;
+use C4::Log qw( logaction );
 use C4::Members::Messaging;
 use C4::Members;
 use Koha::Account::Lines;
 use C4::Members::Messaging;
 use C4::Members;
 use Koha::Account::Lines;
+use Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue;
 use Koha::Biblios;
 use Koha::Calendar;
 use Koha::CirculationRules;
 use Koha::Database;
 use Koha::Biblios;
 use Koha::Calendar;
 use Koha::CirculationRules;
 use Koha::Database;
-use Koha::DateUtils;
-use Koha::Hold;
+use Koha::DateUtils qw( dt_from_string output_pref );
 use Koha::Holds;
 use Koha::Holds;
-use Koha::IssuingRules;
 use Koha::ItemTypes;
 use Koha::Items;
 use Koha::Libraries;
 use Koha::ItemTypes;
 use Koha::Items;
 use Koha::Libraries;
-use Koha::Old::Hold;
+use Koha::Old::Holds;
 use Koha::Patrons;
 use Koha::Patrons;
+use Koha::Plugins;
 
 
-use Carp;
-use Data::Dumper;
-use List::MoreUtils qw( firstidx any );
-
-use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
+use List::MoreUtils qw( any );
 
 =head1 NAME
 
 
 =head1 NAME
 
@@ -69,10 +65,13 @@ This modules provides somes functions to deal with reservations.
   The following columns contains important values :
   - priority >0      : then the reserve is at 1st stage, and not yet affected to any item.
              =0      : then the reserve is being dealed
   The following columns contains important values :
   - priority >0      : then the reserve is at 1st stage, and not yet affected to any item.
              =0      : then the reserve is being dealed
-  - found : NULL       : means the patron requested the 1st available, and we haven't chosen the item
-            T(ransit)  : the reserve is linked to an item but is in transit to the pickup branch
-            W(aiting)  : the reserve is linked to an item, is at the pickup branch, and is waiting on the hold shelf
-            F(inished) : the reserve has been completed, and is done
+  - found : NULL         : means the patron requested the 1st available, and we haven't chosen the item
+            T(ransit)    : the reserve is linked to an item but is in transit to the pickup branch
+            W(aiting)    : the reserve is linked to an item, is at the pickup branch, and is waiting on the hold shelf
+            F(inished)   : the reserve has been completed, and is done
+            P(rocessing) : reserved item has been returned using self-check machine and reserve needs to be confirmed
+                           by librarian before notice is send and status changed to waiting.
+                           Applicable only if HoldsNeedProcessingSIP system preference is set.
   - itemnumber : empty : the reserve is still unaffected to an item
                  filled: the reserve is attached to an item
   The complete workflow is :
   - itemnumber : empty : the reserve is still unaffected to an item
                  filled: the reserve is attached to an item
   The complete workflow is :
@@ -96,53 +95,75 @@ This modules provides somes functions to deal with reservations.
 
 =cut
 
 
 =cut
 
+our (@ISA, @EXPORT_OK);
 BEGIN {
     require Exporter;
     @ISA = qw(Exporter);
 BEGIN {
     require Exporter;
     @ISA = qw(Exporter);
-    @EXPORT = qw(
-        &AddReserve
+    @EXPORT_OK = qw(
+      AddReserve
+
+      GetReserveStatus
+
+      GetOtherReserves
+      ChargeReserveFee
+      GetReserveFee
+
+      ModReserveAffect
+      ModReserve
+      ModReserveStatus
+      ModReserveCancelAll
+      ModReserveMinusPriority
+      MoveReserve
 
 
-        &GetReserveStatus
+      CheckReserves
+      CanBookBeReserved
+      CanItemBeReserved
+      CanReserveBeCanceledFromOpac
+      CancelExpiredReserves
 
 
-        &GetOtherReserves
+      AutoUnsuspendReserves
 
 
-        &ModReserveFill
-        &ModReserveAffect
-        &ModReserve
-        &ModReserveStatus
-        &ModReserveCancelAll
-        &ModReserveMinusPriority
-        &MoveReserve
+      IsAvailableForItemLevelRequest
+      ItemsAnyAvailableAndNotRestricted
 
 
-        &CheckReserves
-        &CanBookBeReserved
-        &CanItemBeReserved
-        &CanReserveBeCanceledFromOpac
-        &CancelExpiredReserves
+      AlterPriority
+      ToggleLowestPriority
 
 
-        &AutoUnsuspendReserves
+      ReserveSlip
+      ToggleSuspend
+      SuspendAll
 
 
-        &IsAvailableForItemLevelRequest
+      GetReservesControlBranch
 
 
-        &AlterPriority
-        &ToggleLowestPriority
+      CalculatePriority
 
 
-        &ReserveSlip
-        &ToggleSuspend
-        &SuspendAll
+      IsItemOnHoldAndFound
 
 
-        &GetReservesControlBranch
+      GetMaxPatronHoldsForRecord
 
 
-        IsItemOnHoldAndFound
+      MergeHolds
 
 
-        GetMaxPatronHoldsForRecord
+      RevertWaitingStatus
     );
     );
-    @EXPORT_OK = qw( MergeHolds );
 }
 
 =head2 AddReserve
 
 }
 
 =head2 AddReserve
 
-    AddReserve($branch,$borrowernumber,$biblionumber,$bibitems,$priority,$resdate,$expdate,$notes,$title,$checkitem,$found)
+    AddReserve(
+        {
+            branchcode       => $branchcode,
+            borrowernumber   => $borrowernumber,
+            biblionumber     => $biblionumber,
+            priority         => $priority,
+            reservation_date => $reservation_date,
+            expiration_date  => $expiration_date,
+            notes            => $notes,
+            title            => $title,
+            itemnumber       => $itemnumber,
+            found            => $found,
+            itemtype         => $itemtype,
+        }
+    );
 
 Adds reserve and generates HOLDPLACED message.
 
 
 Adds reserve and generates HOLDPLACED message.
 
@@ -158,16 +179,21 @@ The following tables are available witin the HOLDPLACED message:
 =cut
 
 sub AddReserve {
 =cut
 
 sub AddReserve {
-    my (
-        $branch,   $borrowernumber, $biblionumber, $bibitems,
-        $priority, $resdate,        $expdate,      $notes,
-        $title,    $checkitem,      $found,        $itemtype
-    ) = @_;
-
-    $resdate = output_pref( { str => dt_from_string( $resdate ), dateonly => 1, dateformat => 'iso' })
-        or output_pref({ dt => dt_from_string, dateonly => 1, dateformat => 'iso' });
-
-    $expdate = output_pref({ str => $expdate, dateonly => 1, dateformat => 'iso' });
+    my ($params)       = @_;
+    my $branch         = $params->{branchcode};
+    my $borrowernumber = $params->{borrowernumber};
+    my $biblionumber   = $params->{biblionumber};
+    my $priority       = $params->{priority};
+    my $resdate        = $params->{reservation_date};
+    my $patron_expiration_date = $params->{expiration_date};
+    my $notes          = $params->{notes};
+    my $title          = $params->{title};
+    my $checkitem      = $params->{itemnumber};
+    my $found          = $params->{found};
+    my $itemtype       = $params->{itemtype};
+    my $non_priority   = $params->{non_priority};
+
+    $resdate ||= dt_from_string;
 
     # if we have an item selectionned, and the pickup branch is the same as the holdingbranch
     # of the document, we force the value $priority and $found .
 
     # if we have an item selectionned, and the pickup branch is the same as the holdingbranch
     # of the document, we force the value $priority and $found .
@@ -194,11 +220,9 @@ sub AddReserve {
             $found = 'W';
         }
     }
             $found = 'W';
         }
     }
-
-    if ( C4::Context->preference('AllowHoldDateInFuture') ) {
-
-        # Make room in reserves for this before those of a later reserve date
-        $priority = _ShiftPriorityByDateAndPriority( $biblionumber, $resdate, $priority );
+    if ( C4::Context->preference( 'AllowHoldDateInFuture' ) ) {
+    # Make room in reserves for this if passed a priority
+    $priority = _ShiftPriority( $biblionumber, $priority );
     }
 
     my $waitingdate;
     }
 
     my $waitingdate;
@@ -223,14 +247,15 @@ sub AddReserve {
             itemnumber     => $checkitem,
             found          => $found,
             waitingdate    => $waitingdate,
             itemnumber     => $checkitem,
             found          => $found,
             waitingdate    => $waitingdate,
-            expirationdate => $expdate,
+            patron_expiration_date => $patron_expiration_date,
             itemtype       => $itemtype,
             item_level_hold => $checkitem ? 1 : 0,
             itemtype       => $itemtype,
             item_level_hold => $checkitem ? 1 : 0,
+            non_priority   => $non_priority ? 1 : 0,
         }
     )->store();
     $hold->set_waiting() if $found && $found eq 'W';
 
         }
     )->store();
     $hold->set_waiting() if $found && $found eq 'W';
 
-    logaction( 'HOLDS', 'CREATE', $hold->id, Dumper($hold->unblessed) )
+    logaction( 'HOLDS', 'CREATE', $hold->id, $hold )
         if C4::Context->preference('HoldsLog');
 
     my $reserve_id = $hold->id();
         if C4::Context->preference('HoldsLog');
 
     my $reserve_id = $hold->id();
@@ -262,44 +287,106 @@ sub AddReserve {
             },
         ) ) {
 
             },
         ) ) {
 
-            my $admin_email_address = $library->branchemail || C4::Context->preference('KohaAdminEmailAddress');
+            my $branch_email_address = $library->inbound_email_address;
 
             C4::Letters::EnqueueLetter(
 
             C4::Letters::EnqueueLetter(
-                {   letter                 => $letter,
+                {
+                    letter                 => $letter,
                     borrowernumber         => $borrowernumber,
                     message_transport_type => 'email',
                     borrowernumber         => $borrowernumber,
                     message_transport_type => 'email',
-                    from_address           => $admin_email_address,
-                    to_address           => $admin_email_address,
+                    to_address             => $branch_email_address,
                 }
             );
         }
     }
 
                 }
             );
         }
     }
 
+    Koha::Plugins->call('after_hold_create', $hold);
+    Koha::Plugins->call(
+        'after_hold_action',
+        {
+            action  => 'place',
+            payload => { hold => $hold->get_from_storage }
+        }
+    );
+
+    Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
+        {
+            biblio_ids => [ $biblionumber ]
+        }
+    ) if C4::Context->preference('RealTimeHoldsQueue');
+
     return $reserve_id;
 }
 
 =head2 CanBookBeReserved
 
     return $reserve_id;
 }
 
 =head2 CanBookBeReserved
 
-  $canReserve = &CanBookBeReserved($borrowernumber, $biblionumber, $branchcode)
+  $canReserve = &CanBookBeReserved($borrowernumber, $biblionumber, $branchcode, $params)
   if ($canReserve eq 'OK') { #We can reserve this Item! }
 
   if ($canReserve eq 'OK') { #We can reserve this Item! }
 
+  $params are passed directly through to CanItemBeReserved
+
 See CanItemBeReserved() for possible return values.
 
 =cut
 
 sub CanBookBeReserved{
 See CanItemBeReserved() for possible return values.
 
 =cut
 
 sub CanBookBeReserved{
-    my ($borrowernumber, $biblionumber, $pickup_branchcode) = @_;
+    my ($borrowernumber, $biblionumber, $pickup_branchcode, $params) = @_;
+
+    # Check that patron have not checked out this biblio (if AllowHoldsOnPatronsPossessions set)
+    if ( !C4::Context->preference('AllowHoldsOnPatronsPossessions')
+        && C4::Circulation::CheckIfIssuedToPatron( $borrowernumber, $biblionumber ) ) {
+        return { status =>'alreadypossession' };
+    }
+
+    if ( $params->{itemtype} ) {
+
+        # biblio-level, item type-contrained
+        my $patron          = Koha::Patrons->find($borrowernumber);
+        my $reservesallowed = Koha::CirculationRules->get_effective_rule(
+            {
+                itemtype     => $params->{itemtype},
+                categorycode => $patron->categorycode,
+                branchcode   => $pickup_branchcode,
+                rule_name    => 'reservesallowed',
+            }
+        )->rule_value;
+
+        $reservesallowed = ( $reservesallowed eq '' ) ? undef : $reservesallowed;
+
+        my $count = $patron->holds->search(
+            {
+                '-or' => [
+                    { 'me.itemtype' => $params->{itemtype} },
+                    { 'item.itype'  => $params->{itemtype} }
+                ]
+            },
+            {
+                join => ['item']
+            }
+        )->count;
+
+        return { status => '' }
+          if defined $reservesallowed and $reservesallowed < $count + 1;
+    }
 
 
-    my @itemnumbers = Koha::Items->search({ biblionumber => $biblionumber})->get_column("itemnumber");
+    my $items;
     #get items linked via host records
     #get items linked via host records
-    my @hostitems = get_hostitemnumbers_of($biblionumber);
-    if (@hostitems){
-        push (@itemnumbers, @hostitems);
+    my @hostitemnumbers = get_hostitemnumbers_of($biblionumber);
+    if (@hostitemnumbers){
+        $items = Koha::Items->search({
+            -or => [
+                biblionumber => $biblionumber,
+                itemnumber => { -in => @hostitemnumbers }
+            ]
+        });
+    } else {
+        $items = Koha::Items->search({ biblionumber => $biblionumber});
     }
 
     my $canReserve = { status => '' };
     }
 
     my $canReserve = { status => '' };
-    foreach my $itemnumber (@itemnumbers) {
-        $canReserve = CanItemBeReserved( $borrowernumber, $itemnumber, $pickup_branchcode );
+    my $patron = Koha::Patrons->find( $borrowernumber );
+    while ( my $item = $items->next ) {
+        $canReserve = CanItemBeReserved( $patron, $item, $pickup_branchcode, $params );
         return { status => 'OK' } if $canReserve->{status} eq 'OK';
     }
     return $canReserve;
         return { status => 'OK' } if $canReserve->{status} eq 'OK';
     }
     return $canReserve;
@@ -307,9 +394,16 @@ sub CanBookBeReserved{
 
 =head2 CanItemBeReserved
 
 
 =head2 CanItemBeReserved
 
-  $canReserve = &CanItemBeReserved($borrowernumber, $itemnumber, $branchcode)
+  $canReserve = &CanItemBeReserved($patron, $item, $branchcode, $params)
   if ($canReserve->{status} eq 'OK') { #We can reserve this Item! }
 
   if ($canReserve->{status} eq 'OK') { #We can reserve this Item! }
 
+  current params are:
+  'ignore_found_holds' - if true holds that have been trapped are not counted
+  toward the patron limit, used by checkHighHolds to avoid counting the hold we will fill with the
+  current checkout against the high holds threshold
+  'ignore_hold_counts' - we use this routine to check if an item can fill a hold - on this case we
+  should not check if there are too many holds as we only csre about reservability
+
 @RETURNS { status => OK },              if the Item can be reserved.
          { status => ageRestricted },   if the Item is age restricted for this borrower.
          { status => damaged },         if the Item is damaged.
 @RETURNS { status => OK },              if the Item can be reserved.
          { status => ageRestricted },   if the Item is age restricted for this borrower.
          { status => damaged },         if the Item is damaged.
@@ -321,23 +415,29 @@ sub CanBookBeReserved{
          { status => libraryNotPickupLocation },   if given branchcode is not configured to be a pickup location
          { status => cannotBeTransferred }, if branch transfer limit applies on given item and branchcode
          { status => pickupNotInHoldGroup }, pickup location is not in hold group, and pickup locations are only allowed from hold groups.
          { status => libraryNotPickupLocation },   if given branchcode is not configured to be a pickup location
          { status => cannotBeTransferred }, if branch transfer limit applies on given item and branchcode
          { status => pickupNotInHoldGroup }, pickup location is not in hold group, and pickup locations are only allowed from hold groups.
+         { status => recall }, if the borrower has already placed a recall on this item
 
 =cut
 
 sub CanItemBeReserved {
 
 =cut
 
 sub CanItemBeReserved {
-    my ( $borrowernumber, $itemnumber, $pickup_branchcode ) = @_;
+    my ( $patron, $item, $pickup_branchcode, $params ) = @_;
 
     my $dbh = C4::Context->dbh;
     my $ruleitemtype;    # itemtype of the matching issuing rule
 
     my $dbh = C4::Context->dbh;
     my $ruleitemtype;    # itemtype of the matching issuing rule
-    my $allowedreserves  = 0; # Total number of holds allowed across all records
-    my $holds_per_record = 1; # Total number of holds allowed for this one given record
-    my $holds_per_day;        # Default to unlimited
+    my $allowedreserves  = 0; # Total number of holds allowed across all records, default to none
+
+    # We check item branch if IndependentBranches is ON
+    # and canreservefromotherbranches is OFF
+    if ( C4::Context->preference('IndependentBranches')
+        and !C4::Context->preference('canreservefromotherbranches') )
+    {
+        if ( $item->homebranch ne $patron->branchcode ) {
+            return { status => 'cannotReserveFromOtherBranches' };
+        }
+    }
 
     # we retrieve borrowers and items informations #
     # item->{itype} will come for biblioitems if necessery
 
     # we retrieve borrowers and items informations #
     # item->{itype} will come for biblioitems if necessery
-    my $item       = Koha::Items->find($itemnumber);
-    my $biblio     = $item->biblio;
-    my $patron = Koha::Patrons->find( $borrowernumber );
     my $borrower = $patron->unblessed;
 
     # If an item is damaged and we don't allow holds on damaged items, we can stop right here
     my $borrower = $patron->unblessed;
 
     # If an item is damaged and we don't allow holds on damaged items, we can stop right here
@@ -345,153 +445,181 @@ sub CanItemBeReserved {
       if ( $item->damaged
         && !C4::Context->preference('AllowHoldsOnDamagedItems') );
 
       if ( $item->damaged
         && !C4::Context->preference('AllowHoldsOnDamagedItems') );
 
-    # Check for the age restriction
-    my ( $ageRestriction, $daysToAgeRestriction ) =
-      C4::Circulation::GetAgeRestriction( $biblio->biblioitem->agerestriction, $borrower );
-    return { status => 'ageRestricted' } if $daysToAgeRestriction && $daysToAgeRestriction > 0;
+    if( GetMarcFromKohaField('biblioitems.agerestriction') ){
+        my $biblio = $item->biblio;
+        # Check for the age restriction
+        my ( $ageRestriction, $daysToAgeRestriction ) =
+          C4::Circulation::GetAgeRestriction( $biblio->biblioitem->agerestriction, $borrower );
+        return { status => 'ageRestricted' } if $daysToAgeRestriction && $daysToAgeRestriction > 0;
+    }
 
     # Check that the patron doesn't have an item level hold on this item already
     return { status =>'itemAlreadyOnHold' }
 
     # Check that the patron doesn't have an item level hold on this item already
     return { status =>'itemAlreadyOnHold' }
-      if Koha::Holds->search( { borrowernumber => $borrowernumber, itemnumber => $itemnumber } )->count();
+      if ( !$params->{ignore_hold_counts} && Koha::Holds->search( { borrowernumber => $patron->borrowernumber, itemnumber => $item->itemnumber } )->count() );
 
 
-    my $controlbranch = C4::Context->preference('ReservesControlBranch');
+    # Check that patron have not checked out this biblio (if AllowHoldsOnPatronsPossessions set)
+    if ( !C4::Context->preference('AllowHoldsOnPatronsPossessions')
+        && C4::Circulation::CheckIfIssuedToPatron( $patron->borrowernumber, $item->biblionumber ) ) {
+        return { status =>'alreadypossession' };
+    }
 
 
-    my $querycount = q{
-        SELECT count(*) AS count
-          FROM reserves
-     LEFT JOIN items USING (itemnumber)
-     LEFT JOIN biblioitems ON (reserves.biblionumber=biblioitems.biblionumber)
-     LEFT JOIN borrowers USING (borrowernumber)
-         WHERE borrowernumber = ?
-    };
+    # check if a recall exists on this item from this borrower
+    return { status => 'recall' }
+      if $patron->recalls->filter_by_current->search({ item_id => $item->itemnumber })->count;
+
+    my $controlbranch = C4::Context->preference('ReservesControlBranch');
 
 
-    my $branchcode  = "";
+    my $reserves_control_branch;
     my $branchfield = "reserves.branchcode";
 
     if ( $controlbranch eq "ItemHomeLibrary" ) {
         $branchfield = "items.homebranch";
     my $branchfield = "reserves.branchcode";
 
     if ( $controlbranch eq "ItemHomeLibrary" ) {
         $branchfield = "items.homebranch";
-        $branchcode  = $item->homebranch;
+        $reserves_control_branch  = $item->homebranch;
     }
     elsif ( $controlbranch eq "PatronLibrary" ) {
         $branchfield = "borrowers.branchcode";
     }
     elsif ( $controlbranch eq "PatronLibrary" ) {
         $branchfield = "borrowers.branchcode";
-        $branchcode  = $borrower->{branchcode};
+        $reserves_control_branch  = $borrower->{branchcode};
     }
 
     # we retrieve rights
     }
 
     # we retrieve rights
-    if ( my $rights = GetHoldRule( $borrower->{'categorycode'}, $item->effective_itemtype, $branchcode ) ) {
-        $ruleitemtype     = $rights->{itemtype};
-        $allowedreserves  = $rights->{reservesallowed};
-        $holds_per_record = $rights->{holds_per_record};
-        $holds_per_day    = $rights->{holds_per_day};
+    if (
+        my $reservesallowed = Koha::CirculationRules->get_effective_rule({
+                itemtype     => $item->effective_itemtype,
+                categorycode => $borrower->{categorycode},
+                branchcode   => $reserves_control_branch,
+                rule_name    => 'reservesallowed',
+        })
+    ) {
+        $ruleitemtype     = $reservesallowed->itemtype;
+        $allowedreserves  = $reservesallowed->rule_value // 0; #undefined is 0, blank is unlimited
     }
     else {
     }
     else {
-        $ruleitemtype = '*';
-    }
-
-    my $holds = Koha::Holds->search(
-        {
-            borrowernumber => $borrowernumber,
-            biblionumber   => $item->biblionumber,
-            found          => undef, # Found holds don't count against a patron's holds limit
-        }
-    );
-    if ( $holds->count() >= $holds_per_record ) {
-        return { status => "tooManyHoldsForThisRecord", limit => $holds_per_record };
+        $ruleitemtype = undef;
     }
 
     }
 
-    my $today_holds = Koha::Holds->search({
-        borrowernumber => $borrowernumber,
-        reservedate    => dt_from_string->date
+    my $rights = Koha::CirculationRules->get_effective_rules({
+        categorycode => $borrower->{'categorycode'},
+        itemtype     => $item->effective_itemtype,
+        branchcode   => $reserves_control_branch,
+        rules        => ['holds_per_record','holds_per_day']
     });
     });
+    my $holds_per_record = $rights->{holds_per_record} // 1;
+    my $holds_per_day    = $rights->{holds_per_day};
 
 
-    if ( defined $holds_per_day &&
-          (   ( $holds_per_day > 0 && $today_holds->count() >= $holds_per_day )
-           or ( $holds_per_day == 0 ) )
-        )  {
-        return { status => 'tooManyReservesToday', limit => $holds_per_day };
+    if (   defined $holds_per_record && $holds_per_record ne '' ){
+        if ( $holds_per_record == 0 ) {
+            return { status => "noReservesAllowed" };
+        }
+        if ( !$params->{ignore_hold_counts} ) {
+            my $search_params = {
+                borrowernumber => $patron->borrowernumber,
+                biblionumber   => $item->biblionumber,
+            };
+            $search_params->{found} = undef if $params->{ignore_found_holds};
+            my $holds = Koha::Holds->search($search_params);
+            return { status => "tooManyHoldsForThisRecord", limit => $holds_per_record } if $holds->count() >= $holds_per_record;
+        }
     }
 
     }
 
-    # we retrieve count
-
-    $querycount .= "AND ( $branchfield = ? OR $branchfield IS NULL )";
+    if (!$params->{ignore_hold_counts} && defined $holds_per_day && $holds_per_day ne '')
+    {
+        my $today_holds = Koha::Holds->search({
+            borrowernumber => $patron->borrowernumber,
+            reservedate    => dt_from_string->date
+        });
+        return { status => 'tooManyReservesToday', limit => $holds_per_day } if $today_holds->count() >= $holds_per_day;
+    }
 
 
-    # If using item-level itypes, fall back to the record
-    # level itemtype if the hold has no associated item
-    $querycount .=
-      C4::Context->preference('item-level_itypes')
-      ? " AND COALESCE( items.itype, biblioitems.itemtype ) = ?"
-      : " AND biblioitems.itemtype = ?"
-      if ( $ruleitemtype ne "*" );
+    # we check if it's ok or not
+    if ( defined $allowedreserves && $allowedreserves ne '' ){
+        if( $allowedreserves == 0 ){
+            return { status => 'noReservesAllowed' };
+        }
+        if ( !$params->{ignore_hold_counts} ) {
+            # we retrieve count
+            my $querycount = q{
+                SELECT count(*) AS count
+                  FROM reserves
+             LEFT JOIN items USING (itemnumber)
+             LEFT JOIN biblioitems ON (reserves.biblionumber=biblioitems.biblionumber)
+             LEFT JOIN borrowers USING (borrowernumber)
+                 WHERE borrowernumber = ?
+            };
+            $querycount .= "AND ( $branchfield = ? OR $branchfield IS NULL )";
+
+            # If using item-level itypes, fall back to the record
+            # level itemtype if the hold has no associated item
+            if ( defined $ruleitemtype ) {
+                if ( C4::Context->preference('item-level_itypes') ) {
+                    $querycount .= q{
+                        AND ( COALESCE( items.itype, biblioitems.itemtype ) = ?
+                           OR reserves.itemtype = ? )
+                    };
+                }
+                else {
+                    $querycount .= q{
+                        AND ( biblioitems.itemtype = ?
+                           OR reserves.itemtype = ? )
+                    };
+                }
+            }
 
 
-    my $sthcount = $dbh->prepare($querycount);
+            my $sthcount = $dbh->prepare($querycount);
 
 
-    if ( $ruleitemtype eq "*" ) {
-        $sthcount->execute( $borrowernumber, $branchcode );
-    }
-    else {
-        $sthcount->execute( $borrowernumber, $branchcode, $ruleitemtype );
-    }
+            if ( defined $ruleitemtype ) {
+                $sthcount->execute( $patron->borrowernumber, $reserves_control_branch, $ruleitemtype, $ruleitemtype );
+            }
+            else {
+                $sthcount->execute( $patron->borrowernumber, $reserves_control_branch );
+            }
 
 
-    my $reservecount = "0";
-    if ( my $rowcount = $sthcount->fetchrow_hashref() ) {
-        $reservecount = $rowcount->{count};
-    }
+            my $reservecount = "0";
+            if ( my $rowcount = $sthcount->fetchrow_hashref() ) {
+                $reservecount = $rowcount->{count};
+            }
 
 
-    # we check if it's ok or not
-    if ( $reservecount >= $allowedreserves ) {
-        return { status => 'tooManyReserves', limit => $allowedreserves };
+            return { status => 'tooManyReserves', limit => $allowedreserves } if $reservecount >= $allowedreserves;
+        }
     }
 
     # Now we need to check hold limits by patron category
     my $rule = Koha::CirculationRules->get_effective_rule(
         {
     }
 
     # Now we need to check hold limits by patron category
     my $rule = Koha::CirculationRules->get_effective_rule(
         {
-            categorycode => $borrower->{categorycode},
-            branchcode   => $branchcode,
+            categorycode => $patron->categorycode,
+            branchcode   => $reserves_control_branch,
             rule_name    => 'max_holds',
         }
     );
             rule_name    => 'max_holds',
         }
     );
-    if ( $rule && defined( $rule->rule_value ) && $rule->rule_value ne '' ) {
+    if (!$params->{ignore_hold_counts} && $rule && defined( $rule->rule_value ) && $rule->rule_value ne '' ) {
         my $total_holds_count = Koha::Holds->search(
             {
         my $total_holds_count = Koha::Holds->search(
             {
-                borrowernumber => $borrower->{borrowernumber}
+                borrowernumber => $patron->borrowernumber
             }
         )->count();
 
         return { status => 'tooManyReserves', limit => $rule->rule_value} if $total_holds_count >= $rule->rule_value;
     }
 
             }
         )->count();
 
         return { status => 'tooManyReserves', limit => $rule->rule_value} if $total_holds_count >= $rule->rule_value;
     }
 
-    my $reserves_control_branch =
-      GetReservesControlBranch( $item->unblessed(), $borrower );
     my $branchitemrule =
     my $branchitemrule =
-      C4::Circulation::GetBranchItemRule( $reserves_control_branch, $item->itype ); # FIXME Should not be item->effective_itemtype?
+      C4::Circulation::GetBranchItemRule( $reserves_control_branch, $item->effective_itemtype );
 
 
-    if ( $branchitemrule->{holdallowed} == 0 ) {
+    if ( $branchitemrule->{holdallowed} eq 'not_allowed' ) {
         return { status => 'notReservable' };
     }
 
         return { status => 'notReservable' };
     }
 
-    if (   $branchitemrule->{holdallowed} == 1
+    if (   $branchitemrule->{holdallowed} eq 'from_home_library'
         && $borrower->{branchcode} ne $item->homebranch )
     {
         return { status => 'cannotReserveFromOtherBranches' };
     }
 
     my $item_library = Koha::Libraries->find( {branchcode => $item->homebranch} );
         && $borrower->{branchcode} ne $item->homebranch )
     {
         return { status => 'cannotReserveFromOtherBranches' };
     }
 
     my $item_library = Koha::Libraries->find( {branchcode => $item->homebranch} );
-    if ( $branchitemrule->{holdallowed} == 3) {
-        if($borrower->{branchcode} ne $item->homebranch && !$item_library->validate_hold_sibling( {branchcode => $borrower->{branchcode}} )) {
+    if ( $branchitemrule->{holdallowed} eq 'from_local_hold_group') {
+        if($patron->branchcode ne $item->homebranch && !$item_library->validate_hold_sibling( {branchcode => $patron->branchcode} )) {
             return { status => 'branchNotInHoldGroup' };
         }
     }
 
             return { status => 'branchNotInHoldGroup' };
         }
     }
 
-    # If reservecount is ok, we check item branch if IndependentBranches is ON
-    # and canreservefromotherbranches is OFF
-    if ( C4::Context->preference('IndependentBranches')
-        and !C4::Context->preference('canreservefromotherbranches') )
-    {
-        if ( $item->homebranch ne $borrower->{branchcode} ) {
-            return { status => 'cannotReserveFromOtherBranches' };
-        }
-    }
-
     if ($pickup_branchcode) {
         my $destination = Koha::Libraries->find({
             branchcode => $pickup_branchcode,
     if ($pickup_branchcode) {
         my $destination = Koha::Libraries->find({
             branchcode => $pickup_branchcode,
@@ -506,10 +634,10 @@ sub CanItemBeReserved {
         unless ($item->can_be_transferred({ to => $destination })) {
             return { status => 'cannotBeTransferred' };
         }
         unless ($item->can_be_transferred({ to => $destination })) {
             return { status => 'cannotBeTransferred' };
         }
-        unless ($branchitemrule->{hold_fulfillment_policy} ne 'holdgroup' || $item_library->validate_hold_sibling( {branchcode => $pickup_branchcode} )) {
+        if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup' && !$item_library->validate_hold_sibling( {branchcode => $pickup_branchcode} )) {
             return { status => 'pickupNotInHoldGroup' };
         }
             return { status => 'pickupNotInHoldGroup' };
         }
-        unless ($branchitemrule->{hold_fulfillment_policy} ne 'patrongroup' || Koha::Libraries->find({branchcode => $borrower->{branchcode}})->validate_hold_sibling({branchcode => $pickup_branchcode})) {
+        if ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup' && !Koha::Libraries->find({branchcode => $borrower->{branchcode}})->validate_hold_sibling({branchcode => $pickup_branchcode})) {
             return { status => 'pickupNotInHoldGroup' };
         }
     }
             return { status => 'pickupNotInHoldGroup' };
         }
     }
@@ -531,13 +659,10 @@ sub CanReserveBeCanceledFromOpac {
     my ($reserve_id, $borrowernumber) = @_;
 
     return unless $reserve_id and $borrowernumber;
     my ($reserve_id, $borrowernumber) = @_;
 
     return unless $reserve_id and $borrowernumber;
-    my $reserve = Koha::Holds->find($reserve_id);
+    my $reserve = Koha::Holds->find($reserve_id) or return;
 
     return 0 unless $reserve->borrowernumber == $borrowernumber;
 
     return 0 unless $reserve->borrowernumber == $borrowernumber;
-    return 0 if ( $reserve->found eq 'W' ) or ( $reserve->found eq 'T' );
-
-    return 1;
-
+    return $reserve->is_cancelable_from_opac;
 }
 
 =head2 GetOtherReserves
 }
 
 =head2 GetOtherReserves
@@ -567,7 +692,8 @@ sub GetOtherReserves {
             C4::Items::ModItemTransfer(
                 $itemnumber,
                 $item->holdingbranch,
             C4::Items::ModItemTransfer(
                 $itemnumber,
                 $item->holdingbranch,
-                $checkreserves->{'branchcode'}
+                $checkreserves->{'branchcode'},
+                'Reserve'
               ),
               ;
         }
               ),
               ;
         }
@@ -582,7 +708,7 @@ sub GetOtherReserves {
             ModReserveStatus($itemnumber,'W');
         }
 
             ModReserveStatus($itemnumber,'W');
         }
 
-        $nextreservinfo = $checkreserves->{'borrowernumber'};
+        $nextreservinfo = $checkreserves;
     }
 
     return ( $messages, $nextreservinfo );
     }
 
     return ( $messages, $nextreservinfo );
@@ -647,10 +773,13 @@ SELECT COUNT(*) FROM reserves WHERE biblionumber=? AND borrowernumber<>?
         my ( $notissued, $reserved );
         ( $notissued ) = $dbh->selectrow_array( $issue_qry, undef,
             ( $biblionumber ) );
         my ( $notissued, $reserved );
         ( $notissued ) = $dbh->selectrow_array( $issue_qry, undef,
             ( $biblionumber ) );
-        if( $notissued ) {
+        if( $notissued == 0 ) {
+            # all items are issued
             ( $reserved ) = $dbh->selectrow_array( $holds_qry, undef,
                 ( $biblionumber, $borrowernumber ) );
             $fee = 0 if $reserved == 0;
             ( $reserved ) = $dbh->selectrow_array( $holds_qry, undef,
                 ( $biblionumber, $borrowernumber ) );
             $fee = 0 if $reserved == 0;
+        } else {
+            $fee = 0;
         }
     }
     return $fee;
         }
     }
     return $fee;
@@ -683,10 +812,11 @@ sub GetReserveStatus {
 
     if(defined $found) {
         return 'Waiting'  if $found eq 'W' and $priority == 0;
 
     if(defined $found) {
         return 'Waiting'  if $found eq 'W' and $priority == 0;
+        return 'Processing'  if $found eq 'P';
         return 'Finished' if $found eq 'F';
     }
 
         return 'Finished' if $found eq 'F';
     }
 
-    return 'Reserved' if $priority > 0;
+    return 'Reserved' if defined $priority && $priority > 0;
 
     return ''; # empty string here will remove need for checking undef, or less log lines
 }
 
     return ''; # empty string here will remove need for checking undef, or less log lines
 }
@@ -771,7 +901,12 @@ sub CheckReserves {
     return unless $itemnumber; # bail if we got nothing.
     # if item is not for loan it cannot be reserved either.....
     # except where items.notforloan < 0 :  This indicates the item is holdable.
     return unless $itemnumber; # bail if we got nothing.
     # if item is not for loan it cannot be reserved either.....
     # except where items.notforloan < 0 :  This indicates the item is holdable.
-    return if  ( $notforloan_per_item > 0 ) or $notforloan_per_itemtype;
+
+    my @SkipHoldTrapOnNotForLoanValue = split( '\|', C4::Context->preference('SkipHoldTrapOnNotForLoanValue') );
+    return if grep { $_ eq $notforloan_per_item } @SkipHoldTrapOnNotForLoanValue;
+
+    my $dont_trap = C4::Context->preference('TrapHoldsOnOrder') ? ($notforloan_per_item > 0) : ($notforloan_per_item && 1 );
+    return if $dont_trap or $notforloan_per_itemtype;
 
     # Find this item in the reserves
     my @reserves = _Findgroupreserve( $bibitem, $biblio, $itemnumber, $lookahead_days, $ignore_borrowers);
 
     # Find this item in the reserves
     my @reserves = _Findgroupreserve( $bibitem, $biblio, $itemnumber, $lookahead_days, $ignore_borrowers);
@@ -781,6 +916,7 @@ sub CheckReserves {
     # the more important the item.)
     # $highest is the most important item we've seen so far.
     my $highest;
     # the more important the item.)
     # $highest is the most important item we've seen so far.
     my $highest;
+
     if (scalar @reserves) {
         my $LocalHoldsPriority = C4::Context->preference('LocalHoldsPriority');
         my $LocalHoldsPriorityPatronControl = C4::Context->preference('LocalHoldsPriorityPatronControl');
     if (scalar @reserves) {
         my $LocalHoldsPriority = C4::Context->preference('LocalHoldsPriority');
         my $LocalHoldsPriorityPatronControl = C4::Context->preference('LocalHoldsPriorityPatronControl');
@@ -788,12 +924,12 @@ sub CheckReserves {
 
         my $priority = 10000000;
         foreach my $res (@reserves) {
 
         my $priority = 10000000;
         foreach my $res (@reserves) {
-            if ( $res->{'itemnumber'} == $itemnumber && $res->{'priority'} == 0) {
-                if ($res->{'found'} eq 'W') {
-                    return ( "Waiting", $res, \@reserves ); # Found it, it is waiting
-                } else {
-                    return ( "Reserved", $res, \@reserves ); # Found determinated hold, e. g. the tranferred one
-                }
+            if ($res->{'found'} && $res->{'found'} eq 'W') {
+                return ( "Waiting", $res, \@reserves ); # Found it, it is waiting
+            } elsif ($res->{'found'} && $res->{'found'} eq 'P') {
+                return ( "Processing", $res, \@reserves ); # Found determinated hold, e. g. the transferred one
+            } elsif ($res->{'found'} && $res->{'found'} eq 'T') {
+                return ( "Transferred", $res, \@reserves ); # Found determinated hold, e. g. the transferred one
             } else {
                 my $patron;
                 my $item;
             } else {
                 my $patron;
                 my $item;
@@ -803,17 +939,19 @@ sub CheckReserves {
                     $patron = Koha::Patrons->find( $res->{borrowernumber} );
                     $item = Koha::Items->find($itemnumber);
 
                     $patron = Koha::Patrons->find( $res->{borrowernumber} );
                     $item = Koha::Items->find($itemnumber);
 
-                    my $local_holds_priority_item_branchcode =
-                      $item->$LocalHoldsPriorityItemControl;
-                    my $local_holds_priority_patron_branchcode =
-                      ( $LocalHoldsPriorityPatronControl eq 'PickupLibrary' )
-                      ? $res->{branchcode}
-                      : ( $LocalHoldsPriorityPatronControl eq 'HomeLibrary' )
-                      ? $patron->branchcode
-                      : undef;
-                    $local_hold_match =
-                      $local_holds_priority_item_branchcode eq
-                      $local_holds_priority_patron_branchcode;
+                    unless ($item->exclude_from_local_holds_priority || $patron->category->exclude_from_local_holds_priority) {
+                        my $local_holds_priority_item_branchcode =
+                            $item->$LocalHoldsPriorityItemControl;
+                        my $local_holds_priority_patron_branchcode =
+                            ( $LocalHoldsPriorityPatronControl eq 'PickupLibrary' )
+                            ? $res->{branchcode}
+                            : ( $LocalHoldsPriorityPatronControl eq 'HomeLibrary' )
+                            ? $patron->branchcode
+                            : undef;
+                        $local_hold_match =
+                            $local_holds_priority_item_branchcode eq
+                            $local_holds_priority_patron_branchcode;
+                    }
                 }
 
                 # See if this item is more important than what we've got so far
                 }
 
                 # See if this item is more important than what we've got so far
@@ -823,15 +961,15 @@ sub CheckReserves {
                     $patron ||= Koha::Patrons->find( $res->{borrowernumber} );
                     my $branch = GetReservesControlBranch( $item->unblessed, $patron->unblessed );
                     my $branchitemrule = C4::Circulation::GetBranchItemRule($branch,$item->effective_itemtype);
                     $patron ||= Koha::Patrons->find( $res->{borrowernumber} );
                     my $branch = GetReservesControlBranch( $item->unblessed, $patron->unblessed );
                     my $branchitemrule = C4::Circulation::GetBranchItemRule($branch,$item->effective_itemtype);
-                    next if ($branchitemrule->{'holdallowed'} == 0);
-                    next if (($branchitemrule->{'holdallowed'} == 1) && ($branch ne $patron->branchcode));
+                    next if ($branchitemrule->{'holdallowed'} eq 'not_allowed');
+                    next if (($branchitemrule->{'holdallowed'} eq 'from_home_library') && ($item->homebranch ne $patron->branchcode));
                     my $library = Koha::Libraries->find({branchcode=>$item->homebranch});
                     my $library = Koha::Libraries->find({branchcode=>$item->homebranch});
-                    next if (($branchitemrule->{'holdallowed'} == 3) && (!$library->validate_hold_sibling({branchcode => $patron->branchcode}) ));
+                    next if (($branchitemrule->{'holdallowed'} eq 'from_local_hold_group') && (!$library->validate_hold_sibling({branchcode => $patron->branchcode}) ));
                     my $hold_fulfillment_policy = $branchitemrule->{hold_fulfillment_policy};
                     next if ( ($hold_fulfillment_policy eq 'holdgroup') && (!$library->validate_hold_sibling({branchcode => $res->{branchcode}})) );
                     next if ( ($hold_fulfillment_policy eq 'homebranch') && ($res->{branchcode} ne $item->$hold_fulfillment_policy) );
                     next if ( ($hold_fulfillment_policy eq 'holdingbranch') && ($res->{branchcode} ne $item->$hold_fulfillment_policy) );
                     my $hold_fulfillment_policy = $branchitemrule->{hold_fulfillment_policy};
                     next if ( ($hold_fulfillment_policy eq 'holdgroup') && (!$library->validate_hold_sibling({branchcode => $res->{branchcode}})) );
                     next if ( ($hold_fulfillment_policy eq 'homebranch') && ($res->{branchcode} ne $item->$hold_fulfillment_policy) );
                     next if ( ($hold_fulfillment_policy eq 'holdingbranch') && ($res->{branchcode} ne $item->$hold_fulfillment_policy) );
-                    next unless $item->can_be_transferred( { to => scalar Koha::Libraries->find( $res->{branchcode} ) } );
+                    next unless $item->can_be_transferred( { to => Koha::Libraries->find( $res->{branchcode} ) } );
                     $priority = $res->{'priority'};
                     $highest  = $res;
                     last if $local_hold_match;
                     $priority = $res->{'priority'};
                     $highest  = $res;
                     last if $local_hold_match;
@@ -859,12 +997,19 @@ Cancels all reserves with an expiration date from before today.
 =cut
 
 sub CancelExpiredReserves {
 =cut
 
 sub CancelExpiredReserves {
+    my $cancellation_reason = shift;
     my $today = dt_from_string();
     my $cancel_on_holidays = C4::Context->preference('ExpireReservesOnHolidays');
     my $expireWaiting = C4::Context->preference('ExpireReservesMaxPickUpDelay');
 
     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
     my $today = dt_from_string();
     my $cancel_on_holidays = C4::Context->preference('ExpireReservesOnHolidays');
     my $expireWaiting = C4::Context->preference('ExpireReservesMaxPickUpDelay');
 
     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
-    my $params = { expirationdate => { '<', $dtf->format_date($today) } };
+    my $params = {
+        -or => [
+            { expirationdate => { '<', $dtf->format_date($today) } },
+            { patron_expiration_date => { '<' => $dtf->format_date($today) } }
+        ]
+    };
+
     $params->{found} = [ { '!=', 'W' }, undef ]  unless $expireWaiting;
 
     # FIXME To move to Koha::Holds->search_expired (?)
     $params->{found} = [ { '!=', 'W' }, undef ]  unless $expireWaiting;
 
     # FIXME To move to Koha::Holds->search_expired (?)
@@ -876,9 +1021,11 @@ sub CancelExpiredReserves {
         next if !$cancel_on_holidays && $calendar->is_holiday( $today );
 
         my $cancel_params = {};
         next if !$cancel_on_holidays && $calendar->is_holiday( $today );
 
         my $cancel_params = {};
-        if ( $hold->found eq 'W' ) {
+        $cancel_params->{cancellation_reason} = $cancellation_reason if defined($cancellation_reason);
+        if ( defined($hold->found) && $hold->found eq 'W' ) {
             $cancel_params->{charge_cancel_fee} = 1;
         }
             $cancel_params->{charge_cancel_fee} = 1;
         }
+        $cancel_params->{autofill} = C4::Context->preference('ExpireReservesAutoFill');
         $hold->cancel( $cancel_params );
     }
 }
         $hold->cancel( $cancel_params );
     }
 }
@@ -894,7 +1041,7 @@ Unsuspends all suspended reserves with a suspend_until date from before today.
 sub AutoUnsuspendReserves {
     my $today = dt_from_string();
 
 sub AutoUnsuspendReserves {
     my $today = dt_from_string();
 
-    my @holds = Koha::Holds->search( { suspend_until => { '<=' => $today->ymd() } } );
+    my @holds = Koha::Holds->search( { suspend_until => { '<=' => $today->ymd() } } )->as_list;
 
     map { $_->resume() } @holds;
 }
 
     map { $_->resume() } @holds;
 }
@@ -911,7 +1058,7 @@ sub AutoUnsuspendReserves {
 Change a hold request's priority or cancel it.
 
 C<$rank> specifies the effect of the change.  If C<$rank>
 Change a hold request's priority or cancel it.
 
 C<$rank> specifies the effect of the change.  If C<$rank>
-is 'W' or 'n', nothing happens.  This corresponds to leaving a
+is 'n', nothing happens.  This corresponds to leaving a
 request alone when changing its priority in the holds queue
 for a bib.
 
 request alone when changing its priority in the holds queue
 for a bib.
 
@@ -923,6 +1070,9 @@ that the item is not waiting on the hold shelf, setting the
 priority to a non-zero value also sets the request's found
 status and waiting date to NULL.
 
 priority to a non-zero value also sets the request's found
 status and waiting date to NULL.
 
+If the hold is 'found' (waiting, in-transit, processing) the
+only field that can be updated is the expiration date.
+
 The optional C<$itemnumber> parameter is used only when
 C<$rank> is a non-zero integer; if supplied, the itemnumber
 of the hold request is set accordingly; if omitted, the itemnumber
 The optional C<$itemnumber> parameter is used only when
 C<$rank> is a non-zero integer; if supplied, the itemnumber
 of the hold request is set accordingly; if omitted, the itemnumber
@@ -945,9 +1095,10 @@ sub ModReserve {
     my $suspend_until = $params->{'suspend_until'};
     my $borrowernumber = $params->{'borrowernumber'};
     my $biblionumber = $params->{'biblionumber'};
     my $suspend_until = $params->{'suspend_until'};
     my $borrowernumber = $params->{'borrowernumber'};
     my $biblionumber = $params->{'biblionumber'};
+    my $cancellation_reason = $params->{'cancellation_reason'};
+    my $date = $params->{expirationdate};
 
 
-    return if $rank eq "W";
-    return if $rank eq "n";
+    return if defined $rank && $rank eq "n";
 
     return unless ( $reserve_id || ( $borrowernumber && ( $biblionumber || $itemnumber ) ) );
 
 
     return unless ( $reserve_id || ( $borrowernumber && ( $biblionumber || $itemnumber ) ) );
 
@@ -961,11 +1112,21 @@ sub ModReserve {
 
     $hold ||= Koha::Holds->find($reserve_id);
 
 
     $hold ||= Koha::Holds->find($reserve_id);
 
+    # FIXME Other calls may fail
+    Koha::Exceptions::ObjectNotFound->throw( 'No hold with id ' . $reserve_id ) unless $hold;
+
     if ( $rank eq "del" ) {
     if ( $rank eq "del" ) {
-        $hold->cancel;
+        $hold->cancel({ cancellation_reason => $cancellation_reason });
+    }
+    elsif ($hold->found && $hold->priority eq '0' && $date) {
+        logaction( 'HOLDS', 'MODIFY', $hold->reserve_id, $hold )
+            if C4::Context->preference('HoldsLog');
+
+        # The only column that can be updated for a found hold is the expiration date
+        $hold->expirationdate($date)->store();
     }
     elsif ($rank =~ /^\d+/ and $rank > 0) {
     }
     elsif ($rank =~ /^\d+/ and $rank > 0) {
-        logaction( 'HOLDS', 'MODIFY', $hold->reserve_id, Dumper($hold->unblessed) )
+        logaction( 'HOLDS', 'MODIFY', $hold->reserve_id, $hold )
             if C4::Context->preference('HoldsLog');
 
         my $properties = {
             if C4::Context->preference('HoldsLog');
 
         my $properties = {
@@ -986,7 +1147,6 @@ sub ModReserve {
 
         if ( defined( $suspend_until ) ) {
             if ( $suspend_until ) {
 
         if ( defined( $suspend_until ) ) {
             if ( $suspend_until ) {
-                $suspend_until = eval { dt_from_string( $suspend_until ) };
                 $hold->suspend_hold( $suspend_until );
             } else {
                 # If the hold is suspended leave the hold suspended, but convert it to an indefinite hold.
                 $hold->suspend_hold( $suspend_until );
             } else {
                 # If the hold is suspended leave the hold suspended, but convert it to an indefinite hold.
@@ -999,51 +1159,6 @@ sub ModReserve {
     }
 }
 
     }
 }
 
-=head2 ModReserveFill
-
-  &ModReserveFill($reserve);
-
-Fill a reserve. If I understand this correctly, this means that the
-reserved book has been found and given to the patron who reserved it.
-
-C<$reserve> specifies the reserve to fill. It is a reference-to-hash
-whose keys are fields from the reserves table in the Koha database.
-
-=cut
-
-sub ModReserveFill {
-    my ($res) = @_;
-    my $reserve_id = $res->{'reserve_id'};
-
-    my $hold = Koha::Holds->find($reserve_id);
-    # get the priority on this record....
-    my $priority = $hold->priority;
-
-    # update the hold statuses, no need to store it though, we will be deleting it anyway
-    $hold->set(
-        {
-            found    => 'F',
-            priority => 0,
-        }
-    );
-
-    # FIXME Must call Koha::Hold->cancel ? => No, should call ->filled and add the correct log
-    Koha::Old::Hold->new( $hold->unblessed() )->store();
-
-    $hold->delete();
-
-    if ( C4::Context->preference('HoldFeeMode') eq 'any_time_is_collected' ) {
-        my $reserve_fee = GetReserveFee( $hold->borrowernumber, $hold->biblionumber );
-        ChargeReserveFee( $hold->borrowernumber, $reserve_fee, $hold->biblio->title );
-    }
-
-    # now fix the priority on the others (if the priority wasn't
-    # already sorted!)....
-    unless ( $priority == 0 ) {
-        _FixPriority( { reserve_id => $reserve_id, biblionumber => $hold->biblionumber } );
-    }
-}
-
 =head2 ModReserveStatus
 
   &ModReserveStatus($itemnumber, $newstatus);
 =head2 ModReserveStatus
 
   &ModReserveStatus($itemnumber, $newstatus);
@@ -1076,7 +1191,7 @@ sub ModReserveStatus {
 
 =head2 ModReserveAffect
 
 
 =head2 ModReserveAffect
 
-  &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend,$reserve_id);
+  &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend,$reserve_id, $desk_id, $notify_library);
 
 This function affect an item and a status for a given reserve, either fetched directly
 by record_id, or by borrowernumber and itemnumber or biblionumber. If only biblionumber
 
 This function affect an item and a status for a given reserve, either fetched directly
 by record_id, or by borrowernumber and itemnumber or biblionumber. If only biblionumber
@@ -1087,10 +1202,12 @@ if $transferToDo is not set, then the status is set to "Waiting" as well.
 otherwise, a transfer is on the way, and the end of the transfer will
 take care of the waiting status
 
 otherwise, a transfer is on the way, and the end of the transfer will
 take care of the waiting status
 
+This function also removes any entry of the hold in holds queue table.
+
 =cut
 
 sub ModReserveAffect {
 =cut
 
 sub ModReserveAffect {
-    my ( $itemnumber, $borrowernumber, $transferToDo, $reserve_id ) = @_;
+    my ( $itemnumber, $borrowernumber, $transferToDo, $reserve_id, $desk_id, $notify_library ) = @_;
     my $dbh = C4::Context->dbh;
 
     # we want to attach $itemnumber to $borrowernumber, find the biblionumber
     my $dbh = C4::Context->dbh;
 
     # we want to attach $itemnumber to $borrowernumber, find the biblionumber
@@ -1115,10 +1232,22 @@ sub ModReserveAffect {
     my $already_on_shelf = $hold->found && $hold->found eq 'W';
 
     $hold->itemnumber($itemnumber);
     my $already_on_shelf = $hold->found && $hold->found eq 'W';
 
     $hold->itemnumber($itemnumber);
-    $hold->set_waiting($transferToDo);
 
 
-    _koha_notify_reserve( $hold->reserve_id )
-      if ( !$transferToDo && !$already_on_shelf );
+    if ($transferToDo) {
+        $hold->set_transfer();
+    } elsif (C4::Context->preference('HoldsNeedProcessingSIP')
+             && C4::Context->interface eq 'sip'
+             && !$already_on_shelf) {
+        $hold->set_processing();
+    } else {
+        $hold->set_waiting($desk_id);
+        _koha_notify_reserve( $hold->reserve_id ) unless $already_on_shelf;
+        # Complete transfer if one exists
+        my $transfer = $hold->item->get_transfer;
+        $transfer->receive if $transfer;
+    }
+
+    _koha_notify_hold_changed( $hold ) if $notify_library;
 
     _FixPriority( { biblionumber => $biblionumber } );
     my $item = Koha::Items->find($itemnumber);
 
     _FixPriority( { biblionumber => $biblionumber } );
     my $item = Koha::Items->find($itemnumber);
@@ -1127,12 +1256,28 @@ sub ModReserveAffect {
       CartToShelf( $itemnumber );
     }
 
       CartToShelf( $itemnumber );
     }
 
+    my $std = $dbh->prepare(q{
+        DELETE  q, t
+        FROM    tmp_holdsqueue q
+        INNER JOIN hold_fill_targets t
+        ON  q.borrowernumber = t.borrowernumber
+            AND q.biblionumber = t.biblionumber
+            AND q.itemnumber = t.itemnumber
+            AND q.item_level_request = t.item_level_request
+            AND q.holdingbranch = t.source_branchcode
+        WHERE t.reserve_id = ?
+    });
+    $std->execute($hold->reserve_id);
+
+    logaction( 'HOLDS', 'MODIFY', $hold->reserve_id, $hold )
+        if C4::Context->preference('HoldsLog');
+
     return;
 }
 
 =head2 ModReserveCancelAll
 
     return;
 }
 
 =head2 ModReserveCancelAll
 
-  ($messages,$nextreservinfo) = &ModReserveCancelAll($itemnumber,$borrowernumber);
+  ($messages,$nextreservinfo) = &ModReserveCancelAll($itemnumber,$borrowernumber,$reason);
 
 function to cancel reserv,check other reserves, and transfer document if it's necessary
 
 
 function to cancel reserv,check other reserves, and transfer document if it's necessary
 
@@ -1141,17 +1286,17 @@ function to cancel reserv,check other reserves, and transfer document if it's ne
 sub ModReserveCancelAll {
     my $messages;
     my $nextreservinfo;
 sub ModReserveCancelAll {
     my $messages;
     my $nextreservinfo;
-    my ( $itemnumber, $borrowernumber ) = @_;
+    my ( $itemnumber, $borrowernumber, $cancellation_reason ) = @_;
 
     #step 1 : cancel the reservation
     my $holds = Koha::Holds->search({ itemnumber => $itemnumber, borrowernumber => $borrowernumber });
     return unless $holds->count;
 
     #step 1 : cancel the reservation
     my $holds = Koha::Holds->search({ itemnumber => $itemnumber, borrowernumber => $borrowernumber });
     return unless $holds->count;
-    $holds->next->cancel;
+    $holds->next->cancel({ cancellation_reason => $cancellation_reason });
 
     #step 2 launch the subroutine of the others reserves
     ( $messages, $nextreservinfo ) = GetOtherReserves($itemnumber);
 
 
     #step 2 launch the subroutine of the others reserves
     ( $messages, $nextreservinfo ) = GetOtherReserves($itemnumber);
 
-    return ( $messages, $nextreservinfo );
+    return ( $messages, $nextreservinfo->{borrowernumber} );
 }
 
 =head2 ModReserveMinusPriority
 }
 
 =head2 ModReserveMinusPriority
@@ -1200,10 +1345,20 @@ a request on the item - in particular,
 this routine does not check IndependentBranches
 and canreservefromotherbranches.
 
 this routine does not check IndependentBranches
 and canreservefromotherbranches.
 
+Note also that this subroutine does not checks smart
+rules limits for item by reservesallowed/holds_per_record
+values, this complemented in calling code with calls and
+checks with CanItemBeReserved or CanBookBeReserved.
+
 =cut
 
 sub IsAvailableForItemLevelRequest {
 =cut
 
 sub IsAvailableForItemLevelRequest {
-    my ( $item, $patron, $pickup_branchcode ) = @_;
+    my $item                = shift;
+    my $patron              = shift;
+    my $pickup_branchcode   = shift;
+    # items_any_available is precalculated status passed from request.pl when set of items
+    # looped outside of IsAvailableForItemLevelRequest to avoid nested loops:
+    my $items_any_available = shift;
 
     my $dbh = C4::Context->dbh;
     # must check the notforloan setting of the itemtype
 
     my $dbh = C4::Context->dbh;
     # must check the notforloan setting of the itemtype
@@ -1211,17 +1366,17 @@ sub IsAvailableForItemLevelRequest {
     #         or something similar - need to be
     #         consolidated
     my $itemtype = $item->effective_itemtype;
     #         or something similar - need to be
     #         consolidated
     my $itemtype = $item->effective_itemtype;
+    return 0
+      unless defined $itemtype;
     my $notforloan_per_itemtype = Koha::ItemTypes->find($itemtype)->notforloan;
 
     return 0 if
         $notforloan_per_itemtype ||
         $item->itemlost        ||
     my $notforloan_per_itemtype = Koha::ItemTypes->find($itemtype)->notforloan;
 
     return 0 if
         $notforloan_per_itemtype ||
         $item->itemlost        ||
-        $item->notforloan > 0  ||
+        $item->notforloan > 0  || # item with negative or zero notforloan value is holdable
         $item->withdrawn        ||
         ($item->damaged && !C4::Context->preference('AllowHoldsOnDamagedItems'));
 
         $item->withdrawn        ||
         ($item->damaged && !C4::Context->preference('AllowHoldsOnDamagedItems'));
 
-    my $on_shelf_holds = Koha::IssuingRules->get_onshelfholds_policy( { item => $item, patron => $patron } );
-
     if ($pickup_branchcode) {
         my $destination = Koha::Libraries->find($pickup_branchcode);
         return 0 unless $destination;
     if ($pickup_branchcode) {
         my $destination = Koha::Libraries->find($pickup_branchcode);
         return 0 unless $destination;
@@ -1231,68 +1386,67 @@ sub IsAvailableForItemLevelRequest {
             GetReservesControlBranch( $item->unblessed(), $patron->unblessed() );
         my $branchitemrule =
             C4::Circulation::GetBranchItemRule( $reserves_control_branch, $item->itype );
             GetReservesControlBranch( $item->unblessed(), $patron->unblessed() );
         my $branchitemrule =
             C4::Circulation::GetBranchItemRule( $reserves_control_branch, $item->itype );
-        my $home_library = Koka::Libraries->find( {branchcode => $item->homebranch} );
+        my $home_library = Koha::Libraries->find( {branchcode => $item->homebranch} );
         return 0 unless $branchitemrule->{hold_fulfillment_policy} ne 'holdgroup' || $home_library->validate_hold_sibling( {branchcode => $pickup_branchcode} );
     }
 
         return 0 unless $branchitemrule->{hold_fulfillment_policy} ne 'holdgroup' || $home_library->validate_hold_sibling( {branchcode => $pickup_branchcode} );
     }
 
+    my $on_shelf_holds = Koha::CirculationRules->get_onshelfholds_policy( { item => $item, patron => $patron } );
+
     if ( $on_shelf_holds == 1 ) {
         return 1;
     } elsif ( $on_shelf_holds == 2 ) {
     if ( $on_shelf_holds == 1 ) {
         return 1;
     } elsif ( $on_shelf_holds == 2 ) {
-        my @items =
-          Koha::Items->search( { biblionumber => $item->biblionumber } );
-
-        my $any_available = 0;
-
-        foreach my $i (@items) {
-            my $reserves_control_branch = GetReservesControlBranch( $i->unblessed(), $patron->unblessed );
-            my $branchitemrule = C4::Circulation::GetBranchItemRule( $reserves_control_branch, $i->itype );
-            my $item_library = Koha::Libraries->find( {branchcode => $i->homebranch} );
-
-
-            $any_available = 1
-              unless $i->itemlost
-              || $i->notforloan > 0
-              || $i->withdrawn
-              || $i->onloan
-              || IsItemOnHoldAndFound( $i->id )
-              || ( $i->damaged
-                && !C4::Context->preference('AllowHoldsOnDamagedItems') )
-              || Koha::ItemTypes->find( $i->effective_itemtype() )->notforloan
-              || $branchitemrule->{holdallowed} == 1 && $patron->branchcode ne $i->homebranch
-              || $branchitemrule->{holdallowed} == 3 && !$item_library->validate_hold_sibling( {branchcode => $patron->branchcode} );
-        }
 
 
+        # if we have this param predefined from outer caller sub, we just need
+        # to return it, so we saving from having loop inside other loop:
+        return  $items_any_available ? 0 : 1
+            if defined $items_any_available;
+
+        my $any_available = ItemsAnyAvailableAndNotRestricted( { biblionumber => $item->biblionumber, patron => $patron });
         return $any_available ? 0 : 1;
     } else { # on_shelf_holds == 0 "If any unavailable" (the description is rather cryptic and could still be improved)
         return $item->onloan || IsItemOnHoldAndFound( $item->itemnumber );
     }
 }
 
         return $any_available ? 0 : 1;
     } else { # on_shelf_holds == 0 "If any unavailable" (the description is rather cryptic and could still be improved)
         return $item->onloan || IsItemOnHoldAndFound( $item->itemnumber );
     }
 }
 
-sub _get_itype {
-    my $item = shift;
+=head2 ItemsAnyAvailableAndNotRestricted
 
 
-    my $itype;
-    if (C4::Context->preference('item-level_itypes')) {
-        # We can't trust GetItem to honour the syspref, so safest to do it ourselves
-        # When GetItem is fixed, we can remove this
-        $itype = $item->{itype};
-    }
-    else {
-        # XXX This is a bit dodgy. It relies on biblio itemtype column having different name.
-        # So if we already have a biblioitems join when calling this function,
-        # we don't need to access the database again
-        $itype = $item->{itemtype};
-    }
-    unless ($itype) {
-        my $dbh = C4::Context->dbh;
-        my $query = "SELECT itemtype FROM biblioitems WHERE biblioitemnumber = ? ";
-        my $sth = $dbh->prepare($query);
-        $sth->execute($item->{biblioitemnumber});
-        if (my $data = $sth->fetchrow_hashref()){
-            $itype = $data->{itemtype};
-        }
-    }
-    return $itype;
+  ItemsAnyAvailableAndNotRestricted( { biblionumber => $biblionumber, patron => $patron });
+
+This function checks all items for specified biblionumber (numeric) against patron (object)
+and returns true (1) if at least one item available for loan/check out/present/not held
+and also checks other parameters logic which not restricts item for hold at all (for ex.
+AllowHoldsOnDamagedItems or 'holdallowed' own/sibling library)
+
+=cut
+
+sub ItemsAnyAvailableAndNotRestricted {
+    my $param = shift;
+
+    my @items = Koha::Items->search( { biblionumber => $param->{biblionumber} } )->as_list;
+
+    foreach my $i (@items) {
+        my $reserves_control_branch =
+            GetReservesControlBranch( $i->unblessed(), $param->{patron}->unblessed );
+        my $branchitemrule =
+            C4::Circulation::GetBranchItemRule( $reserves_control_branch, $i->itype );
+        my $item_library = Koha::Libraries->find( { branchcode => $i->homebranch } );
+
+        # we can return (end the loop) when first one found:
+        return 1
+            unless $i->itemlost
+            || $i->notforloan # items with non-zero notforloan cannot be checked out
+            || $i->withdrawn
+            || $i->onloan
+            || IsItemOnHoldAndFound( $i->id )
+            || ( $i->damaged
+                 && ! C4::Context->preference('AllowHoldsOnDamagedItems') )
+            || Koha::ItemTypes->find( $i->effective_itemtype() )->notforloan
+            || $branchitemrule->{holdallowed} eq 'from_home_library' && $param->{patron}->branchcode ne $i->homebranch
+            || $branchitemrule->{holdallowed} eq 'from_local_hold_group' && ! $item_library->validate_hold_sibling( { branchcode => $param->{patron}->branchcode } )
+            || CanItemBeReserved( $param->{patron}, $i )->{status} ne 'OK';
+    }
+
+    return 0;
 }
 
 =head2 AlterPriority
 }
 
 =head2 AlterPriority
@@ -1327,6 +1481,11 @@ sub AlterPriority {
       _FixPriority({ reserve_id => $reserve_id, rank => $last_priority });
     }
 
       _FixPriority({ reserve_id => $reserve_id, rank => $last_priority });
     }
 
+    Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
+        {
+            biblio_ids => [ $hold->biblionumber ]
+        }
+    ) if C4::Context->preference('RealTimeHoldsQueue');
     # FIXME Should return the new priority
 }
 
     # FIXME Should return the new priority
 }
 
@@ -1362,8 +1521,6 @@ be cleared when it is unsuspended.
 sub ToggleSuspend {
     my ( $reserve_id, $suspend_until ) = @_;
 
 sub ToggleSuspend {
     my ( $reserve_id, $suspend_until ) = @_;
 
-    $suspend_until = dt_from_string($suspend_until) if ($suspend_until);
-
     my $hold = Koha::Holds->find( $reserve_id );
 
     if ( $hold->is_suspended ) {
     my $hold = Koha::Holds->find( $reserve_id );
 
     if ( $hold->is_suspended ) {
@@ -1397,9 +1554,6 @@ sub SuspendAll {
     my $suspend_until  = $params{'suspend_until'}  || undef;
     my $suspend = defined( $params{'suspend'} ) ? $params{'suspend'} : 1;
 
     my $suspend_until  = $params{'suspend_until'}  || undef;
     my $suspend = defined( $params{'suspend'} ) ? $params{'suspend'} : 1;
 
-    $suspend_until = eval { dt_from_string($suspend_until) }
-      if ( defined($suspend_until) );
-
     return unless ( $borrowernumber || $biblionumber );
 
     my $params;
     return unless ( $borrowernumber || $biblionumber );
 
     my $params;
@@ -1407,7 +1561,7 @@ sub SuspendAll {
     $params->{borrowernumber} = $borrowernumber if $borrowernumber;
     $params->{biblionumber}   = $biblionumber if $biblionumber;
 
     $params->{borrowernumber} = $borrowernumber if $borrowernumber;
     $params->{biblionumber}   = $biblionumber if $biblionumber;
 
-    my @holds = Koha::Holds->search($params);
+    my @holds = Koha::Holds->search($params)->as_list;
 
     if ($suspend) {
         map { $_->suspend_hold($suspend_until) } @holds;
 
     if ($suspend) {
         map { $_->suspend_hold($suspend_until) } @holds;
@@ -1465,6 +1619,10 @@ sub _FixPriority {
     my $hold;
     if ( $reserve_id ) {
         $hold = Koha::Holds->find( $reserve_id );
     my $hold;
     if ( $reserve_id ) {
         $hold = Koha::Holds->find( $reserve_id );
+        if (!defined $hold){
+            # may have already been checked out and hold fulfilled
+            $hold = Koha::Old::Holds->find( $reserve_id );
+        }
         return unless $hold;
     }
 
         return unless $hold;
     }
 
@@ -1482,7 +1640,7 @@ sub _FixPriority {
             UPDATE reserves
             SET    priority = 0
             WHERE reserve_id = ?
             UPDATE reserves
             SET    priority = 0
             WHERE reserve_id = ?
-            AND found IN ('W', 'T')
+            AND found IN ('W', 'T', 'P')
         ";
         my $sth = $dbh->prepare($query);
         $sth->execute( $reserve_id );
         ";
         my $sth = $dbh->prepare($query);
         $sth->execute( $reserve_id );
@@ -1494,7 +1652,7 @@ sub _FixPriority {
         SELECT reserve_id, borrowernumber, reservedate
         FROM   reserves
         WHERE  biblionumber   = ?
         SELECT reserve_id, borrowernumber, reservedate
         FROM   reserves
         WHERE  biblionumber   = ?
-          AND  ((found <> 'W' AND found <> 'T') OR found IS NULL)
+          AND  ((found <> 'W' AND found <> 'T' AND found <> 'P') OR found IS NULL)
         ORDER BY priority ASC
     ";
     my $sth = $dbh->prepare($query);
         ORDER BY priority ASC
     ";
     my $sth = $dbh->prepare($query);
@@ -1516,9 +1674,9 @@ sub _FixPriority {
 
     # if index exists in array then move it to new position
     if ( $key > -1 && $rank ne 'del' && $rank > 0 ) {
 
     # if index exists in array then move it to new position
     if ( $key > -1 && $rank ne 'del' && $rank > 0 ) {
-        my $new_rank = $rank -
-          1;    # $new_rank is what you want the new index to be in the array
+        my $new_rank = $rank - 1; # $new_rank is what you want the new index to be in the array
         my $moving_item = splice( @priority, $key, 1 );
         my $moving_item = splice( @priority, $key, 1 );
+        $new_rank = scalar @priority if $new_rank > scalar @priority;
         splice( @priority, $new_rank, 0, $moving_item );
     }
 
         splice( @priority, $new_rank, 0, $moving_item );
     }
 
@@ -1536,10 +1694,9 @@ sub _FixPriority {
         );
     }
 
         );
     }
 
-    $sth = $dbh->prepare( "SELECT reserve_id FROM reserves WHERE lowestPriority = 1 ORDER BY priority" );
-    $sth->execute();
-
     unless ( $ignoreSetLowestRank ) {
     unless ( $ignoreSetLowestRank ) {
+        $sth = $dbh->prepare( "SELECT reserve_id FROM reserves WHERE lowestPriority = 1 AND biblionumber = ? ORDER BY priority" );
+        $sth->execute($biblionumber);
       while ( my $res = $sth->fetchrow_hashref() ) {
         _FixPriority({
             reserve_id => $res->{'reserve_id'},
       while ( my $res = $sth->fetchrow_hashref() ) {
         _FixPriority({
             reserve_id => $res->{'reserve_id'},
@@ -1591,14 +1748,15 @@ sub _Findgroupreserve {
                biblioitems.biblioitemnumber AS biblioitemnumber,
                reserves.itemnumber          AS itemnumber,
                reserves.reserve_id          AS reserve_id,
                biblioitems.biblioitemnumber AS biblioitemnumber,
                reserves.itemnumber          AS itemnumber,
                reserves.reserve_id          AS reserve_id,
-               reserves.itemtype            AS itemtype
+               reserves.itemtype            AS itemtype,
+               reserves.non_priority        AS non_priority
         FROM reserves
         JOIN biblioitems USING (biblionumber)
         FROM reserves
         JOIN biblioitems USING (biblionumber)
-        JOIN hold_fill_targets USING (biblionumber, borrowernumber, itemnumber)
+        JOIN hold_fill_targets USING (reserve_id)
         WHERE found IS NULL
         AND priority > 0
         AND item_level_request = 1
         WHERE found IS NULL
         AND priority > 0
         AND item_level_request = 1
-        AND itemnumber = ?
+        AND hold_fill_targets.itemnumber = ?
         AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
         AND suspend = 0
         ORDER BY priority
         AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
         AND suspend = 0
         ORDER BY priority
@@ -1626,10 +1784,11 @@ sub _Findgroupreserve {
                biblioitems.biblioitemnumber AS biblioitemnumber,
                reserves.itemnumber          AS itemnumber,
                reserves.reserve_id          AS reserve_id,
                biblioitems.biblioitemnumber AS biblioitemnumber,
                reserves.itemnumber          AS itemnumber,
                reserves.reserve_id          AS reserve_id,
-               reserves.itemtype            AS itemtype
+               reserves.itemtype            AS itemtype,
+               reserves.non_priority        AS non_priority
         FROM reserves
         JOIN biblioitems USING (biblionumber)
         FROM reserves
         JOIN biblioitems USING (biblionumber)
-        JOIN hold_fill_targets USING (biblionumber, borrowernumber)
+        JOIN hold_fill_targets USING (reserve_id)
         WHERE found IS NULL
         AND priority > 0
         AND item_level_request = 0
         WHERE found IS NULL
         AND priority > 0
         AND item_level_request = 0
@@ -1660,7 +1819,8 @@ sub _Findgroupreserve {
                reserves.timestamp                  AS timestamp,
                reserves.itemnumber                 AS itemnumber,
                reserves.reserve_id                 AS reserve_id,
                reserves.timestamp                  AS timestamp,
                reserves.itemnumber                 AS itemnumber,
                reserves.reserve_id                 AS reserve_id,
-               reserves.itemtype                   AS itemtype
+               reserves.itemtype                   AS itemtype,
+               reserves.non_priority        AS non_priority
         FROM reserves
         WHERE reserves.biblionumber = ?
           AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?)
         FROM reserves
         WHERE reserves.biblionumber = ?
           AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?)
@@ -1683,7 +1843,7 @@ sub _Findgroupreserve {
   _koha_notify_reserve( $hold->reserve_id );
 
 Sends a notification to the patron that their hold has been filled (through
   _koha_notify_reserve( $hold->reserve_id );
 
 Sends a notification to the patron that their hold has been filled (through
-ModReserveAffect, _not_ ModReserveFill)
+ModReserveAffect)
 
 The letter code for this notice may be found using the following query:
 
 
 The letter code for this notice may be found using the following query:
 
@@ -1708,6 +1868,7 @@ The following tables are availalbe witin the notice:
 
 sub _koha_notify_reserve {
     my $reserve_id = shift;
 
 sub _koha_notify_reserve {
     my $reserve_id = shift;
+
     my $hold = Koha::Holds->find($reserve_id);
     my $borrowernumber = $hold->borrowernumber;
 
     my $hold = Koha::Holds->find($reserve_id);
     my $borrowernumber = $hold->borrowernumber;
 
@@ -1721,16 +1882,15 @@ sub _koha_notify_reserve {
             message_name => 'Hold_Filled'
     } );
 
             message_name => 'Hold_Filled'
     } );
 
-    my $library = Koha::Libraries->find( $hold->branchcode )->unblessed;
-
-    my $admin_email_address = $library->{branchemail} || C4::Context->preference('KohaAdminEmailAddress');
+    my $library = Koha::Libraries->find( $hold->branchcode );
+    my $inbound_email_address = $library->inbound_email_address;
 
     my %letter_params = (
         module => 'reserves',
         branchcode => $hold->branchcode,
         lang => $patron->lang,
         tables => {
 
     my %letter_params = (
         module => 'reserves',
         branchcode => $hold->branchcode,
         lang => $patron->lang,
         tables => {
-            'branches'       => $library,
+            'branches'       => $library->unblessed,
             'borrowers'      => $patron->unblessed,
             'biblio'         => $hold->biblionumber,
             'biblioitems'    => $hold->biblionumber,
             'borrowers'      => $patron->unblessed,
             'biblio'         => $hold->biblionumber,
             'biblioitems'    => $hold->biblionumber,
@@ -1754,7 +1914,7 @@ sub _koha_notify_reserve {
         C4::Letters::EnqueueLetter( {
             letter => $letter,
             borrowernumber => $borrowernumber,
         C4::Letters::EnqueueLetter( {
             letter => $letter,
             borrowernumber => $borrowernumber,
-            from_address => $admin_email_address,
+            from_address => $inbound_email_address,
             message_transport_type => $mtt,
         } );
     };
             message_transport_type => $mtt,
         } );
     };
@@ -1763,7 +1923,8 @@ sub _koha_notify_reserve {
         next if (
                ( $mtt eq 'email' and not $to_address ) # No email address
             or ( $mtt eq 'sms'   and not $patron->smsalertnumber ) # No SMS number
         next if (
                ( $mtt eq 'email' and not $to_address ) # No email address
             or ( $mtt eq 'sms'   and not $patron->smsalertnumber ) # No SMS number
-            or ( $mtt eq 'phone' and C4::Context->preference('TalkingTechItivaPhoneNotification') ) # Notice is handled by TalkingTech_itiva_outbound.pl
+            or ( $mtt eq 'itiva' and C4::Context->preference('TalkingTechItivaPhoneNotification') ) # Notice is handled by TalkingTech_itiva_outbound.pl
+            or ( $mtt eq 'phone' and not $patron->phone ) # No phone number to call
         );
 
         &$send_notification($mtt, $letter_code);
         );
 
         &$send_notification($mtt, $letter_code);
@@ -1776,9 +1937,53 @@ sub _koha_notify_reserve {
 
 }
 
 
 }
 
-=head2 _ShiftPriorityByDateAndPriority
+=head2 _koha_notify_hold_changed
+
+  _koha_notify_hold_changed( $hold_object );
+
+=cut
+
+sub _koha_notify_hold_changed {
+    my $hold = shift;
+
+    my $patron = $hold->patron;
+    my $library = $hold->branch;
+
+    my $letter = C4::Letters::GetPreparedLetter(
+        module      => 'reserves',
+        letter_code => 'HOLD_CHANGED',
+        branchcode  => $hold->branchcode,
+        substitute  => { today => output_pref( dt_from_string ) },
+        tables      => {
+            'branches'    => $library->unblessed,
+            'borrowers'   => $patron->unblessed,
+            'biblio'      => $hold->biblionumber,
+            'biblioitems' => $hold->biblionumber,
+            'reserves'    => $hold->unblessed,
+            'items'       => $hold->itemnumber,
+        },
+    );
+
+    return unless $letter;
+
+    my $email =
+         C4::Context->preference('ExpireReservesAutoFillEmail')
+      || $library->inbound_email_address;
+
+    C4::Letters::EnqueueLetter(
+        {
+            letter                 => $letter,
+            borrowernumber         => $patron->id,
+            message_transport_type => 'email',
+            from_address           => $email,
+            to_address             => $email,
+        }
+    );
+}
+
+=head2 _ShiftPriority
 
 
-  $new_priority = _ShiftPriorityByDateAndPriority( $biblionumber, $reservedate, $priority );
+  $new_priority = _ShiftPriority( $biblionumber, $priority );
 
 This increments the priority of all reserves after the one
 with either the lowest date after C<$reservedate>
 
 This increments the priority of all reserves after the one
 with either the lowest date after C<$reservedate>
@@ -1794,13 +1999,13 @@ the sub accounts for that too.
 
 =cut
 
 
 =cut
 
-sub _ShiftPriorityByDateAndPriority {
-    my ( $biblio, $resdate, $new_priority ) = @_;
+sub _ShiftPriority {
+    my ( $biblio, $new_priority ) = @_;
 
     my $dbh = C4::Context->dbh;
 
     my $dbh = C4::Context->dbh;
-    my $query = "SELECT priority FROM reserves WHERE biblionumber = ? AND ( reservedate > ? OR priority > ? ) ORDER BY priority ASC LIMIT 1";
+    my $query = "SELECT priority FROM reserves WHERE biblionumber = ? AND priority > ? ORDER BY priority ASC LIMIT 1";
     my $sth = $dbh->prepare( $query );
     my $sth = $dbh->prepare( $query );
-    $sth->execute( $biblio, $resdate, $new_priority );
+    $sth->execute( $biblio, $new_priority );
     my $min_priority = $sth->fetchrow;
     # if no such matches are found, $new_priority remains as original value
     $new_priority = $min_priority if ( $min_priority );
     my $min_priority = $sth->fetchrow;
     # if no such matches are found, $new_priority remains as original value
     $new_priority = $min_priority if ( $min_priority );
@@ -1837,14 +2042,17 @@ If $cancelreserve boolean is set to true, it will remove existing reserve
 sub MoveReserve {
     my ( $itemnumber, $borrowernumber, $cancelreserve ) = @_;
 
 sub MoveReserve {
     my ( $itemnumber, $borrowernumber, $cancelreserve ) = @_;
 
+    $cancelreserve //= 0;
+
     my $lookahead = C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
     my ( $restype, $res, undef ) = CheckReserves( $itemnumber, undef, $lookahead );
     return unless $res;
 
     my $lookahead = C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
     my ( $restype, $res, undef ) = CheckReserves( $itemnumber, undef, $lookahead );
     return unless $res;
 
-    my $biblionumber     =  $res->{biblionumber};
+    my $biblionumber = $res->{biblionumber};
 
     if ($res->{borrowernumber} == $borrowernumber) {
 
     if ($res->{borrowernumber} == $borrowernumber) {
-        ModReserveFill($res);
+        my $hold = Koha::Holds->find( $res->{reserve_id} );
+        $hold->fill;
     }
     else {
         # warn "Reserved";
     }
     else {
         # warn "Reserved";
@@ -1860,7 +2068,7 @@ sub MoveReserve {
 
         if ( $borr_res ) {
             # The item is reserved by the current patron
 
         if ( $borr_res ) {
             # The item is reserved by the current patron
-            ModReserveFill($borr_res->unblessed);
+            $borr_res->fill;
         }
 
         if ( $cancelreserve eq 'revert' ) { ## Revert waiting reserve to priority 1
         }
 
         if ( $cancelreserve eq 'revert' ) { ## Revert waiting reserve to priority 1
@@ -1898,13 +2106,13 @@ sub MergeHolds {
         # don't reorder those already waiting
 
         $sth = $dbh->prepare(
         # don't reorder those already waiting
 
         $sth = $dbh->prepare(
-"SELECT * FROM reserves WHERE biblionumber = ? AND (found <> ? AND found <> ? OR found is NULL) ORDER BY reservedate ASC"
+"SELECT * FROM reserves WHERE biblionumber = ? AND (found NOT IN ('W', 'T', 'P') OR found is NULL) ORDER BY reservedate ASC"
         );
         my $upd_sth = $dbh->prepare(
 "UPDATE reserves SET priority = ? WHERE biblionumber = ? AND borrowernumber = ?
         AND reservedate = ? AND (itemnumber = ? or itemnumber is NULL) "
         );
         );
         my $upd_sth = $dbh->prepare(
 "UPDATE reserves SET priority = ? WHERE biblionumber = ? AND borrowernumber = ?
         AND reservedate = ? AND (itemnumber = ? or itemnumber is NULL) "
         );
-        $sth->execute( $to_biblio, 'W', 'T' );
+        $sth->execute( $to_biblio );
         my $priority = 1;
         while ( my $reserve = $sth->fetchrow_hashref() ) {
             $upd_sth->execute(
         my $priority = 1;
         while ( my $reserve = $sth->fetchrow_hashref() ) {
             $upd_sth->execute(
@@ -1940,49 +2148,39 @@ sub RevertWaitingStatus {
     my $dbh = C4::Context->dbh;
 
     ## Get the waiting reserve we want to revert
     my $dbh = C4::Context->dbh;
 
     ## Get the waiting reserve we want to revert
-    my $query = "
-        SELECT * FROM reserves
-        WHERE itemnumber = ?
-        AND found IS NOT NULL
-    ";
-    my $sth = $dbh->prepare( $query );
-    $sth->execute( $itemnumber );
-    my $reserve = $sth->fetchrow_hashref();
-
-    my $hold = Koha::Holds->find( $reserve->{reserve_id} ); # TODO Remove the next raw SQL statements and use this instead
+    my $hold = Koha::Holds->search(
+        {
+            itemnumber => $itemnumber,
+            found => { not => undef },
+        }
+    )->next;
 
     ## Increment the priority of all other non-waiting
     ## reserves for this bib record
 
     ## Increment the priority of all other non-waiting
     ## reserves for this bib record
-    $query = "
-        UPDATE reserves
-        SET
-          priority = priority + 1
-        WHERE
-          biblionumber =  ?
-        AND
-          priority > 0
-    ";
-    $sth = $dbh->prepare( $query );
-    $sth->execute( $reserve->{'biblionumber'} );
+    my $holds = Koha::Holds->search({ biblionumber => $hold->biblionumber, priority => { '>' => 0 } })
+                           ->update({ priority => \'priority + 1' }, { no_triggers => 1 });
 
     ## Fix up the currently waiting reserve
 
     ## Fix up the currently waiting reserve
-    $query = "
-    UPDATE reserves
-    SET
-      priority = 1,
-      found = NULL,
-      waitingdate = NULL
-    WHERE
-      reserve_id = ?
-    ";
-    $sth = $dbh->prepare( $query );
-    $sth->execute( $reserve->{'reserve_id'} );
+    $hold->set(
+        {
+            priority    => 1,
+            found       => undef,
+            waitingdate => undef,
+            expirationdate => $hold->patron_expiration_date,
+            itemnumber  => $hold->item_level_hold ? $hold->itemnumber : undef,
+        }
+    )->store();
 
 
-    unless ( $hold->item_level_hold ) {
-        $hold->itemnumber(undef)->store;
-    }
+    _FixPriority( { biblionumber => $hold->biblionumber } );
+
+    Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
+        {
+            biblio_ids => [ $hold->biblionumber ]
+        }
+    ) if C4::Context->preference('RealTimeHoldsQueue');
 
 
-    _FixPriority( { biblionumber => $reserve->{biblionumber} } );
+
+    return $hold;
 }
 
 =head2 ReserveSlip
 }
 
 =head2 ReserveSlip
@@ -2014,36 +2212,12 @@ available within the slip:
 sub ReserveSlip {
     my ($args) = @_;
     my $branchcode     = $args->{branchcode};
 sub ReserveSlip {
     my ($args) = @_;
     my $branchcode     = $args->{branchcode};
-    my $borrowernumber = $args->{borrowernumber};
-    my $biblionumber   = $args->{biblionumber};
-    my $itemnumber     = $args->{itemnumber};
-    my $barcode        = $args->{barcode};
-
-
-    my $patron = Koha::Patrons->find($borrowernumber);
-
-    my $hold;
-    if ($itemnumber || $barcode ) {
-        $itemnumber ||= Koha::Items->find( { barcode => $barcode } )->itemnumber;
-
-        $hold = Koha::Holds->search(
-            {
-                biblionumber   => $biblionumber,
-                borrowernumber => $borrowernumber,
-                itemnumber     => $itemnumber
-            }
-        )->next;
-    }
-    else {
-        $hold = Koha::Holds->search(
-            {
-                biblionumber   => $biblionumber,
-                borrowernumber => $borrowernumber
-            }
-        )->next;
-    }
+    my $reserve_id = $args->{reserve_id};
 
 
+    my $hold = Koha::Holds->find($reserve_id);
     return unless $hold;
     return unless $hold;
+
+    my $patron = $hold->borrower;
     my $reserve = $hold->unblessed;
 
     return  C4::Letters::GetPreparedLetter (
     my $reserve = $hold->unblessed;
 
     return  C4::Letters::GetPreparedLetter (
@@ -2102,7 +2276,7 @@ priority is based on the set of holds whose start date falls before
 the parameter value.
 
 After calculation of this priority, it is recommended to call
 the parameter value.
 
 After calculation of this priority, it is recommended to call
-_ShiftPriorityByDateAndPriority. Note that this is currently done in
+_ShiftPriority. Note that this is currently done in
 AddReserves.
 
 =cut
 AddReserves.
 
 =cut
@@ -2116,7 +2290,7 @@ sub CalculatePriority {
         AND   priority > 0
         AND   (found IS NULL OR found = '')
     };
         AND   priority > 0
         AND   (found IS NULL OR found = '')
     };
-    #skip found==W or found==T (waiting or transit holds)
+    #skip found==W or found==T or found==P (waiting, transit or processing holds)
     if( $resdate ) {
         $sql.= ' AND ( reservedate <= ? )';
     }
     if( $resdate ) {
         $sql.= ' AND ( reservedate <= ? )';
     }
@@ -2173,7 +2347,7 @@ sub GetMaxPatronHoldsForRecord {
     my ( $borrowernumber, $biblionumber ) = @_;
 
     my $patron = Koha::Patrons->find($borrowernumber);
     my ( $borrowernumber, $biblionumber ) = @_;
 
     my $patron = Koha::Patrons->find($borrowernumber);
-    my @items = Koha::Items->search( { biblionumber => $biblionumber } );
+    my @items = Koha::Items->search( { biblionumber => $biblionumber } )->as_list;
 
     my $controlbranch = C4::Context->preference('ReservesControlBranch');
 
 
     my $controlbranch = C4::Context->preference('ReservesControlBranch');
 
@@ -2187,46 +2361,19 @@ sub GetMaxPatronHoldsForRecord {
 
         $branchcode = $item->homebranch if ( $controlbranch eq "ItemHomeLibrary" );
 
 
         $branchcode = $item->homebranch if ( $controlbranch eq "ItemHomeLibrary" );
 
-        my $rule = GetHoldRule( $categorycode, $itemtype, $branchcode );
-        my $holds_per_record = $rule ? $rule->{holds_per_record} : 0;
+        my $rule = Koha::CirculationRules->get_effective_rule({
+            categorycode => $categorycode,
+            itemtype     => $itemtype,
+            branchcode   => $branchcode,
+            rule_name    => 'holds_per_record'
+        });
+        my $holds_per_record = $rule ? $rule->rule_value : 0;
         $max = $holds_per_record if $holds_per_record > $max;
     }
 
     return $max;
 }
 
         $max = $holds_per_record if $holds_per_record > $max;
     }
 
     return $max;
 }
 
-=head2 GetHoldRule
-
-my $rule = GetHoldRule( $categorycode, $itemtype, $branchcode );
-
-Returns the matching hold related issuingrule fields for a given
-patron category, itemtype, and library.
-
-=cut
-
-sub GetHoldRule {
-    my ( $categorycode, $itemtype, $branchcode ) = @_;
-
-    my $dbh = C4::Context->dbh;
-
-    my $sth = $dbh->prepare(
-        q{
-         SELECT categorycode, itemtype, branchcode, reservesallowed, holds_per_record, holds_per_day
-           FROM issuingrules
-          WHERE (categorycode in (?,'*') )
-            AND (itemtype IN (?,'*'))
-            AND (branchcode IN (?,'*'))
-       ORDER BY categorycode DESC,
-                itemtype     DESC,
-                branchcode   DESC
-        }
-    );
-
-    $sth->execute( $categorycode, $itemtype, $branchcode );
-
-    return $sth->fetchrow_hashref();
-}
-
 =head1 AUTHOR
 
 Koha Development Team <http://koha-community.org/>
 =head1 AUTHOR
 
 Koha Development Team <http://koha-community.org/>