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);
721 my $subscriptions = $self->subscriptions
723 Returns the related Koha::Subscriptions object for this Biblio object
729 my $rs = $self->_result->subscriptions;
730 return Koha::Subscriptions->_new_from_dbic($rs);
733 =head3 has_items_waiting_or_intransit
735 my $itemsWaitingOrInTransit = $biblio->has_items_waiting_or_intransit
737 Tells if this bibliographic record has items waiting or in transit.
741 sub has_items_waiting_or_intransit {
744 if ( Koha::Holds->search({ biblionumber => $self->id,
745 found => ['W', 'T'] })->count ) {
749 foreach my $item ( $self->items->as_list ) {
750 return 1 if $item->get_transfer;
758 my $coins = $biblio->get_coins;
760 Returns the COinS (a span) which can be included in a biblio record
767 my $record = $self->metadata->record;
769 my $pos7 = substr $record->leader(), 7, 1;
770 my $pos6 = substr $record->leader(), 6, 1;
773 my ( $aulast, $aufirst ) = ( '', '' );
784 # For the purposes of generating COinS metadata, LDR/06-07 can be
785 # considered the same for UNIMARC and MARC21
794 'i' => 'audioRecording',
795 'j' => 'audioRecording',
798 'm' => 'computerProgram',
803 'a' => 'journalArticle',
807 $genre = $fmts6->{$pos6} ? $fmts6->{$pos6} : 'book';
809 if ( $genre eq 'book' ) {
810 $genre = $fmts7->{$pos7} if $fmts7->{$pos7};
813 ##### We must transform mtx to a valable mtx and document type ####
814 if ( $genre eq 'book' ) {
817 } elsif ( $genre eq 'journal' ) {
820 } elsif ( $genre eq 'journalArticle' ) {
828 if ( C4::Context->preference("marcflavour") eq "UNIMARC" ) {
831 $aulast = $record->subfield( '700', 'a' ) || '';
832 $aufirst = $record->subfield( '700', 'b' ) || '';
833 push @authors, "$aufirst $aulast" if ($aufirst or $aulast);
836 if ( $record->field('200') ) {
837 for my $au ( $record->field('200')->subfield('g') ) {
842 $title = $record->subfield( '200', 'a' );
843 my $subfield_210d = $record->subfield('210', 'd');
844 if ($subfield_210d and $subfield_210d =~ /(\d{4})/) {
847 $publisher = $record->subfield( '210', 'c' ) || '';
848 $isbn = $record->subfield( '010', 'a' ) || '';
849 $issn = $record->subfield( '011', 'a' ) || '';
852 # MARC21 need some improve
855 if ( $record->field('100') ) {
856 push @authors, $record->subfield( '100', 'a' );
860 if ( $record->field('700') ) {
861 for my $au ( $record->field('700')->subfield('a') ) {
865 $title = $record->field('245');
866 $title &&= $title->as_string('ab');
867 if ($titletype eq 'a') {
868 $pubyear = $record->field('008') || '';
869 $pubyear = substr($pubyear->data(), 7, 4) if $pubyear;
870 $isbn = $record->subfield( '773', 'z' ) || '';
871 $issn = $record->subfield( '773', 'x' ) || '';
872 $hosttitle = $record->subfield( '773', 't' ) || $record->subfield( '773', 'a') || q{};
873 my @rels = $record->subfield( '773', 'g' );
874 $pages = join(', ', @rels);
876 $pubyear = $record->subfield( '260', 'c' ) || '';
877 $publisher = $record->subfield( '260', 'b' ) || '';
878 $isbn = $record->subfield( '020', 'a' ) || '';
879 $issn = $record->subfield( '022', 'a' ) || '';
885 [ 'ctx_ver', 'Z39.88-2004' ],
886 [ 'rft_val_fmt', "info:ofi/fmt:kev:mtx:$mtx" ],
887 [ ($mtx eq 'dc' ? 'rft.type' : 'rft.genre'), $genre ],
888 [ "rft.${titletype}title", $title ],
891 # rft.title is authorized only once, so by checking $titletype
892 # we ensure that rft.title is not already in the list.
893 if ($hosttitle and $titletype) {
894 push @params, [ 'rft.title', $hosttitle ];
898 [ 'rft.isbn', $isbn ],
899 [ 'rft.issn', $issn ],
902 # If it's a subscription, these informations have no meaning.
903 if ($genre ne 'journal') {
905 [ 'rft.aulast', $aulast ],
906 [ 'rft.aufirst', $aufirst ],
907 (map { [ 'rft.au', $_ ] } @authors),
908 [ 'rft.pub', $publisher ],
909 [ 'rft.date', $pubyear ],
910 [ 'rft.pages', $pages ],
914 my $coins_value = join( '&',
915 map { $$_[1] ? $$_[0] . '=' . uri_escape_utf8( $$_[1] ) : () } @params );
922 my $url = $biblio->get_openurl;
924 Returns url for OpenURL resolver set in OpenURLResolverURL system preference
931 my $OpenURLResolverURL = C4::Context->preference('OpenURLResolverURL');
933 if ($OpenURLResolverURL) {
934 my $uri = URI->new($OpenURLResolverURL);
936 if (not defined $uri->query) {
937 $OpenURLResolverURL .= '?';
939 $OpenURLResolverURL .= '&';
941 $OpenURLResolverURL .= $self->get_coins;
944 return $OpenURLResolverURL;
949 my $serial = $biblio->is_serial
951 Return boolean true if this bibbliographic record is continuing resource
958 return 1 if $self->serial;
960 my $record = $self->metadata->record;
961 return 1 if substr($record->leader, 7, 1) eq 's';
966 =head3 custom_cover_image_url
968 my $image_url = $biblio->custom_cover_image_url
970 Return the specific url of the cover image for this bibliographic record.
971 It is built regaring the value of the system preference CustomCoverImagesURL
975 sub custom_cover_image_url {
977 my $url = C4::Context->preference('CustomCoverImagesURL');
978 if ( $url =~ m|{isbn}| ) {
979 my $isbn = $self->biblioitem->isbn;
981 $url =~ s|{isbn}|$isbn|g;
983 if ( $url =~ m|{normalized_isbn}| ) {
984 my $normalized_isbn = $self->normalized_isbn;
985 return unless $normalized_isbn;
986 $url =~ s|{normalized_isbn}|$normalized_isbn|g;
988 if ( $url =~ m|{issn}| ) {
989 my $issn = $self->biblioitem->issn;
991 $url =~ s|{issn}|$issn|g;
994 my $re = qr|{(?<field>\d{3})(\$(?<subfield>.))?}|;
996 my $field = $+{field};
997 my $subfield = $+{subfield};
998 my $marc_record = $self->metadata->record;
1001 $value = $marc_record->subfield( $field, $subfield );
1003 my $controlfield = $marc_record->field($field);
1004 $value = $controlfield->data() if $controlfield;
1006 return unless $value;
1007 $url =~ s|$re|$value|;
1015 Return the cover images associated with this biblio.
1022 my $cover_images_rs = $self->_result->cover_images;
1023 return unless $cover_images_rs;
1024 return Koha::CoverImages->_new_from_dbic($cover_images_rs);
1027 =head3 get_marc_notes
1029 $marcnotesarray = $biblio->get_marc_notes({ opac => 1 });
1031 Get all notes from the MARC record and returns them in an array.
1032 The notes are stored in different fields depending on MARC flavour.
1033 MARC21 5XX $u subfields receive special attention as they are URIs.
1037 sub get_marc_notes {
1038 my ( $self, $params ) = @_;
1040 my $marcflavour = C4::Context->preference('marcflavour');
1041 my $opac = $params->{opac} // '0';
1042 my $interface = $params->{opac} ? 'opac' : 'intranet';
1044 my $record = $params->{record} // $self->metadata->record;
1045 my $record_processor = Koha::RecordProcessor->new(
1047 filters => [ 'ViewPolicy', 'ExpandCodedFields' ],
1049 interface => $interface,
1050 frameworkcode => $self->frameworkcode
1054 $record_processor->process($record);
1056 my $scope = $marcflavour eq "UNIMARC"? '3..': '5..';
1057 #MARC21 specs indicate some notes should be private if first indicator 0
1058 my %maybe_private = (
1066 my %hiddenlist = map { $_ => 1 }
1067 split( /,/, C4::Context->preference('NotesToHide'));
1070 foreach my $field ( $record->field($scope) ) {
1071 my $tag = $field->tag();
1072 next if $hiddenlist{ $tag };
1073 next if $opac && $maybe_private{$tag} && !$field->indicator(1);
1074 if( $marcflavour ne 'UNIMARC' && $field->subfield('u') ) {
1075 # Field 5XX$u always contains URI
1076 # Examples: 505u, 506u, 510u, 514u, 520u, 530u, 538u, 540u, 542u, 552u, 555u, 561u, 563u, 583u
1077 # We first push the other subfields, then all $u's separately
1078 # Leave further actions to the template (see e.g. opac-detail)
1080 join '', ( 'a' .. 't', 'v' .. 'z', '0' .. '9' ); # excl 'u'
1081 push @marcnotes, { marcnote => $field->as_string($othersub) };
1082 foreach my $sub ( $field->subfield('u') ) {
1083 $sub =~ s/^\s+|\s+$//g; # trim
1084 push @marcnotes, { marcnote => $sub, tag => $tag };
1087 push @marcnotes, { marcnote => $field->as_string(), tag => $tag };
1093 =head3 _get_marc_authors
1095 Private method to return the list of authors contained in the MARC record.
1096 See get get_marc_contributors and get_marc_authors for the public methods.
1100 sub _get_marc_authors {
1101 my ( $self, $params ) = @_;
1103 my $fields_filter = $params->{fields_filter};
1104 my $mintag = $params->{mintag};
1105 my $maxtag = $params->{maxtag};
1107 my $AuthoritySeparator = C4::Context->preference('AuthoritySeparator');
1108 my $marcflavour = C4::Context->preference('marcflavour');
1110 # tagslib useful only for UNIMARC author responsibilities
1111 my $tagslib = $marcflavour eq "UNIMARC"
1112 ? C4::Biblio::GetMarcStructure( 1, $self->frameworkcode, { unsafe => 1 } )
1116 foreach my $field ( $self->metadata->record->field($fields_filter) ) {
1119 if $mintag && $field->tag() < $mintag
1120 || $maxtag && $field->tag() > $maxtag;
1124 my @subfields = $field->subfields();
1127 # if there is an authority link, build the link with Koha-Auth-Number: subfield9
1128 my $subfield9 = $field->subfield('9');
1130 my $linkvalue = $subfield9;
1131 $linkvalue =~ s/(\(|\))//g;
1132 @link_loop = ( { 'limit' => 'an', 'link' => $linkvalue } );
1137 for my $authors_subfield (@subfields) {
1138 next if ( $authors_subfield->[0] eq '9' );
1140 # unimarc3 contains the $3 of the author for UNIMARC.
1141 # For french academic libraries, it's the "ppn", and it's required for idref webservice
1142 $unimarc3 = $authors_subfield->[1] if $marcflavour eq 'UNIMARC' and $authors_subfield->[0] =~ /3/;
1144 # don't load unimarc subfields 3, 5
1145 next if ( $marcflavour eq 'UNIMARC' and ( $authors_subfield->[0] =~ /3|5/ ) );
1147 my $code = $authors_subfield->[0];
1148 my $value = $authors_subfield->[1];
1149 my $linkvalue = $value;
1150 $linkvalue =~ s/(\(|\))//g;
1151 # UNIMARC author responsibility
1152 if ( $marcflavour eq 'UNIMARC' and $code eq '4' ) {
1153 $value = C4::Biblio::GetAuthorisedValueDesc( $field->tag(), $code, $value, '', $tagslib );
1154 $linkvalue = "($value)";
1156 # if no authority link, build a search query
1157 unless ($subfield9) {
1160 'link' => $linkvalue,
1161 operator => (scalar @link_loop) ? ' AND ' : undef
1164 my @this_link_loop = @link_loop;
1166 unless ( $code eq '0') {
1167 push @subfields_loop, {
1168 tag => $field->tag(),
1171 link_loop => \@this_link_loop,
1172 separator => (scalar @subfields_loop) ? $AuthoritySeparator : ''
1176 push @marcauthors, {
1177 MARCAUTHOR_SUBFIELDS_LOOP => \@subfields_loop,
1178 authoritylink => $subfield9,
1179 unimarc3 => $unimarc3
1182 return \@marcauthors;
1185 =head3 get_marc_contributors
1187 my $contributors = $biblio->get_marc_contributors;
1189 Get all contributors (but first author) from the MARC record and returns them in an array.
1190 They are stored in different fields depending on MARC flavour (700..720 for MARC21)
1194 sub get_marc_contributors {
1195 my ( $self, $params ) = @_;
1197 my ( $mintag, $maxtag, $fields_filter );
1198 my $marcflavour = C4::Context->preference('marcflavour');
1200 if ( $marcflavour eq "UNIMARC" ) {
1203 $fields_filter = '7..';
1204 } else { # marc21/normarc
1207 $fields_filter = '7..';
1210 return $self->_get_marc_authors(
1212 fields_filter => $fields_filter,
1219 =head3 get_marc_authors
1221 my $authors = $biblio->get_marc_authors;
1223 Get all authors from the MARC record and returns them in an array.
1224 They are stored in different fields depending on MARC flavour
1225 (main author from 100 then secondary authors from 700..720).
1229 sub get_marc_authors {
1230 my ( $self, $params ) = @_;
1232 my ( $mintag, $maxtag, $fields_filter );
1233 my $marcflavour = C4::Context->preference('marcflavour');
1235 if ( $marcflavour eq "UNIMARC" ) {
1236 $fields_filter = '200';
1237 } else { # marc21/normarc
1238 $fields_filter = '100';
1241 my @first_authors = @{$self->_get_marc_authors(
1243 fields_filter => $fields_filter,
1249 my @other_authors = @{$self->get_marc_contributors};
1251 return [@first_authors, @other_authors];
1254 =head3 normalized_isbn
1256 my $normalized_isbn = $biblio->normalized_isbn
1258 Normalizes and returns the first valid ISBN found in the record.
1259 ISBN13 are converted into ISBN10. This is required to get some book cover images.
1263 sub normalized_isbn {
1265 return C4::Koha::GetNormalizedISBN($self->biblioitem->isbn);
1268 =head3 public_read_list
1270 This method returns the list of publicly readable database fields for both API and UI output purposes
1274 sub public_read_list {
1276 'biblionumber', 'frameworkcode', 'author',
1277 'title', 'medium', 'subtitle',
1278 'part_number', 'part_name', 'unititle',
1279 'notes', 'serial', 'seriestitle',
1280 'copyrightdate', 'abstract'
1284 =head3 metadata_extractor
1286 my $extractor = $biblio->metadata_extractor
1288 Return a Koha::Biblio::Metadata::Extractor object to use to extract data from the metadata (ie. MARC record for now)
1292 sub metadata_extractor {
1295 $self->{metadata_extractor} ||= Koha::Biblio::Metadata::Extractor->new( { biblio => $self } );
1297 return $self->{metadata_extractor};
1300 =head3 normalized_upc
1302 my $normalized_upc = $biblio->normalized_upc
1304 Normalizes and returns the UPC value found in the MARC record.
1308 sub normalized_upc {
1310 return $self->metadata_extractor->get_normalized_upc;
1313 =head3 normalized_oclc
1315 my $normalized_oclc = $biblio->normalized_oclc
1317 Normalizes and returns the OCLC number found in the MARC record.
1321 sub normalized_oclc {
1323 my $marc_record = $self->metadata->record;
1324 return C4::Koha::GetNormalizedOCLCNumber($marc_record);
1329 my $json = $biblio->to_api;
1331 Overloaded method that returns a JSON representation of the Koha::Biblio object,
1332 suitable for API output. The related Koha::Biblioitem object is merged as expected
1338 my ($self, $args) = @_;
1340 my $response = $self->SUPER::to_api( $args );
1342 $args = defined $args ? {%$args} : {};
1343 delete $args->{embed};
1345 my $biblioitem = $self->biblioitem->to_api( $args );
1347 return { %$response, %$biblioitem };
1350 =head3 to_api_mapping
1352 This method returns the mapping for representing a Koha::Biblio object
1357 sub to_api_mapping {
1359 biblionumber => 'biblio_id',
1360 frameworkcode => 'framework_id',
1361 unititle => 'uniform_title',
1362 seriestitle => 'series_title',
1363 copyrightdate => 'copyright_date',
1364 datecreated => 'creation_date',
1365 deleted_on => undef,
1369 =head3 get_marc_host
1371 $host = $biblio->get_marc_host;
1373 ( $host, $relatedparts, $hostinfo ) = $biblio->get_marc_host;
1375 Returns host biblio record from MARC21 773 (undef if no 773 present).
1376 It looks at the first 773 field with MARCorgCode or only a control
1377 number. Complete $w or numeric part is used to search host record.
1378 The optional parameter no_items triggers a check if $biblio has items.
1379 If there are, the sub returns undef.
1380 Called in list context, it also returns 773$g (related parts).
1382 If there is no $w, we use $0 (host biblionumber) or $9 (host itemnumber)
1383 to search for the host record. If there is also no $0 and no $9, we search
1384 using author and title. Failing all of that, we return an undef host and
1385 form a concatenation of strings with 773$agt for host information,
1386 returned when called in list context.
1391 my ($self, $params) = @_;
1392 my $no_items = $params->{no_items};
1393 return if C4::Context->preference('marcflavour') eq 'UNIMARC'; # TODO
1394 return if $params->{no_items} && $self->items->count > 0;
1397 eval { $record = $self->metadata->record };
1400 # We pick the first $w with your MARCOrgCode or the first $w that has no
1401 # code (between parentheses) at all.
1402 my $orgcode = C4::Context->preference('MARCOrgCode') // q{};
1404 foreach my $f ( $record->field('773') ) {
1405 my $w = $f->subfield('w') or next;
1406 if( $w =~ /^\($orgcode\)\s*(\d+)/i or $w =~ /^\d+/ ) {
1412 my $engine = Koha::SearchEngine::Search->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1414 if ( !$hostfld and $record->subfield('773','t') ) {
1415 # not linked using $w
1416 my $unlinkedf = $record->field('773');
1418 if ( C4::Context->preference("EasyAnalyticalRecords") ) {
1419 if ( $unlinkedf->subfield('0') ) {
1420 # use 773$0 host biblionumber
1421 $bibno = $unlinkedf->subfield('0');
1422 } elsif ( $unlinkedf->subfield('9') ) {
1423 # use 773$9 host itemnumber
1424 my $linkeditemnumber = $unlinkedf->subfield('9');
1425 $bibno = Koha::Items->find( $linkeditemnumber )->biblionumber;
1429 my $host = Koha::Biblios->find($bibno) or return;
1430 return wantarray ? ( $host, $unlinkedf->subfield('g') ) : $host;
1432 # just return plaintext and no host record
1433 my $hostinfo = join( ", ", $unlinkedf->subfield('a'), $unlinkedf->subfield('t'), $unlinkedf->subfield('g') );
1434 return wantarray ? ( undef, $unlinkedf->subfield('g'), $hostinfo ) : undef;
1436 return if !$hostfld;
1437 my $rcn = $hostfld->subfield('w');
1439 # Look for control number with/without orgcode
1440 for my $try (1..2) {
1441 my ( $error, $results, $total_hits ) = $engine->simple_search_compat( 'Control-number='.$rcn, 0,1 );
1442 if( !$error and $total_hits == 1 ) {
1443 $bibno = $engine->extract_biblionumber( $results->[0] );
1446 # Add or remove orgcode for second try
1447 if( $try == 1 && $rcn =~ /\)\s*(\d+)/ ) {
1448 $rcn = $1; # number only
1449 } elsif( $try == 1 && $rcn =~ /^\d+/ ) {
1450 $rcn = "($orgcode)$rcn";
1456 my $host = Koha::Biblios->find($bibno) or return;
1457 return wantarray ? ( $host, $hostfld->subfield('g') ) : $host;
1461 =head3 get_marc_host_only
1463 my $host = $biblio->get_marc_host_only;
1469 sub get_marc_host_only {
1472 my ( $host ) = $self->get_marc_host;
1477 =head3 get_marc_relatedparts_only
1479 my $relatedparts = $biblio->get_marc_relatedparts_only;
1481 Return related parts only
1485 sub get_marc_relatedparts_only {
1488 my ( undef, $relatedparts ) = $self->get_marc_host;
1490 return $relatedparts;
1493 =head3 get_marc_hostinfo_only
1495 my $hostinfo = $biblio->get_marc_hostinfo_only;
1497 Return host info only
1501 sub get_marc_hostinfo_only {
1504 my ( $host, $relatedparts, $hostinfo ) = $self->get_marc_host;
1511 my $recalls = $biblio->recalls;
1513 Return recalls linked to this biblio
1519 return Koha::Recalls->_new_from_dbic( scalar $self->_result->recalls );
1522 =head3 can_be_recalled
1524 my @items_for_recall = $biblio->can_be_recalled({ patron => $patron_object });
1526 Does biblio-level checks and returns the items attached to this biblio that are available for recall
1530 sub can_be_recalled {
1531 my ( $self, $params ) = @_;
1533 return 0 if !( C4::Context->preference('UseRecalls') );
1535 my $patron = $params->{patron};
1537 my $branchcode = C4::Context->userenv->{'branch'};
1538 if ( C4::Context->preference('CircControl') eq 'PatronLibrary' and $patron ) {
1539 $branchcode = $patron->branchcode;
1542 my @all_items = Koha::Items->search({ biblionumber => $self->biblionumber })->as_list;
1544 # if there are no available items at all, no recall can be placed
1545 return 0 if ( scalar @all_items == 0 );
1550 my @all_itemnumbers;
1551 foreach my $item ( @all_items ) {
1552 push( @all_itemnumbers, $item->itemnumber );
1553 if ( $item->can_be_recalled({ patron => $patron }) ) {
1554 push( @itemtypes, $item->effective_itemtype );
1555 push( @itemnumbers, $item->itemnumber );
1556 push( @items, $item );
1560 # if there are no recallable items, no recall can be placed
1561 return 0 if ( scalar @items == 0 );
1563 # Check the circulation rule for each relevant itemtype for this biblio
1564 my ( @recalls_allowed, @recalls_per_record, @on_shelf_recalls );
1565 foreach my $itemtype ( @itemtypes ) {
1566 my $rule = Koha::CirculationRules->get_effective_rules({
1567 branchcode => $branchcode,
1568 categorycode => $patron ? $patron->categorycode : undef,
1569 itemtype => $itemtype,
1572 'recalls_per_record',
1576 push( @recalls_allowed, $rule->{recalls_allowed} ) if $rule;
1577 push( @recalls_per_record, $rule->{recalls_per_record} ) if $rule;
1578 push( @on_shelf_recalls, $rule->{on_shelf_recalls} ) if $rule;
1580 my $recalls_allowed = (sort {$b <=> $a} @recalls_allowed)[0]; # take highest
1581 my $recalls_per_record = (sort {$b <=> $a} @recalls_per_record)[0]; # take highest
1582 my %on_shelf_recalls_count = ();
1583 foreach my $count ( @on_shelf_recalls ) {
1584 $on_shelf_recalls_count{$count}++;
1586 my $on_shelf_recalls = (sort {$on_shelf_recalls_count{$b} <=> $on_shelf_recalls_count{$a}} @on_shelf_recalls)[0]; # take most common
1588 # check recalls allowed has been set and is not zero
1589 return 0 if ( !defined($recalls_allowed) || $recalls_allowed == 0 );
1592 # check borrower has not reached open recalls allowed limit
1593 return 0 if ( $patron->recalls->filter_by_current->count >= $recalls_allowed );
1595 # check borrower has not reached open recalls allowed per record limit
1596 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $recalls_per_record );
1598 # check if any of the items under this biblio are already checked out by this borrower
1599 return 0 if ( Koha::Checkouts->search({ itemnumber => [ @all_itemnumbers ], borrowernumber => $patron->borrowernumber })->count > 0 );
1602 # check item availability
1603 my $checked_out_count = 0;
1605 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1608 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1609 return 0 if ( $on_shelf_recalls eq 'all' && $checked_out_count < scalar @items );
1611 # can't recall if no items have been checked out
1612 return 0 if ( $checked_out_count == 0 );
1620 my $ratings = $biblio->ratings
1622 Return a Koha::Ratings object representing the ratings of this bibliographic record
1628 my $rs = $self->_result->ratings;
1629 return Koha::Ratings->_new_from_dbic($rs);
1632 =head3 opac_summary_html
1634 my $summary_html = $biblio->opac_summary_html
1636 Based on the syspref OPACMySummaryHTML, returns a string representing the
1637 summary of this bibliographic record.
1638 {AUTHOR}, {TITLE}, {ISBN} and {BIBLIONUMBER} will be replaced.
1642 sub opac_summary_html {
1645 my $summary_html = C4::Context->preference('OPACMySummaryHTML');
1646 return q{} unless $summary_html;
1647 my $author = $self->author || q{};
1648 my $title = $self->title || q{};
1649 $title =~ s/\/+$//; # remove trailing slash
1650 $title =~ s/\s+$//; # remove trailing space
1651 my $normalized_isbn = $self->normalized_isbn || q{};
1652 my $biblionumber = $self->biblionumber;
1654 $summary_html =~ s/{AUTHOR}/$author/g;
1655 $summary_html =~ s/{TITLE}/$title/g;
1656 $summary_html =~ s/{ISBN}/$normalized_isbn/g;
1657 $summary_html =~ s/{BIBLIONUMBER}/$biblionumber/g;
1659 return $summary_html;
1662 =head2 Internal methods
1674 Kyle M Hall <kyle@bywatersolutions.com>