Bug 14695 - Add ability to place multiple item holds on a given record per patron
authorKyle M Hall <kyle@bywatersolutions.com>
Tue, 29 Dec 2015 19:14:19 +0000 (19:14 +0000)
committerKyle M Hall <kyle@bywatersolutions.com>
Sat, 3 Sep 2016 00:17:56 +0000 (00:17 +0000)
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>

Signed-off-by: Jason M. Burds <JBurds@dubuque.lib.ia.us>
Signed-off-by: Benjamin Rokseth <benjamin.rokseth@kul.oslo.kommune.no>

C4/Reserves.pm
Koha/Holds.pm
koha-tmpl/intranet-tmpl/prog/en/modules/reserve/request.tt
opac/opac-reserve.pl
reserve/placerequest.pl
reserve/request.pl

index efb7f04..e7066d2 100644 (file)
@@ -44,6 +44,7 @@ use Koha::Holds;
 use Koha::Libraries;
 use Koha::Items;
 use Koha::ItemTypes;
+use Koha::Patrons;
 
 use List::MoreUtils qw( firstidx any );
 use Carp;
@@ -143,6 +144,8 @@ BEGIN {
         &GetReservesControlBranch
 
         IsItemOnHoldAndFound
+
+        GetMaxPatronHoldsForRecord
     );
     @EXPORT_OK = qw( MergeHolds );
 }
@@ -170,12 +173,7 @@ sub AddReserve {
         $title,    $checkitem,      $found,        $itemtype
     ) = @_;
 
