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;
39 use Koha::CirculationRules;
40 use Koha::Item::Transfer::Limits;
43 use Koha::Old::Checkouts;
45 use Koha::RecordProcessor;
46 use Koha::Suggestions;
47 use Koha::Subscriptions;
48 use Koha::SearchEngine;
49 use Koha::SearchEngine::Search;
50 use Koha::SearchEngine::QueryBuilder;
55 Koha::Biblio - Koha Biblio Object class
65 Overloaded I<store> method to set default values
72 $self->datecreated( dt_from_string ) unless $self->datecreated;
74 return $self->SUPER::store;
79 my $metadata = $biblio->metadata();
81 Returns a Koha::Biblio::Metadata object
88 my $metadata = $self->_result->metadata;
89 return Koha::Biblio::Metadata->_new_from_dbic($metadata);
94 my $orders = $biblio->orders();
96 Returns a Koha::Acquisition::Orders object
103 my $orders = $self->_result->orders;
104 return Koha::Acquisition::Orders->_new_from_dbic($orders);
109 my $active_orders = $biblio->active_orders();
111 Returns the active acquisition orders related to this biblio.
112 An order is considered active when it is not cancelled (i.e. when datecancellation
120 return $self->orders->search({ datecancellationprinted => undef });
125 my $tickets = $biblio->tickets();
127 Returns all tickets linked to the biblio
133 my $rs = $self->_result->tickets;
134 return Koha::Tickets->_new_from_dbic( $rs );
139 my $item_groups = $biblio->item_groups();
141 Returns a Koha::Biblio::ItemGroups object
148 my $item_groups = $self->_result->item_groups;
149 return Koha::Biblio::ItemGroups->_new_from_dbic($item_groups);
152 =head3 can_article_request
154 my $bool = $biblio->can_article_request( $borrower );
156 Returns true if article requests can be made for this record
158 $borrower must be a Koha::Patron object
162 sub can_article_request {
163 my ( $self, $borrower ) = @_;
165 my $rule = $self->article_request_type($borrower);
166 return q{} if $rule eq 'item_only' && !$self->items()->count();
167 return 1 if $rule && $rule ne 'no';
172 =head3 can_be_transferred
174 $biblio->can_be_transferred({ to => $to_library, from => $from_library })
176 Checks if at least one item of a biblio can be transferred to given library.
178 This feature is controlled by two system preferences:
179 UseBranchTransferLimits to enable / disable the feature
180 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
181 for setting the limitations
183 Performance-wise, it is recommended to use this method for a biblio instead of
184 iterating each item of a biblio with Koha::Item->can_be_transferred().
186 Takes HASHref that can have the following parameters:
187 MANDATORY PARAMETERS:
190 $from : Koha::Library # if given, only items from that
191 # holdingbranch are considered
193 Returns 1 if at least one of the item of a biblio can be transferred
194 to $to_library, otherwise 0.
198 sub can_be_transferred {
199 my ($self, $params) = @_;
201 my $to = $params->{to};
202 my $from = $params->{from};
204 return 1 unless C4::Context->preference('UseBranchTransferLimits');
205 my $limittype = C4::Context->preference('BranchTransferLimitsType');
208 foreach my $item_of_bib ($self->items->as_list) {
209 next unless $item_of_bib->holdingbranch;
210 next if $from && $from->branchcode ne $item_of_bib->holdingbranch;
211 return 1 if $item_of_bib->holdingbranch eq $to->branchcode;
212 my $code = $limittype eq 'itemtype'
213 ? $item_of_bib->effective_itemtype
214 : $item_of_bib->ccode;
215 return 1 unless $code;
216 $items->{$code}->{$item_of_bib->holdingbranch} = 1;
219 # At this point we will have a HASHref containing each itemtype/ccode that
220 # this biblio has, inside which are all of the holdingbranches where those
221 # items are located at. Then, we will query Koha::Item::Transfer::Limits to
222 # find out whether a transfer limits for such $limittype from any of the
223 # listed holdingbranches to the given $to library exist. If at least one
224 # holdingbranch for that $limittype does not have a transfer limit to given
225 # $to library, then we know that the transfer is possible.
226 foreach my $code (keys %{$items}) {
227 my @holdingbranches = keys %{$items->{$code}};
228 return 1 if Koha::Item::Transfer::Limits->search({
229 toBranch => $to->branchcode,
230 fromBranch => { 'in' => \@holdingbranches },
233 group_by => [qw/fromBranch/]
234 })->count == scalar(@holdingbranches) ? 0 : 1;
241 =head3 pickup_locations
243 my $pickup_locations = $biblio->pickup_locations( {patron => $patron } );
245 Returns a Koha::Libraries set of possible pickup locations for this biblio's items,
246 according to patron's home library (if patron is defined and holds are allowed
247 only from hold groups) and if item can be transferred to each pickup location.
251 sub pickup_locations {
252 my ( $self, $params ) = @_;
254 my $patron = $params->{patron};
256 my @pickup_locations;
257 foreach my $item_of_bib ( $self->items->as_list ) {
258 push @pickup_locations,
259 $item_of_bib->pickup_locations( { patron => $patron } )
260 ->_resultset->get_column('branchcode')->all;
263 return Koha::Libraries->search(
264 { branchcode => { '-in' => \@pickup_locations } }, { order_by => ['branchname'] } );
267 =head3 hidden_in_opac
269 my $bool = $biblio->hidden_in_opac({ [ rules => $rules ] })
271 Returns true if the biblio matches the hidding criteria defined in $rules.
272 Returns false otherwise. It involves the I<OpacHiddenItems> and
273 I<OpacHiddenItemsHidesRecord> system preferences.
275 Takes HASHref that can have the following parameters:
277 $rules : { <field> => [ value_1, ... ], ... }
279 Note: $rules inherits its structure from the parsed YAML from reading
280 the I<OpacHiddenItems> system preference.
285 my ( $self, $params ) = @_;
287 my $rules = $params->{rules} // {};
289 my @items = $self->items->as_list;
291 return 0 unless @items; # Do not hide if there is no item
293 # Ok, there are items, don't even try the rules unless OpacHiddenItemsHidesRecord
294 return 0 unless C4::Context->preference('OpacHiddenItemsHidesRecord');
296 return !(any { !$_->hidden_in_opac({ rules => $rules }) } @items);
299 =head3 article_request_type
301 my $type = $biblio->article_request_type( $borrower );
303 Returns the article request type based on items, or on the record
304 itself if there are no items.
306 $borrower must be a Koha::Patron object
310 sub article_request_type {
311 my ( $self, $borrower ) = @_;
313 return q{} unless $borrower;
315 my $rule = $self->article_request_type_for_items( $borrower );
316 return $rule if $rule;
318 # If the record has no items that are requestable, go by the record itemtype
319 $rule = $self->article_request_type_for_bib($borrower);
320 return $rule if $rule;
325 =head3 article_request_type_for_bib
327 my $type = $biblio->article_request_type_for_bib
329 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record
333 sub article_request_type_for_bib {
334 my ( $self, $borrower ) = @_;
336 return q{} unless $borrower;
338 my $borrowertype = $borrower->categorycode;
339 my $itemtype = $self->itemtype();
341 my $rule = Koha::CirculationRules->get_effective_rule(
343 rule_name => 'article_requests',
344 categorycode => $borrowertype,
345 itemtype => $itemtype,
349 return q{} unless $rule;
350 return $rule->rule_value || q{}
353 =head3 article_request_type_for_items
355 my $type = $biblio->article_request_type_for_items
357 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record's items
359 If there is a conflict where some items are 'bib_only' and some are 'item_only', 'bib_only' will be returned.
363 sub article_request_type_for_items {
364 my ( $self, $borrower ) = @_;
367 foreach my $item ( $self->items()->as_list() ) {
368 my $rule = $item->article_request_type($borrower);
369 return $rule if $rule eq 'bib_only'; # we don't need to go any further
373 return 'item_only' if $counts->{item_only};
374 return 'yes' if $counts->{yes};
375 return 'no' if $counts->{no};
379 =head3 article_requests
381 my $article_requests = $biblio->article_requests
383 Returns the article requests associated with this biblio
387 sub article_requests {
390 return Koha::ArticleRequests->_new_from_dbic( scalar $self->_result->article_requests );
393 =head3 current_checkouts
395 my $current_checkouts = $biblio->current_checkouts
397 Returns the current checkouts associated with this biblio
401 sub current_checkouts {
404 return Koha::Checkouts->search( { "item.biblionumber" => $self->id },
405 { join => 'item' } );
410 my $old_checkouts = $biblio->old_checkouts
412 Returns the past checkouts associated with this biblio
419 return Koha::Old::Checkouts->search( { "item.biblionumber" => $self->id },
420 { join => 'item' } );
425 my $items = $biblio->items();
427 Returns the related Koha::Items object for this biblio
434 my $items_rs = $self->_result->items;
436 return Koha::Items->_new_from_dbic( $items_rs );
441 my $host_items = $biblio->host_items();
443 Return the host items (easy analytical record)
450 return Koha::Items->new->empty
451 unless C4::Context->preference('EasyAnalyticalRecords');
453 my $marcflavour = C4::Context->preference("marcflavour");
454 my $analyticfield = '773';
455 if ( $marcflavour eq 'MARC21' ) {
456 $analyticfield = '773';
458 elsif ( $marcflavour eq 'UNIMARC' ) {
459 $analyticfield = '461';
461 my $marc_record = $self->metadata->record;
463 foreach my $field ( $marc_record->field($analyticfield) ) {
464 push @itemnumbers, $field->subfield('9');
467 return Koha::Items->search( { itemnumber => { -in => \@itemnumbers } } );
472 my $itemtype = $biblio->itemtype();
474 Returns the itemtype for this record.
481 return $self->biblioitem()->itemtype();
486 my $holds = $biblio->holds();
488 return the current holds placed on this record
493 my ( $self, $params, $attributes ) = @_;
494 $attributes->{order_by} = 'priority' unless exists $attributes->{order_by};
495 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
496 return Koha::Holds->_new_from_dbic($hold_rs);
501 my $holds = $biblio->current_holds
503 Return the holds placed on this bibliographic record.
504 It does not include future holds.
510 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
512 { reservedate => { '<=' => $dtf->format_date(dt_from_string) } } );
517 my $field = $self->biblioitem()->itemtype
519 Returns the related Koha::Biblioitem object for this Biblio object
526 $self->{_biblioitem} ||= Koha::Biblioitems->find( { biblionumber => $self->biblionumber() } );
528 return $self->{_biblioitem};
533 my $suggestions = $self->suggestions
535 Returns the related Koha::Suggestions object for this Biblio object
542 my $suggestions_rs = $self->_result->suggestions;
543 return Koha::Suggestions->_new_from_dbic( $suggestions_rs );
546 =head3 get_marc_components
548 my $components = $self->get_marc_components();
550 Returns an array of search results data, which are component parts of
551 this object (MARC21 773 points to this)
555 sub get_marc_components {
556 my ($self, $max_results) = @_;
558 return [] if (C4::Context->preference('marcflavour') ne 'MARC21');
560 my ( $searchstr, $sort ) = $self->get_components_query;
563 if (defined($searchstr)) {
564 my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
565 my ( $error, $results, $facets );
567 ( $error, $results, $facets ) = $searcher->search_compat( $searchstr, undef, [$sort], ['biblioserver'], $max_results, 0, undef, undef, 'ccl', 0 );
572 warn "Warning from search_compat: '$error'";
576 message => 'component_search',
581 $components = $results->{biblioserver}->{RECORDS} if defined($results) && $results->{biblioserver}->{hits};
584 return $components // [];
587 =head2 get_components_query
589 Returns a query which can be used to search for all component parts of MARC21 biblios
593 sub get_components_query {
596 my $builder = Koha::SearchEngine::QueryBuilder->new(
597 { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
598 my $marc = $self->metadata->record;
599 my $component_sort_field = C4::Context->preference('ComponentSortField') // "title";
600 my $component_sort_order = C4::Context->preference('ComponentSortOrder') // "asc";
601 my $sort = $component_sort_field . "_" . $component_sort_order;
604 if ( C4::Context->preference('UseControlNumber') ) {
605 my $pf001 = $marc->field('001') || undef;
607 if ( defined($pf001) ) {
609 my $pf003 = $marc->field('003') || undef;
611 if ( !defined($pf003) ) {
612 # search for 773$w='Host001'
613 $searchstr .= "rcn:\"" . $pf001->data()."\"";
617 # search for (773$w='Host001' and 003='Host003') or 773$w='(Host003)Host001'
618 $searchstr .= "(rcn:\"" . $pf001->data() . "\" AND cni:\"" . $pf003->data() . "\")";
619 $searchstr .= " OR rcn:\"" . $pf003->data() . " " . $pf001->data() . "\"";
623 # limit to monograph and serial component part records
624 $searchstr .= " AND (bib-level:a OR bib-level:b)";
629 my $cleaned_title = $marc->subfield('245', "a");
630 $cleaned_title =~ tr|/||;
631 $cleaned_title = $builder->clean_search_term($cleaned_title);
632 $searchstr = qq#Host-item:("$cleaned_title")#;
634 my ($error, $query ,$query_str) = $builder->build_query_compat( undef, [$searchstr], undef, undef, [$sort], 0 );
640 return ($query, $query_str, $sort);
645 my $subscriptions = $self->subscriptions
647 Returns the related Koha::Subscriptions object for this Biblio object
654 $self->{_subscriptions} ||= Koha::Subscriptions->search( { biblionumber => $self->biblionumber } );
656 return $self->{_subscriptions};
659 =head3 has_items_waiting_or_intransit
661 my $itemsWaitingOrInTransit = $biblio->has_items_waiting_or_intransit
663 Tells if this bibliographic record has items waiting or in transit.
667 sub has_items_waiting_or_intransit {
670 if ( Koha::Holds->search({ biblionumber => $self->id,
671 found => ['W', 'T'] })->count ) {
675 foreach my $item ( $self->items->as_list ) {
676 return 1 if $item->get_transfer;
684 my $coins = $biblio->get_coins;
686 Returns the COinS (a span) which can be included in a biblio record
693 my $record = $self->metadata->record;
695 my $pos7 = substr $record->leader(), 7, 1;
696 my $pos6 = substr $record->leader(), 6, 1;
699 my ( $aulast, $aufirst ) = ( '', '' );
710 # For the purposes of generating COinS metadata, LDR/06-07 can be
711 # considered the same for UNIMARC and MARC21
720 'i' => 'audioRecording',
721 'j' => 'audioRecording',
724 'm' => 'computerProgram',
729 'a' => 'journalArticle',
733 $genre = $fmts6->{$pos6} ? $fmts6->{$pos6} : 'book';
735 if ( $genre eq 'book' ) {
736 $genre = $fmts7->{$pos7} if $fmts7->{$pos7};
739 ##### We must transform mtx to a valable mtx and document type ####
740 if ( $genre eq 'book' ) {
743 } elsif ( $genre eq 'journal' ) {
746 } elsif ( $genre eq 'journalArticle' ) {
754 if ( C4::Context->preference("marcflavour") eq "UNIMARC" ) {
757 $aulast = $record->subfield( '700', 'a' ) || '';
758 $aufirst = $record->subfield( '700', 'b' ) || '';
759 push @authors, "$aufirst $aulast" if ($aufirst or $aulast);
762 if ( $record->field('200') ) {
763 for my $au ( $record->field('200')->subfield('g') ) {
768 $title = $record->subfield( '200', 'a' );
769 my $subfield_210d = $record->subfield('210', 'd');
770 if ($subfield_210d and $subfield_210d =~ /(\d{4})/) {
773 $publisher = $record->subfield( '210', 'c' ) || '';
774 $isbn = $record->subfield( '010', 'a' ) || '';
775 $issn = $record->subfield( '011', 'a' ) || '';
778 # MARC21 need some improve
781 if ( $record->field('100') ) {
782 push @authors, $record->subfield( '100', 'a' );
786 if ( $record->field('700') ) {
787 for my $au ( $record->field('700')->subfield('a') ) {
791 $title = $record->field('245');
792 $title &&= $title->as_string('ab');
793 if ($titletype eq 'a') {
794 $pubyear = $record->field('008') || '';
795 $pubyear = substr($pubyear->data(), 7, 4) if $pubyear;
796 $isbn = $record->subfield( '773', 'z' ) || '';
797 $issn = $record->subfield( '773', 'x' ) || '';
798 $hosttitle = $record->subfield( '773', 't' ) || $record->subfield( '773', 'a') || q{};
799 my @rels = $record->subfield( '773', 'g' );
800 $pages = join(', ', @rels);
802 $pubyear = $record->subfield( '260', 'c' ) || '';
803 $publisher = $record->subfield( '260', 'b' ) || '';
804 $isbn = $record->subfield( '020', 'a' ) || '';
805 $issn = $record->subfield( '022', 'a' ) || '';
811 [ 'ctx_ver', 'Z39.88-2004' ],
812 [ 'rft_val_fmt', "info:ofi/fmt:kev:mtx:$mtx" ],
813 [ ($mtx eq 'dc' ? 'rft.type' : 'rft.genre'), $genre ],
814 [ "rft.${titletype}title", $title ],
817 # rft.title is authorized only once, so by checking $titletype
818 # we ensure that rft.title is not already in the list.
819 if ($hosttitle and $titletype) {
820 push @params, [ 'rft.title', $hosttitle ];
824 [ 'rft.isbn', $isbn ],
825 [ 'rft.issn', $issn ],
828 # If it's a subscription, these informations have no meaning.
829 if ($genre ne 'journal') {
831 [ 'rft.aulast', $aulast ],
832 [ 'rft.aufirst', $aufirst ],
833 (map { [ 'rft.au', $_ ] } @authors),
834 [ 'rft.pub', $publisher ],
835 [ 'rft.date', $pubyear ],
836 [ 'rft.pages', $pages ],
840 my $coins_value = join( '&',
841 map { $$_[1] ? $$_[0] . '=' . uri_escape_utf8( $$_[1] ) : () } @params );
848 my $url = $biblio->get_openurl;
850 Returns url for OpenURL resolver set in OpenURLResolverURL system preference
857 my $OpenURLResolverURL = C4::Context->preference('OpenURLResolverURL');
859 if ($OpenURLResolverURL) {
860 my $uri = URI->new($OpenURLResolverURL);
862 if (not defined $uri->query) {
863 $OpenURLResolverURL .= '?';
865 $OpenURLResolverURL .= '&';
867 $OpenURLResolverURL .= $self->get_coins;
870 return $OpenURLResolverURL;
875 my $serial = $biblio->is_serial
877 Return boolean true if this bibbliographic record is continuing resource
884 return 1 if $self->serial;
886 my $record = $self->metadata->record;
887 return 1 if substr($record->leader, 7, 1) eq 's';
892 =head3 custom_cover_image_url
894 my $image_url = $biblio->custom_cover_image_url
896 Return the specific url of the cover image for this bibliographic record.
897 It is built regaring the value of the system preference CustomCoverImagesURL
901 sub custom_cover_image_url {
903 my $url = C4::Context->preference('CustomCoverImagesURL');
904 if ( $url =~ m|{isbn}| ) {
905 my $isbn = $self->biblioitem->isbn;
907 $url =~ s|{isbn}|$isbn|g;
909 if ( $url =~ m|{normalized_isbn}| ) {
910 my $normalized_isbn = C4::Koha::GetNormalizedISBN($self->biblioitem->isbn);
911 return unless $normalized_isbn;
912 $url =~ s|{normalized_isbn}|$normalized_isbn|g;
914 if ( $url =~ m|{issn}| ) {
915 my $issn = $self->biblioitem->issn;
917 $url =~ s|{issn}|$issn|g;
920 my $re = qr|{(?<field>\d{3})(\$(?<subfield>.))?}|;
922 my $field = $+{field};
923 my $subfield = $+{subfield};
924 my $marc_record = $self->metadata->record;
927 $value = $marc_record->subfield( $field, $subfield );
929 my $controlfield = $marc_record->field($field);
930 $value = $controlfield->data() if $controlfield;
932 return unless $value;
933 $url =~ s|$re|$value|;
941 Return the cover images associated with this biblio.
948 my $cover_images_rs = $self->_result->cover_images;
949 return unless $cover_images_rs;
950 return Koha::CoverImages->_new_from_dbic($cover_images_rs);
953 =head3 get_marc_notes
955 $marcnotesarray = $biblio->get_marc_notes({ opac => 1 });
957 Get all notes from the MARC record and returns them in an array.
958 The notes are stored in different fields depending on MARC flavour.
959 MARC21 5XX $u subfields receive special attention as they are URIs.
964 my ( $self, $params ) = @_;
966 my $marcflavour = C4::Context->preference('marcflavour');
967 my $opac = $params->{opac} // '0';
968 my $interface = $params->{opac} ? 'opac' : 'intranet';
970 my $record = $params->{record} // $self->metadata->record;
971 my $record_processor = Koha::RecordProcessor->new(
973 filters => [ 'ViewPolicy', 'ExpandCodedFields' ],
975 interface => $interface,
976 frameworkcode => $self->frameworkcode
980 $record_processor->process($record);
982 my $scope = $marcflavour eq "UNIMARC"? '3..': '5..';
983 #MARC21 specs indicate some notes should be private if first indicator 0
984 my %maybe_private = (
992 my %hiddenlist = map { $_ => 1 }
993 split( /,/, C4::Context->preference('NotesToHide'));
996 foreach my $field ( $record->field($scope) ) {
997 my $tag = $field->tag();
998 next if $hiddenlist{ $tag };
999 next if $opac && $maybe_private{$tag} && !$field->indicator(1);
1000 if( $marcflavour ne 'UNIMARC' && $field->subfield('u') ) {
1001 # Field 5XX$u always contains URI
1002 # Examples: 505u, 506u, 510u, 514u, 520u, 530u, 538u, 540u, 542u, 552u, 555u, 561u, 563u, 583u
1003 # We first push the other subfields, then all $u's separately
1004 # Leave further actions to the template (see e.g. opac-detail)
1006 join '', ( 'a' .. 't', 'v' .. 'z', '0' .. '9' ); # excl 'u'
1007 push @marcnotes, { marcnote => $field->as_string($othersub) };
1008 foreach my $sub ( $field->subfield('u') ) {
1009 $sub =~ s/^\s+|\s+$//g; # trim
1010 push @marcnotes, { marcnote => $sub };
1013 push @marcnotes, { marcnote => $field->as_string() };
1019 =head3 _get_marc_authors
1021 Private method to return the list of authors contained in the MARC record.
1022 See get get_marc_contributors and get_marc_authors for the public methods.
1026 sub _get_marc_authors {
1027 my ( $self, $params ) = @_;
1029 my $fields_filter = $params->{fields_filter};
1030 my $mintag = $params->{mintag};
1031 my $maxtag = $params->{maxtag};
1033 my $AuthoritySeparator = C4::Context->preference('AuthoritySeparator');
1034 my $marcflavour = C4::Context->preference('marcflavour');
1036 # tagslib useful only for UNIMARC author responsibilities
1037 my $tagslib = $marcflavour eq "UNIMARC"
1038 ? C4::Biblio::GetMarcStructure( 1, $self->frameworkcode, { unsafe => 1 } )
1042 foreach my $field ( $self->metadata->record->field($fields_filter) ) {
1045 if $mintag && $field->tag() < $mintag
1046 || $maxtag && $field->tag() > $maxtag;
1050 my @subfields = $field->subfields();
1053 # if there is an authority link, build the link with Koha-Auth-Number: subfield9
1054 my $subfield9 = $field->subfield('9');
1056 my $linkvalue = $subfield9;
1057 $linkvalue =~ s/(\(|\))//g;
1058 @link_loop = ( { 'limit' => 'an', 'link' => $linkvalue } );
1063 for my $authors_subfield (@subfields) {
1064 next if ( $authors_subfield->[0] eq '9' );
1066 # unimarc3 contains the $3 of the author for UNIMARC.
1067 # For french academic libraries, it's the "ppn", and it's required for idref webservice
1068 $unimarc3 = $authors_subfield->[1] if $marcflavour eq 'UNIMARC' and $authors_subfield->[0] =~ /3/;
1070 # don't load unimarc subfields 3, 5
1071 next if ( $marcflavour eq 'UNIMARC' and ( $authors_subfield->[0] =~ /3|5/ ) );
1073 my $code = $authors_subfield->[0];
1074 my $value = $authors_subfield->[1];
1075 my $linkvalue = $value;
1076 $linkvalue =~ s/(\(|\))//g;
1077 # UNIMARC author responsibility
1078 if ( $marcflavour eq 'UNIMARC' and $code eq '4' ) {
1079 $value = C4::Biblio::GetAuthorisedValueDesc( $field->tag(), $code, $value, '', $tagslib );
1080 $linkvalue = "($value)";
1082 # if no authority link, build a search query
1083 unless ($subfield9) {
1086 'link' => $linkvalue,
1087 operator => (scalar @link_loop) ? ' AND ' : undef
1090 my @this_link_loop = @link_loop;
1092 unless ( $code eq '0') {
1093 push @subfields_loop, {
1094 tag => $field->tag(),
1097 link_loop => \@this_link_loop,
1098 separator => (scalar @subfields_loop) ? $AuthoritySeparator : ''
1102 push @marcauthors, {
1103 MARCAUTHOR_SUBFIELDS_LOOP => \@subfields_loop,
1104 authoritylink => $subfield9,
1105 unimarc3 => $unimarc3
1108 return \@marcauthors;
1111 =head3 get_marc_contributors
1113 my $contributors = $biblio->get_marc_contributors;
1115 Get all contributors (but first author) from the MARC record and returns them in an array.
1116 They are stored in different fields depending on MARC flavour (700..720 for MARC21)
1120 sub get_marc_contributors {
1121 my ( $self, $params ) = @_;
1123 my ( $mintag, $maxtag, $fields_filter );
1124 my $marcflavour = C4::Context->preference('marcflavour');
1126 if ( $marcflavour eq "UNIMARC" ) {
1129 $fields_filter = '7..';
1130 } else { # marc21/normarc
1133 $fields_filter = '7..';
1136 return $self->_get_marc_authors(
1138 fields_filter => $fields_filter,
1145 =head3 get_marc_authors
1147 my $authors = $biblio->get_marc_authors;
1149 Get all authors from the MARC record and returns them in an array.
1150 They are stored in different fields depending on MARC flavour
1151 (main author from 100 then secondary authors from 700..720).
1155 sub get_marc_authors {
1156 my ( $self, $params ) = @_;
1158 my ( $mintag, $maxtag, $fields_filter );
1159 my $marcflavour = C4::Context->preference('marcflavour');
1161 if ( $marcflavour eq "UNIMARC" ) {
1162 $fields_filter = '200';
1163 } else { # marc21/normarc
1164 $fields_filter = '100';
1167 my @first_authors = @{$self->_get_marc_authors(
1169 fields_filter => $fields_filter,
1175 my @other_authors = @{$self->get_marc_contributors};
1177 return [@first_authors, @other_authors];
1183 my $json = $biblio->to_api;
1185 Overloaded method that returns a JSON representation of the Koha::Biblio object,
1186 suitable for API output. The related Koha::Biblioitem object is merged as expected
1192 my ($self, $args) = @_;
1194 my $response = $self->SUPER::to_api( $args );
1195 my $biblioitem = $self->biblioitem->to_api;
1197 return { %$response, %$biblioitem };
1200 =head3 to_api_mapping
1202 This method returns the mapping for representing a Koha::Biblio object
1207 sub to_api_mapping {
1209 biblionumber => 'biblio_id',
1210 frameworkcode => 'framework_id',
1211 unititle => 'uniform_title',
1212 seriestitle => 'series_title',
1213 copyrightdate => 'copyright_date',
1214 datecreated => 'creation_date',
1215 deleted_on => undef,
1219 =head3 get_marc_host
1221 $host = $biblio->get_marc_host;
1223 ( $host, $relatedparts, $hostinfo ) = $biblio->get_marc_host;
1225 Returns host biblio record from MARC21 773 (undef if no 773 present).
1226 It looks at the first 773 field with MARCorgCode or only a control
1227 number. Complete $w or numeric part is used to search host record.
1228 The optional parameter no_items triggers a check if $biblio has items.
1229 If there are, the sub returns undef.
1230 Called in list context, it also returns 773$g (related parts).
1232 If there is no $w, we use $0 (host biblionumber) or $9 (host itemnumber)
1233 to search for the host record. If there is also no $0 and no $9, we search
1234 using author and title. Failing all of that, we return an undef host and
1235 form a concatenation of strings with 773$agt for host information,
1236 returned when called in list context.
1241 my ($self, $params) = @_;
1242 my $no_items = $params->{no_items};
1243 return if C4::Context->preference('marcflavour') eq 'UNIMARC'; # TODO
1244 return if $params->{no_items} && $self->items->count > 0;
1247 eval { $record = $self->metadata->record };
1250 # We pick the first $w with your MARCOrgCode or the first $w that has no
1251 # code (between parentheses) at all.
1252 my $orgcode = C4::Context->preference('MARCOrgCode') // q{};
1254 foreach my $f ( $record->field('773') ) {
1255 my $w = $f->subfield('w') or next;
1256 if( $w =~ /^\($orgcode\)\s*(\d+)/i or $w =~ /^\d+/ ) {
1262 my $engine = Koha::SearchEngine::Search->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1264 if ( !$hostfld and $record->subfield('773','t') ) {
1265 # not linked using $w
1266 my $unlinkedf = $record->field('773');
1268 if ( C4::Context->preference("EasyAnalyticalRecords") ) {
1269 if ( $unlinkedf->subfield('0') ) {
1270 # use 773$0 host biblionumber
1271 $bibno = $unlinkedf->subfield('0');
1272 } elsif ( $unlinkedf->subfield('9') ) {
1273 # use 773$9 host itemnumber
1274 my $linkeditemnumber = $unlinkedf->subfield('9');
1275 $bibno = Koha::Items->find( $linkeditemnumber )->biblionumber;
1279 my $host = Koha::Biblios->find($bibno) or return;
1280 return wantarray ? ( $host, $unlinkedf->subfield('g') ) : $host;
1282 # just return plaintext and no host record
1283 my $hostinfo = join( ", ", $unlinkedf->subfield('a'), $unlinkedf->subfield('t'), $unlinkedf->subfield('g') );
1284 return wantarray ? ( undef, $unlinkedf->subfield('g'), $hostinfo ) : undef;
1286 return if !$hostfld;
1287 my $rcn = $hostfld->subfield('w');
1289 # Look for control number with/without orgcode
1290 for my $try (1..2) {
1291 my ( $error, $results, $total_hits ) = $engine->simple_search_compat( 'Control-number='.$rcn, 0,1 );
1292 if( !$error and $total_hits == 1 ) {
1293 $bibno = $engine->extract_biblionumber( $results->[0] );
1296 # Add or remove orgcode for second try
1297 if( $try == 1 && $rcn =~ /\)\s*(\d+)/ ) {
1298 $rcn = $1; # number only
1299 } elsif( $try == 1 && $rcn =~ /^\d+/ ) {
1300 $rcn = "($orgcode)$rcn";
1306 my $host = Koha::Biblios->find($bibno) or return;
1307 return wantarray ? ( $host, $hostfld->subfield('g') ) : $host;
1313 my $recalls = $biblio->recalls;
1315 Return recalls linked to this biblio
1321 return Koha::Recalls->_new_from_dbic( scalar $self->_result->recalls );
1324 =head3 can_be_recalled
1326 my @items_for_recall = $biblio->can_be_recalled({ patron => $patron_object });
1328 Does biblio-level checks and returns the items attached to this biblio that are available for recall
1332 sub can_be_recalled {
1333 my ( $self, $params ) = @_;
1335 return 0 if !( C4::Context->preference('UseRecalls') );
1337 my $patron = $params->{patron};
1339 my $branchcode = C4::Context->userenv->{'branch'};
1340 if ( C4::Context->preference('CircControl') eq 'PatronLibrary' and $patron ) {
1341 $branchcode = $patron->branchcode;
1344 my @all_items = Koha::Items->search({ biblionumber => $self->biblionumber })->as_list;
1346 # if there are no available items at all, no recall can be placed
1347 return 0 if ( scalar @all_items == 0 );
1352 my @all_itemnumbers;
1353 foreach my $item ( @all_items ) {
1354 push( @all_itemnumbers, $item->itemnumber );
1355 if ( $item->can_be_recalled({ patron => $patron }) ) {
1356 push( @itemtypes, $item->effective_itemtype );
1357 push( @itemnumbers, $item->itemnumber );
1358 push( @items, $item );
1362 # if there are no recallable items, no recall can be placed
1363 return 0 if ( scalar @items == 0 );
1365 # Check the circulation rule for each relevant itemtype for this biblio
1366 my ( @recalls_allowed, @recalls_per_record, @on_shelf_recalls );
1367 foreach my $itemtype ( @itemtypes ) {
1368 my $rule = Koha::CirculationRules->get_effective_rules({
1369 branchcode => $branchcode,
1370 categorycode => $patron ? $patron->categorycode : undef,
1371 itemtype => $itemtype,
1374 'recalls_per_record',
1378 push( @recalls_allowed, $rule->{recalls_allowed} ) if $rule;
1379 push( @recalls_per_record, $rule->{recalls_per_record} ) if $rule;
1380 push( @on_shelf_recalls, $rule->{on_shelf_recalls} ) if $rule;
1382 my $recalls_allowed = (sort {$b <=> $a} @recalls_allowed)[0]; # take highest
1383 my $recalls_per_record = (sort {$b <=> $a} @recalls_per_record)[0]; # take highest
1384 my %on_shelf_recalls_count = ();
1385 foreach my $count ( @on_shelf_recalls ) {
1386 $on_shelf_recalls_count{$count}++;
1388 my $on_shelf_recalls = (sort {$on_shelf_recalls_count{$b} <=> $on_shelf_recalls_count{$a}} @on_shelf_recalls)[0]; # take most common
1390 # check recalls allowed has been set and is not zero
1391 return 0 if ( !defined($recalls_allowed) || $recalls_allowed == 0 );
1394 # check borrower has not reached open recalls allowed limit
1395 return 0 if ( $patron->recalls->filter_by_current->count >= $recalls_allowed );
1397 # check borrower has not reached open recalls allowed per record limit
1398 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $recalls_per_record );
1400 # check if any of the items under this biblio are already checked out by this borrower
1401 return 0 if ( Koha::Checkouts->search({ itemnumber => [ @all_itemnumbers ], borrowernumber => $patron->borrowernumber })->count > 0 );
1404 # check item availability
1405 my $checked_out_count = 0;
1407 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1410 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1411 return 0 if ( $on_shelf_recalls eq 'all' && $checked_out_count < scalar @items );
1413 # can't recall if no items have been checked out
1414 return 0 if ( $checked_out_count == 0 );
1420 =head2 Internal methods
1432 Kyle M Hall <kyle@bywatersolutions.com>