3 # Copyright ByWater Solutions 2014
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22 use List::MoreUtils qw( any );
24 use URI::Escape qw( uri_escape_utf8 );
26 use C4::Koha qw( GetNormalizedISBN GetNormalizedUPC GetNormalizedOCLCNumber );
29 use Koha::DateUtils qw( dt_from_string );
31 use base qw(Koha::Object);
33 use Koha::Acquisition::Orders;
34 use Koha::ArticleRequests;
35 use Koha::Biblio::Metadatas;
36 use Koha::Biblio::Metadata::Extractor;
37 use Koha::Biblio::ItemGroups;
38 use Koha::Biblioitems;
39 use Koha::Cache::Memory::Lite;
42 use Koha::CirculationRules;
44 use Koha::Illrequests;
45 use Koha::Item::Transfer::Limits;
48 use Koha::Old::Checkouts;
51 use Koha::RecordProcessor;
52 use Koha::Suggestions;
53 use Koha::Subscriptions;
54 use Koha::SearchEngine;
55 use Koha::SearchEngine::Search;
56 use Koha::SearchEngine::QueryBuilder;
61 Koha::Biblio - Koha Biblio Object class
71 Overloaded I<store> method to set default values
78 $self->datecreated( dt_from_string ) unless $self->datecreated;
80 return $self->SUPER::store;
85 my $metadata = $biblio->metadata();
87 Returns a Koha::Biblio::Metadata object
94 my $metadata = $self->_result->metadata;
95 return Koha::Biblio::Metadata->_new_from_dbic($metadata);
100 my $record = $biblio->record();
102 Returns a Marc::Record object
109 return $self->metadata->record;
114 my $schema = $biblio->record_schema();
116 Returns the record schema (MARC21, USMARC or UNIMARC).
123 return $self->metadata->schema // C4::Context->preference("marcflavour");
128 my $orders = $biblio->orders();
130 Returns a Koha::Acquisition::Orders object
137 my $orders = $self->_result->orders;
138 return Koha::Acquisition::Orders->_new_from_dbic($orders);
143 my $active_orders = $biblio->active_orders();
145 Returns the active acquisition orders related to this biblio.
146 An order is considered active when it is not cancelled (i.e. when datecancellation
154 return $self->orders->search({ datecancellationprinted => undef });
159 my $tickets = $biblio->tickets();
161 Returns all tickets linked to the biblio
167 my $rs = $self->_result->tickets;
168 return Koha::Tickets->_new_from_dbic( $rs );
173 my $ill_requests = $biblio->ill_requests();
175 Returns a Koha::Illrequests object
182 my $ill_requests = $self->_result->ill_requests;
183 return Koha::Illrequests->_new_from_dbic($ill_requests);
188 my $item_groups = $biblio->item_groups();
190 Returns a Koha::Biblio::ItemGroups object
197 my $item_groups = $self->_result->item_groups;
198 return Koha::Biblio::ItemGroups->_new_from_dbic($item_groups);
201 =head3 can_article_request
203 my $bool = $biblio->can_article_request( $borrower );
205 Returns true if article requests can be made for this record
207 $borrower must be a Koha::Patron object
211 sub can_article_request {
212 my ( $self, $borrower ) = @_;
214 my $rule = $self->article_request_type($borrower);
215 return q{} if $rule eq 'item_only' && !$self->items()->count();
216 return 1 if $rule && $rule ne 'no';
225 $biblio->check_booking( { start_date => $datetime, end_date => $datetime, [ booking_id => $booking_id ] } );
227 Returns a boolean denoting whether the passed booking can be made without clashing.
229 Optionally, you may pass a booking id to exclude from the checks; This is helpful when you are updating an existing booking.
234 my ( $self, $params ) = @_;
236 my $start_date = dt_from_string( $params->{start_date} );
237 my $end_date = dt_from_string( $params->{end_date} );
238 my $booking_id = $params->{booking_id};
240 my $bookable_items = $self->bookable_items;
241 my $total_bookable = $bookable_items->count;
243 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
244 my $existing_bookings = $self->bookings(
248 $dtf->format_datetime($start_date),
249 $dtf->format_datetime($end_date)
254 $dtf->format_datetime($start_date),
255 $dtf->format_datetime($end_date)
259 start_date => { '<' => $dtf->format_datetime($start_date) },
260 end_date => { '>' => $dtf->format_datetime($end_date) }
267 ? $existing_bookings->search( { booking_id => { '!=' => $booking_id } } )
269 : $existing_bookings->count;
271 my $checkouts = $self->current_checkouts->search( { date_due => { '>=' => $dtf->format_datetime($start_date) } } );
272 $booked_count += $checkouts->count;
274 return ( ( $total_bookable - $booked_count ) > 0 ) ? 1 : 0;
277 =head3 assign_item_for_booking
281 sub assign_item_for_booking {
282 my ( $self, $params ) = @_;
284 my $start_date = dt_from_string( $params->{start_date} );
285 my $end_date = dt_from_string( $params->{end_date} );
287 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
288 my $existing_bookings = $self->bookings(
292 $dtf->format_datetime($start_date),
293 $dtf->format_datetime($end_date)
298 $dtf->format_datetime($start_date),
299 $dtf->format_datetime($end_date)
303 start_date => { '<' => $dtf->format_datetime($start_date) },
304 end_date => { '>' => $dtf->format_datetime($end_date) }
309 my $checkouts = $self->current_checkouts->search( { date_due => { '>=' => $dtf->format_datetime($start_date) } } );
311 my $bookable_items = $self->bookable_items->search(
315 { '-not_in' => $existing_bookings->_resultset->get_column('item_id')->as_query },
316 { '-not_in' => $checkouts->_resultset->get_column('itemnumber')->as_query }
321 return $bookable_items->single->itemnumber;
326 my $booking = $biblio->place_booking(
329 start_date => $datetime,
330 end_date => $datetime
334 Add a booking for this item for the dates passed.
336 Returns the Koha::Booking object or throws an exception if the item cannot be booked for the given dates.
341 my ( $self, $params ) = @_;
343 # check for mandatory params
344 my @mandatory = ( 'start_date', 'end_date', 'patron' );
345 for my $param (@mandatory) {
346 unless ( defined( $params->{$param} ) ) {
347 Koha::Exceptions::MissingParameter->throw(
348 error => "The $param parameter is mandatory" );
351 my $patron = $params->{patron};
354 my $booking = Koha::Booking->new(
356 start_date => $params->{start_date},
357 end_date => $params->{end_date},
358 patron_id => $patron->borrowernumber,
359 biblio_id => $self->biblionumber
365 =head3 can_be_transferred
367 $biblio->can_be_transferred({ to => $to_library, from => $from_library })
369 Checks if at least one item of a biblio can be transferred to given library.
371 This feature is controlled by two system preferences:
372 UseBranchTransferLimits to enable / disable the feature
373 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
374 for setting the limitations
376 Performance-wise, it is recommended to use this method for a biblio instead of
377 iterating each item of a biblio with Koha::Item->can_be_transferred().
379 Takes HASHref that can have the following parameters:
380 MANDATORY PARAMETERS:
383 $from : Koha::Library # if given, only items from that
384 # holdingbranch are considered
386 Returns 1 if at least one of the item of a biblio can be transferred
387 to $to_library, otherwise 0.
391 sub can_be_transferred {
392 my ($self, $params) = @_;
394 my $to = $params->{to};
395 my $from = $params->{from};
397 return 1 unless C4::Context->preference('UseBranchTransferLimits');
398 my $limittype = C4::Context->preference('BranchTransferLimitsType');
401 foreach my $item_of_bib ($self->items->as_list) {
402 next unless $item_of_bib->holdingbranch;
403 next if $from && $from->branchcode ne $item_of_bib->holdingbranch;
404 return 1 if $item_of_bib->holdingbranch eq $to->branchcode;
405 my $code = $limittype eq 'itemtype'
406 ? $item_of_bib->effective_itemtype
407 : $item_of_bib->ccode;
408 return 1 unless $code;
409 $items->{$code}->{$item_of_bib->holdingbranch} = 1;
412 # At this point we will have a HASHref containing each itemtype/ccode that
413 # this biblio has, inside which are all of the holdingbranches where those
414 # items are located at. Then, we will query Koha::Item::Transfer::Limits to
415 # find out whether a transfer limits for such $limittype from any of the
416 # listed holdingbranches to the given $to library exist. If at least one
417 # holdingbranch for that $limittype does not have a transfer limit to given
418 # $to library, then we know that the transfer is possible.
419 foreach my $code (keys %{$items}) {
420 my @holdingbranches = keys %{$items->{$code}};
421 return 1 if Koha::Item::Transfer::Limits->search({
422 toBranch => $to->branchcode,
423 fromBranch => { 'in' => \@holdingbranches },
426 group_by => [qw/fromBranch/]
427 })->count == scalar(@holdingbranches) ? 0 : 1;
434 =head3 pickup_locations
436 my $pickup_locations = $biblio->pickup_locations({ patron => $patron });
438 Returns a Koha::Libraries set of possible pickup locations for this biblio's items,
439 according to patron's home library and if item can be transferred to each pickup location.
441 Throws a I<Koha::Exceptions::MissingParameter> exception if the B<mandatory> parameter I<patron>
446 sub pickup_locations {
447 my ( $self, $params ) = @_;
449 Koha::Exceptions::MissingParameter->throw( parameter => 'patron' )
450 unless exists $params->{patron};
452 my $patron = $params->{patron};
454 my $memory_cache = Koha::Cache::Memory::Lite->get_instance();
455 my @pickup_locations;
456 foreach my $item ( $self->items->as_list ) {
457 my $cache_key = sprintf "Pickup_locations:%s:%s:%s:%s:%s",
458 $item->itype,$item->homebranch,$item->holdingbranch,$item->ccode || "",$patron->branchcode||"" ;
459 my $item_pickup_locations = $memory_cache->get_from_cache( $cache_key );
460 unless( $item_pickup_locations ){
461 @{ $item_pickup_locations } = $item->pickup_locations( { patron => $patron } )->_resultset->get_column('branchcode')->all;
462 $memory_cache->set_in_cache( $cache_key, $item_pickup_locations );
464 push @pickup_locations, @{ $item_pickup_locations }
467 return Koha::Libraries->search(
468 { branchcode => { '-in' => \@pickup_locations } }, { order_by => ['branchname'] } );
471 =head3 hidden_in_opac
473 my $bool = $biblio->hidden_in_opac({ [ rules => $rules ] })
475 Returns true if the biblio matches the hidding criteria defined in $rules.
476 Returns false otherwise. It involves the I<OpacHiddenItems> and
477 I<OpacHiddenItemsHidesRecord> system preferences.
479 Takes HASHref that can have the following parameters:
481 $rules : { <field> => [ value_1, ... ], ... }
483 Note: $rules inherits its structure from the parsed YAML from reading
484 the I<OpacHiddenItems> system preference.
489 my ( $self, $params ) = @_;
491 my $rules = $params->{rules} // {};
493 my @items = $self->items->as_list;
495 return 0 unless @items; # Do not hide if there is no item
497 # Ok, there are items, don't even try the rules unless OpacHiddenItemsHidesRecord
498 return 0 unless C4::Context->preference('OpacHiddenItemsHidesRecord');
500 return !(any { !$_->hidden_in_opac({ rules => $rules }) } @items);
503 =head3 article_request_type
505 my $type = $biblio->article_request_type( $borrower );
507 Returns the article request type based on items, or on the record
508 itself if there are no items.
510 $borrower must be a Koha::Patron object
514 sub article_request_type {
515 my ( $self, $borrower ) = @_;
517 return q{} unless $borrower;
519 my $rule = $self->article_request_type_for_items( $borrower );
520 return $rule if $rule;
522 # If the record has no items that are requestable, go by the record itemtype
523 $rule = $self->article_request_type_for_bib($borrower);
524 return $rule if $rule;
529 =head3 article_request_type_for_bib
531 my $type = $biblio->article_request_type_for_bib
533 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record
537 sub article_request_type_for_bib {
538 my ( $self, $borrower ) = @_;
540 return q{} unless $borrower;
542 my $borrowertype = $borrower->categorycode;
543 my $itemtype = $self->itemtype();
545 my $rule = Koha::CirculationRules->get_effective_rule(
547 rule_name => 'article_requests',
548 categorycode => $borrowertype,
549 itemtype => $itemtype,
553 return q{} unless $rule;
554 return $rule->rule_value || q{}
557 =head3 article_request_type_for_items
559 my $type = $biblio->article_request_type_for_items
561 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record's items
563 If there is a conflict where some items are 'bib_only' and some are 'item_only', 'bib_only' will be returned.
567 sub article_request_type_for_items {
568 my ( $self, $borrower ) = @_;
571 foreach my $item ( $self->items()->as_list() ) {
572 my $rule = $item->article_request_type($borrower);
573 return $rule if $rule eq 'bib_only'; # we don't need to go any further
577 return 'item_only' if $counts->{item_only};
578 return 'yes' if $counts->{yes};
579 return 'no' if $counts->{no};
583 =head3 article_requests
585 my $article_requests = $biblio->article_requests
587 Returns the article requests associated with this biblio
591 sub article_requests {
594 return Koha::ArticleRequests->_new_from_dbic( scalar $self->_result->article_requests );
597 =head3 current_checkouts
599 my $current_checkouts = $biblio->current_checkouts
601 Returns the current checkouts associated with this biblio
605 sub current_checkouts {
608 return Koha::Checkouts->search( { "item.biblionumber" => $self->id },
609 { join => 'item' } );
614 my $old_checkouts = $biblio->old_checkouts
616 Returns the past checkouts associated with this biblio
623 return Koha::Old::Checkouts->search( { "item.biblionumber" => $self->id },
624 { join => 'item' } );
629 my $items = $biblio->items({ [ host_items => 1 ] });
631 The optional param host_items allows you to include 'analytical' items.
633 Returns the related Koha::Items object for this biblio
638 my ($self,$params) = @_;
640 my $items_rs = $self->_result->items;
642 return Koha::Items->_new_from_dbic( $items_rs ) unless $params->{host_items};
644 my @itemnumbers = $items_rs->get_column('itemnumber')->all;
645 my $host_itemnumbers = $self->_host_itemnumbers();
646 push @itemnumbers, @{ $host_itemnumbers };
647 return Koha::Items->search({ "me.itemnumber" => { -in => \@itemnumbers } });
650 =head3 bookable_items
652 my $bookable_items = $biblio->bookable_items;
654 Returns the related Koha::Items resultset filtered to those items that can be booked.
660 return $self->items->filter_by_bookable;
666 my $host_items = $biblio->host_items();
668 Return the host items (easy analytical record)
675 return Koha::Items->new->empty
676 unless C4::Context->preference('EasyAnalyticalRecords');
678 my $host_itemnumbers = $self->_host_itemnumbers;
680 return Koha::Items->search( { itemnumber => { -in => $host_itemnumbers } } );
683 =head3 _host_itemnumbers
685 my $host_itemnumber = $biblio->_host_itemnumbers();
687 Return the itemnumbers for analytical items on this record
691 sub _host_itemnumbers {
694 my $marcflavour = C4::Context->preference("marcflavour");
695 my $analyticfield = '773';
696 if ( $marcflavour eq 'UNIMARC' ) {
697 $analyticfield = '461';
699 my $marc_record = $self->metadata->record;
701 foreach my $field ( $marc_record->field($analyticfield) ) {
702 push @itemnumbers, $field->subfield('9');
704 return \@itemnumbers;
710 my $itemtype = $biblio->itemtype();
712 Returns the itemtype for this record.
719 return $self->biblioitem()->itemtype();
724 my $holds = $biblio->holds();
726 return the current holds placed on this record
731 my ( $self, $params, $attributes ) = @_;
732 $attributes->{order_by} = 'priority' unless exists $attributes->{order_by};
733 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
734 return Koha::Holds->_new_from_dbic($hold_rs);
739 my $holds = $biblio->current_holds
741 Return the holds placed on this bibliographic record.
742 It does not include future holds.
748 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
750 { reservedate => { '<=' => $dtf->format_date(dt_from_string) } } );
755 my $field = $self->biblioitem
757 Returns the related Koha::Biblioitem object for this Biblio object
763 return Koha::Biblioitems->find( { biblionumber => $self->biblionumber } );
768 my $bookings = $item->bookings();
770 Returns the bookings attached to this biblio.
775 my ( $self, $params ) = @_;
776 my $bookings_rs = $self->_result->bookings->search($params);
777 return Koha::Bookings->_new_from_dbic( $bookings_rs );
782 my $suggestions = $self->suggestions
784 Returns the related Koha::Suggestions object for this Biblio object
791 my $suggestions_rs = $self->_result->suggestions;
792 return Koha::Suggestions->_new_from_dbic( $suggestions_rs );
795 =head3 get_marc_components
797 my $components = $self->get_marc_components();
799 Returns an array of search results data, which are component parts of
800 this object (MARC21 773 points to this)
804 sub get_marc_components {
805 my ($self, $max_results) = @_;
807 return [] if (C4::Context->preference('marcflavour') ne 'MARC21');
809 my ( $searchstr, $sort ) = $self->get_components_query;
812 if (defined($searchstr)) {
813 my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
814 my ( $error, $results, $facets );
816 ( $error, $results, $facets ) = $searcher->search_compat( $searchstr, undef, [$sort], ['biblioserver'], $max_results, 0, undef, undef, 'ccl', 0 );
821 warn "Warning from search_compat: '$error'";
825 message => 'component_search',
830 $components = $results->{biblioserver}->{RECORDS} if defined($results) && $results->{biblioserver}->{hits};
833 return $components // [];
836 =head2 get_components_query
838 Returns a query which can be used to search for all component parts of MARC21 biblios
842 sub get_components_query {
845 my $builder = Koha::SearchEngine::QueryBuilder->new(
846 { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
847 my $marc = $self->metadata->record;
848 my $component_sort_field = C4::Context->preference('ComponentSortField') // "title";
849 my $component_sort_order = C4::Context->preference('ComponentSortOrder') // "asc";
850 my $sort = $component_sort_field . "_" . $component_sort_order;
853 if ( C4::Context->preference('UseControlNumber') ) {
854 my $pf001 = $marc->field('001') || undef;
856 if ( defined($pf001) ) {
858 my $pf003 = $marc->field('003') || undef;
860 if ( !defined($pf003) ) {
861 # search for 773$w='Host001'
862 $searchstr .= "rcn:\"" . $pf001->data()."\"";
866 # search for (773$w='Host001' and 003='Host003') or 773$w='(Host003)Host001'
867 $searchstr .= "(rcn:\"" . $pf001->data() . "\" AND cni:\"" . $pf003->data() . "\")";
868 $searchstr .= " OR rcn:\"" . $pf003->data() . " " . $pf001->data() . "\"";
872 # limit to monograph and serial component part records
873 $searchstr .= " AND (bib-level:a OR bib-level:b)";
878 my $cleaned_title = $marc->subfield('245', "a");
879 $cleaned_title =~ tr|/||;
880 $cleaned_title = $builder->clean_search_term($cleaned_title);
881 $searchstr = qq#Host-item:("$cleaned_title")#;
883 my ($error, $query ,$query_str) = $builder->build_query_compat( undef, [$searchstr], undef, undef, [$sort], 0 );
889 return ($query, $query_str, $sort);
892 =head3 get_marc_volumes
894 my $volumes = $self->get_marc_volumes();
896 Returns an array of MARCXML data, which are volumes parts of
897 this object (MARC21 773$w or 8xx$w point to this)
901 sub get_marc_volumes {
902 my ( $self, $max_results ) = @_;
904 return $self->{_volumes} if defined( $self->{_volumes} );
906 my $searchstr = $self->get_volumes_query;
908 if ( defined($searchstr) ) {
909 my $searcher = Koha::SearchEngine::Search->new( { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
910 my ( $errors, $results, $total_hits ) = $searcher->simple_search_compat( $searchstr, 0, $max_results );
912 ( defined($results) && scalar(@$results) ) ? $results : [];
914 $self->{_volumes} = [];
917 return $self->{_volumes};
920 =head2 get_volumes_query
922 Returns a query which can be used to search for all component parts of MARC21 biblios
926 sub get_volumes_query {
929 # MARC21 Only for now
930 return if ( C4::Context->preference('marcflavour') ne 'MARC21' );
932 my $marc = $self->metadata->record;
934 # Only build volumes query if we're in a 'Set' record
935 # or we have a monographic series.
936 # For monographic series the check on LDR 7 in (b or i or s) is omitted
937 my $leader19 = substr( $marc->leader, 19, 1 );
938 my $pf008 = $marc->field('008') || '';
939 my $mseries = ( $pf008 && substr( $pf008->data(), 21, 1 ) eq 'm' ) ? 1 : 0;
940 return unless ( $leader19 eq 'a' || $mseries );
942 my $builder = Koha::SearchEngine::QueryBuilder->new( { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
945 if ( C4::Context->preference('UseControlNumber') ) {
946 my $pf001 = $marc->field('001') || undef;
948 if ( defined($pf001) ) {
950 my $pf003 = $marc->field('003') || undef;
952 if ( !defined($pf003) ) {
954 # search for linking_field$w='Host001'
955 $searchstr .= "rcn:" . $pf001->data();
959 # search for (linking_field$w='Host001' and 003='Host003') or linking_field$w='(Host003)Host001'
960 $searchstr .= "(rcn:" . $pf001->data() . " AND cni:" . $pf003->data() . ")";
961 $searchstr .= " OR rcn:\"" . $pf003->data() . " " . $pf001->data() . "\"";
965 # exclude monograph and serial component part records
966 $searchstr .= " NOT (bib-level:a OR bib-level:b)";
970 my $cleaned_title = $marc->subfield( '245', "a" );
971 $cleaned_title =~ tr|/||;
972 $cleaned_title = $builder->clean_search_term($cleaned_title);
973 $searchstr = qq#(title-series,phr:("$cleaned_title") OR Host-item,phr:("$cleaned_title")#;
974 $searchstr .= " NOT (bib-level:a OR bib-level:b))";
982 my $subscriptions = $self->subscriptions
984 Returns the related Koha::Subscriptions object for this Biblio object
990 my $rs = $self->_result->subscriptions;
991 return Koha::Subscriptions->_new_from_dbic($rs);
994 =head3 has_items_waiting_or_intransit
996 my $itemsWaitingOrInTransit = $biblio->has_items_waiting_or_intransit
998 Tells if this bibliographic record has items waiting or in transit.
1002 sub has_items_waiting_or_intransit {
1005 if ( Koha::Holds->search({ biblionumber => $self->id,
1006 found => ['W', 'T'] })->count ) {
1010 foreach my $item ( $self->items->as_list ) {
1011 return 1 if $item->get_transfer;
1019 my $coins = $biblio->get_coins;
1021 Returns the COinS (a span) which can be included in a biblio record
1028 my $record = $self->metadata->record;
1030 my $pos7 = substr $record->leader(), 7, 1;
1031 my $pos6 = substr $record->leader(), 6, 1;
1034 my ( $aulast, $aufirst ) = ( '', '' );
1045 # For the purposes of generating COinS metadata, LDR/06-07 can be
1046 # considered the same for UNIMARC and MARC21
1049 'b' => 'manuscript',
1051 'd' => 'manuscript',
1055 'i' => 'audioRecording',
1056 'j' => 'audioRecording',
1059 'm' => 'computerProgram',
1064 'a' => 'journalArticle',
1068 $genre = $fmts6->{$pos6} ? $fmts6->{$pos6} : 'book';
1070 if ( $genre eq 'book' ) {
1071 $genre = $fmts7->{$pos7} if $fmts7->{$pos7};
1074 ##### We must transform mtx to a valable mtx and document type ####
1075 if ( $genre eq 'book' ) {
1078 } elsif ( $genre eq 'journal' ) {
1081 } elsif ( $genre eq 'journalArticle' ) {
1089 if ( C4::Context->preference("marcflavour") eq "UNIMARC" ) {
1092 $aulast = $record->subfield( '700', 'a' ) || '';
1093 $aufirst = $record->subfield( '700', 'b' ) || '';
1094 push @authors, "$aufirst $aulast" if ($aufirst or $aulast);
1097 if ( $record->field('200') ) {
1098 for my $au ( $record->field('200')->subfield('g') ) {
1103 $title = $record->subfield( '200', 'a' );
1104 my $subfield_210d = $record->subfield('210', 'd');
1105 if ($subfield_210d and $subfield_210d =~ /(\d{4})/) {
1108 $publisher = $record->subfield( '210', 'c' ) || '';
1109 $isbn = $record->subfield( '010', 'a' ) || '';
1110 $issn = $record->subfield( '011', 'a' ) || '';
1113 # MARC21 need some improve
1116 if ( $record->field('100') ) {
1117 push @authors, $record->subfield( '100', 'a' );
1121 if ( $record->field('700') ) {
1122 for my $au ( $record->field('700')->subfield('a') ) {
1126 $title = $record->field('245');
1127 $title &&= $title->as_string('ab');
1128 if ($titletype eq 'a') {
1129 $pubyear = $record->field('008') || '';
1130 $pubyear = substr($pubyear->data(), 7, 4) if $pubyear;
1131 $isbn = $record->subfield( '773', 'z' ) || '';
1132 $issn = $record->subfield( '773', 'x' ) || '';
1133 $hosttitle = $record->subfield( '773', 't' ) || $record->subfield( '773', 'a') || q{};
1134 my @rels = $record->subfield( '773', 'g' );
1135 $pages = join(', ', @rels);
1137 $pubyear = $record->subfield( '260', 'c' ) || '';
1138 $publisher = $record->subfield( '260', 'b' ) || '';
1139 $isbn = $record->subfield( '020', 'a' ) || '';
1140 $issn = $record->subfield( '022', 'a' ) || '';
1146 [ 'ctx_ver', 'Z39.88-2004' ],
1147 [ 'rft_val_fmt', "info:ofi/fmt:kev:mtx:$mtx" ],
1148 [ ($mtx eq 'dc' ? 'rft.type' : 'rft.genre'), $genre ],
1149 [ "rft.${titletype}title", $title ],
1152 # rft.title is authorized only once, so by checking $titletype
1153 # we ensure that rft.title is not already in the list.
1154 if ($hosttitle and $titletype) {
1155 push @params, [ 'rft.title', $hosttitle ];
1159 [ 'rft.isbn', $isbn ],
1160 [ 'rft.issn', $issn ],
1163 # If it's a subscription, these informations have no meaning.
1164 if ($genre ne 'journal') {
1166 [ 'rft.aulast', $aulast ],
1167 [ 'rft.aufirst', $aufirst ],
1168 (map { [ 'rft.au', $_ ] } @authors),
1169 [ 'rft.pub', $publisher ],
1170 [ 'rft.date', $pubyear ],
1171 [ 'rft.pages', $pages ],
1175 my $coins_value = join( '&',
1176 map { $$_[1] ? $$_[0] . '=' . uri_escape_utf8( $$_[1] ) : () } @params );
1178 return $coins_value;
1183 my $url = $biblio->get_openurl;
1185 Returns url for OpenURL resolver set in OpenURLResolverURL system preference
1192 my $OpenURLResolverURL = C4::Context->preference('OpenURLResolverURL');
1194 if ($OpenURLResolverURL) {
1195 my $uri = URI->new($OpenURLResolverURL);
1197 if (not defined $uri->query) {
1198 $OpenURLResolverURL .= '?';
1200 $OpenURLResolverURL .= '&';
1202 $OpenURLResolverURL .= $self->get_coins;
1205 return $OpenURLResolverURL;
1210 my $serial = $biblio->is_serial
1212 Return boolean true if this bibbliographic record is continuing resource
1219 return 1 if $self->serial;
1221 my $record = $self->metadata->record;
1222 return 1 if substr($record->leader, 7, 1) eq 's';
1227 =head3 custom_cover_image_url
1229 my $image_url = $biblio->custom_cover_image_url
1231 Return the specific url of the cover image for this bibliographic record.
1232 It is built regaring the value of the system preference CustomCoverImagesURL
1236 sub custom_cover_image_url {
1238 my $url = C4::Context->preference('CustomCoverImagesURL');
1239 if ( $url =~ m|{isbn}| ) {
1240 my $isbn = $self->biblioitem->isbn;
1241 return unless $isbn;
1242 $url =~ s|{isbn}|$isbn|g;
1244 if ( $url =~ m|{normalized_isbn}| ) {
1245 my $normalized_isbn = $self->normalized_isbn;
1246 return unless $normalized_isbn;
1247 $url =~ s|{normalized_isbn}|$normalized_isbn|g;
1249 if ( $url =~ m|{issn}| ) {
1250 my $issn = $self->biblioitem->issn;
1251 return unless $issn;
1252 $url =~ s|{issn}|$issn|g;
1255 my $re = qr|{(?<field>\d{3})(\$(?<subfield>.))?}|;
1256 if ( $url =~ $re ) {
1257 my $field = $+{field};
1258 my $subfield = $+{subfield};
1259 my $marc_record = $self->metadata->record;
1262 $value = $marc_record->subfield( $field, $subfield );
1264 my $controlfield = $marc_record->field($field);
1265 $value = $controlfield->data() if $controlfield;
1267 return unless $value;
1268 $url =~ s|$re|$value|;
1276 Return the cover images associated with this biblio.
1283 my $cover_images_rs = $self->_result->cover_images;
1284 return unless $cover_images_rs;
1285 return Koha::CoverImages->_new_from_dbic($cover_images_rs);
1288 =head3 get_marc_notes
1290 $marcnotesarray = $biblio->get_marc_notes({ opac => 1 });
1292 Get all notes from the MARC record and returns them in an array.
1293 The notes are stored in different fields depending on MARC flavour.
1294 MARC21 5XX $u subfields receive special attention as they are URIs.
1298 sub get_marc_notes {
1299 my ( $self, $params ) = @_;
1301 my $marcflavour = C4::Context->preference('marcflavour');
1302 my $opac = $params->{opac} // '0';
1303 my $interface = $params->{opac} ? 'opac' : 'intranet';
1305 my $record = $params->{record} // $self->metadata->record;
1306 my $record_processor = Koha::RecordProcessor->new(
1308 filters => [ 'ViewPolicy', 'ExpandCodedFields' ],
1310 interface => $interface,
1311 frameworkcode => $self->frameworkcode
1315 $record_processor->process($record);
1317 my $scope = $marcflavour eq "UNIMARC"? '3..': '5..';
1318 #MARC21 specs indicate some notes should be private if first indicator 0
1319 my %maybe_private = (
1327 my %hiddenlist = map { $_ => 1 }
1328 split( /,/, C4::Context->preference('NotesToHide'));
1331 foreach my $field ( $record->field($scope) ) {
1332 my $tag = $field->tag();
1333 next if $hiddenlist{ $tag };
1334 next if $opac && $maybe_private{$tag} && !$field->indicator(1);
1335 if( $marcflavour ne 'UNIMARC' && $field->subfield('u') ) {
1336 # Field 5XX$u always contains URI
1337 # Examples: 505u, 506u, 510u, 514u, 520u, 530u, 538u, 540u, 542u, 552u, 555u, 561u, 563u, 583u
1338 # We first push the other subfields, then all $u's separately
1339 # Leave further actions to the template (see e.g. opac-detail)
1341 join '', ( 'a' .. 't', 'v' .. 'z', '0' .. '9' ); # excl 'u'
1342 push @marcnotes, { marcnote => $field->as_string($othersub) };
1343 foreach my $sub ( $field->subfield('u') ) {
1344 $sub =~ s/^\s+|\s+$//g; # trim
1345 push @marcnotes, { marcnote => $sub, tag => $tag };
1348 push @marcnotes, { marcnote => $field->as_string(), tag => $tag };
1354 =head3 _get_marc_authors
1356 Private method to return the list of authors contained in the MARC record.
1357 See get get_marc_contributors and get_marc_authors for the public methods.
1361 sub _get_marc_authors {
1362 my ( $self, $params ) = @_;
1364 my $fields_filter = $params->{fields_filter};
1365 my $mintag = $params->{mintag};
1366 my $maxtag = $params->{maxtag};
1368 my $AuthoritySeparator = C4::Context->preference('AuthoritySeparator');
1369 my $marcflavour = C4::Context->preference('marcflavour');
1371 # tagslib useful only for UNIMARC author responsibilities
1372 my $tagslib = $marcflavour eq "UNIMARC"
1373 ? C4::Biblio::GetMarcStructure( 1, $self->frameworkcode, { unsafe => 1 } )
1377 foreach my $field ( $self->metadata->record->field($fields_filter) ) {
1380 if $mintag && $field->tag() < $mintag
1381 || $maxtag && $field->tag() > $maxtag;
1385 my @subfields = $field->subfields();
1388 # if there is an authority link, build the link with Koha-Auth-Number: subfield9
1389 my $subfield9 = $field->subfield('9');
1391 my $linkvalue = $subfield9;
1392 $linkvalue =~ s/(\(|\))//g;
1393 @link_loop = ( { 'limit' => 'an', 'link' => $linkvalue } );
1398 for my $authors_subfield (@subfields) {
1399 next if ( $authors_subfield->[0] eq '9' );
1401 # unimarc3 contains the $3 of the author for UNIMARC.
1402 # For french academic libraries, it's the "ppn", and it's required for idref webservice
1403 $unimarc3 = $authors_subfield->[1] if $marcflavour eq 'UNIMARC' and $authors_subfield->[0] =~ /3/;
1405 # don't load unimarc subfields 3, 5
1406 next if ( $marcflavour eq 'UNIMARC' and ( $authors_subfield->[0] =~ /3|5/ ) );
1408 my $code = $authors_subfield->[0];
1409 my $value = $authors_subfield->[1];
1410 my $linkvalue = $value;
1411 $linkvalue =~ s/(\(|\))//g;
1412 # UNIMARC author responsibility
1413 if ( $marcflavour eq 'UNIMARC' and $code eq '4' ) {
1414 $value = C4::Biblio::GetAuthorisedValueDesc( $field->tag(), $code, $value, '', $tagslib );
1415 $linkvalue = "($value)";
1417 # if no authority link, build a search query
1418 unless ($subfield9) {
1421 'link' => $linkvalue,
1422 operator => (scalar @link_loop) ? ' AND ' : undef
1425 my @this_link_loop = @link_loop;
1427 unless ( $code eq '0') {
1428 push @subfields_loop, {
1429 tag => $field->tag(),
1432 link_loop => \@this_link_loop,
1433 separator => (scalar @subfields_loop) ? $AuthoritySeparator : ''
1437 push @marcauthors, {
1438 MARCAUTHOR_SUBFIELDS_LOOP => \@subfields_loop,
1439 authoritylink => $subfield9,
1440 unimarc3 => $unimarc3
1443 return \@marcauthors;
1446 =head3 get_marc_contributors
1448 my $contributors = $biblio->get_marc_contributors;
1450 Get all contributors (but first author) from the MARC record and returns them in an array.
1451 They are stored in different fields depending on MARC flavour (700..720 for MARC21)
1455 sub get_marc_contributors {
1456 my ( $self, $params ) = @_;
1458 my ( $mintag, $maxtag, $fields_filter );
1459 my $marcflavour = C4::Context->preference('marcflavour');
1461 if ( $marcflavour eq "UNIMARC" ) {
1464 $fields_filter = '7..';
1465 } else { # marc21/normarc
1468 $fields_filter = '7..';
1471 return $self->_get_marc_authors(
1473 fields_filter => $fields_filter,
1480 =head3 get_marc_authors
1482 my $authors = $biblio->get_marc_authors;
1484 Get all authors from the MARC record and returns them in an array.
1485 They are stored in different fields depending on MARC flavour
1486 (main author from 100 then secondary authors from 700..720).
1490 sub get_marc_authors {
1491 my ( $self, $params ) = @_;
1493 my ( $mintag, $maxtag, $fields_filter );
1494 my $marcflavour = C4::Context->preference('marcflavour');
1496 if ( $marcflavour eq "UNIMARC" ) {
1497 $fields_filter = '200';
1498 } else { # marc21/normarc
1499 $fields_filter = '100';
1502 my @first_authors = @{$self->_get_marc_authors(
1504 fields_filter => $fields_filter,
1510 my @other_authors = @{$self->get_marc_contributors};
1512 return [@first_authors, @other_authors];
1515 =head3 normalized_isbn
1517 my $normalized_isbn = $biblio->normalized_isbn
1519 Normalizes and returns the first valid ISBN found in the record.
1520 ISBN13 are converted into ISBN10. This is required to get some book cover images.
1524 sub normalized_isbn {
1526 return C4::Koha::GetNormalizedISBN($self->biblioitem->isbn);
1529 =head3 public_read_list
1531 This method returns the list of publicly readable database fields for both API and UI output purposes
1535 sub public_read_list {
1537 'biblionumber', 'frameworkcode', 'author',
1538 'title', 'medium', 'subtitle',
1539 'part_number', 'part_name', 'unititle',
1540 'notes', 'serial', 'seriestitle',
1541 'copyrightdate', 'abstract'
1545 =head3 metadata_extractor
1547 my $extractor = $biblio->metadata_extractor
1549 Return a Koha::Biblio::Metadata::Extractor object to use to extract data from the metadata (ie. MARC record for now)
1553 sub metadata_extractor {
1556 $self->{metadata_extractor} ||= Koha::Biblio::Metadata::Extractor->new( { biblio => $self } );
1558 return $self->{metadata_extractor};
1561 =head3 normalized_upc
1563 my $normalized_upc = $biblio->normalized_upc
1565 Normalizes and returns the UPC value found in the MARC record.
1569 sub normalized_upc {
1571 return $self->metadata_extractor->get_normalized_upc;
1574 =head3 normalized_oclc
1576 my $normalized_oclc = $biblio->normalized_oclc
1578 Normalizes and returns the OCLC number found in the MARC record.
1582 sub normalized_oclc {
1584 return $self->metadata_extractor->get_normalized_oclc;
1589 my $json = $biblio->to_api;
1591 Overloaded method that returns a JSON representation of the Koha::Biblio object,
1592 suitable for API output. The related Koha::Biblioitem object is merged as expected
1598 my ($self, $args) = @_;
1600 my $json_biblio = $self->SUPER::to_api( $args );
1601 return unless $json_biblio;
1603 $args = defined $args ? {%$args} : {};
1604 delete $args->{embed};
1606 my $json_biblioitem = $self->biblioitem->to_api( $args );
1607 return unless $json_biblioitem;
1609 return { %$json_biblio, %$json_biblioitem };
1612 =head3 to_api_mapping
1614 This method returns the mapping for representing a Koha::Biblio object
1619 sub to_api_mapping {
1621 biblionumber => 'biblio_id',
1622 frameworkcode => 'framework_id',
1623 unititle => 'uniform_title',
1624 seriestitle => 'series_title',
1625 copyrightdate => 'copyright_date',
1626 datecreated => 'creation_date',
1627 deleted_on => undef,
1631 =head3 get_marc_host
1633 $host = $biblio->get_marc_host;
1635 ( $host, $relatedparts, $hostinfo ) = $biblio->get_marc_host;
1637 Returns host biblio record from MARC21 773 (undef if no 773 present).
1638 It looks at the first 773 field with MARCorgCode or only a control
1639 number. Complete $w or numeric part is used to search host record.
1640 The optional parameter no_items triggers a check if $biblio has items.
1641 If there are, the sub returns undef.
1642 Called in list context, it also returns 773$g (related parts).
1644 If there is no $w, we use $0 (host biblionumber) or $9 (host itemnumber)
1645 to search for the host record. If there is also no $0 and no $9, we search
1646 using author and title. Failing all of that, we return an undef host and
1647 form a concatenation of strings with 773$agt for host information,
1648 returned when called in list context.
1653 my ($self, $params) = @_;
1654 my $no_items = $params->{no_items};
1655 return if C4::Context->preference('marcflavour') eq 'UNIMARC'; # TODO
1656 return if $params->{no_items} && $self->items->count > 0;
1659 eval { $record = $self->metadata->record };
1662 # We pick the first $w with your MARCOrgCode or the first $w that has no
1663 # code (between parentheses) at all.
1664 my $orgcode = C4::Context->preference('MARCOrgCode') // q{};
1666 foreach my $f ( $record->field('773') ) {
1667 my $w = $f->subfield('w') or next;
1668 if( $w =~ /^\($orgcode\)\s*(\d+)/i or $w =~ /^\d+/ ) {
1674 my $engine = Koha::SearchEngine::Search->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1676 if ( !$hostfld and $record->subfield('773','t') ) {
1677 # not linked using $w
1678 my $unlinkedf = $record->field('773');
1680 if ( C4::Context->preference("EasyAnalyticalRecords") ) {
1681 if ( $unlinkedf->subfield('0') ) {
1682 # use 773$0 host biblionumber
1683 $bibno = $unlinkedf->subfield('0');
1684 } elsif ( $unlinkedf->subfield('9') ) {
1685 # use 773$9 host itemnumber
1686 my $linkeditemnumber = $unlinkedf->subfield('9');
1687 $bibno = Koha::Items->find( $linkeditemnumber )->biblionumber;
1691 my $host = Koha::Biblios->find($bibno) or return;
1692 return wantarray ? ( $host, $unlinkedf->subfield('g') ) : $host;
1694 # just return plaintext and no host record
1695 my $hostinfo = join( ", ", $unlinkedf->subfield('a'), $unlinkedf->subfield('t'), $unlinkedf->subfield('g') );
1696 return wantarray ? ( undef, $unlinkedf->subfield('g'), $hostinfo ) : undef;
1698 return if !$hostfld;
1699 my $rcn = $hostfld->subfield('w');
1701 # Look for control number with/without orgcode
1702 for my $try (1..2) {
1703 my ( $error, $results, $total_hits ) = $engine->simple_search_compat( 'Control-number='.$rcn, 0,1 );
1704 if( !$error and $total_hits == 1 ) {
1705 $bibno = $engine->extract_biblionumber( $results->[0] );
1708 # Add or remove orgcode for second try
1709 if( $try == 1 && $rcn =~ /\)\s*(\d+)/ ) {
1710 $rcn = $1; # number only
1711 } elsif( $try == 1 && $rcn =~ /^\d+/ ) {
1712 $rcn = "($orgcode)$rcn";
1718 my $host = Koha::Biblios->find($bibno) or return;
1719 return wantarray ? ( $host, $hostfld->subfield('g') ) : $host;
1723 =head3 get_marc_host_only
1725 my $host = $biblio->get_marc_host_only;
1731 sub get_marc_host_only {
1734 my ( $host ) = $self->get_marc_host;
1739 =head3 get_marc_relatedparts_only
1741 my $relatedparts = $biblio->get_marc_relatedparts_only;
1743 Return related parts only
1747 sub get_marc_relatedparts_only {
1750 my ( undef, $relatedparts ) = $self->get_marc_host;
1752 return $relatedparts;
1755 =head3 get_marc_hostinfo_only
1757 my $hostinfo = $biblio->get_marc_hostinfo_only;
1759 Return host info only
1763 sub get_marc_hostinfo_only {
1766 my ( $host, $relatedparts, $hostinfo ) = $self->get_marc_host;
1773 my $recalls = $biblio->recalls;
1775 Return recalls linked to this biblio
1781 return Koha::Recalls->_new_from_dbic( scalar $self->_result->recalls );
1784 =head3 can_be_recalled
1786 my @items_for_recall = $biblio->can_be_recalled({ patron => $patron_object });
1788 Does biblio-level checks and returns the items attached to this biblio that are available for recall
1792 sub can_be_recalled {
1793 my ( $self, $params ) = @_;
1795 return 0 if !( C4::Context->preference('UseRecalls') );
1797 my $patron = $params->{patron};
1799 my $branchcode = C4::Context->userenv->{'branch'};
1800 if ( C4::Context->preference('CircControl') eq 'PatronLibrary' and $patron ) {
1801 $branchcode = $patron->branchcode;
1804 my @all_items = Koha::Items->search({ biblionumber => $self->biblionumber })->as_list;
1806 # if there are no available items at all, no recall can be placed
1807 return 0 if ( scalar @all_items == 0 );
1812 my @all_itemnumbers;
1813 foreach my $item ( @all_items ) {
1814 push( @all_itemnumbers, $item->itemnumber );
1815 if ( $item->can_be_recalled({ patron => $patron }) ) {
1816 push( @itemtypes, $item->effective_itemtype );
1817 push( @itemnumbers, $item->itemnumber );
1818 push( @items, $item );
1822 # if there are no recallable items, no recall can be placed
1823 return 0 if ( scalar @items == 0 );
1825 # Check the circulation rule for each relevant itemtype for this biblio
1826 my ( @recalls_allowed, @recalls_per_record, @on_shelf_recalls );
1827 foreach my $itemtype ( @itemtypes ) {
1828 my $rule = Koha::CirculationRules->get_effective_rules({
1829 branchcode => $branchcode,
1830 categorycode => $patron ? $patron->categorycode : undef,
1831 itemtype => $itemtype,
1834 'recalls_per_record',
1838 push( @recalls_allowed, $rule->{recalls_allowed} ) if $rule;
1839 push( @recalls_per_record, $rule->{recalls_per_record} ) if $rule;
1840 push( @on_shelf_recalls, $rule->{on_shelf_recalls} ) if $rule;
1842 my $recalls_allowed = (sort {$b <=> $a} @recalls_allowed)[0]; # take highest
1843 my $recalls_per_record = (sort {$b <=> $a} @recalls_per_record)[0]; # take highest
1844 my %on_shelf_recalls_count = ();
1845 foreach my $count ( @on_shelf_recalls ) {
1846 $on_shelf_recalls_count{$count}++;
1848 my $on_shelf_recalls = (sort {$on_shelf_recalls_count{$b} <=> $on_shelf_recalls_count{$a}} @on_shelf_recalls)[0]; # take most common
1850 # check recalls allowed has been set and is not zero
1851 return 0 if ( !defined($recalls_allowed) || $recalls_allowed == 0 );
1854 # check borrower has not reached open recalls allowed limit
1855 return 0 if ( $patron->recalls->filter_by_current->count >= $recalls_allowed );
1857 # check borrower has not reached open recalls allowed per record limit
1858 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $recalls_per_record );
1860 # check if any of the items under this biblio are already checked out by this borrower
1861 return 0 if ( Koha::Checkouts->search({ itemnumber => [ @all_itemnumbers ], borrowernumber => $patron->borrowernumber })->count > 0 );
1864 # check item availability
1865 my $checked_out_count = 0;
1867 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1870 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1871 return 0 if ( $on_shelf_recalls eq 'all' && $checked_out_count < scalar @items );
1873 # can't recall if no items have been checked out
1874 return 0 if ( $checked_out_count == 0 );
1882 my $ratings = $biblio->ratings
1884 Return a Koha::Ratings object representing the ratings of this bibliographic record
1890 my $rs = $self->_result->ratings;
1891 return Koha::Ratings->_new_from_dbic($rs);
1894 =head3 opac_summary_html
1896 my $summary_html = $biblio->opac_summary_html
1898 Based on the syspref OPACMySummaryHTML, returns a string representing the
1899 summary of this bibliographic record.
1900 {AUTHOR}, {TITLE}, {ISBN} and {BIBLIONUMBER} will be replaced.
1904 sub opac_summary_html {
1907 my $summary_html = C4::Context->preference('OPACMySummaryHTML');
1908 return q{} unless $summary_html;
1909 my $author = $self->author || q{};
1910 my $title = $self->title || q{};
1911 $title =~ s/\/+$//; # remove trailing slash
1912 $title =~ s/\s+$//; # remove trailing space
1913 my $normalized_isbn = $self->normalized_isbn || q{};
1914 my $biblionumber = $self->biblionumber;
1916 $summary_html =~ s/{AUTHOR}/$author/g;
1917 $summary_html =~ s/{TITLE}/$title/g;
1918 $summary_html =~ s/{ISBN}/$normalized_isbn/g;
1919 $summary_html =~ s/{BIBLIONUMBER}/$biblionumber/g;
1921 return $summary_html;
1924 =head2 Internal methods
1936 Kyle M Hall <kyle@bywatersolutions.com>