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 );
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::ItemGroups;
37 use Koha::Biblioitems;
38 use Koha::Cache::Memory::Lite;
40 use Koha::CirculationRules;
42 use Koha::Illrequests;
43 use Koha::Item::Transfer::Limits;
46 use Koha::Old::Checkouts;
49 use Koha::RecordProcessor;
50 use Koha::Suggestions;
51 use Koha::Subscriptions;
52 use Koha::SearchEngine;
53 use Koha::SearchEngine::Search;
54 use Koha::SearchEngine::QueryBuilder;
59 Koha::Biblio - Koha Biblio Object class
69 Overloaded I<store> method to set default values
76 $self->datecreated( dt_from_string ) unless $self->datecreated;
78 return $self->SUPER::store;
83 my $metadata = $biblio->metadata();
85 Returns a Koha::Biblio::Metadata object
92 my $metadata = $self->_result->metadata;
93 return Koha::Biblio::Metadata->_new_from_dbic($metadata);
98 my $record = $biblio->record();
100 Returns a Marc::Record object
107 return $self->metadata->record;
112 my $schema = $biblio->record_schema();
114 Returns the record schema (MARC21, USMARC or UNIMARC).
121 return $self->metadata->schema // C4::Context->preference("marcflavour");
126 my $orders = $biblio->orders();
128 Returns a Koha::Acquisition::Orders object
135 my $orders = $self->_result->orders;
136 return Koha::Acquisition::Orders->_new_from_dbic($orders);
141 my $active_orders = $biblio->active_orders();
143 Returns the active acquisition orders related to this biblio.
144 An order is considered active when it is not cancelled (i.e. when datecancellation
152 return $self->orders->search({ datecancellationprinted => undef });
157 my $tickets = $biblio->tickets();
159 Returns all tickets linked to the biblio
165 my $rs = $self->_result->tickets;
166 return Koha::Tickets->_new_from_dbic( $rs );
171 my $ill_requests = $biblio->ill_requests();
173 Returns a Koha::Illrequests object
180 my $ill_requests = $self->_result->ill_requests;
181 return Koha::Illrequests->_new_from_dbic($ill_requests);
186 my $item_groups = $biblio->item_groups();
188 Returns a Koha::Biblio::ItemGroups object
195 my $item_groups = $self->_result->item_groups;
196 return Koha::Biblio::ItemGroups->_new_from_dbic($item_groups);
199 =head3 can_article_request
201 my $bool = $biblio->can_article_request( $borrower );
203 Returns true if article requests can be made for this record
205 $borrower must be a Koha::Patron object
209 sub can_article_request {
210 my ( $self, $borrower ) = @_;
212 my $rule = $self->article_request_type($borrower);
213 return q{} if $rule eq 'item_only' && !$self->items()->count();
214 return 1 if $rule && $rule ne 'no';
219 =head3 can_be_transferred
221 $biblio->can_be_transferred({ to => $to_library, from => $from_library })
223 Checks if at least one item of a biblio can be transferred to given library.
225 This feature is controlled by two system preferences:
226 UseBranchTransferLimits to enable / disable the feature
227 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
228 for setting the limitations
230 Performance-wise, it is recommended to use this method for a biblio instead of
231 iterating each item of a biblio with Koha::Item->can_be_transferred().
233 Takes HASHref that can have the following parameters:
234 MANDATORY PARAMETERS:
237 $from : Koha::Library # if given, only items from that
238 # holdingbranch are considered
240 Returns 1 if at least one of the item of a biblio can be transferred
241 to $to_library, otherwise 0.
245 sub can_be_transferred {
246 my ($self, $params) = @_;
248 my $to = $params->{to};
249 my $from = $params->{from};
251 return 1 unless C4::Context->preference('UseBranchTransferLimits');
252 my $limittype = C4::Context->preference('BranchTransferLimitsType');
255 foreach my $item_of_bib ($self->items->as_list) {
256 next unless $item_of_bib->holdingbranch;
257 next if $from && $from->branchcode ne $item_of_bib->holdingbranch;
258 return 1 if $item_of_bib->holdingbranch eq $to->branchcode;
259 my $code = $limittype eq 'itemtype'
260 ? $item_of_bib->effective_itemtype
261 : $item_of_bib->ccode;
262 return 1 unless $code;
263 $items->{$code}->{$item_of_bib->holdingbranch} = 1;
266 # At this point we will have a HASHref containing each itemtype/ccode that
267 # this biblio has, inside which are all of the holdingbranches where those
268 # items are located at. Then, we will query Koha::Item::Transfer::Limits to
269 # find out whether a transfer limits for such $limittype from any of the
270 # listed holdingbranches to the given $to library exist. If at least one
271 # holdingbranch for that $limittype does not have a transfer limit to given
272 # $to library, then we know that the transfer is possible.
273 foreach my $code (keys %{$items}) {
274 my @holdingbranches = keys %{$items->{$code}};
275 return 1 if Koha::Item::Transfer::Limits->search({
276 toBranch => $to->branchcode,
277 fromBranch => { 'in' => \@holdingbranches },
280 group_by => [qw/fromBranch/]
281 })->count == scalar(@holdingbranches) ? 0 : 1;
288 =head3 pickup_locations
290 my $pickup_locations = $biblio->pickup_locations({ patron => $patron });
292 Returns a Koha::Libraries set of possible pickup locations for this biblio's items,
293 according to patron's home library and if item can be transferred to each pickup location.
295 Throws a I<Koha::Exceptions::MissingParameter> exception if the B<mandatory> parameter I<patron>
300 sub pickup_locations {
301 my ( $self, $params ) = @_;
303 Koha::Exceptions::MissingParameter->throw( parameter => 'patron' )
304 unless exists $params->{patron};
306 my $patron = $params->{patron};
308 my $memory_cache = Koha::Cache::Memory::Lite->get_instance();
309 my @pickup_locations;
310 foreach my $item ( $self->items->as_list ) {
311 my $cache_key = sprintf "Pickup_locations:%s:%s:%s:%s:%s",
312 $item->itype,$item->homebranch,$item->holdingbranch,$item->ccode || "",$patron->branchcode||"" ;
313 my $item_pickup_locations = $memory_cache->get_from_cache( $cache_key );
314 unless( $item_pickup_locations ){
315 @{ $item_pickup_locations } = $item->pickup_locations( { patron => $patron } )->_resultset->get_column('branchcode')->all;
316 $memory_cache->set_in_cache( $cache_key, $item_pickup_locations );
318 push @pickup_locations, @{ $item_pickup_locations }
321 return Koha::Libraries->search(
322 { branchcode => { '-in' => \@pickup_locations } }, { order_by => ['branchname'] } );
325 =head3 hidden_in_opac
327 my $bool = $biblio->hidden_in_opac({ [ rules => $rules ] })
329 Returns true if the biblio matches the hidding criteria defined in $rules.
330 Returns false otherwise. It involves the I<OpacHiddenItems> and
331 I<OpacHiddenItemsHidesRecord> system preferences.
333 Takes HASHref that can have the following parameters:
335 $rules : { <field> => [ value_1, ... ], ... }
337 Note: $rules inherits its structure from the parsed YAML from reading
338 the I<OpacHiddenItems> system preference.
343 my ( $self, $params ) = @_;
345 my $rules = $params->{rules} // {};
347 my @items = $self->items->as_list;
349 return 0 unless @items; # Do not hide if there is no item
351 # Ok, there are items, don't even try the rules unless OpacHiddenItemsHidesRecord
352 return 0 unless C4::Context->preference('OpacHiddenItemsHidesRecord');
354 return !(any { !$_->hidden_in_opac({ rules => $rules }) } @items);
357 =head3 article_request_type
359 my $type = $biblio->article_request_type( $borrower );
361 Returns the article request type based on items, or on the record
362 itself if there are no items.
364 $borrower must be a Koha::Patron object
368 sub article_request_type {
369 my ( $self, $borrower ) = @_;
371 return q{} unless $borrower;
373 my $rule = $self->article_request_type_for_items( $borrower );
374 return $rule if $rule;
376 # If the record has no items that are requestable, go by the record itemtype
377 $rule = $self->article_request_type_for_bib($borrower);
378 return $rule if $rule;
383 =head3 article_request_type_for_bib
385 my $type = $biblio->article_request_type_for_bib
387 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record
391 sub article_request_type_for_bib {
392 my ( $self, $borrower ) = @_;
394 return q{} unless $borrower;
396 my $borrowertype = $borrower->categorycode;
397 my $itemtype = $self->itemtype();
399 my $rule = Koha::CirculationRules->get_effective_rule(
401 rule_name => 'article_requests',
402 categorycode => $borrowertype,
403 itemtype => $itemtype,
407 return q{} unless $rule;
408 return $rule->rule_value || q{}
411 =head3 article_request_type_for_items
413 my $type = $biblio->article_request_type_for_items
415 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record's items
417 If there is a conflict where some items are 'bib_only' and some are 'item_only', 'bib_only' will be returned.
421 sub article_request_type_for_items {
422 my ( $self, $borrower ) = @_;
425 foreach my $item ( $self->items()->as_list() ) {
426 my $rule = $item->article_request_type($borrower);
427 return $rule if $rule eq 'bib_only'; # we don't need to go any further
431 return 'item_only' if $counts->{item_only};
432 return 'yes' if $counts->{yes};
433 return 'no' if $counts->{no};
437 =head3 article_requests
439 my $article_requests = $biblio->article_requests
441 Returns the article requests associated with this biblio
445 sub article_requests {
448 return Koha::ArticleRequests->_new_from_dbic( scalar $self->_result->article_requests );
451 =head3 current_checkouts
453 my $current_checkouts = $biblio->current_checkouts
455 Returns the current checkouts associated with this biblio
459 sub current_checkouts {
462 return Koha::Checkouts->search( { "item.biblionumber" => $self->id },
463 { join => 'item' } );
468 my $old_checkouts = $biblio->old_checkouts
470 Returns the past checkouts associated with this biblio
477 return Koha::Old::Checkouts->search( { "item.biblionumber" => $self->id },
478 { join => 'item' } );
483 my $items = $biblio->items({ [ host_items => 1 ] });
485 The optional param host_items allows you to include 'analytical' items.
487 Returns the related Koha::Items object for this biblio
492 my ($self,$params) = @_;
494 my $items_rs = $self->_result->items;
496 return Koha::Items->_new_from_dbic( $items_rs ) unless $params->{host_items};
498 my @itemnumbers = $items_rs->get_column('itemnumber')->all;
499 my $host_itemnumbers = $self->_host_itemnumbers();
500 push @itemnumbers, @{ $host_itemnumbers };
501 return Koha::Items->search({ "me.itemnumber" => { -in => \@itemnumbers } });
506 my $host_items = $biblio->host_items();
508 Return the host items (easy analytical record)
515 return Koha::Items->new->empty
516 unless C4::Context->preference('EasyAnalyticalRecords');
518 my $host_itemnumbers = $self->_host_itemnumbers;
520 return Koha::Items->search( { itemnumber => { -in => $host_itemnumbers } } );
523 =head3 _host_itemnumbers
525 my $host_itemnumber = $biblio->_host_itemnumbers();
527 Return the itemnumbers for analytical items on this record
531 sub _host_itemnumbers {
534 my $marcflavour = C4::Context->preference("marcflavour");
535 my $analyticfield = '773';
536 if ( $marcflavour eq 'UNIMARC' ) {
537 $analyticfield = '461';
539 my $marc_record = $self->metadata->record;
541 foreach my $field ( $marc_record->field($analyticfield) ) {
542 push @itemnumbers, $field->subfield('9');
544 return \@itemnumbers;
550 my $itemtype = $biblio->itemtype();
552 Returns the itemtype for this record.
559 return $self->biblioitem()->itemtype();
564 my $holds = $biblio->holds();
566 return the current holds placed on this record
571 my ( $self, $params, $attributes ) = @_;
572 $attributes->{order_by} = 'priority' unless exists $attributes->{order_by};
573 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
574 return Koha::Holds->_new_from_dbic($hold_rs);
579 my $holds = $biblio->current_holds
581 Return the holds placed on this bibliographic record.
582 It does not include future holds.
588 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
590 { reservedate => { '<=' => $dtf->format_date(dt_from_string) } } );
595 my $field = $self->biblioitem
597 Returns the related Koha::Biblioitem object for this Biblio object
603 return Koha::Biblioitems->find( { biblionumber => $self->biblionumber } );
608 my $suggestions = $self->suggestions
610 Returns the related Koha::Suggestions object for this Biblio object
617 my $suggestions_rs = $self->_result->suggestions;
618 return Koha::Suggestions->_new_from_dbic( $suggestions_rs );
621 =head3 get_marc_components
623 my $components = $self->get_marc_components();
625 Returns an array of search results data, which are component parts of
626 this object (MARC21 773 points to this)
630 sub get_marc_components {
631 my ($self, $max_results) = @_;
633 return [] if (C4::Context->preference('marcflavour') ne 'MARC21');
635 my ( $searchstr, $sort ) = $self->get_components_query;
638 if (defined($searchstr)) {
639 my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
640 my ( $error, $results, $facets );
642 ( $error, $results, $facets ) = $searcher->search_compat( $searchstr, undef, [$sort], ['biblioserver'], $max_results, 0, undef, undef, 'ccl', 0 );
647 warn "Warning from search_compat: '$error'";
651 message => 'component_search',
656 $components = $results->{biblioserver}->{RECORDS} if defined($results) && $results->{biblioserver}->{hits};
659 return $components // [];
662 =head2 get_components_query
664 Returns a query which can be used to search for all component parts of MARC21 biblios
668 sub get_components_query {
671 my $builder = Koha::SearchEngine::QueryBuilder->new(
672 { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
673 my $marc = $self->metadata->record;
674 my $component_sort_field = C4::Context->preference('ComponentSortField') // "title";
675 my $component_sort_order = C4::Context->preference('ComponentSortOrder') // "asc";
676 my $sort = $component_sort_field . "_" . $component_sort_order;
679 if ( C4::Context->preference('UseControlNumber') ) {
680 my $pf001 = $marc->field('001') || undef;
682 if ( defined($pf001) ) {
684 my $pf003 = $marc->field('003') || undef;
686 if ( !defined($pf003) ) {
687 # search for 773$w='Host001'
688 $searchstr .= "rcn:\"" . $pf001->data()."\"";
692 # search for (773$w='Host001' and 003='Host003') or 773$w='(Host003)Host001'
693 $searchstr .= "(rcn:\"" . $pf001->data() . "\" AND cni:\"" . $pf003->data() . "\")";
694 $searchstr .= " OR rcn:\"" . $pf003->data() . " " . $pf001->data() . "\"";
698 # limit to monograph and serial component part records
699 $searchstr .= " AND (bib-level:a OR bib-level:b)";
704 my $cleaned_title = $marc->subfield('245', "a");
705 $cleaned_title =~ tr|/||;
706 $cleaned_title = $builder->clean_search_term($cleaned_title);
707 $searchstr = qq#Host-item:("$cleaned_title")#;
709 my ($error, $query ,$query_str) = $builder->build_query_compat( undef, [$searchstr], undef, undef, [$sort], 0 );
715 return ($query, $query_str, $sort);
720 my $subscriptions = $self->subscriptions
722 Returns the related Koha::Subscriptions object for this Biblio object
728 my $rs = $self->_result->subscriptions;
729 return Koha::Subscriptions->_new_from_dbic($rs);
732 =head3 has_items_waiting_or_intransit
734 my $itemsWaitingOrInTransit = $biblio->has_items_waiting_or_intransit
736 Tells if this bibliographic record has items waiting or in transit.
740 sub has_items_waiting_or_intransit {
743 if ( Koha::Holds->search({ biblionumber => $self->id,
744 found => ['W', 'T'] })->count ) {
748 foreach my $item ( $self->items->as_list ) {
749 return 1 if $item->get_transfer;
757 my $coins = $biblio->get_coins;
759 Returns the COinS (a span) which can be included in a biblio record
766 my $record = $self->metadata->record;
768 my $pos7 = substr $record->leader(), 7, 1;
769 my $pos6 = substr $record->leader(), 6, 1;
772 my ( $aulast, $aufirst ) = ( '', '' );
783 # For the purposes of generating COinS metadata, LDR/06-07 can be
784 # considered the same for UNIMARC and MARC21
793 'i' => 'audioRecording',
794 'j' => 'audioRecording',
797 'm' => 'computerProgram',
802 'a' => 'journalArticle',
806 $genre = $fmts6->{$pos6} ? $fmts6->{$pos6} : 'book';
808 if ( $genre eq 'book' ) {
809 $genre = $fmts7->{$pos7} if $fmts7->{$pos7};
812 ##### We must transform mtx to a valable mtx and document type ####
813 if ( $genre eq 'book' ) {
816 } elsif ( $genre eq 'journal' ) {
819 } elsif ( $genre eq 'journalArticle' ) {
827 if ( C4::Context->preference("marcflavour") eq "UNIMARC" ) {
830 $aulast = $record->subfield( '700', 'a' ) || '';
831 $aufirst = $record->subfield( '700', 'b' ) || '';
832 push @authors, "$aufirst $aulast" if ($aufirst or $aulast);
835 if ( $record->field('200') ) {
836 for my $au ( $record->field('200')->subfield('g') ) {
841 $title = $record->subfield( '200', 'a' );
842 my $subfield_210d = $record->subfield('210', 'd');
843 if ($subfield_210d and $subfield_210d =~ /(\d{4})/) {
846 $publisher = $record->subfield( '210', 'c' ) || '';
847 $isbn = $record->subfield( '010', 'a' ) || '';
848 $issn = $record->subfield( '011', 'a' ) || '';
851 # MARC21 need some improve
854 if ( $record->field('100') ) {
855 push @authors, $record->subfield( '100', 'a' );
859 if ( $record->field('700') ) {
860 for my $au ( $record->field('700')->subfield('a') ) {
864 $title = $record->field('245');
865 $title &&= $title->as_string('ab');
866 if ($titletype eq 'a') {
867 $pubyear = $record->field('008') || '';
868 $pubyear = substr($pubyear->data(), 7, 4) if $pubyear;
869 $isbn = $record->subfield( '773', 'z' ) || '';
870 $issn = $record->subfield( '773', 'x' ) || '';
871 $hosttitle = $record->subfield( '773', 't' ) || $record->subfield( '773', 'a') || q{};
872 my @rels = $record->subfield( '773', 'g' );
873 $pages = join(', ', @rels);
875 $pubyear = $record->subfield( '260', 'c' ) || '';
876 $publisher = $record->subfield( '260', 'b' ) || '';
877 $isbn = $record->subfield( '020', 'a' ) || '';
878 $issn = $record->subfield( '022', 'a' ) || '';
884 [ 'ctx_ver', 'Z39.88-2004' ],
885 [ 'rft_val_fmt', "info:ofi/fmt:kev:mtx:$mtx" ],
886 [ ($mtx eq 'dc' ? 'rft.type' : 'rft.genre'), $genre ],
887 [ "rft.${titletype}title", $title ],
890 # rft.title is authorized only once, so by checking $titletype
891 # we ensure that rft.title is not already in the list.
892 if ($hosttitle and $titletype) {
893 push @params, [ 'rft.title', $hosttitle ];
897 [ 'rft.isbn', $isbn ],
898 [ 'rft.issn', $issn ],
901 # If it's a subscription, these informations have no meaning.
902 if ($genre ne 'journal') {
904 [ 'rft.aulast', $aulast ],
905 [ 'rft.aufirst', $aufirst ],
906 (map { [ 'rft.au', $_ ] } @authors),
907 [ 'rft.pub', $publisher ],
908 [ 'rft.date', $pubyear ],
909 [ 'rft.pages', $pages ],
913 my $coins_value = join( '&',
914 map { $$_[1] ? $$_[0] . '=' . uri_escape_utf8( $$_[1] ) : () } @params );
921 my $url = $biblio->get_openurl;
923 Returns url for OpenURL resolver set in OpenURLResolverURL system preference
930 my $OpenURLResolverURL = C4::Context->preference('OpenURLResolverURL');
932 if ($OpenURLResolverURL) {
933 my $uri = URI->new($OpenURLResolverURL);
935 if (not defined $uri->query) {
936 $OpenURLResolverURL .= '?';
938 $OpenURLResolverURL .= '&';
940 $OpenURLResolverURL .= $self->get_coins;
943 return $OpenURLResolverURL;
948 my $serial = $biblio->is_serial
950 Return boolean true if this bibbliographic record is continuing resource
957 return 1 if $self->serial;
959 my $record = $self->metadata->record;
960 return 1 if substr($record->leader, 7, 1) eq 's';
965 =head3 custom_cover_image_url
967 my $image_url = $biblio->custom_cover_image_url
969 Return the specific url of the cover image for this bibliographic record.
970 It is built regaring the value of the system preference CustomCoverImagesURL
974 sub custom_cover_image_url {
976 my $url = C4::Context->preference('CustomCoverImagesURL');
977 if ( $url =~ m|{isbn}| ) {
978 my $isbn = $self->biblioitem->isbn;
980 $url =~ s|{isbn}|$isbn|g;
982 if ( $url =~ m|{normalized_isbn}| ) {
983 my $normalized_isbn = $self->normalized_isbn;
984 return unless $normalized_isbn;
985 $url =~ s|{normalized_isbn}|$normalized_isbn|g;
987 if ( $url =~ m|{issn}| ) {
988 my $issn = $self->biblioitem->issn;
990 $url =~ s|{issn}|$issn|g;
993 my $re = qr|{(?<field>\d{3})(\$(?<subfield>.))?}|;
995 my $field = $+{field};
996 my $subfield = $+{subfield};
997 my $marc_record = $self->metadata->record;
1000 $value = $marc_record->subfield( $field, $subfield );
1002 my $controlfield = $marc_record->field($field);
1003 $value = $controlfield->data() if $controlfield;
1005 return unless $value;
1006 $url =~ s|$re|$value|;
1014 Return the cover images associated with this biblio.
1021 my $cover_images_rs = $self->_result->cover_images;
1022 return unless $cover_images_rs;
1023 return Koha::CoverImages->_new_from_dbic($cover_images_rs);
1026 =head3 get_marc_notes
1028 $marcnotesarray = $biblio->get_marc_notes({ opac => 1 });
1030 Get all notes from the MARC record and returns them in an array.
1031 The notes are stored in different fields depending on MARC flavour.
1032 MARC21 5XX $u subfields receive special attention as they are URIs.
1036 sub get_marc_notes {
1037 my ( $self, $params ) = @_;
1039 my $marcflavour = C4::Context->preference('marcflavour');
1040 my $opac = $params->{opac} // '0';
1041 my $interface = $params->{opac} ? 'opac' : 'intranet';
1043 my $record = $params->{record} // $self->metadata->record;
1044 my $record_processor = Koha::RecordProcessor->new(
1046 filters => [ 'ViewPolicy', 'ExpandCodedFields' ],
1048 interface => $interface,
1049 frameworkcode => $self->frameworkcode
1053 $record_processor->process($record);
1055 my $scope = $marcflavour eq "UNIMARC"? '3..': '5..';
1056 #MARC21 specs indicate some notes should be private if first indicator 0
1057 my %maybe_private = (
1065 my %hiddenlist = map { $_ => 1 }
1066 split( /,/, C4::Context->preference('NotesToHide'));
1069 foreach my $field ( $record->field($scope) ) {
1070 my $tag = $field->tag();
1071 next if $hiddenlist{ $tag };
1072 next if $opac && $maybe_private{$tag} && !$field->indicator(1);
1073 if( $marcflavour ne 'UNIMARC' && $field->subfield('u') ) {
1074 # Field 5XX$u always contains URI
1075 # Examples: 505u, 506u, 510u, 514u, 520u, 530u, 538u, 540u, 542u, 552u, 555u, 561u, 563u, 583u
1076 # We first push the other subfields, then all $u's separately
1077 # Leave further actions to the template (see e.g. opac-detail)
1079 join '', ( 'a' .. 't', 'v' .. 'z', '0' .. '9' ); # excl 'u'
1080 push @marcnotes, { marcnote => $field->as_string($othersub) };
1081 foreach my $sub ( $field->subfield('u') ) {
1082 $sub =~ s/^\s+|\s+$//g; # trim
1083 push @marcnotes, { marcnote => $sub };
1086 push @marcnotes, { marcnote => $field->as_string() };
1092 =head3 _get_marc_authors
1094 Private method to return the list of authors contained in the MARC record.
1095 See get get_marc_contributors and get_marc_authors for the public methods.
1099 sub _get_marc_authors {
1100 my ( $self, $params ) = @_;
1102 my $fields_filter = $params->{fields_filter};
1103 my $mintag = $params->{mintag};
1104 my $maxtag = $params->{maxtag};
1106 my $AuthoritySeparator = C4::Context->preference('AuthoritySeparator');
1107 my $marcflavour = C4::Context->preference('marcflavour');
1109 # tagslib useful only for UNIMARC author responsibilities
1110 my $tagslib = $marcflavour eq "UNIMARC"
1111 ? C4::Biblio::GetMarcStructure( 1, $self->frameworkcode, { unsafe => 1 } )
1115 foreach my $field ( $self->metadata->record->field($fields_filter) ) {
1118 if $mintag && $field->tag() < $mintag
1119 || $maxtag && $field->tag() > $maxtag;
1123 my @subfields = $field->subfields();
1126 # if there is an authority link, build the link with Koha-Auth-Number: subfield9
1127 my $subfield9 = $field->subfield('9');
1129 my $linkvalue = $subfield9;
1130 $linkvalue =~ s/(\(|\))//g;
1131 @link_loop = ( { 'limit' => 'an', 'link' => $linkvalue } );
1136 for my $authors_subfield (@subfields) {
1137 next if ( $authors_subfield->[0] eq '9' );
1139 # unimarc3 contains the $3 of the author for UNIMARC.
1140 # For french academic libraries, it's the "ppn", and it's required for idref webservice
1141 $unimarc3 = $authors_subfield->[1] if $marcflavour eq 'UNIMARC' and $authors_subfield->[0] =~ /3/;
1143 # don't load unimarc subfields 3, 5
1144 next if ( $marcflavour eq 'UNIMARC' and ( $authors_subfield->[0] =~ /3|5/ ) );
1146 my $code = $authors_subfield->[0];
1147 my $value = $authors_subfield->[1];
1148 my $linkvalue = $value;
1149 $linkvalue =~ s/(\(|\))//g;
1150 # UNIMARC author responsibility
1151 if ( $marcflavour eq 'UNIMARC' and $code eq '4' ) {
1152 $value = C4::Biblio::GetAuthorisedValueDesc( $field->tag(), $code, $value, '', $tagslib );
1153 $linkvalue = "($value)";
1155 # if no authority link, build a search query
1156 unless ($subfield9) {
1159 'link' => $linkvalue,
1160 operator => (scalar @link_loop) ? ' AND ' : undef
1163 my @this_link_loop = @link_loop;
1165 unless ( $code eq '0') {
1166 push @subfields_loop, {
1167 tag => $field->tag(),
1170 link_loop => \@this_link_loop,
1171 separator => (scalar @subfields_loop) ? $AuthoritySeparator : ''
1175 push @marcauthors, {
1176 MARCAUTHOR_SUBFIELDS_LOOP => \@subfields_loop,
1177 authoritylink => $subfield9,
1178 unimarc3 => $unimarc3
1181 return \@marcauthors;
1184 =head3 get_marc_contributors
1186 my $contributors = $biblio->get_marc_contributors;
1188 Get all contributors (but first author) from the MARC record and returns them in an array.
1189 They are stored in different fields depending on MARC flavour (700..720 for MARC21)
1193 sub get_marc_contributors {
1194 my ( $self, $params ) = @_;
1196 my ( $mintag, $maxtag, $fields_filter );
1197 my $marcflavour = C4::Context->preference('marcflavour');
1199 if ( $marcflavour eq "UNIMARC" ) {
1202 $fields_filter = '7..';
1203 } else { # marc21/normarc
1206 $fields_filter = '7..';
1209 return $self->_get_marc_authors(
1211 fields_filter => $fields_filter,
1218 =head3 get_marc_authors
1220 my $authors = $biblio->get_marc_authors;
1222 Get all authors from the MARC record and returns them in an array.
1223 They are stored in different fields depending on MARC flavour
1224 (main author from 100 then secondary authors from 700..720).
1228 sub get_marc_authors {
1229 my ( $self, $params ) = @_;
1231 my ( $mintag, $maxtag, $fields_filter );
1232 my $marcflavour = C4::Context->preference('marcflavour');
1234 if ( $marcflavour eq "UNIMARC" ) {
1235 $fields_filter = '200';
1236 } else { # marc21/normarc
1237 $fields_filter = '100';
1240 my @first_authors = @{$self->_get_marc_authors(
1242 fields_filter => $fields_filter,
1248 my @other_authors = @{$self->get_marc_contributors};
1250 return [@first_authors, @other_authors];
1253 =head3 normalized_isbn
1255 my $normalized_isbn = $biblio->normalized_isbn
1257 Normalizes and returns the first valid ISBN found in the record.
1258 ISBN13 are converted into ISBN10. This is required to get some book cover images.
1262 sub normalized_isbn {
1264 return C4::Koha::GetNormalizedISBN($self->biblioitem->isbn);
1267 =head3 public_read_list
1269 This method returns the list of publicly readable database fields for both API and UI output purposes
1273 sub public_read_list {
1275 'biblionumber', 'frameworkcode', 'author',
1276 'title', 'medium', 'subtitle',
1277 'part_number', 'part_name', 'unititle',
1278 'notes', 'serial', 'seriestitle',
1279 'copyrightdate', 'abstract'
1285 my $json = $biblio->to_api;
1287 Overloaded method that returns a JSON representation of the Koha::Biblio object,
1288 suitable for API output. The related Koha::Biblioitem object is merged as expected
1294 my ($self, $args) = @_;
1296 my $response = $self->SUPER::to_api( $args );
1298 $args = defined $args ? {%$args} : {};
1299 delete $args->{embed};
1301 my $biblioitem = $self->biblioitem->to_api( $args );
1303 return { %$response, %$biblioitem };
1306 =head3 to_api_mapping
1308 This method returns the mapping for representing a Koha::Biblio object
1313 sub to_api_mapping {
1315 biblionumber => 'biblio_id',
1316 frameworkcode => 'framework_id',
1317 unititle => 'uniform_title',
1318 seriestitle => 'series_title',
1319 copyrightdate => 'copyright_date',
1320 datecreated => 'creation_date',
1321 deleted_on => undef,
1325 =head3 get_marc_host
1327 $host = $biblio->get_marc_host;
1329 ( $host, $relatedparts, $hostinfo ) = $biblio->get_marc_host;
1331 Returns host biblio record from MARC21 773 (undef if no 773 present).
1332 It looks at the first 773 field with MARCorgCode or only a control
1333 number. Complete $w or numeric part is used to search host record.
1334 The optional parameter no_items triggers a check if $biblio has items.
1335 If there are, the sub returns undef.
1336 Called in list context, it also returns 773$g (related parts).
1338 If there is no $w, we use $0 (host biblionumber) or $9 (host itemnumber)
1339 to search for the host record. If there is also no $0 and no $9, we search
1340 using author and title. Failing all of that, we return an undef host and
1341 form a concatenation of strings with 773$agt for host information,
1342 returned when called in list context.
1347 my ($self, $params) = @_;
1348 my $no_items = $params->{no_items};
1349 return if C4::Context->preference('marcflavour') eq 'UNIMARC'; # TODO
1350 return if $params->{no_items} && $self->items->count > 0;
1353 eval { $record = $self->metadata->record };
1356 # We pick the first $w with your MARCOrgCode or the first $w that has no
1357 # code (between parentheses) at all.
1358 my $orgcode = C4::Context->preference('MARCOrgCode') // q{};
1360 foreach my $f ( $record->field('773') ) {
1361 my $w = $f->subfield('w') or next;
1362 if( $w =~ /^\($orgcode\)\s*(\d+)/i or $w =~ /^\d+/ ) {
1368 my $engine = Koha::SearchEngine::Search->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1370 if ( !$hostfld and $record->subfield('773','t') ) {
1371 # not linked using $w
1372 my $unlinkedf = $record->field('773');
1374 if ( C4::Context->preference("EasyAnalyticalRecords") ) {
1375 if ( $unlinkedf->subfield('0') ) {
1376 # use 773$0 host biblionumber
1377 $bibno = $unlinkedf->subfield('0');
1378 } elsif ( $unlinkedf->subfield('9') ) {
1379 # use 773$9 host itemnumber
1380 my $linkeditemnumber = $unlinkedf->subfield('9');
1381 $bibno = Koha::Items->find( $linkeditemnumber )->biblionumber;
1385 my $host = Koha::Biblios->find($bibno) or return;
1386 return wantarray ? ( $host, $unlinkedf->subfield('g') ) : $host;
1388 # just return plaintext and no host record
1389 my $hostinfo = join( ", ", $unlinkedf->subfield('a'), $unlinkedf->subfield('t'), $unlinkedf->subfield('g') );
1390 return wantarray ? ( undef, $unlinkedf->subfield('g'), $hostinfo ) : undef;
1392 return if !$hostfld;
1393 my $rcn = $hostfld->subfield('w');
1395 # Look for control number with/without orgcode
1396 for my $try (1..2) {
1397 my ( $error, $results, $total_hits ) = $engine->simple_search_compat( 'Control-number='.$rcn, 0,1 );
1398 if( !$error and $total_hits == 1 ) {
1399 $bibno = $engine->extract_biblionumber( $results->[0] );
1402 # Add or remove orgcode for second try
1403 if( $try == 1 && $rcn =~ /\)\s*(\d+)/ ) {
1404 $rcn = $1; # number only
1405 } elsif( $try == 1 && $rcn =~ /^\d+/ ) {
1406 $rcn = "($orgcode)$rcn";
1412 my $host = Koha::Biblios->find($bibno) or return;
1413 return wantarray ? ( $host, $hostfld->subfield('g') ) : $host;
1417 =head3 get_marc_host_only
1419 my $host = $biblio->get_marc_host_only;
1425 sub get_marc_host_only {
1428 my ( $host ) = $self->get_marc_host;
1433 =head3 get_marc_relatedparts_only
1435 my $relatedparts = $biblio->get_marc_relatedparts_only;
1437 Return related parts only
1441 sub get_marc_relatedparts_only {
1444 my ( undef, $relatedparts ) = $self->get_marc_host;
1446 return $relatedparts;
1449 =head3 get_marc_hostinfo_only
1451 my $hostinfo = $biblio->get_marc_hostinfo_only;
1453 Return host info only
1457 sub get_marc_hostinfo_only {
1460 my ( $host, $relatedparts, $hostinfo ) = $self->get_marc_host;
1467 my $recalls = $biblio->recalls;
1469 Return recalls linked to this biblio
1475 return Koha::Recalls->_new_from_dbic( scalar $self->_result->recalls );
1478 =head3 can_be_recalled
1480 my @items_for_recall = $biblio->can_be_recalled({ patron => $patron_object });
1482 Does biblio-level checks and returns the items attached to this biblio that are available for recall
1486 sub can_be_recalled {
1487 my ( $self, $params ) = @_;
1489 return 0 if !( C4::Context->preference('UseRecalls') );
1491 my $patron = $params->{patron};
1493 my $branchcode = C4::Context->userenv->{'branch'};
1494 if ( C4::Context->preference('CircControl') eq 'PatronLibrary' and $patron ) {
1495 $branchcode = $patron->branchcode;
1498 my @all_items = Koha::Items->search({ biblionumber => $self->biblionumber })->as_list;
1500 # if there are no available items at all, no recall can be placed
1501 return 0 if ( scalar @all_items == 0 );
1506 my @all_itemnumbers;
1507 foreach my $item ( @all_items ) {
1508 push( @all_itemnumbers, $item->itemnumber );
1509 if ( $item->can_be_recalled({ patron => $patron }) ) {
1510 push( @itemtypes, $item->effective_itemtype );
1511 push( @itemnumbers, $item->itemnumber );
1512 push( @items, $item );
1516 # if there are no recallable items, no recall can be placed
1517 return 0 if ( scalar @items == 0 );
1519 # Check the circulation rule for each relevant itemtype for this biblio
1520 my ( @recalls_allowed, @recalls_per_record, @on_shelf_recalls );
1521 foreach my $itemtype ( @itemtypes ) {
1522 my $rule = Koha::CirculationRules->get_effective_rules({
1523 branchcode => $branchcode,
1524 categorycode => $patron ? $patron->categorycode : undef,
1525 itemtype => $itemtype,
1528 'recalls_per_record',
1532 push( @recalls_allowed, $rule->{recalls_allowed} ) if $rule;
1533 push( @recalls_per_record, $rule->{recalls_per_record} ) if $rule;
1534 push( @on_shelf_recalls, $rule->{on_shelf_recalls} ) if $rule;
1536 my $recalls_allowed = (sort {$b <=> $a} @recalls_allowed)[0]; # take highest
1537 my $recalls_per_record = (sort {$b <=> $a} @recalls_per_record)[0]; # take highest
1538 my %on_shelf_recalls_count = ();
1539 foreach my $count ( @on_shelf_recalls ) {
1540 $on_shelf_recalls_count{$count}++;
1542 my $on_shelf_recalls = (sort {$on_shelf_recalls_count{$b} <=> $on_shelf_recalls_count{$a}} @on_shelf_recalls)[0]; # take most common
1544 # check recalls allowed has been set and is not zero
1545 return 0 if ( !defined($recalls_allowed) || $recalls_allowed == 0 );
1548 # check borrower has not reached open recalls allowed limit
1549 return 0 if ( $patron->recalls->filter_by_current->count >= $recalls_allowed );
1551 # check borrower has not reached open recalls allowed per record limit
1552 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $recalls_per_record );
1554 # check if any of the items under this biblio are already checked out by this borrower
1555 return 0 if ( Koha::Checkouts->search({ itemnumber => [ @all_itemnumbers ], borrowernumber => $patron->borrowernumber })->count > 0 );
1558 # check item availability
1559 my $checked_out_count = 0;
1561 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1564 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1565 return 0 if ( $on_shelf_recalls eq 'all' && $checked_out_count < scalar @items );
1567 # can't recall if no items have been checked out
1568 return 0 if ( $checked_out_count == 0 );
1576 my $ratings = $biblio->ratings
1578 Return a Koha::Ratings object representing the ratings of this bibliographic record
1584 my $rs = $self->_result->ratings;
1585 return Koha::Ratings->_new_from_dbic($rs);
1588 =head3 opac_summary_html
1590 my $summary_html = $biblio->opac_summary_html
1592 Based on the syspref OPACMySummaryHTML, returns a string representing the
1593 summary of this bibliographic record.
1594 {AUTHOR}, {TITLE}, {ISBN} and {BIBLIONUMBER} will be replaced.
1598 sub opac_summary_html {
1601 my $summary_html = C4::Context->preference('OPACMySummaryHTML');
1602 return q{} unless $summary_html;
1603 my $author = $self->author || q{};
1604 my $title = $self->title || q{};
1605 $title =~ s/\/+$//; # remove trailing slash
1606 $title =~ s/\s+$//; # remove trailing space
1607 my $normalized_isbn = $self->normalized_isbn || q{};
1608 my $biblionumber = $self->biblionumber;
1610 $summary_html =~ s/{AUTHOR}/$author/g;
1611 $summary_html =~ s/{TITLE}/$title/g;
1612 $summary_html =~ s/{ISBN}/$normalized_isbn/g;
1613 $summary_html =~ s/{BIBLIONUMBER}/$biblionumber/g;
1615 return $summary_html;
1618 =head2 Internal methods
1630 Kyle M Hall <kyle@bywatersolutions.com>