Bug 32612: (QA follow-up) Add BINMODE method to C4::SIP::Trapper
[koha-ffzg.git] / C4 / Search.pm
index e26d0a6..e6ad8b6 100644 (file)
@@ -21,6 +21,7 @@ use C4::Biblio qw( TransformMarcToKoha GetMarcFromKohaField GetFrameworkCode Get
 use C4::Koha qw( getFacets GetVariationsOfISBN GetNormalizedUPC GetNormalizedEAN GetNormalizedOCLCNumber GetNormalizedISBN getitemtypeimagelocation );
 use Koha::DateUtils;
 use Koha::Libraries;
+use Koha::SearchEngine::QueryBuilder;
 use Lingua::Stem;
 use XML::Simple;
 use C4::XSLT qw( XSLTParse4Display );
@@ -31,7 +32,9 @@ use Koha::ItemTypes;
 use Koha::Libraries;
 use Koha::Logger;
 use Koha::Patrons;
+use Koha::Recalls;
 use Koha::RecordProcessor;
+use Koha::SearchFilters;
 use URI::Escape;
 use Business::ISBN;
 use MARC::Record;
@@ -84,7 +87,7 @@ This function attempts to find duplicate records using a hard-coded, fairly simp
 sub FindDuplicate {
     my ($record) = @_;
     my $dbh = C4::Context->dbh;
-    my $result = TransformMarcToKoha( $record, '' );
+    my $result = TransformMarcToKoha({ record => $record });
     my $sth;
     my $query;
 
@@ -99,7 +102,7 @@ sub FindDuplicate {
 
         my $titleindex = 'ti,ext';
         my $authorindex = 'au,ext';
-        my $op = 'and';
+        my $op = 'AND';
 
         $result->{title} =~ s /\\//g;
         $result->{title} =~ s /\"//g;
@@ -127,7 +130,7 @@ sub FindDuplicate {
                 $possible_duplicate_record
             );
 
-            my $result = TransformMarcToKoha( $marcrecord, '' );
+            my $result = TransformMarcToKoha({ record => $marcrecord });
 
             # FIXME :: why 2 $biblionumber ?
             if ($result) {
@@ -184,7 +187,7 @@ my @results;
 
 for my $r ( @{$marcresults} ) {
     my $marcrecord = MARC::File::USMARC::decode($r);
-    my $biblio = TransformMarcToKoha($marcrecord,q{});
+    my $biblio = TransformMarcToKoha({ record => $marcrecord });
 
     #build the iarray of hashs for the template.
     push @results, {
@@ -307,7 +310,7 @@ sub getRecords {
     my $results_hashref = ();
 
     # TODO simplify this structure ( { branchcode => $branchname } is enought) and remove this parameter
-    $branches ||= { map { $_->branchcode => { branchname => $_->branchname } } Koha::Libraries->search };
+    $branches ||= { map { $_->branchcode => { branchname => $_->branchname } } Koha::Libraries->search->as_list };
 
     # Initialize variables for the faceted results objects
     my $facets_counter = {};
@@ -389,6 +392,12 @@ sub getRecords {
             elsif ( $sort eq "title_za" || $sort eq "title_dsc" ) {
                 $sort_by .= "1=4 >i ";
             }
+            elsif ( $sort eq "biblionumber_az" || $sort eq "biblionumber_asc" ) {
+                $sort_by .= "1=12 <i ";
+            }
+            elsif ( $sort eq "biblionumber_za" || $sort eq "biblionumber_dsc" ) {
+                $sort_by .= "1=12 >i ";
+            }
             else {
                 warn "Ignoring unrecognized sort '$sort' requested" if $sort_by;
             }
@@ -586,7 +595,10 @@ sub getRecords {
     # This sorts the facets into alphabetical order
     if (@facets_loop) {
         foreach my $f (@facets_loop) {
-            $f->{facets} = [ sort { uc($a->{facet_label_value}) cmp uc($b->{facet_label_value}) } @{ $f->{facets} } ];
+            if( C4::Context->preference('FacetOrder') eq 'Alphabetical' ){
+                $f->{facets} =
+                    [ sort { uc($a->{facet_label_value}) cmp uc($b->{facet_label_value}) } @{ $f->{facets} } ];
+            }
         }
     }
 
@@ -885,7 +897,7 @@ sub _build_weighted_query {
     my $fuzzy_enabled = C4::Context->preference("QueryFuzzy")        || 0;
     $operand =~ s/"/ /g;    # Bug 7518: searches with quotation marks don't work
 
-    my $weighted_query .= "(rk=(";    # Specifies that we're applying rank
+    my $weighted_query = "(rk=(";    # Specifies that we're applying rank
 
     # Keyword, or, no index specified
     if ( ( $index eq 'kw' ) || ( !$index ) ) {
@@ -1062,6 +1074,8 @@ sub getIndexes{
                     'mc-itemtype',
                     'mc-rtype',
                     'mus',
+                    'Multipart-resource-level',
+                    'mrl',
                     'name',
                     'Music-number',
                     'Name-geographic',
@@ -1211,7 +1225,7 @@ sub buildQuery {
     my $query_cgi;
     my $query_type;
 
-    my $limit;
+    my $limit = q{};
     my $limit_cgi;
     my $limit_desc;
 
@@ -1225,25 +1239,107 @@ sub buildQuery {
         $query = "ccl=$query" if $cclq;
     }
 
+    # add limits
+    my %group_OR_limits;
+    my $availability_limit;
+    foreach my $this_limit (@limits) {
+        next unless $this_limit;
+        if ( $this_limit =~ /available/ ) {
+#
+## 'available' is defined as (items.onloan is NULL) and (items.itemlost = 0)
+## In English:
+## all records not indexed in the onloan register (zebra) and all records with a value of lost equal to 0
+            $availability_limit .=
+"( (allrecords,AlwaysMatches='') and (not-onloan-count,st-numeric >= 1) and (lost,st-numeric=0) )";
+            $limit_cgi  .= "&limit=available";
+            $limit_desc .= "";
+        }
+
+        # group_OR_limits, prefixed by mc-
+        # OR every member of the group
+        elsif ( $this_limit =~ /mc/ ) {
+            my ($k,$v) = split(/:/, $this_limit,2);
+            if ( $k !~ /mc-i(tem)?type/ ) {
+                # in case the mc-ccode value has complicating chars like ()'s inside it we wrap in quotes
+                $this_limit =~ tr/"//d;
+                $this_limit = $k.':"'.$v.'"';
+            }
+
+            $group_OR_limits{$k} .= " or " if $group_OR_limits{$k};
+            $limit_desc      .= " or " if $group_OR_limits{$k};
+            $group_OR_limits{$k} .= "$this_limit";
+            $limit_cgi       .= "&limit=" . uri_escape_utf8($this_limit);
+            $limit_desc      .= " $this_limit";
+        }
+        elsif ( $this_limit =~ '^multibranchlimit:|^branch:' ) {
+            $limit_cgi  .= "&limit=" . uri_escape_utf8($this_limit);
+            $limit .= " and " if $limit || $query;
+            my $branchfield  = C4::Context->preference('SearchLimitLibrary');
+            my @branchcodes;
+            if(  $this_limit =~ '^multibranchlimit:' ){
+                my ($group_id) = ( $this_limit =~ /^multibranchlimit:(.*)$/ );
+                my $search_group = Koha::Library::Groups->find( $group_id );
+                @branchcodes  = map { $_->branchcode } $search_group->all_libraries;
+                @branchcodes = sort { $a cmp $b } @branchcodes;
+            } else {
+                @branchcodes = ( $this_limit =~ /^branch:(.*)$/ );
+            }
+
+            if (@branchcodes) {
+                if ( $branchfield eq "homebranch" ) {
+                    $this_limit = sprintf "(%s)", join " or ", map { 'homebranch: ' . $_ } @branchcodes;
+                }
+                elsif ( $branchfield eq "holdingbranch" ) {
+                    $this_limit = sprintf "(%s)", join " or ", map { 'holdingbranch: ' . $_ } @branchcodes;
+                }
+                else {
+                    $this_limit =  sprintf "(%s or %s)",
+                      join( " or ", map { 'homebranch: ' . $_ } @branchcodes ),
+                      join( " or ", map { 'holdingbranch: ' . $_ } @branchcodes );
+                }
+            }
+            $limit .= "$this_limit";
+            $limit_desc .= " $this_limit";
+        } elsif ( $this_limit =~ '^search_filter:' ) {
+            # Here we will get the query as a string, append to the limits, and pass through buildQuery
+            # again to clean the terms and handle nested filters
+            $limit_cgi  .= "&limit=" . uri_escape_utf8($this_limit);
+            my ($filter_id) = ( $this_limit =~ /^search_filter:(.*)$/ );
+            my $search_filter = Koha::SearchFilters->find( $filter_id );
+            next unless $search_filter;
+            my ($expanded_lim, $query_lim) = $search_filter->expand_filter;
+            push @$expanded_lim, $query_lim;
+            my ( $error, undef, undef, undef, undef, $fixed_limit, undef, undef, undef ) = buildQuery ( undef, undef, undef, $expanded_lim, undef, undef, $lang);
+            $limit .= " and " if $limit || $query;
+            $limit .= "$fixed_limit";
+            $limit_desc .= " $limit";
+        }
+
+        # Regular old limits
+        else {
+            $limit .= " and " if $limit || $query;
+            $limit      .= "$this_limit";
+            $limit_cgi  .= "&limit=" . uri_escape_utf8($this_limit);
+            $limit_desc .= " $this_limit";
+        }
+    }
+    foreach my $k (keys (%group_OR_limits)) {
+        $limit .= " and " if ( $query || $limit );
+        $limit .= "($group_OR_limits{$k})";
+    }
+    if ($availability_limit) {
+        $limit .= " and " if ( $query || $limit );
+        $limit .= "($availability_limit)";
+    }
+
 # for handling ccl, cql, pqf queries in diagnostic mode, skip the rest of the steps
 # DIAGNOSTIC ONLY!!
     if ( $query =~ /^ccl=/ ) {
         my $q=$';
         # This is needed otherwise ccl= and &limit won't work together, and
         # this happens when selecting a subject on the opac-detail page
-        @limits = grep {!/^$/} @limits;
         my $original_q = $q; # without available part
-        unless ( grep { $_ eq 'available' } @limits ) {
-            $q =~ s| and \( \(allrecords,AlwaysMatches=''\) and \(not-onloan-count,st-numeric >= 1\) and \(lost,st-numeric=0\) \)||;
-            $original_q = $q;
-        }
-        if ( @limits ) {
-            if ( grep { $_ eq 'available' } @limits ) {
-                $q .= q| and ( (allrecords,AlwaysMatches='') and (not-onloan-count,st-numeric >= 1) and (lost,st-numeric=0) )|;
-                @limits = grep {!/^available$/} @limits;
-            }
-            $q .= ' and '.join(' and ', @limits) if @limits;
-        }
+        $q .= $limit if $limit;
         return ( undef, $q, $q, "q=ccl=".uri_escape_utf8($q), $original_q, '', '', '', 'ccl' );
     }
     if ( $query =~ /^cql=/ ) {
@@ -1331,7 +1427,14 @@ sub buildQuery {
                     if ( $index eq 'nb' ) {
                         if ( C4::Context->preference("SearchWithISBNVariations") ) {
                             my @isbns = C4::Koha::GetVariationsOfISBN( $operand );
-                            $operands[$i] = $operand =  '(nb=' . join(' OR nb=', @isbns) . ')';
+                            $operands[$i] = $operand = '(' . join( ' OR ', map { 'nb=' . $_ } @isbns ) . ')';
+                            $indexes[$i] = $index = 'kw';
+                        }
+                    }
+                    if ( $index eq 'ns' ) {
+                        if ( C4::Context->preference("SearchWithISSNVariations") ) {
+                            my @issns = C4::Koha::GetVariationsOfISSN( $operand );
+                            $operands[$i] = $operand = '(' . join( ' OR ', map { 'ns=' . $_ } @issns ) . ')';
                             $indexes[$i] = $index = 'kw';
                         }
                     }
@@ -1440,66 +1543,6 @@ sub buildQuery {
     }
     Koha::Logger->get->debug("QUERY BEFORE LIMITS: >$query<");
 
-    # add limits
-    my %group_OR_limits;
-    my $availability_limit;
-    foreach my $this_limit (@limits) {
-        next unless $this_limit;
-        if ( $this_limit =~ /available/ ) {
-#
-## 'available' is defined as (items.onloan is NULL) and (items.itemlost = 0)
-## In English:
-## all records not indexed in the onloan register (zebra) and all records with a value of lost equal to 0
-            $availability_limit .=
-"( (allrecords,AlwaysMatches='') and (not-onloan-count,st-numeric >= 1) and (lost,st-numeric=0) )";
-            $limit_cgi  .= "&limit=available";
-            $limit_desc .= "";
-        }
-
-        # group_OR_limits, prefixed by mc-
-        # OR every member of the group
-        elsif ( $this_limit =~ /mc/ ) {
-            my ($k,$v) = split(/:/, $this_limit,2);
-            if ( $k !~ /mc-i(tem)?type/ ) {
-                # in case the mc-ccode value has complicating chars like ()'s inside it we wrap in quotes
-                $this_limit =~ tr/"//d;
-                $this_limit = $k.':"'.$v.'"';
-            }
-
-            $group_OR_limits{$k} .= " or " if $group_OR_limits{$k};
-            $limit_desc      .= " or " if $group_OR_limits{$k};
-            $group_OR_limits{$k} .= "$this_limit";
-            $limit_cgi       .= "&limit=" . uri_escape_utf8($this_limit);
-            $limit_desc      .= " $this_limit";
-        }
-
-        # Regular old limits
-        else {
-            $limit .= " and " if $limit || $query;
-            $limit      .= "$this_limit";
-            $limit_cgi  .= "&limit=" . uri_escape_utf8($this_limit);
-            if ($this_limit =~ /^branch:(.+)/) {
-                my $branchcode = $1;
-                my $library = Koha::Libraries->find( $branchcode );
-                if (defined $library) {
-                    $limit_desc .= " branch:" . $library->branchname;
-                } else {
-                    $limit_desc .= " $this_limit";
-                }
-            } else {
-                $limit_desc .= " $this_limit";
-            }
-        }
-    }
-    foreach my $k (keys (%group_OR_limits)) {
-        $limit .= " and " if ( $query || $limit );
-        $limit .= "($group_OR_limits{$k})";
-    }
-    if ($availability_limit) {
-        $limit .= " and " if ( $query || $limit );
-        $limit .= "($availability_limit)";
-    }
-
     # Normalize the query and limit strings
     # This is flawed , means we can't search anything with : in it
     # if user wants to do ccl or cql, start the query with that
@@ -1560,7 +1603,7 @@ sub _build_initial_query {
     my $operator = "";
     if ($params->{previous_operand}){
         #If there is a previous operand, add a supplied operator or the default 'and'
-        $operator = ($params->{operator}) ? " ".($params->{operator})." " : ' and ';
+        $operator = ($params->{operator}) ? ($params->{operator}) : 'AND';
     }
 
     #NOTE: indexes_set is typically set when doing truncation or field weighting
@@ -1568,14 +1611,14 @@ sub _build_initial_query {
 
     #e.g. "kw,wrdl:test"
     #e.g. " and kw,wrdl:test"
-    $params->{query} .= $operator . $operand;
+    $params->{query} .= " " . $operator . " " . $operand;
 
     $params->{query_cgi} .= "&op=".uri_escape_utf8($operator) if $operator;
     $params->{query_cgi} .= "&idx=".uri_escape_utf8($params->{index}) if $params->{index};
-    $params->{query_cgi} .= "&q=".uri_escape_utf8($params->{original_operand}) if $params->{original_operand};
+    $params->{query_cgi} .= "&q=".uri_escape_utf8($params->{original_operand}) if ( $params->{original_operand} ne '' );
 
     #e.g. " and kw,wrdl: test"
-    $params->{query_desc} .= $operator . ( $params->{index_plus} // q{} ) . " " . ( $params->{original_operand} // q{} );
+    $params->{query_desc} .= " " . $operator . " " . ( $params->{index_plus} // q{} ) . " " . ( $params->{original_operand} // q{} );
 
     $params->{previous_operand} = 1 unless $params->{previous_operand}; #If there is no previous operand, mark this as one
 
@@ -1613,7 +1656,7 @@ sub searchResults {
     });
 
     #Build branchnames hash
-    my %branches = map { $_->branchcode => $_->branchname } Koha::Libraries->search({}, { order_by => 'branchname' });
+    my %branches = map { $_->branchcode => $_->branchname } Koha::Libraries->search({}, { order_by => 'branchname' })->as_list;
 
 # FIXME - We build an authorised values hash here, using the default framework
 # though it is possible to have different authvals for different fws.
@@ -1692,7 +1735,7 @@ sub searchResults {
                : GetFrameworkCode($marcrecord->subfield($bibliotag,$bibliosubf));
 
         SetUTF8Flag($marcrecord);
-        my $oldbiblio = TransformMarcToKoha( $marcrecord, $fw, 'no_items' );
+        my $oldbiblio = TransformMarcToKoha({ record => $marcrecord, limit_table => 'no_items' });
         $oldbiblio->{result_number} = $i + 1;
 
                $oldbiblio->{normalized_upc}  = GetNormalizedUPC(       $marcrecord,$marcflavour);
@@ -1712,12 +1755,13 @@ sub searchResults {
 
         # Pull out the items fields
         my @fields = $marcrecord->field($itemtag);
+        $marcrecord->delete_fields( @fields ) unless C4::Context->preference('PassItemMarcToXSLT');
         my $marcflavor = C4::Context->preference("marcflavour");
 
         # adding linked items that belong to host records
         if ( C4::Context->preference('EasyAnalyticalRecords') ) {
             my $analyticsfield = '773';
-            if ($marcflavor eq 'MARC21' || $marcflavor eq 'NORMARC') {
+            if ($marcflavor eq 'MARC21') {
                 $analyticsfield = '773';
             } elsif ($marcflavor eq 'UNIMARC') {
                 $analyticsfield = '461';
@@ -1757,9 +1801,9 @@ sub searchResults {
         my $itembinding_count     = 0;
         my $itemdamaged_count     = 0;
         my $item_in_transit_count = 0;
-        my $can_place_holds       = 0;
         my $item_onhold_count     = 0;
         my $notforloan_count      = 0;
+        my $item_recalled_count   = 0;
         my $items_count           = scalar(@fields);
         my $maxitems_pref = C4::Context->preference('maxItemsinSearchResults');
         my $maxitems = $maxitems_pref ? $maxitems_pref - 1 : 1;
@@ -1773,19 +1817,21 @@ sub searchResults {
             foreach my $code ( keys %subfieldstosearch ) {
                 $item->{$code} = $field->subfield( $subfieldstosearch{$code} );
             }
+
+            unless ( $item->{itemnumber} ) {
+                warn "MARC item without itemnumber retrieved for biblio ($oldbiblio->{biblionumber})";
+                next;
+            }
+
             $item->{description} = $itemtypes{ $item->{itype} }{translated_description} if $item->{itype};
 
-               # OPAC hidden items
+            # OPAC hidden items
             if ($is_opac) {
-                # hidden because lost
-                if ($hidelostitems && $item->{itemlost}) {
-                    $hideatopac_count++;
-                    next;
-                }
-                # hidden based on OpacHiddenItems syspref
-                my @hi = C4::Items::GetHiddenItemnumbers({ items=> [ $item ], borcat => $search_context->{category} });
-                if (scalar @hi) {
-                    push @hiddenitems, @hi;
+                # hidden based on OpacHiddenItems syspref or because lost
+                my $hi = Koha::Items->search( { itemnumber => $item->{itemnumber} } )
+                                    ->filter_by_visible_in_opac({ patron => $search_context->{patron} });
+                unless ( $hi->count ) {
+                    push @hiddenitems, $item->{itemnumber};
                     $hideatopac_count++;
                     next;
                 }
@@ -1828,9 +1874,6 @@ sub searchResults {
                     $onloan_items->{$key}->{longoverdue}++;
                     $longoverdue_count++;
                 }
-                else {    # can place holds as long as item isn't lost
-                    $can_place_holds = 1;
-                }
             }
 
          # items not on loan, but still unavailable ( lost, withdrawn, damaged )
@@ -1854,6 +1897,9 @@ sub searchResults {
                 # is item on the reserve shelf?
                 my $reservestatus = '';
 
+                # is item a waiting recall?
+                my $recallstatus = '';
+
                 unless ($item->{withdrawn}
                         || $item->{itemlost}
                         || $item->{damaged}
@@ -1872,9 +1918,22 @@ sub searchResults {
                     # FIXME: to avoid having the query the database like this, and to make
                     #        the in transit status count as unavailable for search limiting,
                     #        should map transit status to record indexed in Zebra.
-                    #
-                    ($transfertwhen, $transfertfrom, $transfertto) = C4::Circulation::GetTransfers($item->{itemnumber});
+
+                    my $item_object = Koha::Items->find($item->{itemnumber});
+                    my $transfer = defined($item_object) ? $item_object->get_transfer : undef;
+                    ( $transfertwhen, $transfertfrom, $transfertto ) =
+                      defined($transfer)
+                      ? (
+                        $transfer->datesent, $transfer->frombranch,
+                        $transfer->tobranch
+                      )
+                      : ( '', '', '' );
                     $reservestatus = C4::Reserves::GetReserveStatus( $item->{itemnumber} );
+                    if ( C4::Context->preference('UseRecalls') ) {
+                        if ( Koha::Recalls->search({ item_id => $item->{itemnumber}, status => 'waiting' })->count ) {
+                            $recallstatus = 'Waiting';
+                        }
+                    }
                 }
 
                 # item is withdrawn, lost, damaged, not for loan, reserved or in transit
@@ -1883,6 +1942,7 @@ sub searchResults {
                     || $item->{damaged}
                     || $item->{notforloan}
                     || $reservestatus eq 'Waiting'
+                    || $recallstatus eq 'Waiting'
                     || ($transfertwhen && $transfertwhen ne ''))
                 {
                     $withdrawn_count++        if $item->{withdrawn};
@@ -1890,20 +1950,9 @@ sub searchResults {
                     $itemdamaged_count++     if $item->{damaged};
                     $item_in_transit_count++ if $transfertwhen && $transfertwhen ne '';
                     $item_onhold_count++     if $reservestatus eq 'Waiting';
+                    $item_recalled_count++   if $recallstatus eq 'Waiting';
                     $item->{status} = ($item->{withdrawn}//q{}) . "-" . ($item->{itemlost}//q{}) . "-" . ($item->{damaged}//q{}) . "-" . ($item->{notforloan}//q{});
 
-                    # can place a hold on a item if
-                    # not lost nor withdrawn
-                    # not damaged unless AllowHoldsOnDamagedItems is true
-                    # item is either for loan or on order (notforloan < 0)
-                    $can_place_holds = 1
-                      if (
-                           !$item->{itemlost}
-                        && !$item->{withdrawn}
-                        && ( !$item->{damaged} || C4::Context->preference('AllowHoldsOnDamagedItems') )
-                        && ( !$item->{notforloan} || $item->{notforloan} < 0 )
-                      );
-
                     $other_count++;
 
                     my $key = $prefix . $item->{status};
@@ -1911,6 +1960,7 @@ sub searchResults {
                         $other_items->{$key}->{$_} = $item->{$_};
                     }
                     $other_items->{$key}->{intransit} = ( $transfertwhen ne '' ) ? 1 : 0;
+                    $other_items->{$key}->{recalled} = ($recallstatus) ? 1 : 0;
                     $other_items->{$key}->{onhold} = ($reservestatus) ? 1 : 0;
                     $other_items->{$key}->{notforloan} = GetAuthorisedValueDesc('','',$item->{notforloan},'','',$notforloan_authorised_value) if $notforloan_authorised_value and $item->{notforloan};
                     $other_items->{$key}->{count}++ if $item->{$hbranch};
@@ -1920,7 +1970,6 @@ sub searchResults {
                 }
                 # item is available
                 else {
-                    $can_place_holds = 1;
                     $available_count++;
                     $available_items->{$prefix}->{count}++ if $item->{$hbranch};
                     foreach (qw(branchname itemcallnumber description)) {
@@ -1972,16 +2021,26 @@ sub searchResults {
                     ),
                     fix_amps       => 1,
                     hidden_items   => \@hiddenitems,
-                    xslt_variables => $xslt_variables
+                    xslt_variables => $xslt_variables,
                 }
             );
         }
 
+        my $biblio_object = Koha::Biblios->find( $oldbiblio->{biblionumber} );
+        $oldbiblio->{biblio_object} = $biblio_object;
+        $oldbiblio->{coins} = eval { $biblio_object->get_coins }
+          if $biblio_object
+          && C4::Context->preference('COinSinOPACResults')
+          && $is_opac;
+
+        my $can_place_holds = 1;
         # if biblio level itypes are used and itemtype is notforloan, it can't be reserved either
         if (!C4::Context->preference("item-level_itypes")) {
             if ($itemtype && $itemtype->{notforloan}) {
                 $can_place_holds = 0;
             }
+        } else {
+            $can_place_holds = $biblio_object->items->filter_by_for_hold()->count if $biblio_object;
         }
         $oldbiblio->{norequests} = 1 unless $can_place_holds;
         $oldbiblio->{items_count}          = $items_count;
@@ -1999,6 +2058,7 @@ sub searchResults {
         $oldbiblio->{damagedcount}         = $itemdamaged_count;
         $oldbiblio->{intransitcount}       = $item_in_transit_count;
         $oldbiblio->{onholdcount}          = $item_onhold_count;
+        $oldbiblio->{recalledcount}        = $item_recalled_count;
         $oldbiblio->{orderedcount}         = $ordered_count;
         $oldbiblio->{notforloancount}      = $notforloan_count;
 
@@ -2030,8 +2090,6 @@ sub searchResults {
             $oldbiblio->{'alternateholdings_count'} = $alternateholdingscount;
         }
 
-        $oldbiblio->{biblio_object} = Koha::Biblios->find( $oldbiblio->{biblionumber} );
-
         push( @newresults, $oldbiblio );
     }