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;
41 use Koha::CirculationRules;
43 use Koha::Illrequests;
44 use Koha::Item::Transfer::Limits;
47 use Koha::Old::Checkouts;
50 use Koha::RecordProcessor;
51 use Koha::Suggestions;
52 use Koha::Subscriptions;
53 use Koha::SearchEngine;
54 use Koha::SearchEngine::Search;
55 use Koha::SearchEngine::QueryBuilder;
60 Koha::Biblio - Koha Biblio Object class
70 Overloaded I<store> method to set default values
77 $self->datecreated( dt_from_string ) unless $self->datecreated;
79 return $self->SUPER::store;
84 my $metadata = $biblio->metadata();
86 Returns a Koha::Biblio::Metadata object
93 my $metadata = $self->_result->metadata;
94 return Koha::Biblio::Metadata->_new_from_dbic($metadata);
99 my $record = $biblio->record();
101 Returns a Marc::Record object
108 return $self->metadata->record;
113 my $schema = $biblio->record_schema();
115 Returns the record schema (MARC21, USMARC or UNIMARC).
122 return $self->metadata->schema // C4::Context->preference("marcflavour");
127 my $orders = $biblio->orders();
129 Returns a Koha::Acquisition::Orders object
136 my $orders = $self->_result->orders;
137 return Koha::Acquisition::Orders->_new_from_dbic($orders);
142 my $active_orders = $biblio->active_orders();
144 Returns the active acquisition orders related to this biblio.
145 An order is considered active when it is not cancelled (i.e. when datecancellation
153 return $self->orders->search({ datecancellationprinted => undef });
158 my $tickets = $biblio->tickets();
160 Returns all tickets linked to the biblio
166 my $rs = $self->_result->tickets;
167 return Koha::Tickets->_new_from_dbic( $rs );
172 my $ill_requests = $biblio->ill_requests();
174 Returns a Koha::Illrequests object
181 my $ill_requests = $self->_result->ill_requests;
182 return Koha::Illrequests->_new_from_dbic($ill_requests);
187 my $item_groups = $biblio->item_groups();
189 Returns a Koha::Biblio::ItemGroups object
196 my $item_groups = $self->_result->item_groups;
197 return Koha::Biblio::ItemGroups->_new_from_dbic($item_groups);
200 =head3 can_article_request
202 my $bool = $biblio->can_article_request( $borrower );
204 Returns true if article requests can be made for this record
206 $borrower must be a Koha::Patron object
210 sub can_article_request {
211 my ( $self, $borrower ) = @_;
213 my $rule = $self->article_request_type($borrower);
214 return q{} if $rule eq 'item_only' && !$self->items()->count();
215 return 1 if $rule && $rule ne 'no';
220 =head3 can_be_transferred
222 $biblio->can_be_transferred({ to => $to_library, from => $from_library })
224 Checks if at least one item of a biblio can be transferred to given library.
226 This feature is controlled by two system preferences:
227 UseBranchTransferLimits to enable / disable the feature
228 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
229 for setting the limitations
231 Performance-wise, it is recommended to use this method for a biblio instead of
232 iterating each item of a biblio with Koha::Item->can_be_transferred().
234 Takes HASHref that can have the following parameters:
235 MANDATORY PARAMETERS:
238 $from : Koha::Library # if given, only items from that
239 # holdingbranch are considered
241 Returns 1 if at least one of the item of a biblio can be transferred
242 to $to_library, otherwise 0.
246 sub can_be_transferred {
247 my ($self, $params) = @_;
249 my $to = $params->{to};
250 my $from = $params->{from};
252 return 1 unless C4::Context->preference('UseBranchTransferLimits');
253 my $limittype = C4::Context->preference('BranchTransferLimitsType');
256 foreach my $item_of_bib ($self->items->as_list) {
257 next unless $item_of_bib->holdingbranch;
258 next if $from && $from->branchcode ne $item_of_bib->holdingbranch;
259 return 1 if $item_of_bib->holdingbranch eq $to->branchcode;
260 my $code = $limittype eq 'itemtype'
261 ? $item_of_bib->effective_itemtype
262 : $item_of_bib->ccode;
263 return 1 unless $code;
264 $items->{$code}->{$item_of_bib->holdingbranch} = 1;
267 # At this point we will have a HASHref containing each itemtype/ccode that
268 # this biblio has, inside which are all of the holdingbranches where those
269 # items are located at. Then, we will query Koha::Item::Transfer::Limits to
270 # find out whether a transfer limits for such $limittype from any of the
271 # listed holdingbranches to the given $to library exist. If at least one
272 # holdingbranch for that $limittype does not have a transfer limit to given
273 # $to library, then we know that the transfer is possible.
274 foreach my $code (keys %{$items}) {
275 my @holdingbranches = keys %{$items->{$code}};
276 return 1 if Koha::Item::Transfer::Limits->search({
277 toBranch => $to->branchcode,
278 fromBranch => { 'in' => \@holdingbranches },
281 group_by => [qw/fromBranch/]
282 })->count == scalar(@holdingbranches) ? 0 : 1;
289 =head3 pickup_locations
291 my $pickup_locations = $biblio->pickup_locations({ patron => $patron });
293 Returns a Koha::Libraries set of possible pickup locations for this biblio's items,
294 according to patron's home library and if item can be transferred to each pickup location.
296 Throws a I<Koha::Exceptions::MissingParameter> exception if the B<mandatory> parameter I<patron>
301 sub pickup_locations {
302 my ( $self, $params ) = @_;
304 Koha::Exceptions::MissingParameter->throw( parameter => 'patron' )
305 unless exists $params->{patron};
307 my $patron = $params->{patron};
309 my $memory_cache = Koha::Cache::Memory::Lite->get_instance();
310 my @pickup_locations;
311 foreach my $item ( $self->items->as_list ) {
312 my $cache_key = sprintf "Pickup_locations:%s:%s:%s:%s:%s",
313 $item->itype,$item->homebranch,$item->holdingbranch,$item->ccode || "",$patron->branchcode||"" ;
314 my $item_pickup_locations = $memory_cache->get_from_cache( $cache_key );
315 unless( $item_pickup_locations ){
316 @{ $item_pickup_locations } = $item->pickup_locations( { patron => $patron } )->_resultset->get_column('branchcode')->all;
317 $memory_cache->set_in_cache( $cache_key, $item_pickup_locations );
319 push @pickup_locations, @{ $item_pickup_locations }
322 return Koha::Libraries->search(
323 { branchcode => { '-in' => \@pickup_locations } }, { order_by => ['branchname'] } );
326 =head3 hidden_in_opac
328 my $bool = $biblio->hidden_in_opac({ [ rules => $rules ] })
330 Returns true if the biblio matches the hidding criteria defined in $rules.
331 Returns false otherwise. It involves the I<OpacHiddenItems> and
332 I<OpacHiddenItemsHidesRecord> system preferences.
334 Takes HASHref that can have the following parameters:
336 $rules : { <field> => [ value_1, ... ], ... }
338 Note: $rules inherits its structure from the parsed YAML from reading
339 the I<OpacHiddenItems> system preference.
344 my ( $self, $params ) = @_;
346 my $rules = $params->{rules} // {};
348 my @items = $self->items->as_list;
350 return 0 unless @items; # Do not hide if there is no item
352 # Ok, there are items, don't even try the rules unless OpacHiddenItemsHidesRecord
353 return 0 unless C4::Context->preference('OpacHiddenItemsHidesRecord');
355 return !(any { !$_->hidden_in_opac({ rules => $rules }) } @items);
358 =head3 article_request_type
360 my $type = $biblio->article_request_type( $borrower );
362 Returns the article request type based on items, or on the record
363 itself if there are no items.
365 $borrower must be a Koha::Patron object
369 sub article_request_type {
370 my ( $self, $borrower ) = @_;
372 return q{} unless $borrower;
374 my $rule = $self->article_request_type_for_items( $borrower );
375 return $rule if $rule;
377 # If the record has no items that are requestable, go by the record itemtype
378 $rule = $self->article_request_type_for_bib($borrower);
379 return $rule if $rule;
384 =head3 article_request_type_for_bib
386 my $type = $biblio->article_request_type_for_bib
388 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record
392 sub article_request_type_for_bib {
393 my ( $self, $borrower ) = @_;
395 return q{} unless $borrower;
397 my $borrowertype = $borrower->categorycode;
398 my $itemtype = $self->itemtype();
400 my $rule = Koha::CirculationRules->get_effective_rule(
402 rule_name => 'article_requests',
403 categorycode => $borrowertype,
404 itemtype => $itemtype,
408 return q{} unless $rule;
409 return $rule->rule_value || q{}
412 =head3 article_request_type_for_items
414 my $type = $biblio->article_request_type_for_items
416 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record's items
418 If there is a conflict where some items are 'bib_only' and some are 'item_only', 'bib_only' will be returned.
422 sub article_request_type_for_items {
423 my ( $self, $borrower ) = @_;
426 foreach my $item ( $self->items()->as_list() ) {
427 my $rule = $item->article_request_type($borrower);
428 return $rule if $rule eq 'bib_only'; # we don't need to go any further
432 return 'item_only' if $counts->{item_only};
433 return 'yes' if $counts->{yes};
434 return 'no' if $counts->{no};
438 =head3 article_requests
440 my $article_requests = $biblio->article_requests
442 Returns the article requests associated with this biblio
446 sub article_requests {
449 return Koha::ArticleRequests->_new_from_dbic( scalar $self->_result->article_requests );
452 =head3 current_checkouts
454 my $current_checkouts = $biblio->current_checkouts
456 Returns the current checkouts associated with this biblio
460 sub current_checkouts {
463 return Koha::Checkouts->search( { "item.biblionumber" => $self->id },
464 { join => 'item' } );
469 my $old_checkouts = $biblio->old_checkouts
471 Returns the past checkouts associated with this biblio
478 return Koha::Old::Checkouts->search( { "item.biblionumber" => $self->id },
479 { join => 'item' } );
484 my $items = $biblio->items({ [ host_items => 1 ] });
486 The optional param host_items allows you to include 'analytical' items.
488 Returns the related Koha::Items object for this biblio
493 my ($self,$params) = @_;
495 my $items_rs = $self->_result->items;
497 return Koha::Items->_new_from_dbic( $items_rs ) unless $params->{host_items};
499 my @itemnumbers = $items_rs->get_column('itemnumber')->all;
500 my $host_itemnumbers = $self->_host_itemnumbers();
501 push @itemnumbers, @{ $host_itemnumbers };
502 return Koha::Items->search({ "me.itemnumber" => { -in => \@itemnumbers } });
507 my $host_items = $biblio->host_items();
509 Return the host items (easy analytical record)
516 return Koha::Items->new->empty
517 unless C4::Context->preference('EasyAnalyticalRecords');
519 my $host_itemnumbers = $self->_host_itemnumbers;
521 return Koha::Items->search( { itemnumber => { -in => $host_itemnumbers } } );
524 =head3 _host_itemnumbers
526 my $host_itemnumber = $biblio->_host_itemnumbers();
528 Return the itemnumbers for analytical items on this record
532 sub _host_itemnumbers {
535 my $marcflavour = C4::Context->preference("marcflavour");
536 my $analyticfield = '773';
537 if ( $marcflavour eq 'UNIMARC' ) {
538 $analyticfield = '461';
540 my $marc_record = $self->metadata->record;
542 foreach my $field ( $marc_record->field($analyticfield) ) {
543 push @itemnumbers, $field->subfield('9');
545 return \@itemnumbers;
551 my $itemtype = $biblio->itemtype();
553 Returns the itemtype for this record.
560 return $self->biblioitem()->itemtype();
565 my $holds = $biblio->holds();
567 return the current holds placed on this record
572 my ( $self, $params, $attributes ) = @_;
573 $attributes->{order_by} = 'priority' unless exists $attributes->{order_by};
574 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
575 return Koha::Holds->_new_from_dbic($hold_rs);
580 my $holds = $biblio->current_holds
582 Return the holds placed on this bibliographic record.
583 It does not include future holds.
589 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
591 { reservedate => { '<=' => $dtf->format_date(dt_from_string) } } );
596 my $field = $self->biblioitem
598 Returns the related Koha::Biblioitem object for this Biblio object
604 return Koha::Biblioitems->find( { biblionumber => $self->biblionumber } );
609 my $suggestions = $self->suggestions
611 Returns the related Koha::Suggestions object for this Biblio object
618 my $suggestions_rs = $self->_result->suggestions;
619 return Koha::Suggestions->_new_from_dbic( $suggestions_rs );
622 =head3 get_marc_components
624 my $components = $self->get_marc_components();
626 Returns an array of search results data, which are component parts of
627 this object (MARC21 773 points to this)
631 sub get_marc_components {
632 my ($self, $max_results) = @_;
634 return [] if (C4::Context->preference('marcflavour') ne 'MARC21');
636 my ( $searchstr, $sort ) = $self->get_components_query;
639 if (defined($searchstr)) {
640 my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
641 my ( $error, $results, $facets );
643 ( $error, $results, $facets ) = $searcher->search_compat( $searchstr, undef, [$sort], ['biblioserver'], $max_results, 0, undef, undef, 'ccl', 0 );
648 warn "Warning from search_compat: '$error'";
652 message => 'component_search',
657 $components = $results->{biblioserver}->{RECORDS} if defined($results) && $results->{biblioserver}->{hits};
660 return $components // [];
663 =head2 get_components_query
665 Returns a query which can be used to search for all component parts of MARC21 biblios
669 sub get_components_query {
672 my $builder = Koha::SearchEngine::QueryBuilder->new(
673 { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
674 my $marc = $self->metadata->record;
675 my $component_sort_field = C4::Context->preference('ComponentSortField') // "title";
676 my $component_sort_order = C4::Context->preference('ComponentSortOrder') // "asc";
677 my $sort = $component_sort_field . "_" . $component_sort_order;
680 if ( C4::Context->preference('UseControlNumber') ) {
681 my $pf001 = $marc->field('001') || undef;
683 if ( defined($pf001) ) {
685 my $pf003 = $marc->field('003') || undef;
687 if ( !defined($pf003) ) {
688 # search for 773$w='Host001'
689 $searchstr .= "rcn:\"" . $pf001->data()."\"";
693 # search for (773$w='Host001' and 003='Host003') or 773$w='(Host003)Host001'
694 $searchstr .= "(rcn:\"" . $pf001->data() . "\" AND cni:\"" . $pf003->data() . "\")";
695 $searchstr .= " OR rcn:\"" . $pf003->data() . " " . $pf001->data() . "\"";
699 # limit to monograph and serial component part records
700 $searchstr .= " AND (bib-level:a OR bib-level:b)";
705 my $cleaned_title = $marc->subfield('245', "a");
706 $cleaned_title =~ tr|/||;
707 $cleaned_title = $builder->clean_search_term($cleaned_title);
708 $searchstr = qq#Host-item:("$cleaned_title")#;
710 my ($error, $query ,$query_str) = $builder->build_query_compat( undef, [$searchstr], undef, undef, [$sort], 0 );
716 return ($query, $query_str, $sort);
719 =head3 get_marc_volumes
721 my $volumes = $self->get_marc_volumes();
723 Returns an array of MARCXML data, which are volumes parts of
724 this object (MARC21 773$w points to this)
728 sub get_marc_volumes {
729 my ( $self, $max_results ) = @_;
731 return $self->{_volumes} if defined( $self->{_volumes} );
733 my $searchstr = $self->get_volumes_query;
735 if ( defined($searchstr) ) {
736 my $searcher = Koha::SearchEngine::Search->new( { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
737 my ( $errors, $results, $total_hits ) = $searcher->simple_search_compat( $searchstr, 0, $max_results );
739 ( defined($results) && scalar(@$results) ) ? $results : [];
741 $self->{_volumes} = [];
744 return $self->{_volumes};
747 =head2 get_volumes_query
749 Returns a query which can be used to search for all component parts of MARC21 biblios
753 sub get_volumes_query {
756 # MARC21 Only for now
757 return if ( C4::Context->preference('marcflavour') ne 'MARC21' );
759 my $marc = $self->metadata->record;
761 # Only build volumes query if we're in a 'Set' record
762 # or we have a monographic series.
763 my $leader19 = substr( $marc->leader, 19, 1 );
764 my $pf008 = $marc->field('008') || '';
765 my $mseries = ( $pf008 && substr( $pf008->data(), 21, 1 ) eq 'm' ) ? 1 : 0;
766 return unless ( $leader19 eq 'a' || $mseries );
768 my $builder = Koha::SearchEngine::QueryBuilder->new( { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
771 if ( C4::Context->preference('UseControlNumber') ) {
772 my $pf001 = $marc->field('001') || undef;
774 if ( defined($pf001) ) {
776 my $pf003 = $marc->field('003') || undef;
778 if ( !defined($pf003) ) {
780 # search for 773$w='Host001'
781 $searchstr .= "rcn:" . $pf001->data();
785 # search for (773$w='Host001' and 003='Host003') or 773$w='(Host003)Host001'
786 $searchstr .= "(rcn:" . $pf001->data() . " AND cni:" . $pf003->data() . ")";
787 $searchstr .= " OR rcn:\"" . $pf003->data() . " " . $pf001->data() . "\"";
791 # exclude monograph and serial component part records
792 $searchstr .= " NOT (bib-level:a OR bib-level:b)";
796 my $cleaned_title = $marc->subfield( '245', "a" );
797 $cleaned_title =~ tr|/||;
798 $cleaned_title = $builder->clean_search_term($cleaned_title);
799 $searchstr = "ti,phr:($cleaned_title)";
807 my $subscriptions = $self->subscriptions
809 Returns the related Koha::Subscriptions object for this Biblio object
815 my $rs = $self->_result->subscriptions;
816 return Koha::Subscriptions->_new_from_dbic($rs);
819 =head3 has_items_waiting_or_intransit
821 my $itemsWaitingOrInTransit = $biblio->has_items_waiting_or_intransit
823 Tells if this bibliographic record has items waiting or in transit.
827 sub has_items_waiting_or_intransit {
830 if ( Koha::Holds->search({ biblionumber => $self->id,
831 found => ['W', 'T'] })->count ) {
835 foreach my $item ( $self->items->as_list ) {
836 return 1 if $item->get_transfer;
844 my $coins = $biblio->get_coins;
846 Returns the COinS (a span) which can be included in a biblio record
853 my $record = $self->metadata->record;
855 my $pos7 = substr $record->leader(), 7, 1;
856 my $pos6 = substr $record->leader(), 6, 1;
859 my ( $aulast, $aufirst ) = ( '', '' );
870 # For the purposes of generating COinS metadata, LDR/06-07 can be
871 # considered the same for UNIMARC and MARC21
880 'i' => 'audioRecording',
881 'j' => 'audioRecording',
884 'm' => 'computerProgram',
889 'a' => 'journalArticle',
893 $genre = $fmts6->{$pos6} ? $fmts6->{$pos6} : 'book';
895 if ( $genre eq 'book' ) {
896 $genre = $fmts7->{$pos7} if $fmts7->{$pos7};
899 ##### We must transform mtx to a valable mtx and document type ####
900 if ( $genre eq 'book' ) {
903 } elsif ( $genre eq 'journal' ) {
906 } elsif ( $genre eq 'journalArticle' ) {
914 if ( C4::Context->preference("marcflavour") eq "UNIMARC" ) {
917 $aulast = $record->subfield( '700', 'a' ) || '';
918 $aufirst = $record->subfield( '700', 'b' ) || '';
919 push @authors, "$aufirst $aulast" if ($aufirst or $aulast);
922 if ( $record->field('200') ) {
923 for my $au ( $record->field('200')->subfield('g') ) {
928 $title = $record->subfield( '200', 'a' );
929 my $subfield_210d = $record->subfield('210', 'd');
930 if ($subfield_210d and $subfield_210d =~ /(\d{4})/) {
933 $publisher = $record->subfield( '210', 'c' ) || '';
934 $isbn = $record->subfield( '010', 'a' ) || '';
935 $issn = $record->subfield( '011', 'a' ) || '';
938 # MARC21 need some improve
941 if ( $record->field('100') ) {
942 push @authors, $record->subfield( '100', 'a' );
946 if ( $record->field('700') ) {
947 for my $au ( $record->field('700')->subfield('a') ) {
951 $title = $record->field('245');
952 $title &&= $title->as_string('ab');
953 if ($titletype eq 'a') {
954 $pubyear = $record->field('008') || '';
955 $pubyear = substr($pubyear->data(), 7, 4) if $pubyear;
956 $isbn = $record->subfield( '773', 'z' ) || '';
957 $issn = $record->subfield( '773', 'x' ) || '';
958 $hosttitle = $record->subfield( '773', 't' ) || $record->subfield( '773', 'a') || q{};
959 my @rels = $record->subfield( '773', 'g' );
960 $pages = join(', ', @rels);
962 $pubyear = $record->subfield( '260', 'c' ) || '';
963 $publisher = $record->subfield( '260', 'b' ) || '';
964 $isbn = $record->subfield( '020', 'a' ) || '';
965 $issn = $record->subfield( '022', 'a' ) || '';
971 [ 'ctx_ver', 'Z39.88-2004' ],
972 [ 'rft_val_fmt', "info:ofi/fmt:kev:mtx:$mtx" ],
973 [ ($mtx eq 'dc' ? 'rft.type' : 'rft.genre'), $genre ],
974 [ "rft.${titletype}title", $title ],
977 # rft.title is authorized only once, so by checking $titletype
978 # we ensure that rft.title is not already in the list.
979 if ($hosttitle and $titletype) {
980 push @params, [ 'rft.title', $hosttitle ];
984 [ 'rft.isbn', $isbn ],
985 [ 'rft.issn', $issn ],
988 # If it's a subscription, these informations have no meaning.
989 if ($genre ne 'journal') {
991 [ 'rft.aulast', $aulast ],
992 [ 'rft.aufirst', $aufirst ],
993 (map { [ 'rft.au', $_ ] } @authors),
994 [ 'rft.pub', $publisher ],
995 [ 'rft.date', $pubyear ],
996 [ 'rft.pages', $pages ],
1000 my $coins_value = join( '&',
1001 map { $$_[1] ? $$_[0] . '=' . uri_escape_utf8( $$_[1] ) : () } @params );
1003 return $coins_value;
1008 my $url = $biblio->get_openurl;
1010 Returns url for OpenURL resolver set in OpenURLResolverURL system preference
1017 my $OpenURLResolverURL = C4::Context->preference('OpenURLResolverURL');
1019 if ($OpenURLResolverURL) {
1020 my $uri = URI->new($OpenURLResolverURL);
1022 if (not defined $uri->query) {
1023 $OpenURLResolverURL .= '?';
1025 $OpenURLResolverURL .= '&';
1027 $OpenURLResolverURL .= $self->get_coins;
1030 return $OpenURLResolverURL;
1035 my $serial = $biblio->is_serial
1037 Return boolean true if this bibbliographic record is continuing resource
1044 return 1 if $self->serial;
1046 my $record = $self->metadata->record;
1047 return 1 if substr($record->leader, 7, 1) eq 's';
1052 =head3 custom_cover_image_url
1054 my $image_url = $biblio->custom_cover_image_url
1056 Return the specific url of the cover image for this bibliographic record.
1057 It is built regaring the value of the system preference CustomCoverImagesURL
1061 sub custom_cover_image_url {
1063 my $url = C4::Context->preference('CustomCoverImagesURL');
1064 if ( $url =~ m|{isbn}| ) {
1065 my $isbn = $self->biblioitem->isbn;
1066 return unless $isbn;
1067 $url =~ s|{isbn}|$isbn|g;
1069 if ( $url =~ m|{normalized_isbn}| ) {
1070 my $normalized_isbn = $self->normalized_isbn;
1071 return unless $normalized_isbn;
1072 $url =~ s|{normalized_isbn}|$normalized_isbn|g;
1074 if ( $url =~ m|{issn}| ) {
1075 my $issn = $self->biblioitem->issn;
1076 return unless $issn;
1077 $url =~ s|{issn}|$issn|g;
1080 my $re = qr|{(?<field>\d{3})(\$(?<subfield>.))?}|;
1081 if ( $url =~ $re ) {
1082 my $field = $+{field};
1083 my $subfield = $+{subfield};
1084 my $marc_record = $self->metadata->record;
1087 $value = $marc_record->subfield( $field, $subfield );
1089 my $controlfield = $marc_record->field($field);
1090 $value = $controlfield->data() if $controlfield;
1092 return unless $value;
1093 $url =~ s|$re|$value|;
1101 Return the cover images associated with this biblio.
1108 my $cover_images_rs = $self->_result->cover_images;
1109 return unless $cover_images_rs;
1110 return Koha::CoverImages->_new_from_dbic($cover_images_rs);
1113 =head3 get_marc_notes
1115 $marcnotesarray = $biblio->get_marc_notes({ opac => 1 });
1117 Get all notes from the MARC record and returns them in an array.
1118 The notes are stored in different fields depending on MARC flavour.
1119 MARC21 5XX $u subfields receive special attention as they are URIs.
1123 sub get_marc_notes {
1124 my ( $self, $params ) = @_;
1126 my $marcflavour = C4::Context->preference('marcflavour');
1127 my $opac = $params->{opac} // '0';
1128 my $interface = $params->{opac} ? 'opac' : 'intranet';
1130 my $record = $params->{record} // $self->metadata->record;
1131 my $record_processor = Koha::RecordProcessor->new(
1133 filters => [ 'ViewPolicy', 'ExpandCodedFields' ],
1135 interface => $interface,
1136 frameworkcode => $self->frameworkcode
1140 $record_processor->process($record);
1142 my $scope = $marcflavour eq "UNIMARC"? '3..': '5..';
1143 #MARC21 specs indicate some notes should be private if first indicator 0
1144 my %maybe_private = (
1152 my %hiddenlist = map { $_ => 1 }
1153 split( /,/, C4::Context->preference('NotesToHide'));
1156 foreach my $field ( $record->field($scope) ) {
1157 my $tag = $field->tag();
1158 next if $hiddenlist{ $tag };
1159 next if $opac && $maybe_private{$tag} && !$field->indicator(1);
1160 if( $marcflavour ne 'UNIMARC' && $field->subfield('u') ) {
1161 # Field 5XX$u always contains URI
1162 # Examples: 505u, 506u, 510u, 514u, 520u, 530u, 538u, 540u, 542u, 552u, 555u, 561u, 563u, 583u
1163 # We first push the other subfields, then all $u's separately
1164 # Leave further actions to the template (see e.g. opac-detail)
1166 join '', ( 'a' .. 't', 'v' .. 'z', '0' .. '9' ); # excl 'u'
1167 push @marcnotes, { marcnote => $field->as_string($othersub) };
1168 foreach my $sub ( $field->subfield('u') ) {
1169 $sub =~ s/^\s+|\s+$//g; # trim
1170 push @marcnotes, { marcnote => $sub, tag => $tag };
1173 push @marcnotes, { marcnote => $field->as_string(), tag => $tag };
1179 =head3 _get_marc_authors
1181 Private method to return the list of authors contained in the MARC record.
1182 See get get_marc_contributors and get_marc_authors for the public methods.
1186 sub _get_marc_authors {
1187 my ( $self, $params ) = @_;
1189 my $fields_filter = $params->{fields_filter};
1190 my $mintag = $params->{mintag};
1191 my $maxtag = $params->{maxtag};
1193 my $AuthoritySeparator = C4::Context->preference('AuthoritySeparator');
1194 my $marcflavour = C4::Context->preference('marcflavour');
1196 # tagslib useful only for UNIMARC author responsibilities
1197 my $tagslib = $marcflavour eq "UNIMARC"
1198 ? C4::Biblio::GetMarcStructure( 1, $self->frameworkcode, { unsafe => 1 } )
1202 foreach my $field ( $self->metadata->record->field($fields_filter) ) {
1205 if $mintag && $field->tag() < $mintag
1206 || $maxtag && $field->tag() > $maxtag;
1210 my @subfields = $field->subfields();
1213 # if there is an authority link, build the link with Koha-Auth-Number: subfield9
1214 my $subfield9 = $field->subfield('9');
1216 my $linkvalue = $subfield9;
1217 $linkvalue =~ s/(\(|\))//g;
1218 @link_loop = ( { 'limit' => 'an', 'link' => $linkvalue } );
1223 for my $authors_subfield (@subfields) {
1224 next if ( $authors_subfield->[0] eq '9' );
1226 # unimarc3 contains the $3 of the author for UNIMARC.
1227 # For french academic libraries, it's the "ppn", and it's required for idref webservice
1228 $unimarc3 = $authors_subfield->[1] if $marcflavour eq 'UNIMARC' and $authors_subfield->[0] =~ /3/;
1230 # don't load unimarc subfields 3, 5
1231 next if ( $marcflavour eq 'UNIMARC' and ( $authors_subfield->[0] =~ /3|5/ ) );
1233 my $code = $authors_subfield->[0];
1234 my $value = $authors_subfield->[1];
1235 my $linkvalue = $value;
1236 $linkvalue =~ s/(\(|\))//g;
1237 # UNIMARC author responsibility
1238 if ( $marcflavour eq 'UNIMARC' and $code eq '4' ) {
1239 $value = C4::Biblio::GetAuthorisedValueDesc( $field->tag(), $code, $value, '', $tagslib );
1240 $linkvalue = "($value)";
1242 # if no authority link, build a search query
1243 unless ($subfield9) {
1246 'link' => $linkvalue,
1247 operator => (scalar @link_loop) ? ' AND ' : undef
1250 my @this_link_loop = @link_loop;
1252 unless ( $code eq '0') {
1253 push @subfields_loop, {
1254 tag => $field->tag(),
1257 link_loop => \@this_link_loop,
1258 separator => (scalar @subfields_loop) ? $AuthoritySeparator : ''
1262 push @marcauthors, {
1263 MARCAUTHOR_SUBFIELDS_LOOP => \@subfields_loop,
1264 authoritylink => $subfield9,
1265 unimarc3 => $unimarc3
1268 return \@marcauthors;
1271 =head3 get_marc_contributors
1273 my $contributors = $biblio->get_marc_contributors;
1275 Get all contributors (but first author) from the MARC record and returns them in an array.
1276 They are stored in different fields depending on MARC flavour (700..720 for MARC21)
1280 sub get_marc_contributors {
1281 my ( $self, $params ) = @_;
1283 my ( $mintag, $maxtag, $fields_filter );
1284 my $marcflavour = C4::Context->preference('marcflavour');
1286 if ( $marcflavour eq "UNIMARC" ) {
1289 $fields_filter = '7..';
1290 } else { # marc21/normarc
1293 $fields_filter = '7..';
1296 return $self->_get_marc_authors(
1298 fields_filter => $fields_filter,
1305 =head3 get_marc_authors
1307 my $authors = $biblio->get_marc_authors;
1309 Get all authors from the MARC record and returns them in an array.
1310 They are stored in different fields depending on MARC flavour
1311 (main author from 100 then secondary authors from 700..720).
1315 sub get_marc_authors {
1316 my ( $self, $params ) = @_;
1318 my ( $mintag, $maxtag, $fields_filter );
1319 my $marcflavour = C4::Context->preference('marcflavour');
1321 if ( $marcflavour eq "UNIMARC" ) {
1322 $fields_filter = '200';
1323 } else { # marc21/normarc
1324 $fields_filter = '100';
1327 my @first_authors = @{$self->_get_marc_authors(
1329 fields_filter => $fields_filter,
1335 my @other_authors = @{$self->get_marc_contributors};
1337 return [@first_authors, @other_authors];
1340 =head3 normalized_isbn
1342 my $normalized_isbn = $biblio->normalized_isbn
1344 Normalizes and returns the first valid ISBN found in the record.
1345 ISBN13 are converted into ISBN10. This is required to get some book cover images.
1349 sub normalized_isbn {
1351 return C4::Koha::GetNormalizedISBN($self->biblioitem->isbn);
1354 =head3 public_read_list
1356 This method returns the list of publicly readable database fields for both API and UI output purposes
1360 sub public_read_list {
1362 'biblionumber', 'frameworkcode', 'author',
1363 'title', 'medium', 'subtitle',
1364 'part_number', 'part_name', 'unititle',
1365 'notes', 'serial', 'seriestitle',
1366 'copyrightdate', 'abstract'
1370 =head3 metadata_extractor
1372 my $extractor = $biblio->metadata_extractor
1374 Return a Koha::Biblio::Metadata::Extractor object to use to extract data from the metadata (ie. MARC record for now)
1378 sub metadata_extractor {
1381 $self->{metadata_extractor} ||= Koha::Biblio::Metadata::Extractor->new( { biblio => $self } );
1383 return $self->{metadata_extractor};
1386 =head3 normalized_upc
1388 my $normalized_upc = $biblio->normalized_upc
1390 Normalizes and returns the UPC value found in the MARC record.
1394 sub normalized_upc {
1396 return $self->metadata_extractor->get_normalized_upc;
1399 =head3 normalized_oclc
1401 my $normalized_oclc = $biblio->normalized_oclc
1403 Normalizes and returns the OCLC number found in the MARC record.
1407 sub normalized_oclc {
1409 return $self->metadata_extractor->get_normalized_oclc;
1414 my $json = $biblio->to_api;
1416 Overloaded method that returns a JSON representation of the Koha::Biblio object,
1417 suitable for API output. The related Koha::Biblioitem object is merged as expected
1423 my ($self, $args) = @_;
1425 my $response = $self->SUPER::to_api( $args );
1427 $args = defined $args ? {%$args} : {};
1428 delete $args->{embed};
1430 my $biblioitem = $self->biblioitem->to_api( $args );
1432 return { %$response, %$biblioitem };
1435 =head3 to_api_mapping
1437 This method returns the mapping for representing a Koha::Biblio object
1442 sub to_api_mapping {
1444 biblionumber => 'biblio_id',
1445 frameworkcode => 'framework_id',
1446 unititle => 'uniform_title',
1447 seriestitle => 'series_title',
1448 copyrightdate => 'copyright_date',
1449 datecreated => 'creation_date',
1450 deleted_on => undef,
1454 =head3 get_marc_host
1456 $host = $biblio->get_marc_host;
1458 ( $host, $relatedparts, $hostinfo ) = $biblio->get_marc_host;
1460 Returns host biblio record from MARC21 773 (undef if no 773 present).
1461 It looks at the first 773 field with MARCorgCode or only a control
1462 number. Complete $w or numeric part is used to search host record.
1463 The optional parameter no_items triggers a check if $biblio has items.
1464 If there are, the sub returns undef.
1465 Called in list context, it also returns 773$g (related parts).
1467 If there is no $w, we use $0 (host biblionumber) or $9 (host itemnumber)
1468 to search for the host record. If there is also no $0 and no $9, we search
1469 using author and title. Failing all of that, we return an undef host and
1470 form a concatenation of strings with 773$agt for host information,
1471 returned when called in list context.
1476 my ($self, $params) = @_;
1477 my $no_items = $params->{no_items};
1478 return if C4::Context->preference('marcflavour') eq 'UNIMARC'; # TODO
1479 return if $params->{no_items} && $self->items->count > 0;
1482 eval { $record = $self->metadata->record };
1485 # We pick the first $w with your MARCOrgCode or the first $w that has no
1486 # code (between parentheses) at all.
1487 my $orgcode = C4::Context->preference('MARCOrgCode') // q{};
1489 foreach my $f ( $record->field('773') ) {
1490 my $w = $f->subfield('w') or next;
1491 if( $w =~ /^\($orgcode\)\s*(\d+)/i or $w =~ /^\d+/ ) {
1497 my $engine = Koha::SearchEngine::Search->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1499 if ( !$hostfld and $record->subfield('773','t') ) {
1500 # not linked using $w
1501 my $unlinkedf = $record->field('773');
1503 if ( C4::Context->preference("EasyAnalyticalRecords") ) {
1504 if ( $unlinkedf->subfield('0') ) {
1505 # use 773$0 host biblionumber
1506 $bibno = $unlinkedf->subfield('0');
1507 } elsif ( $unlinkedf->subfield('9') ) {
1508 # use 773$9 host itemnumber
1509 my $linkeditemnumber = $unlinkedf->subfield('9');
1510 $bibno = Koha::Items->find( $linkeditemnumber )->biblionumber;
1514 my $host = Koha::Biblios->find($bibno) or return;
1515 return wantarray ? ( $host, $unlinkedf->subfield('g') ) : $host;
1517 # just return plaintext and no host record
1518 my $hostinfo = join( ", ", $unlinkedf->subfield('a'), $unlinkedf->subfield('t'), $unlinkedf->subfield('g') );
1519 return wantarray ? ( undef, $unlinkedf->subfield('g'), $hostinfo ) : undef;
1521 return if !$hostfld;
1522 my $rcn = $hostfld->subfield('w');
1524 # Look for control number with/without orgcode
1525 for my $try (1..2) {
1526 my ( $error, $results, $total_hits ) = $engine->simple_search_compat( 'Control-number='.$rcn, 0,1 );
1527 if( !$error and $total_hits == 1 ) {
1528 $bibno = $engine->extract_biblionumber( $results->[0] );
1531 # Add or remove orgcode for second try
1532 if( $try == 1 && $rcn =~ /\)\s*(\d+)/ ) {
1533 $rcn = $1; # number only
1534 } elsif( $try == 1 && $rcn =~ /^\d+/ ) {
1535 $rcn = "($orgcode)$rcn";
1541 my $host = Koha::Biblios->find($bibno) or return;
1542 return wantarray ? ( $host, $hostfld->subfield('g') ) : $host;
1546 =head3 get_marc_host_only
1548 my $host = $biblio->get_marc_host_only;
1554 sub get_marc_host_only {
1557 my ( $host ) = $self->get_marc_host;
1562 =head3 get_marc_relatedparts_only
1564 my $relatedparts = $biblio->get_marc_relatedparts_only;
1566 Return related parts only
1570 sub get_marc_relatedparts_only {
1573 my ( undef, $relatedparts ) = $self->get_marc_host;
1575 return $relatedparts;
1578 =head3 get_marc_hostinfo_only
1580 my $hostinfo = $biblio->get_marc_hostinfo_only;
1582 Return host info only
1586 sub get_marc_hostinfo_only {
1589 my ( $host, $relatedparts, $hostinfo ) = $self->get_marc_host;
1596 my $recalls = $biblio->recalls;
1598 Return recalls linked to this biblio
1604 return Koha::Recalls->_new_from_dbic( scalar $self->_result->recalls );
1607 =head3 can_be_recalled
1609 my @items_for_recall = $biblio->can_be_recalled({ patron => $patron_object });
1611 Does biblio-level checks and returns the items attached to this biblio that are available for recall
1615 sub can_be_recalled {
1616 my ( $self, $params ) = @_;
1618 return 0 if !( C4::Context->preference('UseRecalls') );
1620 my $patron = $params->{patron};
1622 my $branchcode = C4::Context->userenv->{'branch'};
1623 if ( C4::Context->preference('CircControl') eq 'PatronLibrary' and $patron ) {
1624 $branchcode = $patron->branchcode;
1627 my @all_items = Koha::Items->search({ biblionumber => $self->biblionumber })->as_list;
1629 # if there are no available items at all, no recall can be placed
1630 return 0 if ( scalar @all_items == 0 );
1635 my @all_itemnumbers;
1636 foreach my $item ( @all_items ) {
1637 push( @all_itemnumbers, $item->itemnumber );
1638 if ( $item->can_be_recalled({ patron => $patron }) ) {
1639 push( @itemtypes, $item->effective_itemtype );
1640 push( @itemnumbers, $item->itemnumber );
1641 push( @items, $item );
1645 # if there are no recallable items, no recall can be placed
1646 return 0 if ( scalar @items == 0 );
1648 # Check the circulation rule for each relevant itemtype for this biblio
1649 my ( @recalls_allowed, @recalls_per_record, @on_shelf_recalls );
1650 foreach my $itemtype ( @itemtypes ) {
1651 my $rule = Koha::CirculationRules->get_effective_rules({
1652 branchcode => $branchcode,
1653 categorycode => $patron ? $patron->categorycode : undef,
1654 itemtype => $itemtype,
1657 'recalls_per_record',
1661 push( @recalls_allowed, $rule->{recalls_allowed} ) if $rule;
1662 push( @recalls_per_record, $rule->{recalls_per_record} ) if $rule;
1663 push( @on_shelf_recalls, $rule->{on_shelf_recalls} ) if $rule;
1665 my $recalls_allowed = (sort {$b <=> $a} @recalls_allowed)[0]; # take highest
1666 my $recalls_per_record = (sort {$b <=> $a} @recalls_per_record)[0]; # take highest
1667 my %on_shelf_recalls_count = ();
1668 foreach my $count ( @on_shelf_recalls ) {
1669 $on_shelf_recalls_count{$count}++;
1671 my $on_shelf_recalls = (sort {$on_shelf_recalls_count{$b} <=> $on_shelf_recalls_count{$a}} @on_shelf_recalls)[0]; # take most common
1673 # check recalls allowed has been set and is not zero
1674 return 0 if ( !defined($recalls_allowed) || $recalls_allowed == 0 );
1677 # check borrower has not reached open recalls allowed limit
1678 return 0 if ( $patron->recalls->filter_by_current->count >= $recalls_allowed );
1680 # check borrower has not reached open recalls allowed per record limit
1681 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $recalls_per_record );
1683 # check if any of the items under this biblio are already checked out by this borrower
1684 return 0 if ( Koha::Checkouts->search({ itemnumber => [ @all_itemnumbers ], borrowernumber => $patron->borrowernumber })->count > 0 );
1687 # check item availability
1688 my $checked_out_count = 0;
1690 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1693 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1694 return 0 if ( $on_shelf_recalls eq 'all' && $checked_out_count < scalar @items );
1696 # can't recall if no items have been checked out
1697 return 0 if ( $checked_out_count == 0 );
1705 my $ratings = $biblio->ratings
1707 Return a Koha::Ratings object representing the ratings of this bibliographic record
1713 my $rs = $self->_result->ratings;
1714 return Koha::Ratings->_new_from_dbic($rs);
1717 =head3 opac_summary_html
1719 my $summary_html = $biblio->opac_summary_html
1721 Based on the syspref OPACMySummaryHTML, returns a string representing the
1722 summary of this bibliographic record.
1723 {AUTHOR}, {TITLE}, {ISBN} and {BIBLIONUMBER} will be replaced.
1727 sub opac_summary_html {
1730 my $summary_html = C4::Context->preference('OPACMySummaryHTML');
1731 return q{} unless $summary_html;
1732 my $author = $self->author || q{};
1733 my $title = $self->title || q{};
1734 $title =~ s/\/+$//; # remove trailing slash
1735 $title =~ s/\s+$//; # remove trailing space
1736 my $normalized_isbn = $self->normalized_isbn || q{};
1737 my $biblionumber = $self->biblionumber;
1739 $summary_html =~ s/{AUTHOR}/$author/g;
1740 $summary_html =~ s/{TITLE}/$title/g;
1741 $summary_html =~ s/{ISBN}/$normalized_isbn/g;
1742 $summary_html =~ s/{BIBLIONUMBER}/$biblionumber/g;
1744 return $summary_html;
1747 =head2 Internal methods
1759 Kyle M Hall <kyle@bywatersolutions.com>