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::Biblioitems;
38 use Koha::CirculationRules;
39 use Koha::Item::Transfer::Limits;
42 use Koha::Old::Checkouts;
44 use Koha::RecordProcessor;
45 use Koha::Suggestions;
46 use Koha::Subscriptions;
47 use Koha::SearchEngine;
48 use Koha::SearchEngine::Search;
49 use Koha::SearchEngine::QueryBuilder;
53 Koha::Biblio - Koha Biblio Object class
63 Overloaded I<store> method to set default values
70 $self->datecreated( dt_from_string ) unless $self->datecreated;
72 return $self->SUPER::store;
77 my $metadata = $biblio->metadata();
79 Returns a Koha::Biblio::Metadata object
86 my $metadata = $self->_result->metadata;
87 return Koha::Biblio::Metadata->_new_from_dbic($metadata);
92 my $orders = $biblio->orders();
94 Returns a Koha::Acquisition::Orders object
101 my $orders = $self->_result->orders;
102 return Koha::Acquisition::Orders->_new_from_dbic($orders);
107 my $active_orders = $biblio->active_orders();
109 Returns the active acquisition orders related to this biblio.
110 An order is considered active when it is not cancelled (i.e. when datecancellation
118 return $self->orders->search({ datecancellationprinted => undef });
121 =head3 can_article_request
123 my $bool = $biblio->can_article_request( $borrower );
125 Returns true if article requests can be made for this record
127 $borrower must be a Koha::Patron object
131 sub can_article_request {
132 my ( $self, $borrower ) = @_;
134 my $rule = $self->article_request_type($borrower);
135 return q{} if $rule eq 'item_only' && !$self->items()->count();
136 return 1 if $rule && $rule ne 'no';
141 =head3 can_be_transferred
143 $biblio->can_be_transferred({ to => $to_library, from => $from_library })
145 Checks if at least one item of a biblio can be transferred to given library.
147 This feature is controlled by two system preferences:
148 UseBranchTransferLimits to enable / disable the feature
149 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
150 for setting the limitations
152 Performance-wise, it is recommended to use this method for a biblio instead of
153 iterating each item of a biblio with Koha::Item->can_be_transferred().
155 Takes HASHref that can have the following parameters:
156 MANDATORY PARAMETERS:
159 $from : Koha::Library # if given, only items from that
160 # holdingbranch are considered
162 Returns 1 if at least one of the item of a biblio can be transferred
163 to $to_library, otherwise 0.
167 sub can_be_transferred {
168 my ($self, $params) = @_;
170 my $to = $params->{to};
171 my $from = $params->{from};
173 return 1 unless C4::Context->preference('UseBranchTransferLimits');
174 my $limittype = C4::Context->preference('BranchTransferLimitsType');
177 foreach my $item_of_bib ($self->items->as_list) {
178 next unless $item_of_bib->holdingbranch;
179 next if $from && $from->branchcode ne $item_of_bib->holdingbranch;
180 return 1 if $item_of_bib->holdingbranch eq $to->branchcode;
181 my $code = $limittype eq 'itemtype'
182 ? $item_of_bib->effective_itemtype
183 : $item_of_bib->ccode;
184 return 1 unless $code;
185 $items->{$code}->{$item_of_bib->holdingbranch} = 1;
188 # At this point we will have a HASHref containing each itemtype/ccode that
189 # this biblio has, inside which are all of the holdingbranches where those
190 # items are located at. Then, we will query Koha::Item::Transfer::Limits to
191 # find out whether a transfer limits for such $limittype from any of the
192 # listed holdingbranches to the given $to library exist. If at least one
193 # holdingbranch for that $limittype does not have a transfer limit to given
194 # $to library, then we know that the transfer is possible.
195 foreach my $code (keys %{$items}) {
196 my @holdingbranches = keys %{$items->{$code}};
197 return 1 if Koha::Item::Transfer::Limits->search({
198 toBranch => $to->branchcode,
199 fromBranch => { 'in' => \@holdingbranches },
202 group_by => [qw/fromBranch/]
203 })->count == scalar(@holdingbranches) ? 0 : 1;
210 =head3 pickup_locations
212 my $pickup_locations = $biblio->pickup_locations( {patron => $patron } );
214 Returns a Koha::Libraries set of possible pickup locations for this biblio's items,
215 according to patron's home library (if patron is defined and holds are allowed
216 only from hold groups) and if item can be transferred to each pickup location.
220 sub pickup_locations {
221 my ( $self, $params ) = @_;
223 my $patron = $params->{patron};
225 my @pickup_locations;
226 foreach my $item_of_bib ( $self->items->as_list ) {
227 push @pickup_locations,
228 $item_of_bib->pickup_locations( { patron => $patron } )
229 ->_resultset->get_column('branchcode')->all;
232 return Koha::Libraries->search(
233 { branchcode => { '-in' => \@pickup_locations } }, { order_by => ['branchname'] } );
236 =head3 hidden_in_opac
238 my $bool = $biblio->hidden_in_opac({ [ rules => $rules ] })
240 Returns true if the biblio matches the hidding criteria defined in $rules.
241 Returns false otherwise. It involves the I<OpacHiddenItems> and
242 I<OpacHiddenItemsHidesRecord> system preferences.
244 Takes HASHref that can have the following parameters:
246 $rules : { <field> => [ value_1, ... ], ... }
248 Note: $rules inherits its structure from the parsed YAML from reading
249 the I<OpacHiddenItems> system preference.
254 my ( $self, $params ) = @_;
256 my $rules = $params->{rules} // {};
258 my @items = $self->items->as_list;
260 return 0 unless @items; # Do not hide if there is no item
262 # Ok, there are items, don't even try the rules unless OpacHiddenItemsHidesRecord
263 return 0 unless C4::Context->preference('OpacHiddenItemsHidesRecord');
265 return !(any { !$_->hidden_in_opac({ rules => $rules }) } @items);
268 =head3 article_request_type
270 my $type = $biblio->article_request_type( $borrower );
272 Returns the article request type based on items, or on the record
273 itself if there are no items.
275 $borrower must be a Koha::Patron object
279 sub article_request_type {
280 my ( $self, $borrower ) = @_;
282 return q{} unless $borrower;
284 my $rule = $self->article_request_type_for_items( $borrower );
285 return $rule if $rule;
287 # If the record has no items that are requestable, go by the record itemtype
288 $rule = $self->article_request_type_for_bib($borrower);
289 return $rule if $rule;
294 =head3 article_request_type_for_bib
296 my $type = $biblio->article_request_type_for_bib
298 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record
302 sub article_request_type_for_bib {
303 my ( $self, $borrower ) = @_;
305 return q{} unless $borrower;
307 my $borrowertype = $borrower->categorycode;
308 my $itemtype = $self->itemtype();
310 my $rule = Koha::CirculationRules->get_effective_rule(
312 rule_name => 'article_requests',
313 categorycode => $borrowertype,
314 itemtype => $itemtype,
318 return q{} unless $rule;
319 return $rule->rule_value || q{}
322 =head3 article_request_type_for_items
324 my $type = $biblio->article_request_type_for_items
326 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record's items
328 If there is a conflict where some items are 'bib_only' and some are 'item_only', 'bib_only' will be returned.
332 sub article_request_type_for_items {
333 my ( $self, $borrower ) = @_;
336 foreach my $item ( $self->items()->as_list() ) {
337 my $rule = $item->article_request_type($borrower);
338 return $rule if $rule eq 'bib_only'; # we don't need to go any further
342 return 'item_only' if $counts->{item_only};
343 return 'yes' if $counts->{yes};
344 return 'no' if $counts->{no};
348 =head3 article_requests
350 my $article_requests = $biblio->article_requests
352 Returns the article requests associated with this biblio
356 sub article_requests {
359 return Koha::ArticleRequests->_new_from_dbic( scalar $self->_result->article_requests );
362 =head3 current_checkouts
364 my $current_checkouts = $biblio->current_checkouts
366 Returns the current checkouts associated with this biblio
370 sub current_checkouts {
373 return Koha::Checkouts->search( { "item.biblionumber" => $self->id },
374 { join => 'item' } );
379 my $old_checkouts = $biblio->old_checkouts
381 Returns the past checkouts associated with this biblio
388 return Koha::Old::Checkouts->search( { "item.biblionumber" => $self->id },
389 { join => 'item' } );
394 my $items = $biblio->items();
396 Returns the related Koha::Items object for this biblio
403 my $items_rs = $self->_result->items;
405 return Koha::Items->_new_from_dbic( $items_rs );
410 my $host_items = $biblio->host_items();
412 Return the host items (easy analytical record)
419 return Koha::Items->new->empty
420 unless C4::Context->preference('EasyAnalyticalRecords');
422 my $marcflavour = C4::Context->preference("marcflavour");
423 my $analyticfield = '773';
424 if ( $marcflavour eq 'MARC21' ) {
425 $analyticfield = '773';
427 elsif ( $marcflavour eq 'UNIMARC' ) {
428 $analyticfield = '461';
430 my $marc_record = $self->metadata->record;
432 foreach my $field ( $marc_record->field($analyticfield) ) {
433 push @itemnumbers, $field->subfield('9');
436 return Koha::Items->search( { itemnumber => { -in => \@itemnumbers } } );
441 my $itemtype = $biblio->itemtype();
443 Returns the itemtype for this record.
450 return $self->biblioitem()->itemtype();
455 my $holds = $biblio->holds();
457 return the current holds placed on this record
462 my ( $self, $params, $attributes ) = @_;
463 $attributes->{order_by} = 'priority' unless exists $attributes->{order_by};
464 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
465 return Koha::Holds->_new_from_dbic($hold_rs);
470 my $holds = $biblio->current_holds
472 Return the holds placed on this bibliographic record.
473 It does not include future holds.
479 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
481 { reservedate => { '<=' => $dtf->format_date(dt_from_string) } } );
486 my $field = $self->biblioitem()->itemtype
488 Returns the related Koha::Biblioitem object for this Biblio object
495 $self->{_biblioitem} ||= Koha::Biblioitems->find( { biblionumber => $self->biblionumber() } );
497 return $self->{_biblioitem};
502 my $suggestions = $self->suggestions
504 Returns the related Koha::Suggestions object for this Biblio object
511 my $suggestions_rs = $self->_result->suggestions;
512 return Koha::Suggestions->_new_from_dbic( $suggestions_rs );
515 =head3 get_marc_components
517 my $components = $self->get_marc_components();
519 Returns an array of search results data, which are component parts of
520 this object (MARC21 773 points to this)
524 sub get_marc_components {
525 my ($self, $max_results) = @_;
527 return [] if (C4::Context->preference('marcflavour') ne 'MARC21');
529 my ( $searchstr, $sort ) = $self->get_components_query;
532 if (defined($searchstr)) {
533 my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
534 my ( $error, $results, $facets );
536 ( $error, $results, $facets ) = $searcher->search_compat( $searchstr, undef, [$sort], ['biblioserver'], $max_results, 0, undef, undef, 'ccl', 0 );
541 warn "Warning from search_compat: '$error'";
545 message => 'component_search',
550 $components = $results->{biblioserver}->{RECORDS} if defined($results) && $results->{biblioserver}->{hits};
553 return $components // [];
556 =head2 get_components_query
558 Returns a query which can be used to search for all component parts of MARC21 biblios
562 sub get_components_query {
565 my $builder = Koha::SearchEngine::QueryBuilder->new(
566 { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
567 my $marc = $self->metadata->record;
568 my $component_sort_field = C4::Context->preference('ComponentSortField') // "title";
569 my $component_sort_order = C4::Context->preference('ComponentSortOrder') // "asc";
570 my $sort = $component_sort_field . "_" . $component_sort_order;
573 if ( C4::Context->preference('UseControlNumber') ) {
574 my $pf001 = $marc->field('001') || undef;
576 if ( defined($pf001) ) {
578 my $pf003 = $marc->field('003') || undef;
580 if ( !defined($pf003) ) {
581 # search for 773$w='Host001'
582 $searchstr .= "rcn:" . $pf001->data();
586 # search for (773$w='Host001' and 003='Host003') or 773$w='(Host003)Host001'
587 $searchstr .= "(rcn:" . $pf001->data() . " AND cni:" . $pf003->data() . ")";
588 $searchstr .= " OR rcn:\"" . $pf003->data() . " " . $pf001->data() . "\"";
592 # limit to monograph and serial component part records
593 $searchstr .= " AND (bib-level:a OR bib-level:b)";
598 my $cleaned_title = $marc->subfield('245', "a");
599 $cleaned_title =~ tr|/||;
600 $cleaned_title = $builder->clean_search_term($cleaned_title);
601 $searchstr = qq#Host-item:("$cleaned_title")#;
603 my ($error, $query_str) = $builder->build_query_compat( undef, [$searchstr], undef, undef, [$sort], 0 );
609 return ($query_str, $sort);
614 my $subscriptions = $self->subscriptions
616 Returns the related Koha::Subscriptions object for this Biblio object
623 $self->{_subscriptions} ||= Koha::Subscriptions->search( { biblionumber => $self->biblionumber } );
625 return $self->{_subscriptions};
628 =head3 has_items_waiting_or_intransit
630 my $itemsWaitingOrInTransit = $biblio->has_items_waiting_or_intransit
632 Tells if this bibliographic record has items waiting or in transit.
636 sub has_items_waiting_or_intransit {
639 if ( Koha::Holds->search({ biblionumber => $self->id,
640 found => ['W', 'T'] })->count ) {
644 foreach my $item ( $self->items->as_list ) {
645 return 1 if $item->get_transfer;
653 my $coins = $biblio->get_coins;
655 Returns the COinS (a span) which can be included in a biblio record
662 my $record = $self->metadata->record;
664 my $pos7 = substr $record->leader(), 7, 1;
665 my $pos6 = substr $record->leader(), 6, 1;
668 my ( $aulast, $aufirst ) = ( '', '' );
679 # For the purposes of generating COinS metadata, LDR/06-07 can be
680 # considered the same for UNIMARC and MARC21
689 'i' => 'audioRecording',
690 'j' => 'audioRecording',
693 'm' => 'computerProgram',
698 'a' => 'journalArticle',
702 $genre = $fmts6->{$pos6} ? $fmts6->{$pos6} : 'book';
704 if ( $genre eq 'book' ) {
705 $genre = $fmts7->{$pos7} if $fmts7->{$pos7};
708 ##### We must transform mtx to a valable mtx and document type ####
709 if ( $genre eq 'book' ) {
712 } elsif ( $genre eq 'journal' ) {
715 } elsif ( $genre eq 'journalArticle' ) {
723 if ( C4::Context->preference("marcflavour") eq "UNIMARC" ) {
726 $aulast = $record->subfield( '700', 'a' ) || '';
727 $aufirst = $record->subfield( '700', 'b' ) || '';
728 push @authors, "$aufirst $aulast" if ($aufirst or $aulast);
731 if ( $record->field('200') ) {
732 for my $au ( $record->field('200')->subfield('g') ) {
737 $title = $record->subfield( '200', 'a' );
738 my $subfield_210d = $record->subfield('210', 'd');
739 if ($subfield_210d and $subfield_210d =~ /(\d{4})/) {
742 $publisher = $record->subfield( '210', 'c' ) || '';
743 $isbn = $record->subfield( '010', 'a' ) || '';
744 $issn = $record->subfield( '011', 'a' ) || '';
747 # MARC21 need some improve
750 if ( $record->field('100') ) {
751 push @authors, $record->subfield( '100', 'a' );
755 if ( $record->field('700') ) {
756 for my $au ( $record->field('700')->subfield('a') ) {
760 $title = $record->field('245');
761 $title &&= $title->as_string('ab');
762 if ($titletype eq 'a') {
763 $pubyear = $record->field('008') || '';
764 $pubyear = substr($pubyear->data(), 7, 4) if $pubyear;
765 $isbn = $record->subfield( '773', 'z' ) || '';
766 $issn = $record->subfield( '773', 'x' ) || '';
767 $hosttitle = $record->subfield( '773', 't' ) || $record->subfield( '773', 'a') || q{};
768 my @rels = $record->subfield( '773', 'g' );
769 $pages = join(', ', @rels);
771 $pubyear = $record->subfield( '260', 'c' ) || '';
772 $publisher = $record->subfield( '260', 'b' ) || '';
773 $isbn = $record->subfield( '020', 'a' ) || '';
774 $issn = $record->subfield( '022', 'a' ) || '';
780 [ 'ctx_ver', 'Z39.88-2004' ],
781 [ 'rft_val_fmt', "info:ofi/fmt:kev:mtx:$mtx" ],
782 [ ($mtx eq 'dc' ? 'rft.type' : 'rft.genre'), $genre ],
783 [ "rft.${titletype}title", $title ],
786 # rft.title is authorized only once, so by checking $titletype
787 # we ensure that rft.title is not already in the list.
788 if ($hosttitle and $titletype) {
789 push @params, [ 'rft.title', $hosttitle ];
793 [ 'rft.isbn', $isbn ],
794 [ 'rft.issn', $issn ],
797 # If it's a subscription, these informations have no meaning.
798 if ($genre ne 'journal') {
800 [ 'rft.aulast', $aulast ],
801 [ 'rft.aufirst', $aufirst ],
802 (map { [ 'rft.au', $_ ] } @authors),
803 [ 'rft.pub', $publisher ],
804 [ 'rft.date', $pubyear ],
805 [ 'rft.pages', $pages ],
809 my $coins_value = join( '&',
810 map { $$_[1] ? $$_[0] . '=' . uri_escape_utf8( $$_[1] ) : () } @params );
817 my $url = $biblio->get_openurl;
819 Returns url for OpenURL resolver set in OpenURLResolverURL system preference
826 my $OpenURLResolverURL = C4::Context->preference('OpenURLResolverURL');
828 if ($OpenURLResolverURL) {
829 my $uri = URI->new($OpenURLResolverURL);
831 if (not defined $uri->query) {
832 $OpenURLResolverURL .= '?';
834 $OpenURLResolverURL .= '&';
836 $OpenURLResolverURL .= $self->get_coins;
839 return $OpenURLResolverURL;
844 my $serial = $biblio->is_serial
846 Return boolean true if this bibbliographic record is continuing resource
853 return 1 if $self->serial;
855 my $record = $self->metadata->record;
856 return 1 if substr($record->leader, 7, 1) eq 's';
861 =head3 custom_cover_image_url
863 my $image_url = $biblio->custom_cover_image_url
865 Return the specific url of the cover image for this bibliographic record.
866 It is built regaring the value of the system preference CustomCoverImagesURL
870 sub custom_cover_image_url {
872 my $url = C4::Context->preference('CustomCoverImagesURL');
873 if ( $url =~ m|{isbn}| ) {
874 my $isbn = $self->biblioitem->isbn;
876 $url =~ s|{isbn}|$isbn|g;
878 if ( $url =~ m|{normalized_isbn}| ) {
879 my $normalized_isbn = C4::Koha::GetNormalizedISBN($self->biblioitem->isbn);
880 return unless $normalized_isbn;
881 $url =~ s|{normalized_isbn}|$normalized_isbn|g;
883 if ( $url =~ m|{issn}| ) {
884 my $issn = $self->biblioitem->issn;
886 $url =~ s|{issn}|$issn|g;
889 my $re = qr|{(?<field>\d{3})(\$(?<subfield>.))?}|;
891 my $field = $+{field};
892 my $subfield = $+{subfield};
893 my $marc_record = $self->metadata->record;
896 $value = $marc_record->subfield( $field, $subfield );
898 my $controlfield = $marc_record->field($field);
899 $value = $controlfield->data() if $controlfield;
901 return unless $value;
902 $url =~ s|$re|$value|;
910 Return the cover images associated with this biblio.
917 my $cover_images_rs = $self->_result->cover_images;
918 return unless $cover_images_rs;
919 return Koha::CoverImages->_new_from_dbic($cover_images_rs);
922 =head3 get_marc_notes
924 $marcnotesarray = $biblio->get_marc_notes({ opac => 1 });
926 Get all notes from the MARC record and returns them in an array.
927 The notes are stored in different fields depending on MARC flavour.
928 MARC21 5XX $u subfields receive special attention as they are URIs.
933 my ( $self, $params ) = @_;
935 my $marcflavour = C4::Context->preference('marcflavour');
936 my $opac = $params->{opac} // '0';
937 my $interface = $params->{opac} ? 'opac' : 'intranet';
939 my $record = $params->{record} // $self->metadata->record;
940 my $record_processor = Koha::RecordProcessor->new(
942 filters => [ 'ViewPolicy', 'ExpandCodedFields' ],
944 interface => $interface,
945 frameworkcode => $self->frameworkcode
949 $record_processor->process($record);
951 my $scope = $marcflavour eq "UNIMARC"? '3..': '5..';
952 #MARC21 specs indicate some notes should be private if first indicator 0
953 my %maybe_private = (
961 my %hiddenlist = map { $_ => 1 }
962 split( /,/, C4::Context->preference('NotesToHide'));
965 foreach my $field ( $record->field($scope) ) {
966 my $tag = $field->tag();
967 next if $hiddenlist{ $tag };
968 next if $opac && $maybe_private{$tag} && !$field->indicator(1);
969 if( $marcflavour ne 'UNIMARC' && $field->subfield('u') ) {
970 # Field 5XX$u always contains URI
971 # Examples: 505u, 506u, 510u, 514u, 520u, 530u, 538u, 540u, 542u, 552u, 555u, 561u, 563u, 583u
972 # We first push the other subfields, then all $u's separately
973 # Leave further actions to the template (see e.g. opac-detail)
975 join '', ( 'a' .. 't', 'v' .. 'z', '0' .. '9' ); # excl 'u'
976 push @marcnotes, { marcnote => $field->as_string($othersub) };
977 foreach my $sub ( $field->subfield('u') ) {
978 $sub =~ s/^\s+|\s+$//g; # trim
979 push @marcnotes, { marcnote => $sub };
982 push @marcnotes, { marcnote => $field->as_string() };
988 =head3 get_marc_authors
990 my $authors = $biblio->get_marc_authors;
992 Get all authors from the MARC record and returns them in an array.
993 The authors are stored in different fields depending on MARC flavour
997 sub get_marc_authors {
998 my ( $self, $params ) = @_;
1000 my ( $mintag, $maxtag, $fields_filter );
1001 my $marcflavour = C4::Context->preference('marcflavour');
1003 # tagslib useful only for UNIMARC author responsibilities
1005 if ( $marcflavour eq "UNIMARC" ) {
1006 $tagslib = C4::Biblio::GetMarcStructure( 1, $self->frameworkcode, { unsafe => 1 });
1009 $fields_filter = '7..';
1010 } else { # marc21/normarc
1013 $fields_filter = '7..';
1017 my $AuthoritySeparator = C4::Context->preference('AuthoritySeparator');
1019 foreach my $field ( $self->metadata->record->field($fields_filter) ) {
1020 next unless $field->tag() >= $mintag && $field->tag() <= $maxtag;
1023 my @subfields = $field->subfields();
1026 # if there is an authority link, build the link with Koha-Auth-Number: subfield9
1027 my $subfield9 = $field->subfield('9');
1029 my $linkvalue = $subfield9;
1030 $linkvalue =~ s/(\(|\))//g;
1031 @link_loop = ( { 'limit' => 'an', 'link' => $linkvalue } );
1036 for my $authors_subfield (@subfields) {
1037 next if ( $authors_subfield->[0] eq '9' );
1039 # unimarc3 contains the $3 of the author for UNIMARC.
1040 # For french academic libraries, it's the "ppn", and it's required for idref webservice
1041 $unimarc3 = $authors_subfield->[1] if $marcflavour eq 'UNIMARC' and $authors_subfield->[0] =~ /3/;
1043 # don't load unimarc subfields 3, 5
1044 next if ( $marcflavour eq 'UNIMARC' and ( $authors_subfield->[0] =~ /3|5/ ) );
1046 my $code = $authors_subfield->[0];
1047 my $value = $authors_subfield->[1];
1048 my $linkvalue = $value;
1049 $linkvalue =~ s/(\(|\))//g;
1050 # UNIMARC author responsibility
1051 if ( $marcflavour eq 'UNIMARC' and $code eq '4' ) {
1052 $value = C4::Biblio::GetAuthorisedValueDesc( $field->tag(), $code, $value, '', $tagslib );
1053 $linkvalue = "($value)";
1055 # if no authority link, build a search query
1056 unless ($subfield9) {
1059 'link' => $linkvalue,
1060 operator => (scalar @link_loop) ? ' AND ' : undef
1063 my @this_link_loop = @link_loop;
1065 unless ( $code eq '0') {
1066 push @subfields_loop, {
1067 tag => $field->tag(),
1070 link_loop => \@this_link_loop,
1071 separator => (scalar @subfields_loop) ? $AuthoritySeparator : ''
1075 push @marcauthors, {
1076 MARCAUTHOR_SUBFIELDS_LOOP => \@subfields_loop,
1077 authoritylink => $subfield9,
1078 unimarc3 => $unimarc3
1081 return \@marcauthors;
1086 my $json = $biblio->to_api;
1088 Overloaded method that returns a JSON representation of the Koha::Biblio object,
1089 suitable for API output. The related Koha::Biblioitem object is merged as expected
1095 my ($self, $args) = @_;
1097 my $response = $self->SUPER::to_api( $args );
1098 my $biblioitem = $self->biblioitem->to_api;
1100 return { %$response, %$biblioitem };
1103 =head3 to_api_mapping
1105 This method returns the mapping for representing a Koha::Biblio object
1110 sub to_api_mapping {
1112 biblionumber => 'biblio_id',
1113 frameworkcode => 'framework_id',
1114 unititle => 'uniform_title',
1115 seriestitle => 'series_title',
1116 copyrightdate => 'copyright_date',
1117 datecreated => 'creation_date'
1121 =head3 get_marc_host
1123 $host = $biblio->get_marc_host;
1125 ( $host, $relatedparts ) = $biblio->get_marc_host;
1127 Returns host biblio record from MARC21 773 (undef if no 773 present).
1128 It looks at the first 773 field with MARCorgCode or only a control
1129 number. Complete $w or numeric part is used to search host record.
1130 The optional parameter no_items triggers a check if $biblio has items.
1131 If there are, the sub returns undef.
1132 Called in list context, it also returns 773$g (related parts).
1137 my ($self, $params) = @_;
1138 my $no_items = $params->{no_items};
1139 return if C4::Context->preference('marcflavour') eq 'UNIMARC'; # TODO
1140 return if $params->{no_items} && $self->items->count > 0;
1143 eval { $record = $self->metadata->record };
1146 # We pick the first $w with your MARCOrgCode or the first $w that has no
1147 # code (between parentheses) at all.
1148 my $orgcode = C4::Context->preference('MARCOrgCode') // q{};
1150 foreach my $f ( $record->field('773') ) {
1151 my $w = $f->subfield('w') or next;
1152 if( $w =~ /^\($orgcode\)\s*(\d+)/i or $w =~ /^\d+/ ) {
1157 return if !$hostfld;
1158 my $rcn = $hostfld->subfield('w');
1160 # Look for control number with/without orgcode
1161 my $engine = Koha::SearchEngine::Search->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1163 for my $try (1..2) {
1164 my ( $error, $results, $total_hits ) = $engine->simple_search_compat( 'Control-number='.$rcn, 0,1 );
1165 if( !$error and $total_hits == 1 ) {
1166 $bibno = $engine->extract_biblionumber( $results->[0] );
1169 # Add or remove orgcode for second try
1170 if( $try == 1 && $rcn =~ /\)\s*(\d+)/ ) {
1171 $rcn = $1; # number only
1172 } elsif( $try == 1 && $rcn =~ /^\d+/ ) {
1173 $rcn = "($orgcode)$rcn";
1179 my $host = Koha::Biblios->find($bibno) or return;
1180 return wantarray ? ( $host, $hostfld->subfield('g') ) : $host;
1186 my $recalls = $biblio->recalls;
1188 Return recalls linked to this biblio
1194 return Koha::Recalls->_new_from_dbic( scalar $self->_result->recalls );
1197 =head3 can_be_recalled
1199 my @items_for_recall = $biblio->can_be_recalled({ patron => $patron_object });
1201 Does biblio-level checks and returns the items attached to this biblio that are available for recall
1205 sub can_be_recalled {
1206 my ( $self, $params ) = @_;
1208 return 0 if !( C4::Context->preference('UseRecalls') );
1210 my $patron = $params->{patron};
1212 my $branchcode = C4::Context->userenv->{'branch'};
1213 if ( C4::Context->preference('CircControl') eq 'PatronLibrary' and $patron ) {
1214 $branchcode = $patron->branchcode;
1217 my @all_items = Koha::Items->search({ biblionumber => $self->biblionumber })->as_list;
1219 # if there are no available items at all, no recall can be placed
1220 return 0 if ( scalar @all_items == 0 );
1225 my @all_itemnumbers;
1226 foreach my $item ( @all_items ) {
1227 push( @all_itemnumbers, $item->itemnumber );
1228 if ( $item->can_be_recalled({ patron => $patron }) ) {
1229 push( @itemtypes, $item->effective_itemtype );
1230 push( @itemnumbers, $item->itemnumber );
1231 push( @items, $item );
1235 # if there are no recallable items, no recall can be placed
1236 return 0 if ( scalar @items == 0 );
1238 # Check the circulation rule for each relevant itemtype for this biblio
1239 my ( @recalls_allowed, @recalls_per_record, @on_shelf_recalls );
1240 foreach my $itemtype ( @itemtypes ) {
1241 my $rule = Koha::CirculationRules->get_effective_rules({
1242 branchcode => $branchcode,
1243 categorycode => $patron ? $patron->categorycode : undef,
1244 itemtype => $itemtype,
1247 'recalls_per_record',
1251 push( @recalls_allowed, $rule->{recalls_allowed} ) if $rule;
1252 push( @recalls_per_record, $rule->{recalls_per_record} ) if $rule;
1253 push( @on_shelf_recalls, $rule->{on_shelf_recalls} ) if $rule;
1255 my $recalls_allowed = (sort {$b <=> $a} @recalls_allowed)[0]; # take highest
1256 my $recalls_per_record = (sort {$b <=> $a} @recalls_per_record)[0]; # take highest
1257 my %on_shelf_recalls_count = ();
1258 foreach my $count ( @on_shelf_recalls ) {
1259 $on_shelf_recalls_count{$count}++;
1261 my $on_shelf_recalls = (sort {$on_shelf_recalls_count{$b} <=> $on_shelf_recalls_count{$a}} @on_shelf_recalls)[0]; # take most common
1263 # check recalls allowed has been set and is not zero
1264 return 0 if ( !defined($recalls_allowed) || $recalls_allowed == 0 );
1267 # check borrower has not reached open recalls allowed limit
1268 return 0 if ( $patron->recalls->filter_by_current->count >= $recalls_allowed );
1270 # check borrower has not reached open recalls allowed per record limit
1271 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $recalls_per_record );
1273 # check if any of the items under this biblio are already checked out by this borrower
1274 return 0 if ( Koha::Checkouts->search({ itemnumber => [ @all_itemnumbers ], borrowernumber => $patron->borrowernumber })->count > 0 );
1277 # check item availability
1278 my $checked_out_count = 0;
1280 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1283 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1284 return 0 if ( $on_shelf_recalls eq 'all' && $checked_out_count < scalar @items );
1286 # can't recall if no items have been checked out
1287 return 0 if ( $checked_out_count == 0 );
1293 =head2 Internal methods
1305 Kyle M Hall <kyle@bywatersolutions.com>