Bug 15006: Introduce client_timeout in SIPconfig.xml
[srvgit] / C4 / Reserves.pm
index ed24d32..79fb11a 100644 (file)
@@ -34,16 +34,20 @@ use C4::Accounts;
 use C4::Members::Messaging;
 use C4::Members qw();
 use C4::Letters;
-use C4::Branch qw( GetBranchDetail );
-use C4::Dates qw( format_date_in_iso );
 
 use Koha::DateUtils;
 use Koha::Calendar;
 use Koha::Database;
+use Koha::Hold;
+use Koha::Holds;
+use Koha::Libraries;
+use Koha::Items;
+use Koha::ItemTypes;
 
 use List::MoreUtils qw( firstidx any );
+use Carp;
 
-use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
+use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
 
 =head1 NAME
 
@@ -89,8 +93,6 @@ This modules provides somes functions to deal with reservations.
 =cut
 
 BEGIN {
-    # set the version for version checking
-    $VERSION = 3.07.00.049;
     require Exporter;
     @ISA = qw(Exporter);
     @EXPORT = qw(
@@ -147,26 +149,43 @@ BEGIN {
 
     AddReserve($branch,$borrowernumber,$biblionumber,$bibitems,$priority,$resdate,$expdate,$notes,$title,$checkitem,$found)
 
+Adds reserve and generates HOLDPLACED message.
+
+The following tables are available witin the HOLDPLACED message:
+
+    branches
+    borrowers
+    biblio
+    biblioitems
+    items
+
 =cut
 
 sub AddReserve {
     my (
-        $branch,    $borrowernumber, $biblionumber,
-        $bibitems,  $priority, $resdate, $expdate, $notes,
-        $title,      $checkitem, $found
+        $branch,   $borrowernumber, $biblionumber, $bibitems,
+        $priority, $resdate,        $expdate,      $notes,
+        $title,    $checkitem,      $found,        $itemtype
     ) = @_;
-    my $dbh     = C4::Context->dbh;
-    $resdate = format_date_in_iso( $resdate ) if ( $resdate );
-    $resdate = C4::Dates->today( 'iso' ) unless ( $resdate );
-    if ($expdate) {
-        $expdate = format_date_in_iso( $expdate );
-    } else {
-        undef $expdate; # make reserves.expirationdate default to null rather than '0000-00-00'
+
+    if ( Koha::Holds->search( { borrowernumber => $borrowernumber, biblionumber => $biblionumber } )->count() > 0 ) {
+        carp("AddReserve: borrower $borrowernumber already has a hold for biblionumber $biblionumber");
+        return;
     }
-    if ( C4::Context->preference( 'AllowHoldDateInFuture' ) ) {
-       # Make room in reserves for this before those of a later reserve date
-       $priority = _ShiftPriorityByDateAndPriority( $biblionumber, $resdate, $priority );
+
+    my $dbh     = C4::Context->dbh;
+
+    $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' });
+
+    if ( C4::Context->preference('AllowHoldDateInFuture') ) {
+
+        # Make room in reserves for this before those of a later reserve date
+        $priority = _ShiftPriorityByDateAndPriority( $biblionumber, $resdate, $priority );
     }
+
     my $waitingdate;
 
     # If the reserv had the waiting status, we had the value of the resdate
@@ -174,42 +193,51 @@ sub AddReserve {
         $waitingdate = $resdate;
     }
 
+    # Don't add itemtype limit if specific item is selected
+    $itemtype = undef if $checkitem;
+
     # updates take place here
-    my $query = qq{
-        INSERT INTO reserves
-            (borrowernumber,biblionumber,reservedate,branchcode,
-            priority,reservenotes,itemnumber,found,waitingdate,expirationdate)
-        VALUES
-             (?,?,?,?,?,
-             ?,?,?,?,?)
-             };
-    my $sth = $dbh->prepare($query);
-    $sth->execute(
-        $borrowernumber, $biblionumber, $resdate, $branch,      $priority,
-        $notes,          $checkitem,    $found,   $waitingdate, $expdate
-    );
-    my $reserve_id = $sth->{mysql_insertid};
+    my $hold = Koha::Hold->new(
+        {
+            borrowernumber => $borrowernumber,
+            biblionumber   => $biblionumber,
+            reservedate    => $resdate,
+            branchcode     => $branch,
+            priority       => $priority,
+            reservenotes   => $notes,
+            itemnumber     => $checkitem,
+            found          => $found,
+            waitingdate    => $waitingdate,
+            expirationdate => $expdate,
+            itemtype       => $itemtype,
+        }
+    )->store();
+    my $reserve_id = $hold->id();
+
     # add a reserve fee if needed
     my $fee = GetReserveFee( $borrowernumber, $biblionumber );
     ChargeReserveFee( $borrowernumber, $fee, $title );
 
+    _FixPriority({ biblionumber => $biblionumber});
+
     # Send e-mail to librarian if syspref is active
     if(C4::Context->preference("emailLibrarianWhenHoldIsPlaced")){
         my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber);
-        my $branch_details = C4::Branch::GetBranchDetail($borrower->{branchcode});
+        my $library = Koha::Libraries->find($borrower->{branchcode})->unblessed;
         if ( my $letter =  C4::Letters::GetPreparedLetter (
             module => 'reserves',
             letter_code => 'HOLDPLACED',
             branchcode => $branch,
             tables => {
-                'branches'  => $branch_details,
-                'borrowers' => $borrower,
-                'biblio'    => $biblionumber,
-                'items'     => $checkitem,
+                'branches'    => $library,
+                'borrowers'   => $borrower,
+                'biblio'      => $biblionumber,
+                'biblioitems' => $biblionumber,
+                'items'       => $checkitem,
             },
         ) ) {
 
-            my $admin_email_address =$branch_details->{'branchemail'} || C4::Context->preference('KohaAdminEmailAddress');
+            my $admin_email_address = $library->{'branchemail'} || C4::Context->preference('KohaAdminEmailAddress');
 
             C4::Letters::EnqueueLetter(
                 {   letter                 => $letter,
@@ -237,6 +265,7 @@ sub GetReserve {
     my ($reserve_id) = @_;
 
     my $dbh = C4::Context->dbh;
+
     my $query = "SELECT * FROM reserves WHERE reserve_id = ?";
     my $sth = $dbh->prepare( $query );
     $sth->execute( $reserve_id );
@@ -287,7 +316,8 @@ sub GetReservesFromBiblionumber {
                 expirationdate,
                 lowestPriority,
                 suspend,
-                suspend_until
+                suspend_until,
+                itemtype
         FROM     reserves
         WHERE biblionumber = ? ";
     push( @params, $biblionumber );
@@ -495,8 +525,8 @@ sub CanItemBeReserved{
     # level itemtype if the hold has no associated item
     $querycount .=
       C4::Context->preference('item-level_itypes')
-      ? " AND COALESCE( itype, itemtype ) = ?"
-      : " AND itemtype = ?"
+      ? " AND COALESCE( items.itype, biblioitems.itemtype ) = ?"
+      : " AND biblioitems.itemtype = ?"
       if ( $ruleitemtype ne "*" );
 
     my $sthcount = $dbh->prepare($querycount);
@@ -683,7 +713,8 @@ SELECT COUNT(*) FROM reserves WHERE biblionumber=? AND borrowernumber<>?
 
     my $dbh = C4::Context->dbh;
     my ( $fee ) = $dbh->selectrow_array( $borquery, undef, ($borrowernumber) );
-    if( $fee && $fee > 0 ) {
+    my $hold_fee_mode = C4::Context->preference('HoldFeeMode') || 'not_always';
+    if( $fee and $fee > 0 and $hold_fee_mode ne 'always' ) {
         # This is a reconstruction of the old code:
         # Compare number of items with items issued, and optionally check holds
         # If not all items are issued and there are no holds: charge no fee
@@ -922,12 +953,14 @@ sub CheckReserves {
 
                 # See if this item is more important than what we've got so far
                 if ( ( $res->{'priority'} && $res->{'priority'} < $priority ) || $local_hold_match ) {
-                    $borrowerinfo ||= C4::Members::GetMember( borrowernumber => $res->{'borrowernumber'} );
                     $iteminfo ||= C4::Items::GetItem($itemnumber);
+                    next if $res->{itemtype} && $res->{itemtype} ne _get_itype( $iteminfo );
+                    $borrowerinfo ||= C4::Members::GetMember( borrowernumber => $res->{'borrowernumber'} );
                     my $branch = GetReservesControlBranch( $iteminfo, $borrowerinfo );
                     my $branchitemrule = C4::Circulation::GetBranchItemRule($branch,$iteminfo->{'itype'});
                     next if ($branchitemrule->{'holdallowed'} == 0);
                     next if (($branchitemrule->{'holdallowed'} == 1) && ($branch ne $borrowerinfo->{'branchcode'}));
+                    next if ( ($branchitemrule->{hold_fulfillment_policy} ne 'any') && ($res->{branchcode} ne $iteminfo->{ $branchitemrule->{hold_fulfillment_policy} }) );
                     $priority = $res->{'priority'};
                     $highest  = $res;
                     last if $local_hold_match;
@@ -1008,13 +1041,11 @@ Unsuspends all suspended reserves with a suspend_until date from before today.
 =cut
 
 sub AutoUnsuspendReserves {
+    my $today = dt_from_string();
 
-    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();
+    my @holds = Koha::Holds->search( { suspend_until => { '<' => $today->ymd() } } );
 
+    map { $_->suspend(0)->suspend_until(undef)->store() } @holds;
 }
 
 =head2 CancelReserve
@@ -1042,7 +1073,6 @@ sub CancelReserve {
         my $query = "
             UPDATE reserves
             SET    cancellationdate = now(),
-                   found            = Null,
                    priority         = 0
             WHERE  reserve_id = ?
         ";
@@ -1070,7 +1100,7 @@ sub CancelReserve {
         # and, if desired, charge a cancel fee
         my $charge = C4::Context->preference("ExpireReservesMaxPickUpDelayCharge");
         if ( $charge && $params->{'charge_cancel_fee'} ) {
-            manualinvoice($reserve->{'borrowernumber'}, $reserve->{'itemnumber'}, 'Hold waiting too long', 'F', $charge);
+            manualinvoice($reserve->{'borrowernumber'}, $reserve->{'itemnumber'}, '', 'HE', $charge);
         }
     }
 
@@ -1135,19 +1165,26 @@ sub ModReserve {
         CancelReserve({ reserve_id => $reserve_id });
     }
     elsif ($rank =~ /^\d+/ and $rank > 0) {
-        my $query = "
-            UPDATE reserves SET priority = ? ,branchcode = ?, itemnumber = ?, found = NULL, waitingdate = NULL
-            WHERE reserve_id = ?
-        ";
-        my $sth = $dbh->prepare($query);
-        $sth->execute( $rank, $branchcode, $itemnumber, $reserve_id );
+        my $hold = Koha::Holds->find($reserve_id);
+
+        $hold->set(
+            {
+                priority    => $rank,
+                branchcode  => $branchcode,
+                itemnumber  => $itemnumber,
+                found       => undef,
+                waitingdate => undef
+            }
+        )->store();
 
         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 reserve_id = ?", undef, ( $suspend_until, $reserve_id ) );
+                $suspend_until = eval { dt_from_string( $suspend_until ) };
+                $hold->suspend_hold( $suspend_until );
             } else {
-                $dbh->do("UPDATE reserves SET suspend_until = NULL WHERE reserve_id = ?", undef, ( $reserve_id ) );
+                # If the hold is suspended leave the hold suspended, but convert it to an indefinite hold.
+                # If the hold is not suspended, this does nothing.
+                $hold->set( { suspend_until => undef } )->store();
             }
         }
 
@@ -1462,8 +1499,30 @@ sub IsAvailableForItemLevelRequest {
         $item->{withdrawn}        ||
         ($item->{damaged} && !C4::Context->preference('AllowHoldsOnDamagedItems'));
 
+    my $on_shelf_holds = _OnShelfHoldsAllowed($itype,$borrower->{categorycode},$item->{holdingbranch});
+
+    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) {
+            $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;
+        }
 
-    return 1 if _OnShelfHoldsAllowed($itype,$borrower->{categorycode},$item->{holdingbranch});
+        return $any_available ? 0 : 1;
+    }
 
     return $item->{onloan} || GetReserveStatus($item->{itemnumber}) eq "Waiting";
 }
@@ -1588,29 +1647,15 @@ be cleared when it is unsuspended.
 sub ToggleSuspend {
     my ( $reserve_id, $suspend_until ) = @_;
 
-    $suspend_until = output_pref(
-        {
-            dt         => dt_from_string($suspend_until),
-            dateformat => 'iso',
-            dateonly   => 1
-        }
-    ) if ($suspend_until);
+    $suspend_until = dt_from_string($suspend_until) 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 reserve_id = ?
-    ");
-
-    my @params;
-    push( @params, $suspend_until ) if ( $suspend_until );
-    push( @params, $reserve_id );
+    my $hold = Koha::Holds->find( $reserve_id );
 
-    $sth->execute( @params );
+    if ( $hold->is_suspended ) {
+        $hold->resume()
+    } else {
+        $hold->suspend_hold( $suspend_until );
+    }
 }
 
 =head2 SuspendAll
@@ -1635,37 +1680,26 @@ sub SuspendAll {
     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;
+    my $suspend = defined( $params{'suspend'} ) ? $params{'suspend'} : 1;
 
-    $suspend_until = C4::Dates->new( $suspend_until )->output("iso") if ( defined( $suspend_until ) );
+    $suspend_until = eval { dt_from_string($suspend_until) }
+      if ( defined($suspend_until) );
 
     return unless ( $borrowernumber || $biblionumber );
 
-    my ( $query, $sth, $dbh, @query_params );
+    my $params;
+    $params->{found}          = undef;
+    $params->{borrowernumber} = $borrowernumber if $borrowernumber;
+    $params->{biblionumber}   = $biblionumber if $biblionumber;
 
-    $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 );
+    my @holds = Koha::Holds->search($params);
+
+    if ($suspend) {
+        map { $_->suspend_hold($suspend_until) } @holds;
     }
-    $query .= " AND " if ( $borrowernumber && $biblionumber );
-    if ( $biblionumber ) {
-        $query .= " biblionumber = ? ";
-        push( @query_params, $biblionumber );
+    else {
+        map { $_->resume() } @holds;
     }
-    $query .= " AND found IS NULL ";
-
-    $dbh = C4::Context->dbh;
-    $sth = $dbh->prepare( $query );
-    $sth->execute( @query_params );
 }
 
 
@@ -1828,7 +1862,8 @@ sub _Findgroupreserve {
                reserves.timestamp           AS timestamp,
                biblioitems.biblioitemnumber AS biblioitemnumber,
                reserves.itemnumber          AS itemnumber,
-               reserves.reserve_id          AS reserve_id
+               reserves.reserve_id          AS reserve_id,
+               reserves.itemtype            AS itemtype
         FROM reserves
         JOIN biblioitems USING (biblionumber)
         JOIN hold_fill_targets USING (biblionumber, borrowernumber, itemnumber)
@@ -1862,7 +1897,8 @@ sub _Findgroupreserve {
                reserves.timestamp           AS timestamp,
                biblioitems.biblioitemnumber AS biblioitemnumber,
                reserves.itemnumber          AS itemnumber,
-               reserves.reserve_id          AS reserve_id
+               reserves.reserve_id          AS reserve_id,
+               reserves.itemtype            AS itemtype
         FROM reserves
         JOIN biblioitems USING (biblionumber)
         JOIN hold_fill_targets USING (biblionumber, borrowernumber)
@@ -1895,7 +1931,8 @@ sub _Findgroupreserve {
                reserves.priority                   AS priority,
                reserves.timestamp                  AS timestamp,
                reserves.itemnumber                 AS itemnumber,
-               reserves.reserve_id                 AS reserve_id
+               reserves.reserve_id                 AS reserve_id,
+               reserves.itemtype                   AS itemtype
         FROM reserves
         WHERE reserves.biblionumber = ?
           AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?)
@@ -1920,6 +1957,25 @@ sub _Findgroupreserve {
 Sends a notification to the patron that their hold has been filled (through
 ModReserveAffect, _not_ ModReserveFill)
 
+The letter code for this notice may be found using the following query:
+
+    select distinct letter_code
+    from message_transports
+    inner join message_attributes using (message_attribute_id)
+    where message_name = 'Hold_Filled'
+
+This will probably sipmly be 'HOLD', but because it is defined in the database,
+it is subject to addition or change.
+
+The following tables are availalbe witin the notice:
+
+    branches
+    borrowers
+    biblio
+    biblioitems
+    reserves
+    items
+
 =cut
 
 sub _koha_notify_reserve {
@@ -1944,21 +2000,22 @@ sub _koha_notify_reserve {
     ");
     $sth->execute( $borrowernumber, $biblionumber );
     my $reserve = $sth->fetchrow_hashref;
-    my $branch_details = GetBranchDetail( $reserve->{'branchcode'} );
+    my $library = Koha::Libraries->find( $reserve->{branchcode} )->unblessed;
 
-    my $admin_email_address = $branch_details->{'branchemail'} || C4::Context->preference('KohaAdminEmailAddress');
+    my $admin_email_address = $library->{branchemail} || C4::Context->preference('KohaAdminEmailAddress');
 
     my %letter_params = (
         module => 'reserves',
         branchcode => $reserve->{branchcode},
         tables => {
-            'branches'  => $branch_details,
-            'borrowers' => $borrower,
-            'biblio'    => $biblionumber,
-            'reserves'  => $reserve,
+            'branches'       => $library,
+            'borrowers'      => $borrower,
+            'biblio'         => $biblionumber,
+            'biblioitems'    => $biblionumber,
+            'reserves'       => $reserve,
             'items', $reserve->{'itemnumber'},
         },
-        substitute => { today => C4::Dates->new()->output() },
+        substitute => { today => output_pref( { dt => dt_from_string, dateonly => 1 } ) },
     );
 
     my $notification_sent = 0; #Keeping track if a Hold_filled message is sent. If no message can be sent, then default to a print message.
@@ -2140,11 +2197,7 @@ sub MoveReserve {
             RevertWaitingStatus({ itemnumber => $itemnumber });
         }
         elsif ( $cancelreserve eq 'cancel' || $cancelreserve ) { # cancel reserves on this item
-            CancelReserve({
-                biblionumber   => $res->{'biblionumber'},
-                itemnumber     => $res->{'itemnumber'},
-                borrowernumber => $res->{'borrowernumber'}
-            });
+            CancelReserve( { reserve_id => $res->{'reserve_id'} } );
         }
     }
 }
@@ -2293,7 +2346,17 @@ sub GetReserveId {
 
   ReserveSlip($branchcode, $borrowernumber, $biblionumber)
 
-  Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
+Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
+
+The letter code will be HOLD_SLIP, and the following tables are
+available within the slip:
+
+    reserves
+    branches
+    borrowers
+    biblio
+    biblioitems
+    items
 
 =cut
 
@@ -2310,13 +2373,14 @@ sub ReserveSlip {
 
     return  C4::Letters::GetPreparedLetter (
         module => 'circulation',
-        letter_code => 'RESERVESLIP',
+        letter_code => 'HOLD_SLIP',
         branchcode => $branch,
         tables => {
             'reserves'    => $reserve,
             'branches'    => $reserve->{branchcode},
             'borrowers'   => $reserve->{borrowernumber},
             'biblio'      => $reserve->{biblionumber},
+            'biblioitems' => $reserve->{biblionumber},
             'items'       => $reserve->{itemnumber},
         },
     );