-    if ( Koha::Holds->search( { borrowernumber => $borrowernumber, biblionumber => $biblionumber } )->count() > 0 ) {
-        carp("AddReserve: borrower $borrowernumber already has a hold for biblionumber $biblionumber");
-        return;
-    }
-
-    my $dbh     = C4::Context->dbh;
+    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' });
@@ -464,7 +462,8 @@ sub CanItemBeReserved {
 
     my $dbh = C4::Context->dbh;
     my $ruleitemtype;    # itemtype of the matching issuing rule
-    my $allowedreserves = 0;
+    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
 
     # we retrieve borrowers and items informations #
     # item->{itype} will come for biblioitems if necessery
@@ -477,26 +476,16 @@ sub CanItemBeReserved {
       if ( $item->{damaged}
         && !C4::Context->preference('AllowHoldsOnDamagedItems') );
 
-    #Check for the age restriction
+    # Check for the age restriction
     my ( $ageRestriction, $daysToAgeRestriction ) =
       C4::Circulation::GetAgeRestriction( $biblioData->{agerestriction}, $borrower );
     return 'ageRestricted' if $daysToAgeRestriction && $daysToAgeRestriction > 0;
 
-    my $controlbranch = C4::Context->preference('ReservesControlBranch');
+    # Check that the patron doesn't have an item level hold on this item already
+    return 'itemAlreadyOnHold'
+      if Koha::Holds->search( { borrowernumber => $borrowernumber, itemnumber => $itemnumber } )->count();
 
-    # we retrieve user rights on this itemtype and branchcode
-    my $sth = $dbh->prepare(
-        q{
-         SELECT categorycode, itemtype, branchcode, reservesallowed
-           FROM issuingrules
-          WHERE (categorycode in (?,'*') )
-            AND (itemtype IN (?,'*'))
-            AND (branchcode IN (?,'*'))
-       ORDER BY categorycode DESC,
-                itemtype     DESC,
-                branchcode   DESC
-        }
-    );
+    my $controlbranch = C4::Context->preference('ReservesControlBranch');
 
     my $querycount = q{
         SELECT count(*) AS count
@@ -520,15 +509,27 @@ sub CanItemBeReserved {
     }
 
     # we retrieve rights
-    $sth->execute( $borrower->{'categorycode'}, $item->{'itype'}, $branchcode );
-    if ( my $rights = $sth->fetchrow_hashref() ) {
-        $ruleitemtype    = $rights->{itemtype};
-        $allowedreserves = $rights->{reservesallowed};
+    if ( my $rights = GetHoldRule( $borrower->{'categorycode'}, $item->{'itype'}, $branchcode ) ) {
+        $ruleitemtype     = $rights->{itemtype};
+        $allowedreserves  = $rights->{reservesallowed};
+        $holds_per_record = $rights->{holds_per_record};
     }
     else {
         $ruleitemtype = '*';
     }
 
+    my $item = Koha::Items->find( $itemnumber );
+    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 "tooManyHoldsForThisRecord";
+    }
+
     # we retrieve count
 
     $querycount .= "AND $branchfield = ?";
@@ -758,8 +759,8 @@ sub GetReservesToBranch {
     my $dbh = C4::Context->dbh;
     my $sth = $dbh->prepare(
         "SELECT reserve_id,borrowernumber,reservedate,itemnumber,timestamp
-         FROM reserves 
-         WHERE priority='0' 
+         FROM reserves
+         WHERE priority='0'
            AND branchcode=?"
     );
     $sth->execute( $frombranch );
@@ -784,7 +785,7 @@ sub GetReservesForBranch {
 
     my $query = "
         SELECT reserve_id,borrowernumber,reservedate,itemnumber,waitingdate
-        FROM   reserves 
+        FROM   reserves
         WHERE   priority='0'
         AND found='W'
     ";
@@ -1414,7 +1415,7 @@ sub ModReserveMinusPriority {
     my $dbh   = C4::Context->dbh;
     my $query = "
         UPDATE reserves
-        SET    priority = 0 , itemnumber = ? 
+        SET    priority = 0 , itemnumber = ?
         WHERE  reserve_id = ?
     ";
     my $sth_upd = $dbh->prepare($query);
@@ -1651,7 +1652,7 @@ sub ToggleLowestPriority {
 
     my $sth = $dbh->prepare( "UPDATE reserves SET lowestPriority = NOT lowestPriority WHERE reserve_id = ?");
     $sth->execute( $reserve_id );
-    
+
     _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
 }
 
@@ -1835,7 +1836,7 @@ sub _FixPriority {
             $priority[$j]->{'reserve_id'}
         );
     }
-    
+
     $sth = $dbh->prepare( "SELECT reserve_id FROM reserves WHERE lowestPriority = 1 ORDER BY priority" );
     $sth->execute();
 
@@ -2073,7 +2074,7 @@ sub _koha_notify_reserve {
     if (! $notification_sent) {
         &$send_notification('print', 'HOLD');
     }
-    
+
 }
 
 =head2 _ShiftPriorityByDateAndPriority
@@ -2502,6 +2503,76 @@ sub IsItemOnHoldAndFound {
     return $found;
 }
 
+=head2 GetMaxPatronHoldsForRecord
+
+my $holds_per_record = ReservesControlBranch( $borrowernumber, $biblionumber );
+
+For multiple holds on a given record for a given patron, the max
+number of record level holds that a patron can be placed is the highest
+value of the holds_per_record rule for each item if the record for that
+patron. This subroutine finds and returns the highest holds_per_record
+rule value for a given patron id and record id.
+
+=cut
+
+sub GetMaxPatronHoldsForRecord {
+    my ( $borrowernumber, $biblionumber ) = @_;
+
+    my $patron = Koha::Patrons->find($borrowernumber);
+    my @items = Koha::Items->search( { biblionumber => $biblionumber } );
+
+    my $controlbranch = C4::Context->preference('ReservesControlBranch');
+
+    my $categorycode = $patron->categorycode;
+    my $branchcode;
+    $branchcode = $patron->branchcode if ( $controlbranch eq "PatronLibrary" );
+
+    my $max = 0;
+    foreach my $item (@items) {
+        my $itemtype = $item->effective_itemtype();
+
+        $branchcode = $item->homebranch if ( $controlbranch eq "ItemHomeLibrary" );
+
+        my $rule = GetHoldRule( $categorycode, $itemtype, $branchcode );
+        my $holds_per_record = $rule ? $rule->{holds_per_record} : 0;
+        $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
+           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/>
index d1d8b29..b5c24aa 100644 (file)
@@ -49,6 +49,38 @@ sub waiting {
     return $self->search( { found => 'W' } );
 }
 
+=head3 forced_hold_level
+
+If a patron has multiple holds for a single record,
+those holds must be either all record level holds,
+or they must all be item level holds.
+
+This method should be used with Hold sets where all
+Hold objects share the same patron and record.
+
+This method will return 'item' if the patron has
+at least one item level hold. It will return 'record'
+if the patron has holds but none are item level,
+Finally, if the patron has no holds, it will return
+undef which indicateds the patron may select either
+record or item level holds, barring any other rules
+that would prevent one or the other.
+=cut
+
+sub forced_hold_level {
+    my ($self) = @_;
+
+    my $force_hold_level;
+
+    if ( $self->count() ) {
+        my $has_item_level_holds;
+        map { $has_item_level_holds ||= $_->itemnumber } $self->as_list();
+        $force_hold_level = $has_item_level_holds ? 'item' : 'record';
+    }
+
+    return $force_hold_level;
+}
+
 =head3 type
 
 =cut
index ba53b3a..e4e5467 100644 (file)
@@ -49,49 +49,39 @@ $(document).ready(function() {
 });
 
 function check() {
-       var msg = "";
-       var count_reserv = 0;
-       var alreadyreserved = 0;
+    var msg = "";
+    var count_reserv = 0;
 
     // check if we have checkitem form
     if (document.form.checkitem){
         for (i=0;i<document.form.checkitem.length;i++){
             if (document.form.checkitem[i].checked == true) {
-                               count_reserv++ ;
-                       }
+                count_reserv++ ;
+            }
         }
         // for only one item, check the checkitem without consider the loop checkitem
         if (i==0){
-                   if (document.form.checkitem.checked == true) {
-                           count_reserv++;
-                   }
-           }
-    }
-
-    if (document.form.request.checked == true){
-               count_reserv++ ;
+            if (document.form.checkitem.checked == true) {
+                count_reserv++;
+            }
+        }
     }
 
-    if (document.form.alreadyreserved && document.form.alreadyreserved.value == "1"){
-                alreadyreserved++ ;
+    if (document.form.requestany.checked == true){
+        count_reserv++ ;
     }
 
     if (count_reserv == "0"){
-               msg += (_("- Please select an item to place a hold") + "\n");
-    }
-    if (count_reserv >= "2"){
-               msg += (_("- You may only place a hold on one item at a time") + "\n");
+        msg += (_("- Please select an item to place a hold") + "\n");
     }
 
-    if (alreadyreserved > "0"){
-               msg += (_("- This patron had already placed a hold on this item") + "\n" + _("Please cancel the previous hold first") + "\n");
+    if (msg == "") {
+        $('#hold-request-form').preventDoubleFormSubmit();
+        return(true);
+    } else {
+        alert(msg);
+        return(false);
     }
-
-       if (msg == "") return(true);
-       else    {
-               alert(msg);
-               return(false);
-       }
 }
 
 function checkMultiHold() {
@@ -117,6 +107,8 @@ function checkMultiHold() {
     $("#multi_hold_bibs").val(biblionumbers);
     $("#bad_bibs").val(badBibs);
 
+    $('#hold-request-form').preventDoubleFormSubmit();
+
     return true;
 }
 
@@ -164,7 +156,6 @@ function checkMultiHold() {
         $("#" + fieldID).val("");
     });
 
-    $('#hold-request-form').preventDoubleFormSubmit();
 
 [% UNLESS ( borrowernumber || borrowers || noitems ) %]
     [% IF ( CircAutocompl ) %]
@@ -271,7 +262,7 @@ function checkMultiHold() {
         [% IF ( exceeded_maxreserves ) %]
           <li><strong>Too many holds: </strong> <a href="/cgi-bin/koha/members/moremember.pl?borrowernumber=[% borrowernumber %]">[% borrowerfirstname %] [% borrowersurname %] </a> can only place a maximum of [% maxreserves %] total holds.</li>
         [% ELSIF ( alreadypossession ) %]
-          <li> <a href="/cgi-bin/koha/members/moremember.pl?borrowernumber=[% borrowernumber %]">[% borrowerfirstname %] [% borrowersurname %]</a> <strong>is already in possession</strong> of one item</li>
+          <li> <a href="/cgi-bin/koha/members/moremember.pl?borrowernumber=[% borrowernumber %]">[% borrowerfirstname %] [% borrowersurname %]</a> <strong>is already in possession</strong> of one item</lie
         [% ELSIF ( alreadyreserved ) %]
           <li><a href="/cgi-bin/koha/members/moremember.pl?borrowernumber=[% borrowernumber %]">[% borrowerfirstname %] [% borrowersurname %]</a> <strong>already has a hold</strong> on this item </li>
         [% ELSIF ( ageRestricted ) %]
@@ -402,11 +393,34 @@ function checkMultiHold() {
        </li>
 
         [% UNLESS ( multi_hold ) %]
-          <li> <label for="requestany">Place a hold on the next available item </label>
-               <input type="checkbox" id="requestany" name="request" checked="checked" value="Any" />
+          <li> <label for="requestany">Hold next available item </label>
+               [% IF force_hold_level == 'item' %]
+                   <input type="checkbox" id="requestany" name="request" disabled="true" />
+               [% ELSIF force_hold_level == 'record' %]
+                   <input type="checkbox" id="requestany" checked="checked" value="Any" disabled="true"/>
+                   <input type="hidden" name="request" value="Any"/>
+               [% ELSE %]
+                   <input type="checkbox" id="requestany" name="request" checked="checked" value="Any" />
+                [% END %]
                <input type="hidden" name="biblioitem" value="[% biblioitemnumber %]" />
                <input type="hidden" name="alreadyreserved" value="[% alreadyreserved %]" />
           </li>
+
+          [% IF max_holds_for_record > 1 %]
+              [% SET count = 1 %]
+              <li>
+                   <label for="holds_to_place_count">Holds to place (count)</label>
+                   <select name="holds_to_place_count" id="holds_to_place_count">
+                   [% WHILE count <= max_holds_for_record %]
+                        <option value="[% count %]">[% count %]</option>
+                        [% SET count = count + 1 %]
+                   [% END %]
+
+                   </select>
+              </li>
+            [% ELSE %]
+                <input type="hidden" name="holds_to_place_count" value="1";
+            [% END %]
         [% END %]
 
 </ol>
@@ -431,7 +445,12 @@ function checkMultiHold() {
             [% IF ( bibitemloo.publicationyear ) %]<li><span class="label">Publication year:</span> [% bibitemloo.publicationyear %]</li>[% END %]
           </ol>
 
-        <h2 style="padding: 0 1em;">Place a hold on a specific item</h2>
+        <h2 style="padding: 0 1em;">
+            Place a hold on a specific item
+            [% IF bibitemloo.force_hold_level == 'item' %]
+                <span class="error"><i>(Required)</i></span>
+            [% END %]
+        </h2>
         <table id="requestspecific">
             <thead>
                 <tr>
@@ -451,18 +470,44 @@ function checkMultiHold() {
                 </tr>
             </thead>
             <tbody>
+            [% SET selected = 0 %]
             [% FOREACH itemloo IN bibitemloo.itemloop %]
             [% UNLESS ( itemloo.hide ) %]
                 <tr class="[% itemloo.backgroundcolor %]">
                     <td>
-                [% IF ( itemloo.available ) %]
+                [% IF itemloo.force_hold_level == 'record' # Patron has placed a record level hold previously for this record %]
+                    <span class="error">
+                        <i class="fa fa-times fa-lg" alt="Cannot be put on hold"></i>
+                        Hold must be record level
+                    </span>
+                [% ELSIF ( itemloo.available ) %]
                     <input type="radio" name="checkitem" value="[% itemloo.itemnumber %]" />
                 [% ELSIF ( itemloo.override ) %]
                     <input type="radio" name="checkitem" class="needsoverride" value="[% itemloo.itemnumber %]" />
-                    <img src="[% interface %]/[% theme %]/img/famfamfam/silk/error.png" alt="Requires override of hold policy" />
+                    <i class="fa fa-exclamation-triangle fa-lg" style="color:gold" alt="Requires override of hold policy"/></i>
                 [% ELSE %]
-                    <input disabled="disabled" type="radio" name="checkitem" value="[% itemloo.itemnumber %]" />
-                    <img src="[% interface %]/[% theme %]/img/famfamfam/silk/cross.png" alt="Cannot be put on hold" />
+                    <span class="error">
+                        <i class="fa fa-times fa-lg" alt="Cannot be put on hold"></i>
+                        [% IF itemloo.not_holdable %]
+                            [% IF itemloo.not_holdable == 'damaged' %]
+                                Item damaged
+                            [% ELSIF itemloo.not_holdable == 'ageRestricted' %]
+                                Age restricted
+                            [% ELSIF itemloo.not_holdable == 'tooManyHoldsForThisRecord' %]
+                                Exceeded max holds per record
+                            [% ELSIF itemloo.not_holdable == 'tooManyReserves' %]
+                                Too many holds
+                            [% ELSIF itemloo.not_holdable == 'notReservable' %]
+                                Not holdable
+                            [% ELSIF itemloo.not_holdable == 'cannotReserveFromOtherBranches' %]
+                                Patron is from different library
+                            [% ELSIF itemloo.not_holdable == 'itemAlreadyOnHold' %]
+                                Patron already has hold for this item
+                            [% ELSE %]
+                                [% itemloo.not_holdable %]
+                            [% END %]
+                        [% END %]
+                    </span>
                 [% END %]
                     </td>
                 [% IF ( item_level_itypes ) %]
index 58dac17..1fd2f82 100755 (executable)
@@ -359,17 +359,6 @@ unless ( $noreserves ) {
     }
 }
 
-foreach my $res (@reserves) {
-    foreach my $biblionumber (@biblionumbers) {
-        if ( $res->{'biblionumber'} == $biblionumber && $res->{'borrowernumber'} == $borrowernumber) {
-#            $template->param( message => 1 );
-#            $noreserves = 1;
-#            $template->param( already_reserved => 1 );
-            $biblioDataHash{$biblionumber}->{already_reserved} = 1;
-        }
-    }
-}
-
 unless ($noreserves) {
     $template->param( select_item_types => 1 );
 }
@@ -468,9 +457,6 @@ foreach my $biblioNum (@biblionumbers) {
 
         # the item could be reserved for this borrower vi a host record, flag this
         $reservedfor //= '';
-        if ($reservedfor eq $borrowernumber){
-            $itemLoopIter->{already_reserved} = 1;
-        }
 
         if ( defined $reservedate ) {
             $itemLoopIter->{backgroundcolor} = 'reserved';
@@ -515,12 +501,12 @@ foreach my $biblioNum (@biblionumbers) {
             $itemLoopIter->{nocancel} = 1;
         }
 
-       # if the items belongs to a host record, show link to host record
-       if ($itemInfo->{biblionumber} ne $biblioNum){
-               $biblioLoopIter{hostitemsflag} = 1;
-               $itemLoopIter->{hostbiblionumber} = $itemInfo->{biblionumber};
-               $itemLoopIter->{hosttitle} = GetBiblioData($itemInfo->{biblionumber})->{title};
-       }
+        # if the items belongs to a host record, show link to host record
+        if ( $itemInfo->{biblionumber} ne $biblioNum ) {
+            $biblioLoopIter{hostitemsflag}    = 1;
+            $itemLoopIter->{hostbiblionumber} = $itemInfo->{biblionumber};
+            $itemLoopIter->{hosttitle}        = GetBiblioData( $itemInfo->{biblionumber} )->{title};
+        }
 
         # If there is no loan, return and transfer, we show a checkbox.
         $itemLoopIter->{notforloan} = $itemLoopIter->{notforloan} || 0;
@@ -569,6 +555,24 @@ foreach my $biblioNum (@biblionumbers) {
 
     $biblioLoopIter{holdable} &&= CanBookBeReserved($borrowernumber,$biblioNum) eq 'OK';
 
+    # For multiple holds per record, if a patron has previously placed a hold,
+    # the patron can only place more holds of the same type. That is, if the
+    # patron placed a record level hold, all the holds the patron places must
+    # be record level. If the patron placed an item level hold, all holds
+    # the patron places must be item level
+    my $forced_hold_level = Koha::Holds->search(
+        {
+            borrowernumber => $borrowernumber,
+            biblionumber   => $biblioNum,
+            found          => undef,
+        }
+    )->forced_hold_level();
+    if ($forced_hold_level) {
+        $biblioLoopIter{force_hold}   = 1 if $forced_hold_level eq 'item';
+        $biblioLoopIter{itemholdable} = 0 if $forced_hold_level eq 'record';
+    }
+
+
     push @$biblioLoop, \%biblioLoopIter;
 
     $anyholdable = 1 if $biblioLoopIter{holdable};
index f48103b..c896c9d 100755 (executable)
@@ -56,6 +56,7 @@ my $borrower = GetMember( 'borrowernumber' => $borrowernumber );
 my $multi_hold = $input->param('multi_hold');
 my $biblionumbers = $multi_hold ? $input->param('biblionumbers') : ($biblionumber . '/');
 my $bad_bibs = $input->param('bad_bibs');
+my $holds_to_place_count = $input->param('holds_to_place_count') || 1;
 
 my %bibinfos = ();
 my @biblionumbers = split '/', $biblionumbers;
@@ -79,30 +80,28 @@ if (defined $checkitem && $checkitem ne ''){
     }
 }
 
-if ($type eq 'str8' && $borrower){
+if ( $type eq 'str8' && $borrower ) {
 
-    foreach my $biblionumber (keys %bibinfos) {
-        my $count=@bibitems;
-        @bibitems=sort @bibitems;
-        my $i2=1;
+    foreach my $biblionumber ( keys %bibinfos ) {
+        my $count = @bibitems;
+        @bibitems = sort @bibitems;
+        my $i2 = 1;
         my @realbi;
-        $realbi[0]=$bibitems[0];
-        for (my $i=1;$i<$count;$i++) {
-            my $i3=$i2-1;
-            if ($realbi[$i3] ne $bibitems[$i]) {
-                $realbi[$i2]=$bibitems[$i];
+        $realbi[0] = $bibitems[0];
+        for ( my $i = 1 ; $i < $count ; $i++ ) {
+            my $i3 = $i2 - 1;
+            if ( $realbi[$i3] ne $bibitems[$i] ) {
+                $realbi[$i2] = $bibitems[$i];
                 $i2++;
             }
         }
 
-    if (defined $checkitem && $checkitem ne ''){
-               my $item = GetItem($checkitem);
-               if ($item->{'biblionumber'} ne $biblionumber) {
-                       $biblionumber = $item->{'biblionumber'};
-               }
-       }
-
-
+        if ( defined $checkitem && $checkitem ne '' ) {
+            my $item = GetItem($checkitem);
+            if ( $item->{'biblionumber'} ne $biblionumber ) {
+                $biblionumber = $item->{'biblionumber'};
+            }
+        }
 
         if ($multi_hold) {
             my $bibinfo = $bibinfos{$biblionumber};
@@ -110,7 +109,11 @@ if ($type eq 'str8' && $borrower){
                        $bibinfo->{rank},$startdate,$expirationdate,$notes,$bibinfo->{title},$checkitem,$found);
         } else {
             # place a request on 1st available
-            AddReserve($branch,$borrower->{'borrowernumber'},$biblionumber,\@realbi,$rank[0],$startdate,$expirationdate,$notes,$title,$checkitem,$found, $itemtype);
+            for ( my $i = 0 ; $i < $holds_to_place_count ; $i++ ) {
+                AddReserve( $branch, $borrower->{'borrowernumber'},
+                    $biblionumber, \@realbi, $rank[0], $startdate, $expirationdate, $notes, $title,
+                    $checkitem, $found, $itemtype );
+            }
         }
     }
 
@@ -119,13 +122,16 @@ if ($type eq 'str8' && $borrower){
             $biblionumbers .= $bad_bibs;
         }
         print $input->redirect("request.pl?biblionumbers=$biblionumbers&multi_hold=1");
-    } else {
+    }
+    else {
         print $input->redirect("request.pl?biblionumber=$biblionumber");
     }
-} elsif ($borrower eq ''){
-       print $input->header();
-       print "Invalid borrower number please try again";
-# Not sure that Dump() does HTML escaping. Use firebug or something to trace
-# instead.
-#      print $input->Dump;
+}
+elsif ( $borrower eq '' ) {
+    print $input->header();
+    print "Invalid borrower number please try again";
+
+    # Not sure that Dump() does HTML escaping. Use firebug or something to trace
+    # instead.
+    #  print $input->Dump;
 }
index 1980abb..b44493f 100755 (executable)
@@ -26,8 +26,8 @@ script to place reserves/requests
 
 =cut
 
-use strict;
-use warnings;
+use Modern::Perl;
+
 use C4::Branch;
 use CGI qw ( -utf8 );
 use List::MoreUtils qw/uniq/;
@@ -235,38 +235,44 @@ foreach my $biblionumber (@biblionumbers) {
         $biblioloopiter{$canReserve} = 1;
     }
 
-    my $alreadypossession;
-    if (not C4::Context->preference('AllowHoldsOnPatronsPossessions') and CheckIfIssuedToPatron($borrowerinfo->{borrowernumber},$biblionumber)) {
-        $alreadypossession = 1;
+    my $force_hold_level;
+    if ( $borrowerinfo->{borrowernumber} ) {
+        # For multiple holds per record, if a patron has previously placed a hold,
+        # the patron can only place more holds of the same type. That is, if the
+        # patron placed a record level hold, all the holds the patron places must
+        # be record level. If the patron placed an item level hold, all holds
+        # the patron places must be item level
+        my $holds = Koha::Holds->search(
+            {
+                borrowernumber => $borrowerinfo->{borrowernumber},
+                biblionumber   => $biblionumber,
+                found          => undef,
+            }
+        );
+        $force_hold_level = $holds->forced_hold_level();
+        $biblioloopiter{force_hold_level} = $force_hold_level;
+        $template->param( force_hold_level => $force_hold_level );
+
+        # For a librarian to be able to place multiple record holds for a patron for a record,
+        # we must find out what the maximum number of holds they can place for the patron is
+        my $max_holds_for_record = GetMaxPatronHoldsForRecord( $borrowerinfo->{borrowernumber}, $biblionumber );
+        $max_holds_for_record = $max_holds_for_record - $holds->count();
+        $biblioloopiter{max_holds_for_record} = $max_holds_for_record;
+        $template->param( max_holds_for_record => $max_holds_for_record );
     }
 
-    # get existing reserves .....
-    my $reserves = GetReservesFromBiblionumber({ biblionumber => $biblionumber, all_dates => 1 });
-    my $count = scalar( @$reserves );
-    my $totalcount = $count;
-    my $holds_count = 0;
-    my $alreadyreserved = 0;
-
-    foreach my $res (@$reserves) {
-        if ( defined $res->{found} ) { # found can be 'W' or 'T'
-            $count--;
-        }
-
-        if ( defined $borrowerinfo && defined($borrowerinfo->{borrowernumber}) && ($borrowerinfo->{borrowernumber} eq $res->{borrowernumber}) ) {
-            $holds_count++;
-        }
+    # Check to see if patron is allowed to place holds on records where the
+    # patron already has an item from that record checked out
+    my $alreadypossession;
+    if ( !C4::Context->preference('AllowHoldsOnPatronsPossessions')
+        && CheckIfIssuedToPatron( $borrowerinfo->{borrowernumber}, $biblionumber ) )
+    {
+        $template->param( alreadypossession => $alreadypossession, );
     }
 
-    if ( $holds_count ) {
-            $alreadyreserved = 1;
-            $biblioloopiter{warn} = 1;
-            $biblioloopiter{alreadyres} = 1;
-    }
 
-    $template->param(
-        alreadyreserved => $alreadyreserved,
-        alreadypossession => $alreadypossession,
-    );
+    my $count = Koha::Holds->search( { biblionumber => $biblionumber } )->count();
+    my $totalcount = $count;
 
     # FIXME think @optionloop, is maybe obsolete, or  must be switchable by a systeme preference fixed rank or not
     # make priorities options
@@ -329,6 +335,8 @@ foreach my $biblionumber (@biblionumbers) {
         my $num_override  = 0;
         my $hiddencount   = 0;
 
+        $biblioitem->{force_hold_level} = $force_hold_level;
+
         if ( $biblioitem->{biblioitemnumber} ne $biblionumber ) {
             $biblioitem->{hostitemsflag} = 1;
         }
@@ -348,6 +356,8 @@ foreach my $biblionumber (@biblionumbers) {
         foreach my $itemnumber ( @{ $itemnumbers_of_biblioitem{$biblioitemnumber} } )    {
             my $item = $iteminfos_of->{$itemnumber};
 
+            $item->{force_hold_level} = $force_hold_level;
+
             unless (C4::Context->preference('item-level_itypes')) {
                 $item->{itype} = $biblioitem->{itemtype};
             }
@@ -445,13 +455,14 @@ foreach my $biblionumber (@biblionumbers) {
 
             $item->{'holdallowed'} = $branchitemrule->{'holdallowed'};
 
+            my $can_item_be_reserved = CanItemBeReserved( $borrowerinfo->{borrowernumber}, $itemnumber );
+            $item->{not_holdable} = $can_item_be_reserved unless ( $can_item_be_reserved eq 'OK' );
+
             if (
                    !$item->{cantreserve}
                 && !$exceeded_maxreserves
                 && IsAvailableForItemLevelRequest($item, $borrowerinfo)
-                && CanItemBeReserved(
-                    $borrowerinfo->{borrowernumber}, $itemnumber
-                ) eq 'OK'
+                && $can_item_be_reserved eq 'OK'
               )
             {
                 $item->{available} = 1;