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;
48 use Koha::RecordProcessor;
49 use Koha::Suggestions;
50 use Koha::Subscriptions;
51 use Koha::SearchEngine;
52 use Koha::SearchEngine::Search;
53 use Koha::SearchEngine::QueryBuilder;
58 Koha::Biblio - Koha Biblio Object class
68 Overloaded I<store> method to set default values
75 $self->datecreated( dt_from_string ) unless $self->datecreated;
77 return $self->SUPER::store;
82 my $metadata = $biblio->metadata();
84 Returns a Koha::Biblio::Metadata object
91 my $metadata = $self->_result->metadata;
92 return Koha::Biblio::Metadata->_new_from_dbic($metadata);
97 my $record = $biblio->record();
99 Returns a Marc::Record object
106 return $self->metadata->record;
111 my $schema = $biblio->record_schema();
113 Returns the record schema (MARC21, USMARC or UNIMARC).
120 return $self->metadata->schema // C4::Context->preference("marcflavour");
125 my $orders = $biblio->orders();
127 Returns a Koha::Acquisition::Orders object
134 my $orders = $self->_result->orders;
135 return Koha::Acquisition::Orders->_new_from_dbic($orders);
140 my $active_orders = $biblio->active_orders();
142 Returns the active acquisition orders related to this biblio.
143 An order is considered active when it is not cancelled (i.e. when datecancellation
151 return $self->orders->search({ datecancellationprinted => undef });
156 my $tickets = $biblio->tickets();
158 Returns all tickets linked to the biblio
164 my $rs = $self->_result->tickets;
165 return Koha::Tickets->_new_from_dbic( $rs );
170 my $ill_requests = $biblio->ill_requests();
172 Returns a Koha::Illrequests object
179 my $ill_requests = $self->_result->ill_requests;
180 return Koha::Illrequests->_new_from_dbic($ill_requests);
185 my $item_groups = $biblio->item_groups();
187 Returns a Koha::Biblio::ItemGroups object
194 my $item_groups = $self->_result->item_groups;
195 return Koha::Biblio::ItemGroups->_new_from_dbic($item_groups);
198 =head3 can_article_request
200 my $bool = $biblio->can_article_request( $borrower );
202 Returns true if article requests can be made for this record
204 $borrower must be a Koha::Patron object
208 sub can_article_request {
209 my ( $self, $borrower ) = @_;
211 my $rule = $self->article_request_type($borrower);
212 return q{} if $rule eq 'item_only' && !$self->items()->count();
213 return 1 if $rule && $rule ne 'no';
218 =head3 can_be_transferred
220 $biblio->can_be_transferred({ to => $to_library, from => $from_library })
222 Checks if at least one item of a biblio can be transferred to given library.
224 This feature is controlled by two system preferences:
225 UseBranchTransferLimits to enable / disable the feature
226 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
227 for setting the limitations
229 Performance-wise, it is recommended to use this method for a biblio instead of
230 iterating each item of a biblio with Koha::Item->can_be_transferred().
232 Takes HASHref that can have the following parameters:
233 MANDATORY PARAMETERS:
236 $from : Koha::Library # if given, only items from that
237 # holdingbranch are considered
239 Returns 1 if at least one of the item of a biblio can be transferred
240 to $to_library, otherwise 0.
244 sub can_be_transferred {
245 my ($self, $params) = @_;
247 my $to = $params->{to};
248 my $from = $params->{from};
250 return 1 unless C4::Context->preference('UseBranchTransferLimits');
251 my $limittype = C4::Context->preference('BranchTransferLimitsType');
254 foreach my $item_of_bib ($self->items->as_list) {
255 next unless $item_of_bib->holdingbranch;
256 next if $from && $from->branchcode ne $item_of_bib->holdingbranch;
257 return 1 if $item_of_bib->holdingbranch eq $to->branchcode;
258 my $code = $limittype eq 'itemtype'
259 ? $item_of_bib->effective_itemtype
260 : $item_of_bib->ccode;
261 return 1 unless $code;
262 $items->{$code}->{$item_of_bib->holdingbranch} = 1;
265 # At this point we will have a HASHref containing each itemtype/ccode that
266 # this biblio has, inside which are all of the holdingbranches where those
267 # items are located at. Then, we will query Koha::Item::Transfer::Limits to
268 # find out whether a transfer limits for such $limittype from any of the
269 # listed holdingbranches to the given $to library exist. If at least one
270 # holdingbranch for that $limittype does not have a transfer limit to given
271 # $to library, then we know that the transfer is possible.
272 foreach my $code (keys %{$items}) {
273 my @holdingbranches = keys %{$items->{$code}};
274 return 1 if Koha::Item::Transfer::Limits->search({
275 toBranch => $to->branchcode,
276 fromBranch => { 'in' => \@holdingbranches },
279 group_by => [qw/fromBranch/]
280 })->count == scalar(@holdingbranches) ? 0 : 1;
287 =head3 pickup_locations
289 my $pickup_locations = $biblio->pickup_locations({ patron => $patron });
291 Returns a Koha::Libraries set of possible pickup locations for this biblio's items,
292 according to patron's home library and if item can be transferred to each pickup location.
294 Throws a I<Koha::Exceptions::MissingParameter> exception if the B<mandatory> parameter I<patron>
299 sub pickup_locations {
300 my ( $self, $params ) = @_;
302 Koha::Exceptions::MissingParameter->throw( parameter => 'patron' )
303 unless exists $params->{patron};
305 my $patron = $params->{patron};
307 my $memory_cache = Koha::Cache::Memory::Lite->get_instance();
308 my @pickup_locations;
309 foreach my $item ( $self->items->as_list ) {
310 my $cache_key = sprintf "Pickup_locations:%s:%s:%s:%s:%s",
311 $item->itype,$item->homebranch,$item->holdingbranch,$item->ccode || "",$patron->branchcode||"" ;
312 my $item_pickup_locations = $memory_cache->get_from_cache( $cache_key );
313 unless( $item_pickup_locations ){
314 @{ $item_pickup_locations } = $item->pickup_locations( { patron => $patron } )->_resultset->get_column('branchcode')->all;
315 $memory_cache->set_in_cache( $cache_key, $item_pickup_locations );
317 push @pickup_locations, @{ $item_pickup_locations }
320 return Koha::Libraries->search(
321 { branchcode => { '-in' => \@pickup_locations } }, { order_by => ['branchname'] } );
324 =head3 hidden_in_opac
326 my $bool = $biblio->hidden_in_opac({ [ rules => $rules ] })
328 Returns true if the biblio matches the hidding criteria defined in $rules.
329 Returns false otherwise. It involves the I<OpacHiddenItems> and
330 I<OpacHiddenItemsHidesRecord> system preferences.
332 Takes HASHref that can have the following parameters:
334 $rules : { <field> => [ value_1, ... ], ... }
336 Note: $rules inherits its structure from the parsed YAML from reading
337 the I<OpacHiddenItems> system preference.
342 my ( $self, $params ) = @_;
344 my $rules = $params->{rules} // {};
346 my @items = $self->items->as_list;
348 return 0 unless @items; # Do not hide if there is no item
350 # Ok, there are items, don't even try the rules unless OpacHiddenItemsHidesRecord
351 return 0 unless C4::Context->preference('OpacHiddenItemsHidesRecord');
353 return !(any { !$_->hidden_in_opac({ rules => $rules }) } @items);
356 =head3 article_request_type
358 my $type = $biblio->article_request_type( $borrower );
360 Returns the article request type based on items, or on the record
361 itself if there are no items.
363 $borrower must be a Koha::Patron object
367 sub article_request_type {
368 my ( $self, $borrower ) = @_;
370 return q{} unless $borrower;
372 my $rule = $self->article_request_type_for_items( $borrower );
373 return $rule if $rule;
375 # If the record has no items that are requestable, go by the record itemtype
376 $rule = $self->article_request_type_for_bib($borrower);
377 return $rule if $rule;
382 =head3 article_request_type_for_bib
384 my $type = $biblio->article_request_type_for_bib
386 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record
390 sub article_request_type_for_bib {
391 my ( $self, $borrower ) = @_;
393 return q{} unless $borrower;
395 my $borrowertype = $borrower->categorycode;
396 my $itemtype = $self->itemtype();
398 my $rule = Koha::CirculationRules->get_effective_rule(
400 rule_name => 'article_requests',
401 categorycode => $borrowertype,
402 itemtype => $itemtype,
406 return q{} unless $rule;
407 return $rule->rule_value || q{}
410 =head3 article_request_type_for_items
412 my $type = $biblio->article_request_type_for_items
414 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record's items
416 If there is a conflict where some items are 'bib_only' and some are 'item_only', 'bib_only' will be returned.
420 sub article_request_type_for_items {
421 my ( $self, $borrower ) = @_;
424 foreach my $item ( $self->items()->as_list() ) {
425 my $rule = $item->article_request_type($borrower);
426 return $rule if $rule eq 'bib_only'; # we don't need to go any further
430 return 'item_only' if $counts->{item_only};
431 return 'yes' if $counts->{yes};
432 return 'no' if $counts->{no};
436 =head3 article_requests
438 my $article_requests = $biblio->article_requests
440 Returns the article requests associated with this biblio
444 sub article_requests {
447 return Koha::ArticleRequests->_new_from_dbic( scalar $self->_result->article_requests );
450 =head3 current_checkouts
452 my $current_checkouts = $biblio->current_checkouts
454 Returns the current checkouts associated with this biblio
458 sub current_checkouts {
461 return Koha::Checkouts->search( { "item.biblionumber" => $self->id },
462 { join => 'item' } );
467 my $old_checkouts = $biblio->old_checkouts
469 Returns the past checkouts associated with this biblio
476 return Koha::Old::Checkouts->search( { "item.biblionumber" => $self->id },
477 { join => 'item' } );
482 my $items = $biblio->items({ [ host_items => 1 ] });
484 The optional param host_items allows you to include 'analytical' items.
486 Returns the related Koha::Items object for this biblio
491 my ($self,$params) = @_;
493 my $items_rs = $self->_result->items;
495 return Koha::Items->_new_from_dbic( $items_rs ) unless $params->{host_items};
497 my @itemnumbers = $items_rs->get_column('itemnumber')->all;
498 my $host_itemnumbers = $self->_host_itemnumbers();
499 push @itemnumbers, @{ $host_itemnumbers };
500 return Koha::Items->search({ "me.itemnumber" => { -in => \@itemnumbers } });
505 my $host_items = $biblio->host_items();
507 Return the host items (easy analytical record)
514 return Koha::Items->new->empty
515 unless C4::Context->preference('EasyAnalyticalRecords');
517 my $host_itemnumbers = $self->_host_itemnumbers;
519 return Koha::Items->search( { itemnumber => { -in => $host_itemnumbers } } );
522 =head3 _host_itemnumbers
524 my $host_itemnumber = $biblio->_host_itemnumbers();
526 Return the itemnumbers for analytical items on this record
530 sub _host_itemnumbers {
533 my $marcflavour = C4::Context->preference("marcflavour");
534 my $analyticfield = '773';
535 if ( $marcflavour eq 'UNIMARC' ) {
536 $analyticfield = '461';
538 my $marc_record = $self->metadata->record;
540 foreach my $field ( $marc_record->field($analyticfield) ) {
541 push @itemnumbers, $field->subfield('9');
543 return \@itemnumbers;
549 my $itemtype = $biblio->itemtype();
551 Returns the itemtype for this record.
558 return $self->biblioitem()->itemtype();
563 my $holds = $biblio->holds();
565 return the current holds placed on this record
570 my ( $self, $params, $attributes ) = @_;
571 $attributes->{order_by} = 'priority' unless exists $attributes->{order_by};
572 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
573 return Koha::Holds->_new_from_dbic($hold_rs);
578 my $holds = $biblio->current_holds
580 Return the holds placed on this bibliographic record.
581 It does not include future holds.
587 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
589 { reservedate => { '<=' => $dtf->format_date(dt_from_string) } } );
594 my $field = $self->biblioitem
596 Returns the related Koha::Biblioitem object for this Biblio object
602 return Koha::Biblioitems->find( { biblionumber => $self->biblionumber } );
607 my $suggestions = $self->suggestions
609 Returns the related Koha::Suggestions object for this Biblio object
616 my $suggestions_rs = $self->_result->suggestions;
617 return Koha::Suggestions->_new_from_dbic( $suggestions_rs );
620 =head3 get_marc_components
622 my $components = $self->get_marc_components();
624 Returns an array of search results data, which are component parts of
625 this object (MARC21 773 points to this)
629 sub get_marc_components {
630 my ($self, $max_results) = @_;
632 return [] if (C4::Context->preference('marcflavour') ne 'MARC21');
634 my ( $searchstr, $sort ) = $self->get_components_query;
637 if (defined($searchstr)) {
638 my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
639 my ( $error, $results, $facets );
641 ( $error, $results, $facets ) = $searcher->search_compat( $searchstr, undef, [$sort], ['biblioserver'], $max_results, 0, undef, undef, 'ccl', 0 );
646 warn "Warning from search_compat: '$error'";
650 message => 'component_search',
655 $components = $results->{biblioserver}->{RECORDS} if defined($results) && $results->{biblioserver}->{hits};
658 return $components // [];
661 =head2 get_components_query
663 Returns a query which can be used to search for all component parts of MARC21 biblios
667 sub get_components_query {
670 my $builder = Koha::SearchEngine::QueryBuilder->new(
671 { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
672 my $marc = $self->metadata->record;
673 my $component_sort_field = C4::Context->preference('ComponentSortField') // "title";
674 my $component_sort_order = C4::Context->preference('ComponentSortOrder') // "asc";
675 my $sort = $component_sort_field . "_" . $component_sort_order;
678 if ( C4::Context->preference('UseControlNumber') ) {
679 my $pf001 = $marc->field('001') || undef;
681 if ( defined($pf001) ) {
683 my $pf003 = $marc->field('003') || undef;
685 if ( !defined($pf003) ) {
686 # search for 773$w='Host001'
687 $searchstr .= "rcn:\"" . $pf001->data()."\"";
691 # search for (773$w='Host001' and 003='Host003') or 773$w='(Host003)Host001'
692 $searchstr .= "(rcn:\"" . $pf001->data() . "\" AND cni:\"" . $pf003->data() . "\")";
693 $searchstr .= " OR rcn:\"" . $pf003->data() . " " . $pf001->data() . "\"";
697 # limit to monograph and serial component part records
698 $searchstr .= " AND (bib-level:a OR bib-level:b)";
703 my $cleaned_title = $marc->subfield('245', "a");
704 $cleaned_title =~ tr|/||;
705 $cleaned_title = $builder->clean_search_term($cleaned_title);
706 $searchstr = qq#Host-item:("$cleaned_title")#;
708 my ($error, $query ,$query_str) = $builder->build_query_compat( undef, [$searchstr], undef, undef, [$sort], 0 );
714 return ($query, $query_str, $sort);
719 my $subscriptions = $self->subscriptions
721 Returns the related Koha::Subscriptions object for this Biblio object
727 my $rs = $self->_result->subscriptions;
728 return Koha::Subscriptions->_new_from_dbic($rs);
731 =head3 has_items_waiting_or_intransit
733 my $itemsWaitingOrInTransit = $biblio->has_items_waiting_or_intransit
735 Tells if this bibliographic record has items waiting or in transit.
739 sub has_items_waiting_or_intransit {
742 if ( Koha::Holds->search({ biblionumber => $self->id,
743 found => ['W', 'T'] })->count ) {
747 foreach my $item ( $self->items->as_list ) {
748 return 1 if $item->get_transfer;
756 my $coins = $biblio->get_coins;
758 Returns the COinS (a span) which can be included in a biblio record
765 my $record = $self->metadata->record;
767 my $pos7 = substr $record->leader(), 7, 1;
768 my $pos6 = substr $record->leader(), 6, 1;
771 my ( $aulast, $aufirst ) = ( '', '' );
782 # For the purposes of generating COinS metadata, LDR/06-07 can be
783 # considered the same for UNIMARC and MARC21
792 'i' => 'audioRecording',
793 'j' => 'audioRecording',
796 'm' => 'computerProgram',
801 'a' => 'journalArticle',
805 $genre = $fmts6->{$pos6} ? $fmts6->{$pos6} : 'book';
807 if ( $genre eq 'book' ) {
808 $genre = $fmts7->{$pos7} if $fmts7->{$pos7};
811 ##### We must transform mtx to a valable mtx and document type ####
812 if ( $genre eq 'book' ) {
815 } elsif ( $genre eq 'journal' ) {
818 } elsif ( $genre eq 'journalArticle' ) {
826 if ( C4::Context->preference("marcflavour") eq "UNIMARC" ) {
829 $aulast = $record->subfield( '700', 'a' ) || '';
830 $aufirst = $record->subfield( '700', 'b' ) || '';
831 push @authors, "$aufirst $aulast" if ($aufirst or $aulast);
834 if ( $record->field('200') ) {
835 for my $au ( $record->field('200')->subfield('g') ) {
840 $title = $record->subfield( '200', 'a' );
841 my $subfield_210d = $record->subfield('210', 'd');
842 if ($subfield_210d and $subfield_210d =~ /(\d{4})/) {
845 $publisher = $record->subfield( '210', 'c' ) || '';
846 $isbn = $record->subfield( '010', 'a' ) || '';
847 $issn = $record->subfield( '011', 'a' ) || '';
850 # MARC21 need some improve
853 if ( $record->field('100') ) {
854 push @authors, $record->subfield( '100', 'a' );
858 if ( $record->field('700') ) {
859 for my $au ( $record->field('700')->subfield('a') ) {
863 $title = $record->field('245');
864 $title &&= $title->as_string('ab');
865 if ($titletype eq 'a') {
866 $pubyear = $record->field('008') || '';
867 $pubyear = substr($pubyear->data(), 7, 4) if $pubyear;
868 $isbn = $record->subfield( '773', 'z' ) || '';
869 $issn = $record->subfield( '773', 'x' ) || '';
870 $hosttitle = $record->subfield( '773', 't' ) || $record->subfield( '773', 'a') || q{};
871 my @rels = $record->subfield( '773', 'g' );
872 $pages = join(', ', @rels);
874 $pubyear = $record->subfield( '260', 'c' ) || '';
875 $publisher = $record->subfield( '260', 'b' ) || '';
876 $isbn = $record->subfield( '020', 'a' ) || '';
877 $issn = $record->subfield( '022', 'a' ) || '';
883 [ 'ctx_ver', 'Z39.88-2004' ],
884 [ 'rft_val_fmt', "info:ofi/fmt:kev:mtx:$mtx" ],
885 [ ($mtx eq 'dc' ? 'rft.type' : 'rft.genre'), $genre ],
886 [ "rft.${titletype}title", $title ],
889 # rft.title is authorized only once, so by checking $titletype
890 # we ensure that rft.title is not already in the list.
891 if ($hosttitle and $titletype) {
892 push @params, [ 'rft.title', $hosttitle ];
896 [ 'rft.isbn', $isbn ],
897 [ 'rft.issn', $issn ],
900 # If it's a subscription, these informations have no meaning.
901 if ($genre ne 'journal') {
903 [ 'rft.aulast', $aulast ],
904 [ 'rft.aufirst', $aufirst ],
905 (map { [ 'rft.au', $_ ] } @authors),
906 [ 'rft.pub', $publisher ],
907 [ 'rft.date', $pubyear ],
908 [ 'rft.pages', $pages ],
912 my $coins_value = join( '&',
913 map { $$_[1] ? $$_[0] . '=' . uri_escape_utf8( $$_[1] ) : () } @params );
920 my $url = $biblio->get_openurl;
922 Returns url for OpenURL resolver set in OpenURLResolverURL system preference
929 my $OpenURLResolverURL = C4::Context->preference('OpenURLResolverURL');
931 if ($OpenURLResolverURL) {
932 my $uri = URI->new($OpenURLResolverURL);
934 if (not defined $uri->query) {
935 $OpenURLResolverURL .= '?';
937 $OpenURLResolverURL .= '&';
939 $OpenURLResolverURL .= $self->get_coins;
942 return $OpenURLResolverURL;
947 my $serial = $biblio->is_serial
949 Return boolean true if this bibbliographic record is continuing resource
956 return 1 if $self->serial;
958 my $record = $self->metadata->record;
959 return 1 if substr($record->leader, 7, 1) eq 's';
964 =head3 custom_cover_image_url
966 my $image_url = $biblio->custom_cover_image_url
968 Return the specific url of the cover image for this bibliographic record.
969 It is built regaring the value of the system preference CustomCoverImagesURL
973 sub custom_cover_image_url {
975 my $url = C4::Context->preference('CustomCoverImagesURL');
976 if ( $url =~ m|{isbn}| ) {
977 my $isbn = $self->biblioitem->isbn;
979 $url =~ s|{isbn}|$isbn|g;
981 if ( $url =~ m|{normalized_isbn}| ) {
982 my $normalized_isbn = $self->normalized_isbn;
983 return unless $normalized_isbn;
984 $url =~ s|{normalized_isbn}|$normalized_isbn|g;
986 if ( $url =~ m|{issn}| ) {
987 my $issn = $self->biblioitem->issn;
989 $url =~ s|{issn}|$issn|g;
992 my $re = qr|{(?<field>\d{3})(\$(?<subfield>.))?}|;
994 my $field = $+{field};
995 my $subfield = $+{subfield};
996 my $marc_record = $self->metadata->record;
999 $value = $marc_record->subfield( $field, $subfield );
1001 my $controlfield = $marc_record->field($field);
1002 $value = $controlfield->data() if $controlfield;
1004 return unless $value;
1005 $url =~ s|$re|$value|;
1013 Return the cover images associated with this biblio.
1020 my $cover_images_rs = $self->_result->cover_images;
1021 return unless $cover_images_rs;
1022 return Koha::CoverImages->_new_from_dbic($cover_images_rs);
1025 =head3 get_marc_notes
1027 $marcnotesarray = $biblio->get_marc_notes({ opac => 1 });
1029 Get all notes from the MARC record and returns them in an array.
1030 The notes are stored in different fields depending on MARC flavour.
1031 MARC21 5XX $u subfields receive special attention as they are URIs.
1035 sub get_marc_notes {
1036 my ( $self, $params ) = @_;
1038 my $marcflavour = C4::Context->preference('marcflavour');
1039 my $opac = $params->{opac} // '0';
1040 my $interface = $params->{opac} ? 'opac' : 'intranet';
1042 my $record = $params->{record} // $self->metadata->record;
1043 my $record_processor = Koha::RecordProcessor->new(
1045 filters => [ 'ViewPolicy', 'ExpandCodedFields' ],
1047 interface => $interface,
1048 frameworkcode => $self->frameworkcode
1052 $record_processor->process($record);
1054 my $scope = $marcflavour eq "UNIMARC"? '3..': '5..';
1055 #MARC21 specs indicate some notes should be private if first indicator 0
1056 my %maybe_private = (
1064 my %hiddenlist = map { $_ => 1 }
1065 split( /,/, C4::Context->preference('NotesToHide'));
1068 foreach my $field ( $record->field($scope) ) {
1069 my $tag = $field->tag();
1070 next if $hiddenlist{ $tag };
1071 next if $opac && $maybe_private{$tag} && !$field->indicator(1);
1072 if( $marcflavour ne 'UNIMARC' && $field->subfield('u') ) {
1073 # Field 5XX$u always contains URI
1074 # Examples: 505u, 506u, 510u, 514u, 520u, 530u, 538u, 540u, 542u, 552u, 555u, 561u, 563u, 583u
1075 # We first push the other subfields, then all $u's separately
1076 # Leave further actions to the template (see e.g. opac-detail)
1078 join '', ( 'a' .. 't', 'v' .. 'z', '0' .. '9' ); # excl 'u'
1079 push @marcnotes, { marcnote => $field->as_string($othersub) };
1080 foreach my $sub ( $field->subfield('u') ) {
1081 $sub =~ s/^\s+|\s+$//g; # trim
1082 push @marcnotes, { marcnote => $sub };
1085 push @marcnotes, { marcnote => $field->as_string() };
1091 =head3 _get_marc_authors
1093 Private method to return the list of authors contained in the MARC record.
1094 See get get_marc_contributors and get_marc_authors for the public methods.
1098 sub _get_marc_authors {
1099 my ( $self, $params ) = @_;
1101 my $fields_filter = $params->{fields_filter};
1102 my $mintag = $params->{mintag};
1103 my $maxtag = $params->{maxtag};
1105 my $AuthoritySeparator = C4::Context->preference('AuthoritySeparator');
1106 my $marcflavour = C4::Context->preference('marcflavour');
1108 # tagslib useful only for UNIMARC author responsibilities
1109 my $tagslib = $marcflavour eq "UNIMARC"
1110 ? C4::Biblio::GetMarcStructure( 1, $self->frameworkcode, { unsafe => 1 } )
1114 foreach my $field ( $self->metadata->record->field($fields_filter) ) {
1117 if $mintag && $field->tag() < $mintag
1118 || $maxtag && $field->tag() > $maxtag;
1122 my @subfields = $field->subfields();
1125 # if there is an authority link, build the link with Koha-Auth-Number: subfield9
1126 my $subfield9 = $field->subfield('9');
1128 my $linkvalue = $subfield9;
1129 $linkvalue =~ s/(\(|\))//g;
1130 @link_loop = ( { 'limit' => 'an', 'link' => $linkvalue } );
1135 for my $authors_subfield (@subfields) {
1136 next if ( $authors_subfield->[0] eq '9' );
1138 # unimarc3 contains the $3 of the author for UNIMARC.
1139 # For french academic libraries, it's the "ppn", and it's required for idref webservice
1140 $unimarc3 = $authors_subfield->[1] if $marcflavour eq 'UNIMARC' and $authors_subfield->[0] =~ /3/;
1142 # don't load unimarc subfields 3, 5
1143 next if ( $marcflavour eq 'UNIMARC' and ( $authors_subfield->[0] =~ /3|5/ ) );
1145 my $code = $authors_subfield->[0];
1146 my $value = $authors_subfield->[1];
1147 my $linkvalue = $value;
1148 $linkvalue =~ s/(\(|\))//g;
1149 # UNIMARC author responsibility
1150 if ( $marcflavour eq 'UNIMARC' and $code eq '4' ) {
1151 $value = C4::Biblio::GetAuthorisedValueDesc( $field->tag(), $code, $value, '', $tagslib );
1152 $linkvalue = "($value)";
1154 # if no authority link, build a search query
1155 unless ($subfield9) {
1158 'link' => $linkvalue,
1159 operator => (scalar @link_loop) ? ' AND ' : undef
1162 my @this_link_loop = @link_loop;
1164 unless ( $code eq '0') {
1165 push @subfields_loop, {
1166 tag => $field->tag(),
1169 link_loop => \@this_link_loop,
1170 separator => (scalar @subfields_loop) ? $AuthoritySeparator : ''
1174 push @marcauthors, {
1175 MARCAUTHOR_SUBFIELDS_LOOP => \@subfields_loop,
1176 authoritylink => $subfield9,
1177 unimarc3 => $unimarc3
1180 return \@marcauthors;
1183 =head3 get_marc_contributors
1185 my $contributors = $biblio->get_marc_contributors;
1187 Get all contributors (but first author) from the MARC record and returns them in an array.
1188 They are stored in different fields depending on MARC flavour (700..720 for MARC21)
1192 sub get_marc_contributors {
1193 my ( $self, $params ) = @_;
1195 my ( $mintag, $maxtag, $fields_filter );
1196 my $marcflavour = C4::Context->preference('marcflavour');
1198 if ( $marcflavour eq "UNIMARC" ) {
1201 $fields_filter = '7..';
1202 } else { # marc21/normarc
1205 $fields_filter = '7..';
1208 return $self->_get_marc_authors(
1210 fields_filter => $fields_filter,
1217 =head3 get_marc_authors
1219 my $authors = $biblio->get_marc_authors;
1221 Get all authors from the MARC record and returns them in an array.
1222 They are stored in different fields depending on MARC flavour
1223 (main author from 100 then secondary authors from 700..720).
1227 sub get_marc_authors {
1228 my ( $self, $params ) = @_;
1230 my ( $mintag, $maxtag, $fields_filter );
1231 my $marcflavour = C4::Context->preference('marcflavour');
1233 if ( $marcflavour eq "UNIMARC" ) {
1234 $fields_filter = '200';
1235 } else { # marc21/normarc
1236 $fields_filter = '100';
1239 my @first_authors = @{$self->_get_marc_authors(
1241 fields_filter => $fields_filter,
1247 my @other_authors = @{$self->get_marc_contributors};
1249 return [@first_authors, @other_authors];
1252 =head3 normalized_isbn
1254 my $normalized_isbn = $biblio->normalized_isbn
1256 Normalizes and returns the first valid ISBN found in the record.
1257 ISBN13 are converted into ISBN10. This is required to get some book cover images.
1261 sub normalized_isbn {
1263 return C4::Koha::GetNormalizedISBN($self->biblioitem->isbn);
1268 my $json = $biblio->to_api;
1270 Overloaded method that returns a JSON representation of the Koha::Biblio object,
1271 suitable for API output. The related Koha::Biblioitem object is merged as expected
1277 my ($self, $args) = @_;
1279 my $response = $self->SUPER::to_api( $args );
1280 my $biblioitem = $self->biblioitem->to_api;
1282 return { %$response, %$biblioitem };
1285 =head3 to_api_mapping
1287 This method returns the mapping for representing a Koha::Biblio object
1292 sub to_api_mapping {
1294 biblionumber => 'biblio_id',
1295 frameworkcode => 'framework_id',
1296 unititle => 'uniform_title',
1297 seriestitle => 'series_title',
1298 copyrightdate => 'copyright_date',
1299 datecreated => 'creation_date',
1300 deleted_on => undef,
1304 =head3 get_marc_host
1306 $host = $biblio->get_marc_host;
1308 ( $host, $relatedparts, $hostinfo ) = $biblio->get_marc_host;
1310 Returns host biblio record from MARC21 773 (undef if no 773 present).
1311 It looks at the first 773 field with MARCorgCode or only a control
1312 number. Complete $w or numeric part is used to search host record.
1313 The optional parameter no_items triggers a check if $biblio has items.
1314 If there are, the sub returns undef.
1315 Called in list context, it also returns 773$g (related parts).
1317 If there is no $w, we use $0 (host biblionumber) or $9 (host itemnumber)
1318 to search for the host record. If there is also no $0 and no $9, we search
1319 using author and title. Failing all of that, we return an undef host and
1320 form a concatenation of strings with 773$agt for host information,
1321 returned when called in list context.
1326 my ($self, $params) = @_;
1327 my $no_items = $params->{no_items};
1328 return if C4::Context->preference('marcflavour') eq 'UNIMARC'; # TODO
1329 return if $params->{no_items} && $self->items->count > 0;
1332 eval { $record = $self->metadata->record };
1335 # We pick the first $w with your MARCOrgCode or the first $w that has no
1336 # code (between parentheses) at all.
1337 my $orgcode = C4::Context->preference('MARCOrgCode') // q{};
1339 foreach my $f ( $record->field('773') ) {
1340 my $w = $f->subfield('w') or next;
1341 if( $w =~ /^\($orgcode\)\s*(\d+)/i or $w =~ /^\d+/ ) {
1347 my $engine = Koha::SearchEngine::Search->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1349 if ( !$hostfld and $record->subfield('773','t') ) {
1350 # not linked using $w
1351 my $unlinkedf = $record->field('773');
1353 if ( C4::Context->preference("EasyAnalyticalRecords") ) {
1354 if ( $unlinkedf->subfield('0') ) {
1355 # use 773$0 host biblionumber
1356 $bibno = $unlinkedf->subfield('0');
1357 } elsif ( $unlinkedf->subfield('9') ) {
1358 # use 773$9 host itemnumber
1359 my $linkeditemnumber = $unlinkedf->subfield('9');
1360 $bibno = Koha::Items->find( $linkeditemnumber )->biblionumber;
1364 my $host = Koha::Biblios->find($bibno) or return;
1365 return wantarray ? ( $host, $unlinkedf->subfield('g') ) : $host;
1367 # just return plaintext and no host record
1368 my $hostinfo = join( ", ", $unlinkedf->subfield('a'), $unlinkedf->subfield('t'), $unlinkedf->subfield('g') );
1369 return wantarray ? ( undef, $unlinkedf->subfield('g'), $hostinfo ) : undef;
1371 return if !$hostfld;
1372 my $rcn = $hostfld->subfield('w');
1374 # Look for control number with/without orgcode
1375 for my $try (1..2) {
1376 my ( $error, $results, $total_hits ) = $engine->simple_search_compat( 'Control-number='.$rcn, 0,1 );
1377 if( !$error and $total_hits == 1 ) {
1378 $bibno = $engine->extract_biblionumber( $results->[0] );
1381 # Add or remove orgcode for second try
1382 if( $try == 1 && $rcn =~ /\)\s*(\d+)/ ) {
1383 $rcn = $1; # number only
1384 } elsif( $try == 1 && $rcn =~ /^\d+/ ) {
1385 $rcn = "($orgcode)$rcn";
1391 my $host = Koha::Biblios->find($bibno) or return;
1392 return wantarray ? ( $host, $hostfld->subfield('g') ) : $host;
1396 =head3 get_marc_host_only
1398 my $host = $biblio->get_marc_host_only;
1404 sub get_marc_host_only {
1407 my ( $host ) = $self->get_marc_host;
1412 =head3 get_marc_relatedparts_only
1414 my $relatedparts = $biblio->get_marc_relatedparts_only;
1416 Return related parts only
1420 sub get_marc_relatedparts_only {
1423 my ( undef, $relatedparts ) = $self->get_marc_host;
1425 return $relatedparts;
1428 =head3 get_marc_hostinfo_only
1430 my $hostinfo = $biblio->get_marc_hostinfo_only;
1432 Return host info only
1436 sub get_marc_hostinfo_only {
1439 my ( $host, $relatedparts, $hostinfo ) = $self->get_marc_host;
1446 my $recalls = $biblio->recalls;
1448 Return recalls linked to this biblio
1454 return Koha::Recalls->_new_from_dbic( scalar $self->_result->recalls );
1457 =head3 can_be_recalled
1459 my @items_for_recall = $biblio->can_be_recalled({ patron => $patron_object });
1461 Does biblio-level checks and returns the items attached to this biblio that are available for recall
1465 sub can_be_recalled {
1466 my ( $self, $params ) = @_;
1468 return 0 if !( C4::Context->preference('UseRecalls') );
1470 my $patron = $params->{patron};
1472 my $branchcode = C4::Context->userenv->{'branch'};
1473 if ( C4::Context->preference('CircControl') eq 'PatronLibrary' and $patron ) {
1474 $branchcode = $patron->branchcode;
1477 my @all_items = Koha::Items->search({ biblionumber => $self->biblionumber })->as_list;
1479 # if there are no available items at all, no recall can be placed
1480 return 0 if ( scalar @all_items == 0 );
1485 my @all_itemnumbers;
1486 foreach my $item ( @all_items ) {
1487 push( @all_itemnumbers, $item->itemnumber );
1488 if ( $item->can_be_recalled({ patron => $patron }) ) {
1489 push( @itemtypes, $item->effective_itemtype );
1490 push( @itemnumbers, $item->itemnumber );
1491 push( @items, $item );
1495 # if there are no recallable items, no recall can be placed
1496 return 0 if ( scalar @items == 0 );
1498 # Check the circulation rule for each relevant itemtype for this biblio
1499 my ( @recalls_allowed, @recalls_per_record, @on_shelf_recalls );
1500 foreach my $itemtype ( @itemtypes ) {
1501 my $rule = Koha::CirculationRules->get_effective_rules({
1502 branchcode => $branchcode,
1503 categorycode => $patron ? $patron->categorycode : undef,
1504 itemtype => $itemtype,
1507 'recalls_per_record',
1511 push( @recalls_allowed, $rule->{recalls_allowed} ) if $rule;
1512 push( @recalls_per_record, $rule->{recalls_per_record} ) if $rule;
1513 push( @on_shelf_recalls, $rule->{on_shelf_recalls} ) if $rule;
1515 my $recalls_allowed = (sort {$b <=> $a} @recalls_allowed)[0]; # take highest
1516 my $recalls_per_record = (sort {$b <=> $a} @recalls_per_record)[0]; # take highest
1517 my %on_shelf_recalls_count = ();
1518 foreach my $count ( @on_shelf_recalls ) {
1519 $on_shelf_recalls_count{$count}++;
1521 my $on_shelf_recalls = (sort {$on_shelf_recalls_count{$b} <=> $on_shelf_recalls_count{$a}} @on_shelf_recalls)[0]; # take most common
1523 # check recalls allowed has been set and is not zero
1524 return 0 if ( !defined($recalls_allowed) || $recalls_allowed == 0 );
1527 # check borrower has not reached open recalls allowed limit
1528 return 0 if ( $patron->recalls->filter_by_current->count >= $recalls_allowed );
1530 # check borrower has not reached open recalls allowed per record limit
1531 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $recalls_per_record );
1533 # check if any of the items under this biblio are already checked out by this borrower
1534 return 0 if ( Koha::Checkouts->search({ itemnumber => [ @all_itemnumbers ], borrowernumber => $patron->borrowernumber })->count > 0 );
1537 # check item availability
1538 my $checked_out_count = 0;
1540 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1543 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1544 return 0 if ( $on_shelf_recalls eq 'all' && $checked_out_count < scalar @items );
1546 # can't recall if no items have been checked out
1547 return 0 if ( $checked_out_count == 0 );
1553 =head2 Internal methods
1565 Kyle M Hall <kyle@bywatersolutions.com>