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';
226 $biblio->check_booking( { start_date => $datetime, end_date => $datetime, [ booking_id => $booking_id ] } );
228 Returns a boolean denoting whether the passed booking can be made without clashing.
230 Optionally, you may pass a booking id to exclude from the checks; This is helpful when you are updating an existing booking.
235 my ( $self, $params ) = @_;
237 my $start_date = dt_from_string( $params->{start_date} );
238 my $end_date = dt_from_string( $params->{end_date} );
239 my $booking_id = $params->{booking_id};
241 my $bookable_items = $self->bookable_items;
242 my $total_bookable = $bookable_items->count;
244 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
245 my $existing_bookings = $self->bookings(
249 $dtf->format_datetime($start_date),
250 $dtf->format_datetime($end_date)
255 $dtf->format_datetime($start_date),
256 $dtf->format_datetime($end_date)
260 start_date => { '<' => $dtf->format_datetime($start_date) },
261 end_date => { '>' => $dtf->format_datetime($end_date) }
268 ? $existing_bookings->search( { booking_id => { '!=' => $booking_id } } )->count
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;
289 my $existing_bookings = $self->bookings(
293 $dtf->format_datetime($start_date),
294 $dtf->format_datetime($end_date)
299 $dtf->format_datetime($start_date),
300 $dtf->format_datetime($end_date)
304 start_date => { '<' => $dtf->format_datetime($start_date) },
305 end_date => { '>' => $dtf->format_datetime($end_date) }
310 my $checkouts = $self->current_checkouts->search( { date_due => { '>=' => $dtf->format_datetime($start_date) } } );
312 my $bookable_items = $self->bookable_items->search(
315 '-and' => { '-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( error => "The $param parameter is mandatory" );
350 my $patron = $params->{patron};
353 my $booking = Koha::Booking->new(
355 start_date => $params->{start_date},
356 end_date => $params->{end_date},
357 patron_id => $patron->borrowernumber,
358 biblio_id => $self->biblionumber
364 =head3 can_be_transferred
366 $biblio->can_be_transferred({ to => $to_library, from => $from_library })
368 Checks if at least one item of a biblio can be transferred to given library.
370 This feature is controlled by two system preferences:
371 UseBranchTransferLimits to enable / disable the feature
372 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
373 for setting the limitations
375 Performance-wise, it is recommended to use this method for a biblio instead of
376 iterating each item of a biblio with Koha::Item->can_be_transferred().
378 Takes HASHref that can have the following parameters:
379 MANDATORY PARAMETERS:
382 $from : Koha::Library # if given, only items from that
383 # holdingbranch are considered
385 Returns 1 if at least one of the item of a biblio can be transferred
386 to $to_library, otherwise 0.
390 sub can_be_transferred {
391 my ($self, $params) = @_;
393 my $to = $params->{to};
394 my $from = $params->{from};
396 return 1 unless C4::Context->preference('UseBranchTransferLimits');
397 my $limittype = C4::Context->preference('BranchTransferLimitsType');
400 foreach my $item_of_bib ($self->items->as_list) {
401 next unless $item_of_bib->holdingbranch;
402 next if $from && $from->branchcode ne $item_of_bib->holdingbranch;
403 return 1 if $item_of_bib->holdingbranch eq $to->branchcode;
404 my $code = $limittype eq 'itemtype'
405 ? $item_of_bib->effective_itemtype
406 : $item_of_bib->ccode;
407 return 1 unless $code;
408 $items->{$code}->{$item_of_bib->holdingbranch} = 1;
411 # At this point we will have a HASHref containing each itemtype/ccode that
412 # this biblio has, inside which are all of the holdingbranches where those
413 # items are located at. Then, we will query Koha::Item::Transfer::Limits to
414 # find out whether a transfer limits for such $limittype from any of the
415 # listed holdingbranches to the given $to library exist. If at least one
416 # holdingbranch for that $limittype does not have a transfer limit to given
417 # $to library, then we know that the transfer is possible.
418 foreach my $code (keys %{$items}) {
419 my @holdingbranches = keys %{$items->{$code}};
420 return 1 if Koha::Item::Transfer::Limits->search({
421 toBranch => $to->branchcode,
422 fromBranch => { 'in' => \@holdingbranches },
425 group_by => [qw/fromBranch/]
426 })->count == scalar(@holdingbranches) ? 0 : 1;
433 =head3 pickup_locations
435 my $pickup_locations = $biblio->pickup_locations({ patron => $patron });
437 Returns a Koha::Libraries set of possible pickup locations for this biblio's items,
438 according to patron's home library and if item can be transferred to each pickup location.
440 Throws a I<Koha::Exceptions::MissingParameter> exception if the B<mandatory> parameter I<patron>
445 sub pickup_locations {
446 my ( $self, $params ) = @_;
448 Koha::Exceptions::MissingParameter->throw( parameter => 'patron' )
449 unless exists $params->{patron};
451 my $patron = $params->{patron};
453 my $memory_cache = Koha::Cache::Memory::Lite->get_instance();
454 my @pickup_locations;
455 foreach my $item ( $self->items->as_list ) {
456 my $cache_key = sprintf "Pickup_locations:%s:%s:%s:%s:%s",
457 $item->itype,$item->homebranch,$item->holdingbranch,$item->ccode || "",$patron->branchcode||"" ;
458 my $item_pickup_locations = $memory_cache->get_from_cache( $cache_key );
459 unless( $item_pickup_locations ){
460 @{ $item_pickup_locations } = $item->pickup_locations( { patron => $patron } )->_resultset->get_column('branchcode')->all;
461 $memory_cache->set_in_cache( $cache_key, $item_pickup_locations );
463 push @pickup_locations, @{ $item_pickup_locations }
466 return Koha::Libraries->search(
467 { branchcode => { '-in' => \@pickup_locations } }, { order_by => ['branchname'] } );
470 =head3 hidden_in_opac
472 my $bool = $biblio->hidden_in_opac({ [ rules => $rules ] })
474 Returns true if the biblio matches the hidding criteria defined in $rules.
475 Returns false otherwise. It involves the I<OpacHiddenItems> and
476 I<OpacHiddenItemsHidesRecord> system preferences.
478 Takes HASHref that can have the following parameters:
480 $rules : { <field> => [ value_1, ... ], ... }
482 Note: $rules inherits its structure from the parsed YAML from reading
483 the I<OpacHiddenItems> system preference.
488 my ( $self, $params ) = @_;
490 my $rules = $params->{rules} // {};
492 my @items = $self->items->as_list;
494 return 0 unless @items; # Do not hide if there is no item
496 # Ok, there are items, don't even try the rules unless OpacHiddenItemsHidesRecord
497 return 0 unless C4::Context->preference('OpacHiddenItemsHidesRecord');
499 return !(any { !$_->hidden_in_opac({ rules => $rules }) } @items);
502 =head3 article_request_type
504 my $type = $biblio->article_request_type( $borrower );
506 Returns the article request type based on items, or on the record
507 itself if there are no items.
509 $borrower must be a Koha::Patron object
513 sub article_request_type {
514 my ( $self, $borrower ) = @_;
516 return q{} unless $borrower;
518 my $rule = $self->article_request_type_for_items( $borrower );
519 return $rule if $rule;
521 # If the record has no items that are requestable, go by the record itemtype
522 $rule = $self->article_request_type_for_bib($borrower);
523 return $rule if $rule;
528 =head3 article_request_type_for_bib
530 my $type = $biblio->article_request_type_for_bib
532 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record
536 sub article_request_type_for_bib {
537 my ( $self, $borrower ) = @_;
539 return q{} unless $borrower;
541 my $borrowertype = $borrower->categorycode;
542 my $itemtype = $self->itemtype();
544 my $rule = Koha::CirculationRules->get_effective_rule(
546 rule_name => 'article_requests',
547 categorycode => $borrowertype,
548 itemtype => $itemtype,
552 return q{} unless $rule;
553 return $rule->rule_value || q{}
556 =head3 article_request_type_for_items
558 my $type = $biblio->article_request_type_for_items
560 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record's items
562 If there is a conflict where some items are 'bib_only' and some are 'item_only', 'bib_only' will be returned.
566 sub article_request_type_for_items {
567 my ( $self, $borrower ) = @_;
570 foreach my $item ( $self->items()->as_list() ) {
571 my $rule = $item->article_request_type($borrower);
572 return $rule if $rule eq 'bib_only'; # we don't need to go any further
576 return 'item_only' if $counts->{item_only};
577 return 'yes' if $counts->{yes};
578 return 'no' if $counts->{no};
582 =head3 article_requests
584 my $article_requests = $biblio->article_requests
586 Returns the article requests associated with this biblio
590 sub article_requests {
593 return Koha::ArticleRequests->_new_from_dbic( scalar $self->_result->article_requests );
596 =head3 current_checkouts
598 my $current_checkouts = $biblio->current_checkouts
600 Returns the current checkouts associated with this biblio
604 sub current_checkouts {
607 return Koha::Checkouts->search( { "item.biblionumber" => $self->id },
608 { join => 'item' } );
613 my $old_checkouts = $biblio->old_checkouts
615 Returns the past checkouts associated with this biblio
622 return Koha::Old::Checkouts->search( { "item.biblionumber" => $self->id },
623 { join => 'item' } );
628 my $items = $biblio->items({ [ host_items => 1 ] });
630 The optional param host_items allows you to include 'analytical' items.
632 Returns the related Koha::Items object for this biblio
637 my ($self,$params) = @_;
639 my $items_rs = $self->_result->items;
641 return Koha::Items->_new_from_dbic( $items_rs ) unless $params->{host_items};
643 my @itemnumbers = $items_rs->get_column('itemnumber')->all;
644 my $host_itemnumbers = $self->_host_itemnumbers();
645 push @itemnumbers, @{ $host_itemnumbers };
646 return Koha::Items->search({ "me.itemnumber" => { -in => \@itemnumbers } });
649 =head3 bookable_items
651 my $bookable_items = $biblio->bookable_items;
653 Returns the related Koha::Items resultset filtered to those items that can be booked.
659 return $self->items->filter_by_bookable;
664 my $host_items = $biblio->host_items();
666 Return the host items (easy analytical record)
673 return Koha::Items->new->empty
674 unless C4::Context->preference('EasyAnalyticalRecords');
676 my $host_itemnumbers = $self->_host_itemnumbers;
678 return Koha::Items->search( { itemnumber => { -in => $host_itemnumbers } } );
681 =head3 _host_itemnumbers
683 my $host_itemnumber = $biblio->_host_itemnumbers();
685 Return the itemnumbers for analytical items on this record
689 sub _host_itemnumbers {
692 my $marcflavour = C4::Context->preference("marcflavour");
693 my $analyticfield = '773';
694 if ( $marcflavour eq 'UNIMARC' ) {
695 $analyticfield = '461';
697 my $marc_record = $self->metadata->record;
699 foreach my $field ( $marc_record->field($analyticfield) ) {
700 push @itemnumbers, $field->subfield('9');
702 return \@itemnumbers;
708 my $itemtype = $biblio->itemtype();
710 Returns the itemtype for this record.
717 return $self->biblioitem()->itemtype();
722 my $holds = $biblio->holds();
724 return the current holds placed on this record
729 my ( $self, $params, $attributes ) = @_;
730 $attributes->{order_by} = 'priority' unless exists $attributes->{order_by};
731 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
732 return Koha::Holds->_new_from_dbic($hold_rs);
737 my $holds = $biblio->current_holds
739 Return the holds placed on this bibliographic record.
740 It does not include future holds.
746 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
748 { reservedate => { '<=' => $dtf->format_date(dt_from_string) } } );
753 my $field = $self->biblioitem
755 Returns the related Koha::Biblioitem object for this Biblio object
761 return Koha::Biblioitems->find( { biblionumber => $self->biblionumber } );
766 my $bookings = $item->bookings();
768 Returns the bookings attached to this biblio.
773 my ( $self, $params ) = @_;
774 my $bookings_rs = $self->_result->bookings->search($params);
775 return Koha::Bookings->_new_from_dbic($bookings_rs);
780 my $suggestions = $self->suggestions
782 Returns the related Koha::Suggestions object for this Biblio object
789 my $suggestions_rs = $self->_result->suggestions;
790 return Koha::Suggestions->_new_from_dbic( $suggestions_rs );
793 =head3 get_marc_components
795 my $components = $self->get_marc_components();
797 Returns an array of search results data, which are component parts of
798 this object (MARC21 773 points to this)
802 sub get_marc_components {
803 my ($self, $max_results) = @_;
805 return [] if (C4::Context->preference('marcflavour') ne 'MARC21');
807 my ( $searchstr, $sort ) = $self->get_components_query;
810 if (defined($searchstr)) {
811 my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
812 my ( $error, $results, $facets );
814 ( $error, $results, $facets ) = $searcher->search_compat( $searchstr, undef, [$sort], ['biblioserver'], $max_results, 0, undef, undef, 'ccl', 0 );
819 warn "Warning from search_compat: '$error'";
823 message => 'component_search',
828 $components = $results->{biblioserver}->{RECORDS} if defined($results) && $results->{biblioserver}->{hits};
831 return $components // [];
834 =head2 get_components_query
836 Returns a query which can be used to search for all component parts of MARC21 biblios
840 sub get_components_query {
843 my $builder = Koha::SearchEngine::QueryBuilder->new(
844 { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
845 my $marc = $self->metadata->record;
846 my $component_sort_field = C4::Context->preference('ComponentSortField') // "title";
847 my $component_sort_order = C4::Context->preference('ComponentSortOrder') // "asc";
848 my $sort = $component_sort_field . "_" . $component_sort_order;
851 if ( C4::Context->preference('UseControlNumber') ) {
852 my $pf001 = $marc->field('001') || undef;
854 if ( defined($pf001) ) {
856 my $pf003 = $marc->field('003') || undef;
858 if ( !defined($pf003) ) {
859 # search for 773$w='Host001'
860 $searchstr .= "rcn:\"" . $pf001->data()."\"";
864 # search for (773$w='Host001' and 003='Host003') or 773$w='(Host003)Host001'
865 $searchstr .= "(rcn:\"" . $pf001->data() . "\" AND cni:\"" . $pf003->data() . "\")";
866 $searchstr .= " OR rcn:\"" . $pf003->data() . " " . $pf001->data() . "\"";
870 # limit to monograph and serial component part records
871 $searchstr .= " AND (bib-level:a OR bib-level:b)";
876 my $cleaned_title = $marc->subfield('245', "a");
877 $cleaned_title =~ tr|/||;
878 $cleaned_title = $builder->clean_search_term($cleaned_title);
879 $searchstr = qq#Host-item:("$cleaned_title")#;
881 my ($error, $query ,$query_str) = $builder->build_query_compat( undef, [$searchstr], undef, undef, [$sort], 0 );
887 return ($query, $query_str, $sort);
890 =head3 get_marc_volumes
892 my $volumes = $self->get_marc_volumes();
894 Returns an array of MARCXML data, which are volumes parts of
895 this object (MARC21 773$w or 8xx$w point to this)
899 sub get_marc_volumes {
900 my ( $self, $max_results ) = @_;
902 return $self->{_volumes} if defined( $self->{_volumes} );
904 my $searchstr = $self->get_volumes_query;
906 if ( defined($searchstr) ) {
907 my $searcher = Koha::SearchEngine::Search->new( { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
908 my ( $errors, $results, $total_hits ) = $searcher->simple_search_compat( $searchstr, 0, $max_results );
910 ( defined($results) && scalar(@$results) ) ? $results : [];
912 $self->{_volumes} = [];
915 return $self->{_volumes};
918 =head2 get_volumes_query
920 Returns a query which can be used to search for all component parts of MARC21 biblios
924 sub get_volumes_query {
927 # MARC21 Only for now
928 return if ( C4::Context->preference('marcflavour') ne 'MARC21' );
930 my $marc = $self->metadata->record;
932 # Only build volumes query if we're in a 'Set' record
933 # or we have a monographic series.
934 # For monographic series the check on LDR 7 in (b or i or s) is omitted
935 my $leader19 = substr( $marc->leader, 19, 1 );
936 my $pf008 = $marc->field('008') || '';
937 my $mseries = ( $pf008 && substr( $pf008->data(), 21, 1 ) eq 'm' ) ? 1 : 0;
938 return unless ( $leader19 eq 'a' || $mseries );
940 my $builder = Koha::SearchEngine::QueryBuilder->new( { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
943 if ( C4::Context->preference('UseControlNumber') ) {
944 my $pf001 = $marc->field('001') || undef;
946 if ( defined($pf001) ) {
948 my $pf003 = $marc->field('003') || undef;
950 if ( !defined($pf003) ) {
952 # search for linking_field$w='Host001'
953 $searchstr .= "rcn:" . $pf001->data();
957 # search for (linking_field$w='Host001' and 003='Host003') or linking_field$w='(Host003)Host001'
958 $searchstr .= "(rcn:" . $pf001->data() . " AND cni:" . $pf003->data() . ")";
959 $searchstr .= " OR rcn:\"" . $pf003->data() . " " . $pf001->data() . "\"";
963 # exclude monograph and serial component part records
964 $searchstr .= " NOT (bib-level:a OR bib-level:b)";
968 my $cleaned_title = $marc->subfield( '245', "a" );
969 $cleaned_title =~ tr|/||;
970 $cleaned_title = $builder->clean_search_term($cleaned_title);
971 $searchstr = qq#(title-series,phr:("$cleaned_title") OR Host-item,phr:("$cleaned_title")#;
972 $searchstr .= " NOT (bib-level:a OR bib-level:b))";
980 my $subscriptions = $self->subscriptions
982 Returns the related Koha::Subscriptions object for this Biblio object
988 my $rs = $self->_result->subscriptions;
989 return Koha::Subscriptions->_new_from_dbic($rs);
992 =head3 has_items_waiting_or_intransit
994 my $itemsWaitingOrInTransit = $biblio->has_items_waiting_or_intransit
996 Tells if this bibliographic record has items waiting or in transit.
1000 sub has_items_waiting_or_intransit {
1003 if ( Koha::Holds->search({ biblionumber => $self->id,
1004 found => ['W', 'T'] })->count ) {
1008 foreach my $item ( $self->items->as_list ) {
1009 return 1 if $item->get_transfer;
1017 my $coins = $biblio->get_coins;
1019 Returns the COinS (a span) which can be included in a biblio record
1026 my $record = $self->metadata->record;
1028 my $pos7 = substr $record->leader(), 7, 1;
1029 my $pos6 = substr $record->leader(), 6, 1;
1032 my ( $aulast, $aufirst ) = ( '', '' );
1043 # For the purposes of generating COinS metadata, LDR/06-07 can be
1044 # considered the same for UNIMARC and MARC21
1047 'b' => 'manuscript',
1049 'd' => 'manuscript',
1053 'i' => 'audioRecording',
1054 'j' => 'audioRecording',
1057 'm' => 'computerProgram',
1062 'a' => 'journalArticle',
1066 $genre = $fmts6->{$pos6} ? $fmts6->{$pos6} : 'book';
1068 if ( $genre eq 'book' ) {
1069 $genre = $fmts7->{$pos7} if $fmts7->{$pos7};
1072 ##### We must transform mtx to a valable mtx and document type ####
1073 if ( $genre eq 'book' ) {
1076 } elsif ( $genre eq 'journal' ) {
1079 } elsif ( $genre eq 'journalArticle' ) {
1087 if ( C4::Context->preference("marcflavour") eq "UNIMARC" ) {
1090 $aulast = $record->subfield( '700', 'a' ) || '';
1091 $aufirst = $record->subfield( '700', 'b' ) || '';
1092 push @authors, "$aufirst $aulast" if ($aufirst or $aulast);
1095 if ( $record->field('200') ) {
1096 for my $au ( $record->field('200')->subfield('g') ) {
1101 $title = $record->subfield( '200', 'a' );
1102 my $subfield_210d = $record->subfield('210', 'd');
1103 if ($subfield_210d and $subfield_210d =~ /(\d{4})/) {
1106 $publisher = $record->subfield( '210', 'c' ) || '';
1107 $isbn = $record->subfield( '010', 'a' ) || '';
1108 $issn = $record->subfield( '011', 'a' ) || '';
1111 # MARC21 need some improve
1114 if ( $record->field('100') ) {
1115 push @authors, $record->subfield( '100', 'a' );
1119 if ( $record->field('700') ) {
1120 for my $au ( $record->field('700')->subfield('a') ) {
1124 $title = $record->field('245');
1125 $title &&= $title->as_string('ab');
1126 if ($titletype eq 'a') {
1127 $pubyear = $record->field('008') || '';
1128 $pubyear = substr($pubyear->data(), 7, 4) if $pubyear;
1129 $isbn = $record->subfield( '773', 'z' ) || '';
1130 $issn = $record->subfield( '773', 'x' ) || '';
1131 $hosttitle = $record->subfield( '773', 't' ) || $record->subfield( '773', 'a') || q{};
1132 my @rels = $record->subfield( '773', 'g' );
1133 $pages = join(', ', @rels);
1135 $pubyear = $record->subfield( '260', 'c' ) || '';
1136 $publisher = $record->subfield( '260', 'b' ) || '';
1137 $isbn = $record->subfield( '020', 'a' ) || '';
1138 $issn = $record->subfield( '022', 'a' ) || '';
1144 [ 'ctx_ver', 'Z39.88-2004' ],
1145 [ 'rft_val_fmt', "info:ofi/fmt:kev:mtx:$mtx" ],
1146 [ ($mtx eq 'dc' ? 'rft.type' : 'rft.genre'), $genre ],
1147 [ "rft.${titletype}title", $title ],
1150 # rft.title is authorized only once, so by checking $titletype
1151 # we ensure that rft.title is not already in the list.
1152 if ($hosttitle and $titletype) {
1153 push @params, [ 'rft.title', $hosttitle ];
1157 [ 'rft.isbn', $isbn ],
1158 [ 'rft.issn', $issn ],
1161 # If it's a subscription, these informations have no meaning.
1162 if ($genre ne 'journal') {
1164 [ 'rft.aulast', $aulast ],
1165 [ 'rft.aufirst', $aufirst ],
1166 (map { [ 'rft.au', $_ ] } @authors),
1167 [ 'rft.pub', $publisher ],
1168 [ 'rft.date', $pubyear ],
1169 [ 'rft.pages', $pages ],
1173 my $coins_value = join( '&',
1174 map { $$_[1] ? $$_[0] . '=' . uri_escape_utf8( $$_[1] ) : () } @params );
1176 return $coins_value;
1181 my $url = $biblio->get_openurl;
1183 Returns url for OpenURL resolver set in OpenURLResolverURL system preference
1190 my $OpenURLResolverURL = C4::Context->preference('OpenURLResolverURL');
1192 if ($OpenURLResolverURL) {
1193 my $uri = URI->new($OpenURLResolverURL);
1195 if (not defined $uri->query) {
1196 $OpenURLResolverURL .= '?';
1198 $OpenURLResolverURL .= '&';
1200 $OpenURLResolverURL .= $self->get_coins;
1203 return $OpenURLResolverURL;
1208 my $serial = $biblio->is_serial
1210 Return boolean true if this bibbliographic record is continuing resource
1217 return 1 if $self->serial;
1219 my $record = $self->metadata->record;
1220 return 1 if substr($record->leader, 7, 1) eq 's';
1225 =head3 custom_cover_image_url
1227 my $image_url = $biblio->custom_cover_image_url
1229 Return the specific url of the cover image for this bibliographic record.
1230 It is built regaring the value of the system preference CustomCoverImagesURL
1234 sub custom_cover_image_url {
1236 my $url = C4::Context->preference('CustomCoverImagesURL');
1237 if ( $url =~ m|{isbn}| ) {
1238 my $isbn = $self->biblioitem->isbn;
1239 return unless $isbn;
1240 $url =~ s|{isbn}|$isbn|g;
1242 if ( $url =~ m|{normalized_isbn}| ) {
1243 my $normalized_isbn = $self->normalized_isbn;
1244 return unless $normalized_isbn;
1245 $url =~ s|{normalized_isbn}|$normalized_isbn|g;
1247 if ( $url =~ m|{issn}| ) {
1248 my $issn = $self->biblioitem->issn;
1249 return unless $issn;
1250 $url =~ s|{issn}|$issn|g;
1253 my $re = qr|{(?<field>\d{3})(\$(?<subfield>.))?}|;
1254 if ( $url =~ $re ) {
1255 my $field = $+{field};
1256 my $subfield = $+{subfield};
1257 my $marc_record = $self->metadata->record;
1260 $value = $marc_record->subfield( $field, $subfield );
1262 my $controlfield = $marc_record->field($field);
1263 $value = $controlfield->data() if $controlfield;
1265 return unless $value;
1266 $url =~ s|$re|$value|;
1274 Return the cover images associated with this biblio.
1281 my $cover_images_rs = $self->_result->cover_images;
1282 return unless $cover_images_rs;
1283 return Koha::CoverImages->_new_from_dbic($cover_images_rs);
1286 =head3 get_marc_notes
1288 $marcnotesarray = $biblio->get_marc_notes({ opac => 1 });
1290 Get all notes from the MARC record and returns them in an array.
1291 The notes are stored in different fields depending on MARC flavour.
1292 MARC21 5XX $u subfields receive special attention as they are URIs.
1296 sub get_marc_notes {
1297 my ( $self, $params ) = @_;
1299 my $marcflavour = C4::Context->preference('marcflavour');
1300 my $opac = $params->{opac} // '0';
1301 my $interface = $params->{opac} ? 'opac' : 'intranet';
1303 my $record = $params->{record} // $self->metadata->record;
1304 my $record_processor = Koha::RecordProcessor->new(
1306 filters => [ 'ViewPolicy', 'ExpandCodedFields' ],
1308 interface => $interface,
1309 frameworkcode => $self->frameworkcode
1313 $record_processor->process($record);
1315 my $scope = $marcflavour eq "UNIMARC"? '3..': '5..';
1316 #MARC21 specs indicate some notes should be private if first indicator 0
1317 my %maybe_private = (
1325 my %hiddenlist = map { $_ => 1 }
1326 split( /,/, C4::Context->preference('NotesToHide'));
1329 foreach my $field ( $record->field($scope) ) {
1330 my $tag = $field->tag();
1331 next if $hiddenlist{ $tag };
1332 next if $opac && $maybe_private{$tag} && !$field->indicator(1);
1333 if( $marcflavour ne 'UNIMARC' && $field->subfield('u') ) {
1334 # Field 5XX$u always contains URI
1335 # Examples: 505u, 506u, 510u, 514u, 520u, 530u, 538u, 540u, 542u, 552u, 555u, 561u, 563u, 583u
1336 # We first push the other subfields, then all $u's separately
1337 # Leave further actions to the template (see e.g. opac-detail)
1339 join '', ( 'a' .. 't', 'v' .. 'z', '0' .. '9' ); # excl 'u'
1340 push @marcnotes, { marcnote => $field->as_string($othersub) };
1341 foreach my $sub ( $field->subfield('u') ) {
1342 $sub =~ s/^\s+|\s+$//g; # trim
1343 push @marcnotes, { marcnote => $sub, tag => $tag };
1346 push @marcnotes, { marcnote => $field->as_string(), tag => $tag };
1352 =head3 _get_marc_authors
1354 Private method to return the list of authors contained in the MARC record.
1355 See get get_marc_contributors and get_marc_authors for the public methods.
1359 sub _get_marc_authors {
1360 my ( $self, $params ) = @_;
1362 my $fields_filter = $params->{fields_filter};
1363 my $mintag = $params->{mintag};
1364 my $maxtag = $params->{maxtag};
1366 my $AuthoritySeparator = C4::Context->preference('AuthoritySeparator');
1367 my $marcflavour = C4::Context->preference('marcflavour');
1369 # tagslib useful only for UNIMARC author responsibilities
1370 my $tagslib = $marcflavour eq "UNIMARC"
1371 ? C4::Biblio::GetMarcStructure( 1, $self->frameworkcode, { unsafe => 1 } )
1375 foreach my $field ( $self->metadata->record->field($fields_filter) ) {
1378 if $mintag && $field->tag() < $mintag
1379 || $maxtag && $field->tag() > $maxtag;
1383 my @subfields = $field->subfields();
1386 # if there is an authority link, build the link with Koha-Auth-Number: subfield9
1387 my $subfield9 = $field->subfield('9');
1389 my $linkvalue = $subfield9;
1390 $linkvalue =~ s/(\(|\))//g;
1391 @link_loop = ( { 'limit' => 'an', 'link' => $linkvalue } );
1396 for my $authors_subfield (@subfields) {
1397 next if ( $authors_subfield->[0] eq '9' );
1399 # unimarc3 contains the $3 of the author for UNIMARC.
1400 # For french academic libraries, it's the "ppn", and it's required for idref webservice
1401 $unimarc3 = $authors_subfield->[1] if $marcflavour eq 'UNIMARC' and $authors_subfield->[0] =~ /3/;
1403 # don't load unimarc subfields 3, 5
1404 next if ( $marcflavour eq 'UNIMARC' and ( $authors_subfield->[0] =~ /3|5/ ) );
1406 my $code = $authors_subfield->[0];
1407 my $value = $authors_subfield->[1];
1408 my $linkvalue = $value;
1409 $linkvalue =~ s/(\(|\))//g;
1410 # UNIMARC author responsibility
1411 if ( $marcflavour eq 'UNIMARC' and $code eq '4' ) {
1412 $value = C4::Biblio::GetAuthorisedValueDesc( $field->tag(), $code, $value, '', $tagslib );
1413 $linkvalue = "($value)";
1415 # if no authority link, build a search query
1416 unless ($subfield9) {
1419 'link' => $linkvalue,
1420 operator => (scalar @link_loop) ? ' AND ' : undef
1423 my @this_link_loop = @link_loop;
1425 unless ( $code eq '0') {
1426 push @subfields_loop, {
1427 tag => $field->tag(),
1430 link_loop => \@this_link_loop,
1431 separator => (scalar @subfields_loop) ? $AuthoritySeparator : ''
1435 push @marcauthors, {
1436 MARCAUTHOR_SUBFIELDS_LOOP => \@subfields_loop,
1437 authoritylink => $subfield9,
1438 unimarc3 => $unimarc3
1441 return \@marcauthors;
1444 =head3 get_marc_contributors
1446 my $contributors = $biblio->get_marc_contributors;
1448 Get all contributors (but first author) from the MARC record and returns them in an array.
1449 They are stored in different fields depending on MARC flavour (700..720 for MARC21)
1453 sub get_marc_contributors {
1454 my ( $self, $params ) = @_;
1456 my ( $mintag, $maxtag, $fields_filter );
1457 my $marcflavour = C4::Context->preference('marcflavour');
1459 if ( $marcflavour eq "UNIMARC" ) {
1462 $fields_filter = '7..';
1463 } else { # marc21/normarc
1466 $fields_filter = '7..';
1469 return $self->_get_marc_authors(
1471 fields_filter => $fields_filter,
1478 =head3 get_marc_authors
1480 my $authors = $biblio->get_marc_authors;
1482 Get all authors from the MARC record and returns them in an array.
1483 They are stored in different fields depending on MARC flavour
1484 (main author from 100 then secondary authors from 700..720).
1488 sub get_marc_authors {
1489 my ( $self, $params ) = @_;
1491 my ( $mintag, $maxtag, $fields_filter );
1492 my $marcflavour = C4::Context->preference('marcflavour');
1494 if ( $marcflavour eq "UNIMARC" ) {
1495 $fields_filter = '200';
1496 } else { # marc21/normarc
1497 $fields_filter = '100';
1500 my @first_authors = @{$self->_get_marc_authors(
1502 fields_filter => $fields_filter,
1508 my @other_authors = @{$self->get_marc_contributors};
1510 return [@first_authors, @other_authors];
1513 =head3 normalized_isbn
1515 my $normalized_isbn = $biblio->normalized_isbn
1517 Normalizes and returns the first valid ISBN found in the record.
1518 ISBN13 are converted into ISBN10. This is required to get some book cover images.
1522 sub normalized_isbn {
1524 return C4::Koha::GetNormalizedISBN($self->biblioitem->isbn);
1527 =head3 public_read_list
1529 This method returns the list of publicly readable database fields for both API and UI output purposes
1533 sub public_read_list {
1535 'biblionumber', 'frameworkcode', 'author',
1536 'title', 'medium', 'subtitle',
1537 'part_number', 'part_name', 'unititle',
1538 'notes', 'serial', 'seriestitle',
1539 'copyrightdate', 'abstract'
1543 =head3 metadata_extractor
1545 my $extractor = $biblio->metadata_extractor
1547 Return a Koha::Biblio::Metadata::Extractor object to use to extract data from the metadata (ie. MARC record for now)
1551 sub metadata_extractor {
1554 $self->{metadata_extractor} ||= Koha::Biblio::Metadata::Extractor->new( { biblio => $self } );
1556 return $self->{metadata_extractor};
1559 =head3 normalized_upc
1561 my $normalized_upc = $biblio->normalized_upc
1563 Normalizes and returns the UPC value found in the MARC record.
1567 sub normalized_upc {
1569 return $self->metadata_extractor->get_normalized_upc;
1572 =head3 normalized_oclc
1574 my $normalized_oclc = $biblio->normalized_oclc
1576 Normalizes and returns the OCLC number found in the MARC record.
1580 sub normalized_oclc {
1582 return $self->metadata_extractor->get_normalized_oclc;
1587 my $json = $biblio->to_api;
1589 Overloaded method that returns a JSON representation of the Koha::Biblio object,
1590 suitable for API output. The related Koha::Biblioitem object is merged as expected
1596 my ($self, $args) = @_;
1598 my $json_biblio = $self->SUPER::to_api( $args );
1599 return unless $json_biblio;
1601 $args = defined $args ? {%$args} : {};
1602 delete $args->{embed};
1604 my $json_biblioitem = $self->biblioitem->to_api( $args );
1605 return unless $json_biblioitem;
1607 return { %$json_biblio, %$json_biblioitem };
1610 =head3 to_api_mapping
1612 This method returns the mapping for representing a Koha::Biblio object
1617 sub to_api_mapping {
1619 biblionumber => 'biblio_id',
1620 frameworkcode => 'framework_id',
1621 unititle => 'uniform_title',
1622 seriestitle => 'series_title',
1623 copyrightdate => 'copyright_date',
1624 datecreated => 'creation_date',
1625 deleted_on => undef,
1629 =head3 get_marc_host
1631 $host = $biblio->get_marc_host;
1633 ( $host, $relatedparts, $hostinfo ) = $biblio->get_marc_host;
1635 Returns host biblio record from MARC21 773 (undef if no 773 present).
1636 It looks at the first 773 field with MARCorgCode or only a control
1637 number. Complete $w or numeric part is used to search host record.
1638 The optional parameter no_items triggers a check if $biblio has items.
1639 If there are, the sub returns undef.
1640 Called in list context, it also returns 773$g (related parts).
1642 If there is no $w, we use $0 (host biblionumber) or $9 (host itemnumber)
1643 to search for the host record. If there is also no $0 and no $9, we search
1644 using author and title. Failing all of that, we return an undef host and
1645 form a concatenation of strings with 773$agt for host information,
1646 returned when called in list context.
1651 my ($self, $params) = @_;
1652 my $no_items = $params->{no_items};
1653 return if C4::Context->preference('marcflavour') eq 'UNIMARC'; # TODO
1654 return if $params->{no_items} && $self->items->count > 0;
1657 eval { $record = $self->metadata->record };
1660 # We pick the first $w with your MARCOrgCode or the first $w that has no
1661 # code (between parentheses) at all.
1662 my $orgcode = C4::Context->preference('MARCOrgCode') // q{};
1664 foreach my $f ( $record->field('773') ) {
1665 my $w = $f->subfield('w') or next;
1666 if( $w =~ /^\($orgcode\)\s*(\d+)/i or $w =~ /^\d+/ ) {
1672 my $engine = Koha::SearchEngine::Search->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1674 if ( !$hostfld and $record->subfield('773','t') ) {
1675 # not linked using $w
1676 my $unlinkedf = $record->field('773');
1678 if ( C4::Context->preference("EasyAnalyticalRecords") ) {
1679 if ( $unlinkedf->subfield('0') ) {
1680 # use 773$0 host biblionumber
1681 $bibno = $unlinkedf->subfield('0');
1682 } elsif ( $unlinkedf->subfield('9') ) {
1683 # use 773$9 host itemnumber
1684 my $linkeditemnumber = $unlinkedf->subfield('9');
1685 $bibno = Koha::Items->find( $linkeditemnumber )->biblionumber;
1689 my $host = Koha::Biblios->find($bibno) or return;
1690 return wantarray ? ( $host, $unlinkedf->subfield('g') ) : $host;
1692 # just return plaintext and no host record
1693 my $hostinfo = join( ", ", $unlinkedf->subfield('a'), $unlinkedf->subfield('t'), $unlinkedf->subfield('g') );
1694 return wantarray ? ( undef, $unlinkedf->subfield('g'), $hostinfo ) : undef;
1696 return if !$hostfld;
1697 my $rcn = $hostfld->subfield('w');
1699 # Look for control number with/without orgcode
1700 for my $try (1..2) {
1701 my ( $error, $results, $total_hits ) = $engine->simple_search_compat( 'Control-number='.$rcn, 0,1 );
1702 if( !$error and $total_hits == 1 ) {
1703 $bibno = $engine->extract_biblionumber( $results->[0] );
1706 # Add or remove orgcode for second try
1707 if( $try == 1 && $rcn =~ /\)\s*(\d+)/ ) {
1708 $rcn = $1; # number only
1709 } elsif( $try == 1 && $rcn =~ /^\d+/ ) {
1710 $rcn = "($orgcode)$rcn";
1716 my $host = Koha::Biblios->find($bibno) or return;
1717 return wantarray ? ( $host, $hostfld->subfield('g') ) : $host;
1721 =head3 get_marc_host_only
1723 my $host = $biblio->get_marc_host_only;
1729 sub get_marc_host_only {
1732 my ( $host ) = $self->get_marc_host;
1737 =head3 get_marc_relatedparts_only
1739 my $relatedparts = $biblio->get_marc_relatedparts_only;
1741 Return related parts only
1745 sub get_marc_relatedparts_only {
1748 my ( undef, $relatedparts ) = $self->get_marc_host;
1750 return $relatedparts;
1753 =head3 get_marc_hostinfo_only
1755 my $hostinfo = $biblio->get_marc_hostinfo_only;
1757 Return host info only
1761 sub get_marc_hostinfo_only {
1764 my ( $host, $relatedparts, $hostinfo ) = $self->get_marc_host;
1771 my $recalls = $biblio->recalls;
1773 Return recalls linked to this biblio
1779 return Koha::Recalls->_new_from_dbic( scalar $self->_result->recalls );
1782 =head3 can_be_recalled
1784 my @items_for_recall = $biblio->can_be_recalled({ patron => $patron_object });
1786 Does biblio-level checks and returns the items attached to this biblio that are available for recall
1790 sub can_be_recalled {
1791 my ( $self, $params ) = @_;
1793 return 0 if !( C4::Context->preference('UseRecalls') );
1795 my $patron = $params->{patron};
1797 my $branchcode = C4::Context->userenv->{'branch'};
1798 if ( C4::Context->preference('CircControl') eq 'PatronLibrary' and $patron ) {
1799 $branchcode = $patron->branchcode;
1802 my @all_items = Koha::Items->search({ biblionumber => $self->biblionumber })->as_list;
1804 # if there are no available items at all, no recall can be placed
1805 return 0 if ( scalar @all_items == 0 );
1810 my @all_itemnumbers;
1811 foreach my $item ( @all_items ) {
1812 push( @all_itemnumbers, $item->itemnumber );
1813 if ( $item->can_be_recalled({ patron => $patron }) ) {
1814 push( @itemtypes, $item->effective_itemtype );
1815 push( @itemnumbers, $item->itemnumber );
1816 push( @items, $item );
1820 # if there are no recallable items, no recall can be placed
1821 return 0 if ( scalar @items == 0 );
1823 # Check the circulation rule for each relevant itemtype for this biblio
1824 my ( @recalls_allowed, @recalls_per_record, @on_shelf_recalls );
1825 foreach my $itemtype ( @itemtypes ) {
1826 my $rule = Koha::CirculationRules->get_effective_rules({
1827 branchcode => $branchcode,
1828 categorycode => $patron ? $patron->categorycode : undef,
1829 itemtype => $itemtype,
1832 'recalls_per_record',
1836 push( @recalls_allowed, $rule->{recalls_allowed} ) if $rule;
1837 push( @recalls_per_record, $rule->{recalls_per_record} ) if $rule;
1838 push( @on_shelf_recalls, $rule->{on_shelf_recalls} ) if $rule;
1840 my $recalls_allowed = (sort {$b <=> $a} @recalls_allowed)[0]; # take highest
1841 my $recalls_per_record = (sort {$b <=> $a} @recalls_per_record)[0]; # take highest
1842 my %on_shelf_recalls_count = ();
1843 foreach my $count ( @on_shelf_recalls ) {
1844 $on_shelf_recalls_count{$count}++;
1846 my $on_shelf_recalls = (sort {$on_shelf_recalls_count{$b} <=> $on_shelf_recalls_count{$a}} @on_shelf_recalls)[0]; # take most common
1848 # check recalls allowed has been set and is not zero
1849 return 0 if ( !defined($recalls_allowed) || $recalls_allowed == 0 );
1852 # check borrower has not reached open recalls allowed limit
1853 return 0 if ( $patron->recalls->filter_by_current->count >= $recalls_allowed );
1855 # check borrower has not reached open recalls allowed per record limit
1856 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $recalls_per_record );
1858 # check if any of the items under this biblio are already checked out by this borrower
1859 return 0 if ( Koha::Checkouts->search({ itemnumber => [ @all_itemnumbers ], borrowernumber => $patron->borrowernumber })->count > 0 );
1862 # check item availability
1863 my $checked_out_count = 0;
1865 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1868 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1869 return 0 if ( $on_shelf_recalls eq 'all' && $checked_out_count < scalar @items );
1871 # can't recall if no items have been checked out
1872 return 0 if ( $checked_out_count == 0 );
1880 my $ratings = $biblio->ratings
1882 Return a Koha::Ratings object representing the ratings of this bibliographic record
1888 my $rs = $self->_result->ratings;
1889 return Koha::Ratings->_new_from_dbic($rs);
1892 =head3 opac_summary_html
1894 my $summary_html = $biblio->opac_summary_html
1896 Based on the syspref OPACMySummaryHTML, returns a string representing the
1897 summary of this bibliographic record.
1898 {AUTHOR}, {TITLE}, {ISBN} and {BIBLIONUMBER} will be replaced.
1902 sub opac_summary_html {
1905 my $summary_html = C4::Context->preference('OPACMySummaryHTML');
1906 return q{} unless $summary_html;
1907 my $author = $self->author || q{};
1908 my $title = $self->title || q{};
1909 $title =~ s/\/+$//; # remove trailing slash
1910 $title =~ s/\s+$//; # remove trailing space
1911 my $normalized_isbn = $self->normalized_isbn || q{};
1912 my $biblionumber = $self->biblionumber;
1914 $summary_html =~ s/{AUTHOR}/$author/g;
1915 $summary_html =~ s/{TITLE}/$title/g;
1916 $summary_html =~ s/{ISBN}/$normalized_isbn/g;
1917 $summary_html =~ s/{BIBLIONUMBER}/$biblionumber/g;
1919 return $summary_html;
1922 =head2 Internal methods
1934 Kyle M Hall <kyle@bywatersolutions.com>