Bug 10457: fix SQL error when using 'export select items data' in serials claims...
[koha.git] / C4 / Reserves.pm
index 4af8a85..5ff12e4 100644 (file)
@@ -36,6 +36,9 @@ use C4::Members qw();
 use C4::Letters;
 use C4::Branch qw( GetBranchDetail );
 use C4::Dates qw( format_date_in_iso );
+
+use Koha::DateUtils;
+
 use List::MoreUtils qw( firstidx );
 
 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
@@ -85,7 +88,7 @@ This modules provides somes functions to deal with reservations.
 
 BEGIN {
     # set the version for version checking
-    $VERSION = 3.01;
+    $VERSION = 3.07.00.049;
     require Exporter;
     @ISA = qw(Exporter);
     @EXPORT = qw(
@@ -98,7 +101,7 @@ BEGIN {
         &GetReservesToBranch
         &GetReserveCount
         &GetReserveFee
-               &GetReserveInfo
+        &GetReserveInfo
         &GetReserveStatus
         
         &GetOtherReserves
@@ -109,6 +112,7 @@ BEGIN {
         &ModReserveStatus
         &ModReserveCancelAll
         &ModReserveMinusPriority
+        &MoveReserve
         
         &CheckReserves
         &CanBookBeReserved
@@ -116,10 +120,16 @@ BEGIN {
         &CancelReserve
         &CancelExpiredReserves
 
+        &AutoUnsuspendReserves
+
         &IsAvailableForItemLevelRequest
         
         &AlterPriority
         &ToggleLowestPriority
+
+        &ReserveSlip
+        &ToggleSuspend
+        &SuspendAll
     );
     @EXPORT_OK = qw( MergeHolds );
 }    
@@ -193,32 +203,31 @@ sub AddReserve {
     # Send e-mail to librarian if syspref is active
     if(C4::Context->preference("emailLibrarianWhenHoldIsPlaced")){
         my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber);
-        my $biblio   = GetBiblioData($biblionumber);
-        my $letter = C4::Letters::getletter( 'reserves', 'HOLDPLACED');
-       my $branchcode = $borrower->{branchcode};
-        my $branch_details = C4::Branch::GetBranchDetail($branchcode);
-        my $admin_email_address =$branch_details->{'branchemail'} || C4::Context->preference('KohaAdminEmailAddress');
-
-        my %keys = (%$borrower, %$biblio);
-        foreach my $key (keys %keys) {
-            my $replacefield = "<<$key>>";
-            $letter->{content} =~ s/$replacefield/$keys{$key}/g;
-            $letter->{title} =~ s/$replacefield/$keys{$key}/g;
+        my $branch_details = C4::Branch::GetBranchDetail($borrower->{branchcode});
+        if ( my $letter =  C4::Letters::GetPreparedLetter (
+            module => 'reserves',
+            letter_code => 'HOLDPLACED',
+            branchcode => $branch,
+            tables => {
+                'branches'  => $branch_details,
+                'borrowers' => $borrower,
+                'biblio'    => $biblionumber,
+            },
+        ) ) {
+
+            my $admin_email_address =$branch_details->{'branchemail'} || C4::Context->preference('KohaAdminEmailAddress');
+
+            C4::Letters::EnqueueLetter(
+                {   letter                 => $letter,
+                    borrowernumber         => $borrowernumber,
+                    message_transport_type => 'email',
+                    from_address           => $admin_email_address,
+                    to_address           => $admin_email_address,
+                }
+            );
         }
-        
-        C4::Letters::EnqueueLetter(
-                            {   letter                 => $letter,
-                                borrowernumber         => $borrowernumber,
-                                message_transport_type => 'email',
-                                from_address           => $admin_email_address,
-                                to_address           => $admin_email_address,
-                            }
-                        );
-        
-
     }
 
-
     #}
     ($const eq "o" || $const eq "e") or return;   # FIXME: why not have a useful return value?
     $query = qq/
@@ -262,7 +271,9 @@ sub GetReservesFromBiblionumber {
                 itemnumber,
                 reservenotes,
                 expirationdate,
-                lowestPriority
+                lowestPriority,
+                suspend,
+                suspend_until
         FROM     reserves
         WHERE biblionumber = ? ";
     unless ( $all_dates ) {
@@ -383,14 +394,14 @@ sub GetReservesFromBorrowernumber {
 sub CanBookBeReserved{
     my ($borrowernumber, $biblionumber) = @_;
 
-    my @items = get_itemnumbers_of($biblionumber);
+    my $items = GetItemnumbersForBiblio($biblionumber);
     #get items linked via host records
     my @hostitems = get_hostitemnumbers_of($biblionumber);
     if (@hostitems){
-       push (@items,@hostitems);
+    push (@$items,@hostitems);
     }
 
-    foreach my $item (@items){
+    foreach my $item (@$items){
         return 1 if CanItemBeReserved($borrowernumber, $item);
     }
     return 0;
@@ -523,7 +534,7 @@ sub GetOtherReserves {
     my ($itemnumber) = @_;
     my $messages;
     my $nextreservinfo;
-    my ( $restype, $checkreserves ) = CheckReserves($itemnumber);
+    my ( undef, $checkreserves, undef ) = CheckReserves($itemnumber);
     if ($checkreserves) {
         my $iteminfo = GetItem($itemnumber);
         if ( $iteminfo->{'holdingbranch'} ne $checkreserves->{'branchcode'} ) {
@@ -724,22 +735,46 @@ sub GetReservesForBranch {
     return (@transreserv);
 }
 
+=head2 GetReserveStatus
+
+  $reservestatus = GetReserveStatus($itemnumber, $biblionumber);
+
+Take an itemnumber or a biblionumber and return the status of the reserve places on it.
+If several reserves exist, the reserve with the lower priority is given.
+
+=cut
+
 sub GetReserveStatus {
-    my ($itemnumber) = @_;
-    
+    my ($itemnumber, $biblionumber) = @_;
+
     my $dbh = C4::Context->dbh;
-    
-    my $itemstatus = $dbh->prepare("SELECT found FROM reserves WHERE itemnumber = ?");
-    
-    $itemstatus->execute($itemnumber);
-    my ($found) = $itemstatus->fetchrow_array;
-    return $found;
+
+    my ($sth, $found, $priority);
+    if ( $itemnumber ) {
+        $sth = $dbh->prepare("SELECT found, priority FROM reserves WHERE itemnumber = ? order by priority LIMIT 1");
+        $sth->execute($itemnumber);
+        ($found, $priority) = $sth->fetchrow_array;
+    }
+
+    if ( $biblionumber and not defined $found and not defined $priority ) {
+        $sth = $dbh->prepare("SELECT found, priority FROM reserves WHERE biblionumber = ? order by priority LIMIT 1");
+        $sth->execute($biblionumber);
+        ($found, $priority) = $sth->fetchrow_array;
+    }
+
+    if(defined $found) {
+        return 'Waiting'  if $found eq 'W' and $priority == 0;
+        return 'Finished' if $found eq 'F';
+        return 'Reserved' if $priority > 0;
+    }
+    return '';
+    #empty string here will remove need for checking undef, or less log lines
 }
 
 =head2 CheckReserves
 
-  ($status, $reserve) = &CheckReserves($itemnumber);
-  ($status, $reserve) = &CheckReserves(undef, $barcode);
+  ($status, $reserve, $all_reserves) = &CheckReserves($itemnumber);
+  ($status, $reserve, $all_reserves) = &CheckReserves(undef, $barcode);
 
 Find a book in the reserves.
 
@@ -804,11 +839,11 @@ sub CheckReserves {
     # note: we get the itemnumber because we might have started w/ just the barcode.  Now we know for sure we have it.
     my ( $biblio, $bibitem, $notforloan_per_itemtype, $notforloan_per_item, $itemnumber ) = $sth->fetchrow_array;
 
-    return ( 0, 0 ) unless $itemnumber; # bail if we got nothing.
+    return ( '' ) unless $itemnumber; # bail if we got nothing.
 
     # if item is not for loan it cannot be reserved either.....
     #    execpt where items.notforloan < 0 :  This indicates the item is holdable. 
-    return ( 0, 0 ) if  ( $notforloan_per_item > 0 ) or $notforloan_per_itemtype;
+    return ( '' ) if  ( $notforloan_per_item > 0 ) or $notforloan_per_itemtype;
 
     # Find this item in the reserves
     my @reserves = _Findgroupreserve( $bibitem, $biblio, $itemnumber );
@@ -822,7 +857,7 @@ sub CheckReserves {
         my $priority = 10000000;
         foreach my $res (@reserves) {
             if ( $res->{'itemnumber'} == $itemnumber && $res->{'priority'} == 0) {
-                return ( "Waiting", $res ); # Found it
+                return ( "Waiting", $res, \@reserves ); # Found it
             } else {
                 # See if this item is more important than what we've got so far
                 if ( $res->{'priority'} && $res->{'priority'} < $priority ) {
@@ -843,11 +878,10 @@ sub CheckReserves {
     # We return the most important (i.e. next) reservation.
     if ($highest) {
         $highest->{'itemnumber'} = $item;
-        return ( "Reserved", $highest );
-    }
-    else {
-        return ( 0, 0 );
+        return ( "Reserved", $highest, \@reserves );
     }
+
+    return ( '' );
 }
 
 =head2 CancelExpiredReserves
@@ -860,10 +894,12 @@ Cancels all reserves with an expiration date from before today.
 
 sub CancelExpiredReserves {
 
+    # Cancel reserves that have passed their expiration date.
     my $dbh = C4::Context->dbh;
     my $sth = $dbh->prepare( "
         SELECT * FROM reserves WHERE DATE(expirationdate) < DATE( CURDATE() ) 
         AND expirationdate IS NOT NULL
+        AND found IS NULL
     " );
     $sth->execute();
 
@@ -871,6 +907,42 @@ sub CancelExpiredReserves {
         CancelReserve( $res->{'biblionumber'}, '', $res->{'borrowernumber'} );
     }
   
+    # Cancel reserves that have been waiting too long
+    if ( C4::Context->preference("ExpireReservesMaxPickUpDelay") ) {
+        my $max_pickup_delay = C4::Context->preference("ReservesMaxPickUpDelay");
+        my $charge = C4::Context->preference("ExpireReservesMaxPickUpDelayCharge");
+
+        my $query = "SELECT * FROM reserves WHERE TO_DAYS( NOW() ) - TO_DAYS( waitingdate ) > ? AND found = 'W' AND priority = 0";
+        $sth = $dbh->prepare( $query );
+        $sth->execute( $max_pickup_delay );
+
+        while (my $res = $sth->fetchrow_hashref ) {
+            if ( $charge ) {
+                manualinvoice($res->{'borrowernumber'}, $res->{'itemnumber'}, 'Hold waiting too long', 'F', $charge);
+            }
+
+            CancelReserve( $res->{'biblionumber'}, '', $res->{'borrowernumber'} );
+        }
+    }
+
+}
+
+=head2 AutoUnsuspendReserves
+
+  AutoUnsuspendReserves();
+
+Unsuspends all suspended reserves with a suspend_until date from before today.
+
+=cut
+
+sub AutoUnsuspendReserves {
+
+    my $dbh = C4::Context->dbh;
+
+    my $query = "UPDATE reserves SET suspend = 0, suspend_until = NULL WHERE DATE( suspend_until ) < DATE( CURDATE() )";
+    my $sth = $dbh->prepare( $query );
+    $sth->execute();
+
 }
 
 =head2 CancelReserve
@@ -880,8 +952,8 @@ sub CancelExpiredReserves {
 Cancels a reserve.
 
 Use either C<$biblionumber> or C<$itemnumber> to specify the item to
-cancel, but not both: if both are given, C<&CancelReserve> does
-nothing.
+cancel, but not both: if both are given, C<&CancelReserve> uses
+C<$itemnumber>.
 
 C<$borrowernumber> is the borrower number of the patron on whose
 behalf the book was reserved.
@@ -1008,7 +1080,7 @@ itemnumber and supplying itemnumber.
 
 sub ModReserve {
     #subroutine to update a reserve
-    my ( $rank, $biblio, $borrower, $branch , $itemnumber) = @_;
+    my ( $rank, $biblio, $borrower, $branch , $itemnumber, $suspend_until) = @_;
      return if $rank eq "W";
      return if $rank eq "n";
     my $dbh = C4::Context->dbh;
@@ -1041,14 +1113,24 @@ sub ModReserve {
         
     }
     elsif ($rank =~ /^\d+/ and $rank > 0) {
-        my $query = qq/
-        UPDATE reserves SET priority = ? ,branchcode = ?, itemnumber = ?, found = NULL, waitingdate = NULL
+        my $query = "
+            UPDATE reserves SET priority = ? ,branchcode = ?, itemnumber = ?, found = NULL, waitingdate = NULL
             WHERE biblionumber   = ?
-             AND borrowernumber = ?
-        /;
+            AND borrowernumber = ?
+        ";
         my $sth = $dbh->prepare($query);
         $sth->execute( $rank, $branch,$itemnumber, $biblio, $borrower);
         $sth->finish;
+
+        if ( defined( $suspend_until ) ) {
+            if ( $suspend_until ) {
+                $suspend_until = C4::Dates->new( $suspend_until )->output("iso");
+                $dbh->do("UPDATE reserves SET suspend = 1, suspend_until = ? WHERE biblionumber = ? AND borrowernumber = ?", undef, ( $suspend_until, $biblio, $borrower ) );
+            } else {
+                $dbh->do("UPDATE reserves SET suspend_until = NULL WHERE biblionumber = ? AND borrowernumber = ?", undef, ( $biblio, $borrower ) );
+            }
+        }
+
         _FixPriority( $biblio, $borrower, $rank);
     }
 }
@@ -1137,13 +1219,9 @@ sub ModReserveStatus {
 
     #first : check if we have a reservation for this item .
     my ($itemnumber, $newstatus) = @_;
-    my $dbh          = C4::Context->dbh;
-    my $query = " UPDATE reserves
-    SET    found=?,waitingdate = now()
-    WHERE itemnumber=?
-      AND found IS NULL
-      AND priority = 0
-    ";
+    my $dbh = C4::Context->dbh;
+
+    my $query = "UPDATE reserves SET found = ?, waitingdate = NOW() WHERE itemnumber = ? AND found IS NULL AND priority = 0";
     my $sth_set = $dbh->prepare($query);
     $sth_set->execute( $newstatus, $itemnumber );
 
@@ -1196,15 +1274,15 @@ sub ModReserveAffect {
     }
     else {
     # affect the reserve to Waiting as well.
-    $query = "
-        UPDATE reserves
-        SET     priority = 0,
-                found = 'W',
-                waitingdate=now(),
-                itemnumber = ?
-        WHERE borrowernumber = ?
-          AND biblionumber = ?
-    ";
+        $query = "
+            UPDATE reserves
+            SET     priority = 0,
+                    found = 'W',
+                    waitingdate = NOW(),
+                    itemnumber = ?
+            WHERE borrowernumber = ?
+              AND biblionumber = ?
+        ";
     }
     $sth = $dbh->prepare($query);
     $sth->execute( $itemnumber, $borrowernumber,$biblionumber);
@@ -1341,7 +1419,7 @@ be the target of an item-level hold request.
 Note that IsAvailableForItemLevelRequest() does not
 check if the staff operator is authorized to place
 a request on the item - in particular,
-this routine does not check IndependantBranches
+this routine does not check IndependentBranches
 and canreservefromotherbranches.
 
 =cut
@@ -1387,7 +1465,7 @@ sub IsAvailableForItemLevelRequest {
     if (C4::Context->preference('AllowOnShelfHolds')) {
         return $available_per_item;
     } else {
-        return ($available_per_item and ($item->{onloan} or GetReserveStatus($itemnumber) eq "W")); 
+        return ($available_per_item and ($item->{onloan} or GetReserveStatus($itemnumber) eq "Waiting"));
     }
 }
 
@@ -1455,6 +1533,98 @@ sub ToggleLowestPriority {
     _FixPriority( $biblionumber, $borrowernumber, '999999' );
 }
 
+=head2 ToggleSuspend
+
+  ToggleSuspend( $borrowernumber, $biblionumber );
+
+This function sets the suspend field to true if is false, and false if it is true.
+If the reserve is currently suspended with a suspend_until date, that date will
+be cleared when it is unsuspended.
+
+=cut
+
+sub ToggleSuspend {
+    my ( $borrowernumber, $biblionumber, $suspend_until ) = @_;
+
+    $suspend_until = output_pref( dt_from_string( $suspend_until ), 'iso' ) if ( $suspend_until );
+
+    my $do_until = ( $suspend_until ) ? '?' : 'NULL';
+
+    my $dbh = C4::Context->dbh;
+
+    my $sth = $dbh->prepare(
+        "UPDATE reserves SET suspend = NOT suspend,
+        suspend_until = CASE WHEN suspend = 0 THEN NULL ELSE $do_until END
+        WHERE biblionumber = ?
+        AND borrowernumber = ?
+    ");
+
+    my @params;
+    push( @params, $suspend_until ) if ( $suspend_until );
+    push( @params, $biblionumber );
+    push( @params, $borrowernumber );
+
+    $sth->execute( @params );
+    $sth->finish;
+}
+
+=head2 SuspendAll
+
+  SuspendAll(
+      borrowernumber   => $borrowernumber,
+      [ biblionumber   => $biblionumber, ]
+      [ suspend_until  => $suspend_until, ]
+      [ suspend        => $suspend ]
+  );
+
+  This function accepts a set of hash keys as its parameters.
+  It requires either borrowernumber or biblionumber, or both.
+
+  suspend_until is wholly optional.
+
+=cut
+
+sub SuspendAll {
+    my %params = @_;
+
+    my $borrowernumber = $params{'borrowernumber'} || undef;
+    my $biblionumber   = $params{'biblionumber'}   || undef;
+    my $suspend_until  = $params{'suspend_until'}  || undef;
+    my $suspend        = defined( $params{'suspend'} ) ? $params{'suspend'} :  1;
+
+    $suspend_until = C4::Dates->new( $suspend_until )->output("iso") if ( defined( $suspend_until ) );
+
+    return unless ( $borrowernumber || $biblionumber );
+
+    my ( $query, $sth, $dbh, @query_params );
+
+    $query = "UPDATE reserves SET suspend = ? ";
+    push( @query_params, $suspend );
+    if ( !$suspend ) {
+        $query .= ", suspend_until = NULL ";
+    } elsif ( $suspend_until ) {
+        $query .= ", suspend_until = ? ";
+        push( @query_params, $suspend_until );
+    }
+    $query .= " WHERE ";
+    if ( $borrowernumber ) {
+        $query .= " borrowernumber = ? ";
+        push( @query_params, $borrowernumber );
+    }
+    $query .= " AND " if ( $borrowernumber && $biblionumber );
+    if ( $biblionumber ) {
+        $query .= " biblionumber = ? ";
+        push( @query_params, $biblionumber );
+    }
+    $query .= " AND found IS NULL ";
+
+    $dbh = C4::Context->dbh;
+    $sth = $dbh->prepare( $query );
+    $sth->execute( @query_params );
+    $sth->finish;
+}
+
+
 =head2 _FixPriority
 
   &_FixPriority($biblio,$borrowernumber,$rank,$ignoreSetLowestRank);
@@ -1599,6 +1769,7 @@ sub _Findgroupreserve {
         AND item_level_request = 1
         AND itemnumber = ?
         AND reservedate <= CURRENT_DATE()
+        AND suspend = 0
     /;
     my $sth = $dbh->prepare($item_level_target_query);
     $sth->execute($itemnumber);
@@ -1629,6 +1800,7 @@ sub _Findgroupreserve {
         AND item_level_request = 0
         AND hold_fill_targets.itemnumber = ?
         AND reservedate <= CURRENT_DATE()
+        AND suspend = 0
     /;
     $sth = $dbh->prepare($title_level_target_query);
     $sth->execute($itemnumber);
@@ -1660,6 +1832,7 @@ sub _Findgroupreserve {
           OR  reserves.constrainttype='a' )
           AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?)
           AND reserves.reservedate <= CURRENT_DATE()
+          AND suspend = 0
     /;
     $sth = $dbh->prepare($query);
     $sth->execute( $biblio, $bibitem, $itemnumber );
@@ -1686,25 +1859,14 @@ sub _koha_notify_reserve {
     my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber);
     
     # Try to get the borrower's email address
-    my $to_address;
-    my $which_address = C4::Context->preference('AutoEmailPrimaryAddress');
-    # If the system preference is set to 'first valid' (value == OFF), look up email address
-    if ($which_address eq 'OFF') {
-        $to_address = C4::Members::GetFirstValidEmailAddress( $borrowernumber );
-    } else {
-        $to_address = $borrower->{$which_address};
-    }
+    my $to_address = C4::Members::GetNoticeEmailAddress($borrowernumber);
     
     my $letter_code;
     my $print_mode = 0;
     my $messagingprefs;
     if ( $to_address || $borrower->{'smsalertnumber'} ) {
         $messagingprefs = C4::Members::Messaging::GetMessagingPreferences( { borrowernumber => $borrowernumber, message_name => 'Hold_Filled' } );
-
-        return if ( !defined( $messagingprefs->{'letter_code'} ) );
-        $letter_code = $messagingprefs->{'letter_code'};
     } else {
-        $letter_code = 'HOLD_PRINT';
         $print_mode = 1;
     }
 
@@ -1720,23 +1882,24 @@ sub _koha_notify_reserve {
 
     my $admin_email_address = $branch_details->{'branchemail'} || C4::Context->preference('KohaAdminEmailAddress');
 
-    my $letter = getletter( 'reserves', $letter_code );
-    die "Could not find a letter called '$letter_code' in the 'reserves' module" unless( $letter );
-
-    C4::Letters::parseletter( $letter, 'branches', $reserve->{'branchcode'} );
-    C4::Letters::parseletter( $letter, 'borrowers', $borrowernumber );
-    C4::Letters::parseletter( $letter, 'biblio', $biblionumber );
-    C4::Letters::parseletter( $letter, 'reserves', $borrowernumber, $biblionumber );
+    my %letter_params = (
+        module => 'reserves',
+        branchcode => $reserve->{branchcode},
+        tables => {
+            'branches'  => $branch_details,
+            'borrowers' => $borrower,
+            'biblio'    => $biblionumber,
+            'reserves'  => $reserve,
+            'items', $reserve->{'itemnumber'},
+        },
+        substitute => { today => C4::Dates->new()->output() },
+    );
 
-    if ( $reserve->{'itemnumber'} ) {
-        C4::Letters::parseletter( $letter, 'items', $reserve->{'itemnumber'} );
-    }
-    my $today = C4::Dates->new()->output();
-    $letter->{'title'} =~ s/<<today>>/$today/g;
-    $letter->{'content'} =~ s/<<today>>/$today/g;
-    $letter->{'content'} =~ s/<<[a-z0-9_]+\.[a-z0-9]+>>//g; #remove any stragglers
 
     if ( $print_mode ) {
+        $letter_params{ 'letter_code' } = 'HOLD_PRINT';
+        my $letter =  C4::Letters::GetPreparedLetter ( %letter_params ) or die "Could not find a letter called '$letter_params{'letter_code'}' in the 'reserves' module";
+
         C4::Letters::EnqueueLetter( {
             letter => $letter,
             borrowernumber => $borrowernumber,
@@ -1746,8 +1909,10 @@ sub _koha_notify_reserve {
         return;
     }
 
-    if ( grep { $_ eq 'email' } @{$messagingprefs->{transports}} ) {
-        # aka, 'email' in ->{'transports'}
+    if ( $to_address && defined $messagingprefs->{transports}->{'email'} ) {
+        $letter_params{ 'letter_code' } = $messagingprefs->{transports}->{'email'};
+        my $letter =  C4::Letters::GetPreparedLetter ( %letter_params ) or die "Could not find a letter called '$letter_params{'letter_code'}' in the 'reserves' module";
+
         C4::Letters::EnqueueLetter(
             {   letter                 => $letter,
                 borrowernumber         => $borrowernumber,
@@ -1757,7 +1922,10 @@ sub _koha_notify_reserve {
         );
     }
 
-    if ( grep { $_ eq 'sms' } @{$messagingprefs->{transports}} ) {
+    if ( $borrower->{'smsalertnumber'} && defined $messagingprefs->{transports}->{'sms'} ) {
+        $letter_params{ 'letter_code' } = $messagingprefs->{transports}->{'sms'};
+        my $letter =  C4::Letters::GetPreparedLetter ( %letter_params ) or die "Could not find a letter called '$letter_params{'letter_code'}' in the 'reserves' module";
+
         C4::Letters::EnqueueLetter(
             {   letter                 => $letter,
                 borrowernumber         => $borrowernumber,
@@ -1816,6 +1984,56 @@ sub _ShiftPriorityByDateAndPriority {
     return $new_priority;  # so the caller knows what priority they wind up receiving
 }
 
+=head2 MoveReserve
+
+  MoveReserve( $itemnumber, $borrowernumber, $cancelreserve )
+
+Use when checking out an item to handle reserves
+If $cancelreserve boolean is set to true, it will remove existing reserve
+
+=cut
+
+sub MoveReserve {
+    my ( $itemnumber, $borrowernumber, $cancelreserve ) = @_;
+
+    my ( $restype, $res, $all_reserves ) = CheckReserves( $itemnumber );
+    return unless $res;
+
+    my $biblionumber     =  $res->{biblionumber};
+    my $biblioitemnumber = $res->{biblioitemnumber};
+
+    if ($res->{borrowernumber} == $borrowernumber) {
+        ModReserveFill($res);
+    }
+    else {
+        # warn "Reserved";
+        # The item is reserved by someone else.
+        # Find this item in the reserves
+
+        my $borr_res;
+        foreach (@$all_reserves) {
+            $_->{'borrowernumber'} == $borrowernumber or next;
+            $_->{'biblionumber'}   == $biblionumber   or next;
+
+            $borr_res = $_;
+            last;
+        }
+
+        if ( $borr_res ) {
+            # The item is reserved by the current patron
+            ModReserveFill($borr_res);
+        }
+
+        if ( $cancelreserve eq 'revert' ) { ## Revert waiting reserve to priority 1
+            RevertWaitingStatus({ itemnumber => $itemnumber });
+        }
+        elsif ( $cancelreserve eq 'cancel' || $cancelreserve ) { # cancel reserves on this item
+            CancelReserve(0, $res->{'itemnumber'}, $res->{'borrowernumber'});
+            CancelReserve($res->{'biblionumber'}, 0, $res->{'borrowernumber'});
+        }
+    }
+}
+
 =head2 MergeHolds
 
   MergeHolds($dbh,$to_biblio, $from_biblio);
@@ -1860,6 +2078,95 @@ sub MergeHolds {
     }
 }
 
+=head2 RevertWaitingStatus
+
+  $success = RevertWaitingStatus({ itemnumber => $itemnumber });
+
+  Reverts a 'waiting' hold back to a regular hold with a priority of 1.
+
+  Caveat: Any waiting hold fixed with RevertWaitingStatus will be an
+          item level hold, even if it was only a bibliolevel hold to
+          begin with. This is because we can no longer know if a hold
+          was item-level or bib-level after a hold has been set to
+          waiting status.
+
+=cut
+
+sub RevertWaitingStatus {
+    my ( $params ) = @_;
+    my $itemnumber = $params->{'itemnumber'};
+
+    return unless ( $itemnumber );
+
+    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();
+
+    ## 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'} );
+
+    ## Fix up the currently waiting reserve
+    $query = "
+    UPDATE reserves
+    SET
+      priority = 1,
+      found = NULL,
+      waitingdate = NULL
+    WHERE
+      reserve_id = ?
+    ";
+    $sth = $dbh->prepare( $query );
+    return $sth->execute( $reserve->{'reserve_id'} );
+}
+
+=head2 ReserveSlip
+
+  ReserveSlip($branchcode, $borrowernumber, $biblionumber)
+
+  Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
+
+=cut
+
+sub ReserveSlip {
+    my ($branch, $borrowernumber, $biblionumber) = @_;
+
+#   return unless ( C4::Context->boolean_preference('printreserveslips') );
+
+    my $reserve = GetReserveInfo($borrowernumber,$biblionumber )
+      or return;
+
+    return  C4::Letters::GetPreparedLetter (
+        module => 'circulation',
+        letter_code => 'RESERVESLIP',
+        branchcode => $branch,
+        tables => {
+            'reserves'    => $reserve,
+            'branches'    => $reserve->{branchcode},
+            'borrowers'   => $reserve->{borrowernumber},
+            'biblio'      => $reserve->{biblionumber},
+            'items'       => $reserve->{itemnumber},
+        },
+    );
+}
 
 =head1 AUTHOR