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 $record = $biblio->record();
96 Returns a Marc::Record object
103 return $self->metadata->record;
108 my $orders = $biblio->orders();
110 Returns a Koha::Acquisition::Orders object
117 my $orders = $self->_result->orders;
118 return Koha::Acquisition::Orders->_new_from_dbic($orders);
123 my $active_orders = $biblio->active_orders();
125 Returns the active acquisition orders related to this biblio.
126 An order is considered active when it is not cancelled (i.e. when datecancellation
134 return $self->orders->search({ datecancellationprinted => undef });
139 my $tickets = $biblio->tickets();
141 Returns all tickets linked to the biblio
147 my $rs = $self->_result->tickets;
148 return Koha::Tickets->_new_from_dbic( $rs );
153 my $item_groups = $biblio->item_groups();
155 Returns a Koha::Biblio::ItemGroups object
162 my $item_groups = $self->_result->item_groups;
163 return Koha::Biblio::ItemGroups->_new_from_dbic($item_groups);
166 =head3 can_article_request
168 my $bool = $biblio->can_article_request( $borrower );
170 Returns true if article requests can be made for this record
172 $borrower must be a Koha::Patron object
176 sub can_article_request {
177 my ( $self, $borrower ) = @_;
179 my $rule = $self->article_request_type($borrower);
180 return q{} if $rule eq 'item_only' && !$self->items()->count();
181 return 1 if $rule && $rule ne 'no';
186 =head3 can_be_transferred
188 $biblio->can_be_transferred({ to => $to_library, from => $from_library })
190 Checks if at least one item of a biblio can be transferred to given library.
192 This feature is controlled by two system preferences:
193 UseBranchTransferLimits to enable / disable the feature
194 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
195 for setting the limitations
197 Performance-wise, it is recommended to use this method for a biblio instead of
198 iterating each item of a biblio with Koha::Item->can_be_transferred().
200 Takes HASHref that can have the following parameters:
201 MANDATORY PARAMETERS:
204 $from : Koha::Library # if given, only items from that
205 # holdingbranch are considered
207 Returns 1 if at least one of the item of a biblio can be transferred
208 to $to_library, otherwise 0.
212 sub can_be_transferred {
213 my ($self, $params) = @_;
215 my $to = $params->{to};
216 my $from = $params->{from};
218 return 1 unless C4::Context->preference('UseBranchTransferLimits');
219 my $limittype = C4::Context->preference('BranchTransferLimitsType');
222 foreach my $item_of_bib ($self->items->as_list) {
223 next unless $item_of_bib->holdingbranch;
224 next if $from && $from->branchcode ne $item_of_bib->holdingbranch;
225 return 1 if $item_of_bib->holdingbranch eq $to->branchcode;
226 my $code = $limittype eq 'itemtype'
227 ? $item_of_bib->effective_itemtype
228 : $item_of_bib->ccode;
229 return 1 unless $code;
230 $items->{$code}->{$item_of_bib->holdingbranch} = 1;
233 # At this point we will have a HASHref containing each itemtype/ccode that
234 # this biblio has, inside which are all of the holdingbranches where those
235 # items are located at. Then, we will query Koha::Item::Transfer::Limits to
236 # find out whether a transfer limits for such $limittype from any of the
237 # listed holdingbranches to the given $to library exist. If at least one
238 # holdingbranch for that $limittype does not have a transfer limit to given
239 # $to library, then we know that the transfer is possible.
240 foreach my $code (keys %{$items}) {
241 my @holdingbranches = keys %{$items->{$code}};
242 return 1 if Koha::Item::Transfer::Limits->search({
243 toBranch => $to->branchcode,
244 fromBranch => { 'in' => \@holdingbranches },
247 group_by => [qw/fromBranch/]
248 })->count == scalar(@holdingbranches) ? 0 : 1;
255 =head3 pickup_locations
257 my $pickup_locations = $biblio->pickup_locations( {patron => $patron } );
259 Returns a Koha::Libraries set of possible pickup locations for this biblio's items,
260 according to patron's home library (if patron is defined and holds are allowed
261 only from hold groups) and if item can be transferred to each pickup location.
265 sub pickup_locations {
266 my ( $self, $params ) = @_;
268 my $patron = $params->{patron};
270 my @pickup_locations;
271 foreach my $item_of_bib ( $self->items->as_list ) {
272 push @pickup_locations,
273 $item_of_bib->pickup_locations( { patron => $patron } )
274 ->_resultset->get_column('branchcode')->all;
277 return Koha::Libraries->search(
278 { branchcode => { '-in' => \@pickup_locations } }, { order_by => ['branchname'] } );
281 =head3 hidden_in_opac
283 my $bool = $biblio->hidden_in_opac({ [ rules => $rules ] })
285 Returns true if the biblio matches the hidding criteria defined in $rules.
286 Returns false otherwise. It involves the I<OpacHiddenItems> and
287 I<OpacHiddenItemsHidesRecord> system preferences.
289 Takes HASHref that can have the following parameters:
291 $rules : { <field> => [ value_1, ... ], ... }
293 Note: $rules inherits its structure from the parsed YAML from reading
294 the I<OpacHiddenItems> system preference.
299 my ( $self, $params ) = @_;
301 my $rules = $params->{rules} // {};
303 my @items = $self->items->as_list;
305 return 0 unless @items; # Do not hide if there is no item
307 # Ok, there are items, don't even try the rules unless OpacHiddenItemsHidesRecord
308 return 0 unless C4::Context->preference('OpacHiddenItemsHidesRecord');
310 return !(any { !$_->hidden_in_opac({ rules => $rules }) } @items);
313 =head3 article_request_type
315 my $type = $biblio->article_request_type( $borrower );
317 Returns the article request type based on items, or on the record
318 itself if there are no items.
320 $borrower must be a Koha::Patron object
324 sub article_request_type {
325 my ( $self, $borrower ) = @_;
327 return q{} unless $borrower;
329 my $rule = $self->article_request_type_for_items( $borrower );
330 return $rule if $rule;
332 # If the record has no items that are requestable, go by the record itemtype
333 $rule = $self->article_request_type_for_bib($borrower);
334 return $rule if $rule;
339 =head3 article_request_type_for_bib
341 my $type = $biblio->article_request_type_for_bib
343 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record
347 sub article_request_type_for_bib {
348 my ( $self, $borrower ) = @_;
350 return q{} unless $borrower;
352 my $borrowertype = $borrower->categorycode;
353 my $itemtype = $self->itemtype();
355 my $rule = Koha::CirculationRules->get_effective_rule(
357 rule_name => 'article_requests',
358 categorycode => $borrowertype,
359 itemtype => $itemtype,
363 return q{} unless $rule;
364 return $rule->rule_value || q{}
367 =head3 article_request_type_for_items
369 my $type = $biblio->article_request_type_for_items
371 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record's items
373 If there is a conflict where some items are 'bib_only' and some are 'item_only', 'bib_only' will be returned.
377 sub article_request_type_for_items {
378 my ( $self, $borrower ) = @_;
381 foreach my $item ( $self->items()->as_list() ) {
382 my $rule = $item->article_request_type($borrower);
383 return $rule if $rule eq 'bib_only'; # we don't need to go any further
387 return 'item_only' if $counts->{item_only};
388 return 'yes' if $counts->{yes};
389 return 'no' if $counts->{no};
393 =head3 article_requests
395 my $article_requests = $biblio->article_requests
397 Returns the article requests associated with this biblio
401 sub article_requests {
404 return Koha::ArticleRequests->_new_from_dbic( scalar $self->_result->article_requests );
407 =head3 current_checkouts
409 my $current_checkouts = $biblio->current_checkouts
411 Returns the current checkouts associated with this biblio
415 sub current_checkouts {
418 return Koha::Checkouts->search( { "item.biblionumber" => $self->id },
419 { join => 'item' } );
424 my $old_checkouts = $biblio->old_checkouts
426 Returns the past checkouts associated with this biblio
433 return Koha::Old::Checkouts->search( { "item.biblionumber" => $self->id },
434 { join => 'item' } );
439 my $items = $biblio->items();
441 Returns the related Koha::Items object for this biblio
448 my $items_rs = $self->_result->items;
450 return Koha::Items->_new_from_dbic( $items_rs );
455 my $host_items = $biblio->host_items();
457 Return the host items (easy analytical record)
464 return Koha::Items->new->empty
465 unless C4::Context->preference('EasyAnalyticalRecords');
467 my $marcflavour = C4::Context->preference("marcflavour");
468 my $analyticfield = '773';
469 if ( $marcflavour eq 'MARC21' ) {
470 $analyticfield = '773';
472 elsif ( $marcflavour eq 'UNIMARC' ) {
473 $analyticfield = '461';
475 my $marc_record = $self->metadata->record;
477 foreach my $field ( $marc_record->field($analyticfield) ) {
478 push @itemnumbers, $field->subfield('9');
481 return Koha::Items->search( { itemnumber => { -in => \@itemnumbers } } );
486 my $itemtype = $biblio->itemtype();
488 Returns the itemtype for this record.
495 return $self->biblioitem()->itemtype();
500 my $holds = $biblio->holds();
502 return the current holds placed on this record
507 my ( $self, $params, $attributes ) = @_;
508 $attributes->{order_by} = 'priority' unless exists $attributes->{order_by};
509 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
510 return Koha::Holds->_new_from_dbic($hold_rs);
515 my $holds = $biblio->current_holds
517 Return the holds placed on this bibliographic record.
518 It does not include future holds.
524 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
526 { reservedate => { '<=' => $dtf->format_date(dt_from_string) } } );
531 my $field = $self->biblioitem()->itemtype
533 Returns the related Koha::Biblioitem object for this Biblio object
540 $self->{_biblioitem} ||= Koha::Biblioitems->find( { biblionumber => $self->biblionumber() } );
542 return $self->{_biblioitem};
547 my $suggestions = $self->suggestions
549 Returns the related Koha::Suggestions object for this Biblio object
556 my $suggestions_rs = $self->_result->suggestions;
557 return Koha::Suggestions->_new_from_dbic( $suggestions_rs );
560 =head3 get_marc_components
562 my $components = $self->get_marc_components();
564 Returns an array of search results data, which are component parts of
565 this object (MARC21 773 points to this)
569 sub get_marc_components {
570 my ($self, $max_results) = @_;
572 return [] if (C4::Context->preference('marcflavour') ne 'MARC21');
574 my ( $searchstr, $sort ) = $self->get_components_query;
577 if (defined($searchstr)) {
578 my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
579 my ( $error, $results, $facets );
581 ( $error, $results, $facets ) = $searcher->search_compat( $searchstr, undef, [$sort], ['biblioserver'], $max_results, 0, undef, undef, 'ccl', 0 );
586 warn "Warning from search_compat: '$error'";
590 message => 'component_search',
595 $components = $results->{biblioserver}->{RECORDS} if defined($results) && $results->{biblioserver}->{hits};
598 return $components // [];
601 =head2 get_components_query
603 Returns a query which can be used to search for all component parts of MARC21 biblios
607 sub get_components_query {
610 my $builder = Koha::SearchEngine::QueryBuilder->new(
611 { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
612 my $marc = $self->metadata->record;
613 my $component_sort_field = C4::Context->preference('ComponentSortField') // "title";
614 my $component_sort_order = C4::Context->preference('ComponentSortOrder') // "asc";
615 my $sort = $component_sort_field . "_" . $component_sort_order;
618 if ( C4::Context->preference('UseControlNumber') ) {
619 my $pf001 = $marc->field('001') || undef;
621 if ( defined($pf001) ) {
623 my $pf003 = $marc->field('003') || undef;
625 if ( !defined($pf003) ) {
626 # search for 773$w='Host001'
627 $searchstr .= "rcn:\"" . $pf001->data()."\"";
631 # search for (773$w='Host001' and 003='Host003') or 773$w='(Host003)Host001'
632 $searchstr .= "(rcn:\"" . $pf001->data() . "\" AND cni:\"" . $pf003->data() . "\")";
633 $searchstr .= " OR rcn:\"" . $pf003->data() . " " . $pf001->data() . "\"";
637 # limit to monograph and serial component part records
638 $searchstr .= " AND (bib-level:a OR bib-level:b)";
643 my $cleaned_title = $marc->subfield('245', "a");
644 $cleaned_title =~ tr|/||;
645 $cleaned_title = $builder->clean_search_term($cleaned_title);
646 $searchstr = qq#Host-item:("$cleaned_title")#;
648 my ($error, $query ,$query_str) = $builder->build_query_compat( undef, [$searchstr], undef, undef, [$sort], 0 );
654 return ($query, $query_str, $sort);
659 my $subscriptions = $self->subscriptions
661 Returns the related Koha::Subscriptions object for this Biblio object
668 $self->{_subscriptions} ||= Koha::Subscriptions->search( { biblionumber => $self->biblionumber } );
670 return $self->{_subscriptions};
673 =head3 has_items_waiting_or_intransit
675 my $itemsWaitingOrInTransit = $biblio->has_items_waiting_or_intransit
677 Tells if this bibliographic record has items waiting or in transit.
681 sub has_items_waiting_or_intransit {
684 if ( Koha::Holds->search({ biblionumber => $self->id,
685 found => ['W', 'T'] })->count ) {
689 foreach my $item ( $self->items->as_list ) {
690 return 1 if $item->get_transfer;
698 my $coins = $biblio->get_coins;
700 Returns the COinS (a span) which can be included in a biblio record
707 my $record = $self->metadata->record;
709 my $pos7 = substr $record->leader(), 7, 1;
710 my $pos6 = substr $record->leader(), 6, 1;
713 my ( $aulast, $aufirst ) = ( '', '' );
724 # For the purposes of generating COinS metadata, LDR/06-07 can be
725 # considered the same for UNIMARC and MARC21
734 'i' => 'audioRecording',
735 'j' => 'audioRecording',
738 'm' => 'computerProgram',
743 'a' => 'journalArticle',
747 $genre = $fmts6->{$pos6} ? $fmts6->{$pos6} : 'book';
749 if ( $genre eq 'book' ) {
750 $genre = $fmts7->{$pos7} if $fmts7->{$pos7};
753 ##### We must transform mtx to a valable mtx and document type ####
754 if ( $genre eq 'book' ) {
757 } elsif ( $genre eq 'journal' ) {
760 } elsif ( $genre eq 'journalArticle' ) {
768 if ( C4::Context->preference("marcflavour") eq "UNIMARC" ) {
771 $aulast = $record->subfield( '700', 'a' ) || '';
772 $aufirst = $record->subfield( '700', 'b' ) || '';
773 push @authors, "$aufirst $aulast" if ($aufirst or $aulast);
776 if ( $record->field('200') ) {
777 for my $au ( $record->field('200')->subfield('g') ) {
782 $title = $record->subfield( '200', 'a' );
783 my $subfield_210d = $record->subfield('210', 'd');
784 if ($subfield_210d and $subfield_210d =~ /(\d{4})/) {
787 $publisher = $record->subfield( '210', 'c' ) || '';
788 $isbn = $record->subfield( '010', 'a' ) || '';
789 $issn = $record->subfield( '011', 'a' ) || '';
792 # MARC21 need some improve
795 if ( $record->field('100') ) {
796 push @authors, $record->subfield( '100', 'a' );
800 if ( $record->field('700') ) {
801 for my $au ( $record->field('700')->subfield('a') ) {
805 $title = $record->field('245');
806 $title &&= $title->as_string('ab');
807 if ($titletype eq 'a') {
808 $pubyear = $record->field('008') || '';
809 $pubyear = substr($pubyear->data(), 7, 4) if $pubyear;
810 $isbn = $record->subfield( '773', 'z' ) || '';
811 $issn = $record->subfield( '773', 'x' ) || '';
812 $hosttitle = $record->subfield( '773', 't' ) || $record->subfield( '773', 'a') || q{};
813 my @rels = $record->subfield( '773', 'g' );
814 $pages = join(', ', @rels);
816 $pubyear = $record->subfield( '260', 'c' ) || '';
817 $publisher = $record->subfield( '260', 'b' ) || '';
818 $isbn = $record->subfield( '020', 'a' ) || '';
819 $issn = $record->subfield( '022', 'a' ) || '';
825 [ 'ctx_ver', 'Z39.88-2004' ],
826 [ 'rft_val_fmt', "info:ofi/fmt:kev:mtx:$mtx" ],
827 [ ($mtx eq 'dc' ? 'rft.type' : 'rft.genre'), $genre ],
828 [ "rft.${titletype}title", $title ],
831 # rft.title is authorized only once, so by checking $titletype
832 # we ensure that rft.title is not already in the list.
833 if ($hosttitle and $titletype) {
834 push @params, [ 'rft.title', $hosttitle ];
838 [ 'rft.isbn', $isbn ],
839 [ 'rft.issn', $issn ],
842 # If it's a subscription, these informations have no meaning.
843 if ($genre ne 'journal') {
845 [ 'rft.aulast', $aulast ],
846 [ 'rft.aufirst', $aufirst ],
847 (map { [ 'rft.au', $_ ] } @authors),
848 [ 'rft.pub', $publisher ],
849 [ 'rft.date', $pubyear ],
850 [ 'rft.pages', $pages ],
854 my $coins_value = join( '&',
855 map { $$_[1] ? $$_[0] . '=' . uri_escape_utf8( $$_[1] ) : () } @params );
862 my $url = $biblio->get_openurl;
864 Returns url for OpenURL resolver set in OpenURLResolverURL system preference
871 my $OpenURLResolverURL = C4::Context->preference('OpenURLResolverURL');
873 if ($OpenURLResolverURL) {
874 my $uri = URI->new($OpenURLResolverURL);
876 if (not defined $uri->query) {
877 $OpenURLResolverURL .= '?';
879 $OpenURLResolverURL .= '&';
881 $OpenURLResolverURL .= $self->get_coins;
884 return $OpenURLResolverURL;
889 my $serial = $biblio->is_serial
891 Return boolean true if this bibbliographic record is continuing resource
898 return 1 if $self->serial;
900 my $record = $self->metadata->record;
901 return 1 if substr($record->leader, 7, 1) eq 's';
906 =head3 custom_cover_image_url
908 my $image_url = $biblio->custom_cover_image_url
910 Return the specific url of the cover image for this bibliographic record.
911 It is built regaring the value of the system preference CustomCoverImagesURL
915 sub custom_cover_image_url {
917 my $url = C4::Context->preference('CustomCoverImagesURL');
918 if ( $url =~ m|{isbn}| ) {
919 my $isbn = $self->biblioitem->isbn;
921 $url =~ s|{isbn}|$isbn|g;
923 if ( $url =~ m|{normalized_isbn}| ) {
924 my $normalized_isbn = C4::Koha::GetNormalizedISBN($self->biblioitem->isbn);
925 return unless $normalized_isbn;
926 $url =~ s|{normalized_isbn}|$normalized_isbn|g;
928 if ( $url =~ m|{issn}| ) {
929 my $issn = $self->biblioitem->issn;
931 $url =~ s|{issn}|$issn|g;
934 my $re = qr|{(?<field>\d{3})(\$(?<subfield>.))?}|;
936 my $field = $+{field};
937 my $subfield = $+{subfield};
938 my $marc_record = $self->metadata->record;
941 $value = $marc_record->subfield( $field, $subfield );
943 my $controlfield = $marc_record->field($field);
944 $value = $controlfield->data() if $controlfield;
946 return unless $value;
947 $url =~ s|$re|$value|;
955 Return the cover images associated with this biblio.
962 my $cover_images_rs = $self->_result->cover_images;
963 return unless $cover_images_rs;
964 return Koha::CoverImages->_new_from_dbic($cover_images_rs);
967 =head3 get_marc_notes
969 $marcnotesarray = $biblio->get_marc_notes({ opac => 1 });
971 Get all notes from the MARC record and returns them in an array.
972 The notes are stored in different fields depending on MARC flavour.
973 MARC21 5XX $u subfields receive special attention as they are URIs.
978 my ( $self, $params ) = @_;
980 my $marcflavour = C4::Context->preference('marcflavour');
981 my $opac = $params->{opac} // '0';
982 my $interface = $params->{opac} ? 'opac' : 'intranet';
984 my $record = $params->{record} // $self->metadata->record;
985 my $record_processor = Koha::RecordProcessor->new(
987 filters => [ 'ViewPolicy', 'ExpandCodedFields' ],
989 interface => $interface,
990 frameworkcode => $self->frameworkcode
994 $record_processor->process($record);
996 my $scope = $marcflavour eq "UNIMARC"? '3..': '5..';
997 #MARC21 specs indicate some notes should be private if first indicator 0
998 my %maybe_private = (
1006 my %hiddenlist = map { $_ => 1 }
1007 split( /,/, C4::Context->preference('NotesToHide'));
1010 foreach my $field ( $record->field($scope) ) {
1011 my $tag = $field->tag();
1012 next if $hiddenlist{ $tag };
1013 next if $opac && $maybe_private{$tag} && !$field->indicator(1);
1014 if( $marcflavour ne 'UNIMARC' && $field->subfield('u') ) {
1015 # Field 5XX$u always contains URI
1016 # Examples: 505u, 506u, 510u, 514u, 520u, 530u, 538u, 540u, 542u, 552u, 555u, 561u, 563u, 583u
1017 # We first push the other subfields, then all $u's separately
1018 # Leave further actions to the template (see e.g. opac-detail)
1020 join '', ( 'a' .. 't', 'v' .. 'z', '0' .. '9' ); # excl 'u'
1021 push @marcnotes, { marcnote => $field->as_string($othersub) };
1022 foreach my $sub ( $field->subfield('u') ) {
1023 $sub =~ s/^\s+|\s+$//g; # trim
1024 push @marcnotes, { marcnote => $sub };
1027 push @marcnotes, { marcnote => $field->as_string() };
1033 =head3 _get_marc_authors
1035 Private method to return the list of authors contained in the MARC record.
1036 See get get_marc_contributors and get_marc_authors for the public methods.
1040 sub _get_marc_authors {
1041 my ( $self, $params ) = @_;
1043 my $fields_filter = $params->{fields_filter};
1044 my $mintag = $params->{mintag};
1045 my $maxtag = $params->{maxtag};
1047 my $AuthoritySeparator = C4::Context->preference('AuthoritySeparator');
1048 my $marcflavour = C4::Context->preference('marcflavour');
1050 # tagslib useful only for UNIMARC author responsibilities
1051 my $tagslib = $marcflavour eq "UNIMARC"
1052 ? C4::Biblio::GetMarcStructure( 1, $self->frameworkcode, { unsafe => 1 } )
1056 foreach my $field ( $self->metadata->record->field($fields_filter) ) {
1059 if $mintag && $field->tag() < $mintag
1060 || $maxtag && $field->tag() > $maxtag;
1064 my @subfields = $field->subfields();
1067 # if there is an authority link, build the link with Koha-Auth-Number: subfield9
1068 my $subfield9 = $field->subfield('9');
1070 my $linkvalue = $subfield9;
1071 $linkvalue =~ s/(\(|\))//g;
1072 @link_loop = ( { 'limit' => 'an', 'link' => $linkvalue } );
1077 for my $authors_subfield (@subfields) {
1078 next if ( $authors_subfield->[0] eq '9' );
1080 # unimarc3 contains the $3 of the author for UNIMARC.
1081 # For french academic libraries, it's the "ppn", and it's required for idref webservice
1082 $unimarc3 = $authors_subfield->[1] if $marcflavour eq 'UNIMARC' and $authors_subfield->[0] =~ /3/;
1084 # don't load unimarc subfields 3, 5
1085 next if ( $marcflavour eq 'UNIMARC' and ( $authors_subfield->[0] =~ /3|5/ ) );
1087 my $code = $authors_subfield->[0];
1088 my $value = $authors_subfield->[1];
1089 my $linkvalue = $value;
1090 $linkvalue =~ s/(\(|\))//g;
1091 # UNIMARC author responsibility
1092 if ( $marcflavour eq 'UNIMARC' and $code eq '4' ) {
1093 $value = C4::Biblio::GetAuthorisedValueDesc( $field->tag(), $code, $value, '', $tagslib );
1094 $linkvalue = "($value)";
1096 # if no authority link, build a search query
1097 unless ($subfield9) {
1100 'link' => $linkvalue,
1101 operator => (scalar @link_loop) ? ' AND ' : undef
1104 my @this_link_loop = @link_loop;
1106 unless ( $code eq '0') {
1107 push @subfields_loop, {
1108 tag => $field->tag(),
1111 link_loop => \@this_link_loop,
1112 separator => (scalar @subfields_loop) ? $AuthoritySeparator : ''
1116 push @marcauthors, {
1117 MARCAUTHOR_SUBFIELDS_LOOP => \@subfields_loop,
1118 authoritylink => $subfield9,
1119 unimarc3 => $unimarc3
1122 return \@marcauthors;
1125 =head3 get_marc_contributors
1127 my $contributors = $biblio->get_marc_contributors;
1129 Get all contributors (but first author) from the MARC record and returns them in an array.
1130 They are stored in different fields depending on MARC flavour (700..720 for MARC21)
1134 sub get_marc_contributors {
1135 my ( $self, $params ) = @_;
1137 my ( $mintag, $maxtag, $fields_filter );
1138 my $marcflavour = C4::Context->preference('marcflavour');
1140 if ( $marcflavour eq "UNIMARC" ) {
1143 $fields_filter = '7..';
1144 } else { # marc21/normarc
1147 $fields_filter = '7..';
1150 return $self->_get_marc_authors(
1152 fields_filter => $fields_filter,
1159 =head3 get_marc_authors
1161 my $authors = $biblio->get_marc_authors;
1163 Get all authors from the MARC record and returns them in an array.
1164 They are stored in different fields depending on MARC flavour
1165 (main author from 100 then secondary authors from 700..720).
1169 sub get_marc_authors {
1170 my ( $self, $params ) = @_;
1172 my ( $mintag, $maxtag, $fields_filter );
1173 my $marcflavour = C4::Context->preference('marcflavour');
1175 if ( $marcflavour eq "UNIMARC" ) {
1176 $fields_filter = '200';
1177 } else { # marc21/normarc
1178 $fields_filter = '100';
1181 my @first_authors = @{$self->_get_marc_authors(
1183 fields_filter => $fields_filter,
1189 my @other_authors = @{$self->get_marc_contributors};
1191 return [@first_authors, @other_authors];
1197 my $json = $biblio->to_api;
1199 Overloaded method that returns a JSON representation of the Koha::Biblio object,
1200 suitable for API output. The related Koha::Biblioitem object is merged as expected
1206 my ($self, $args) = @_;
1208 my $response = $self->SUPER::to_api( $args );
1209 my $biblioitem = $self->biblioitem->to_api;
1211 return { %$response, %$biblioitem };
1214 =head3 to_api_mapping
1216 This method returns the mapping for representing a Koha::Biblio object
1221 sub to_api_mapping {
1223 biblionumber => 'biblio_id',
1224 frameworkcode => 'framework_id',
1225 unititle => 'uniform_title',
1226 seriestitle => 'series_title',
1227 copyrightdate => 'copyright_date',
1228 datecreated => 'creation_date',
1229 deleted_on => undef,
1233 =head3 get_marc_host
1235 $host = $biblio->get_marc_host;
1237 ( $host, $relatedparts, $hostinfo ) = $biblio->get_marc_host;
1239 Returns host biblio record from MARC21 773 (undef if no 773 present).
1240 It looks at the first 773 field with MARCorgCode or only a control
1241 number. Complete $w or numeric part is used to search host record.
1242 The optional parameter no_items triggers a check if $biblio has items.
1243 If there are, the sub returns undef.
1244 Called in list context, it also returns 773$g (related parts).
1246 If there is no $w, we use $0 (host biblionumber) or $9 (host itemnumber)
1247 to search for the host record. If there is also no $0 and no $9, we search
1248 using author and title. Failing all of that, we return an undef host and
1249 form a concatenation of strings with 773$agt for host information,
1250 returned when called in list context.
1255 my ($self, $params) = @_;
1256 my $no_items = $params->{no_items};
1257 return if C4::Context->preference('marcflavour') eq 'UNIMARC'; # TODO
1258 return if $params->{no_items} && $self->items->count > 0;
1261 eval { $record = $self->metadata->record };
1264 # We pick the first $w with your MARCOrgCode or the first $w that has no
1265 # code (between parentheses) at all.
1266 my $orgcode = C4::Context->preference('MARCOrgCode') // q{};
1268 foreach my $f ( $record->field('773') ) {
1269 my $w = $f->subfield('w') or next;
1270 if( $w =~ /^\($orgcode\)\s*(\d+)/i or $w =~ /^\d+/ ) {
1276 my $engine = Koha::SearchEngine::Search->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1278 if ( !$hostfld and $record->subfield('773','t') ) {
1279 # not linked using $w
1280 my $unlinkedf = $record->field('773');
1282 if ( C4::Context->preference("EasyAnalyticalRecords") ) {
1283 if ( $unlinkedf->subfield('0') ) {
1284 # use 773$0 host biblionumber
1285 $bibno = $unlinkedf->subfield('0');
1286 } elsif ( $unlinkedf->subfield('9') ) {
1287 # use 773$9 host itemnumber
1288 my $linkeditemnumber = $unlinkedf->subfield('9');
1289 $bibno = Koha::Items->find( $linkeditemnumber )->biblionumber;
1293 my $host = Koha::Biblios->find($bibno) or return;
1294 return wantarray ? ( $host, $unlinkedf->subfield('g') ) : $host;
1296 # just return plaintext and no host record
1297 my $hostinfo = join( ", ", $unlinkedf->subfield('a'), $unlinkedf->subfield('t'), $unlinkedf->subfield('g') );
1298 return wantarray ? ( undef, $unlinkedf->subfield('g'), $hostinfo ) : undef;
1300 return if !$hostfld;
1301 my $rcn = $hostfld->subfield('w');
1303 # Look for control number with/without orgcode
1304 for my $try (1..2) {
1305 my ( $error, $results, $total_hits ) = $engine->simple_search_compat( 'Control-number='.$rcn, 0,1 );
1306 if( !$error and $total_hits == 1 ) {
1307 $bibno = $engine->extract_biblionumber( $results->[0] );
1310 # Add or remove orgcode for second try
1311 if( $try == 1 && $rcn =~ /\)\s*(\d+)/ ) {
1312 $rcn = $1; # number only
1313 } elsif( $try == 1 && $rcn =~ /^\d+/ ) {
1314 $rcn = "($orgcode)$rcn";
1320 my $host = Koha::Biblios->find($bibno) or return;
1321 return wantarray ? ( $host, $hostfld->subfield('g') ) : $host;
1325 =head3 get_marc_host_only
1327 my $host = $biblio->get_marc_host_only;
1333 sub get_marc_host_only {
1336 my ( $host ) = $self->get_marc_host;
1341 =head3 get_marc_relatedparts_only
1343 my $relatedparts = $biblio->get_marc_relatedparts_only;
1345 Return related parts only
1349 sub get_marc_relatedparts_only {
1352 my ( undef, $relatedparts ) = $self->get_marc_host;
1354 return $relatedparts;
1357 =head3 get_marc_hostinfo_only
1359 my $hostinfo = $biblio->get_marc_hostinfo_only;
1361 Return host info only
1365 sub get_marc_hostinfo_only {
1368 my ( $host, $relatedparts, $hostinfo ) = $self->get_marc_host;
1375 my $recalls = $biblio->recalls;
1377 Return recalls linked to this biblio
1383 return Koha::Recalls->_new_from_dbic( scalar $self->_result->recalls );
1386 =head3 can_be_recalled
1388 my @items_for_recall = $biblio->can_be_recalled({ patron => $patron_object });
1390 Does biblio-level checks and returns the items attached to this biblio that are available for recall
1394 sub can_be_recalled {
1395 my ( $self, $params ) = @_;
1397 return 0 if !( C4::Context->preference('UseRecalls') );
1399 my $patron = $params->{patron};
1401 my $branchcode = C4::Context->userenv->{'branch'};
1402 if ( C4::Context->preference('CircControl') eq 'PatronLibrary' and $patron ) {
1403 $branchcode = $patron->branchcode;
1406 my @all_items = Koha::Items->search({ biblionumber => $self->biblionumber })->as_list;
1408 # if there are no available items at all, no recall can be placed
1409 return 0 if ( scalar @all_items == 0 );
1414 my @all_itemnumbers;
1415 foreach my $item ( @all_items ) {
1416 push( @all_itemnumbers, $item->itemnumber );
1417 if ( $item->can_be_recalled({ patron => $patron }) ) {
1418 push( @itemtypes, $item->effective_itemtype );
1419 push( @itemnumbers, $item->itemnumber );
1420 push( @items, $item );
1424 # if there are no recallable items, no recall can be placed
1425 return 0 if ( scalar @items == 0 );
1427 # Check the circulation rule for each relevant itemtype for this biblio
1428 my ( @recalls_allowed, @recalls_per_record, @on_shelf_recalls );
1429 foreach my $itemtype ( @itemtypes ) {
1430 my $rule = Koha::CirculationRules->get_effective_rules({
1431 branchcode => $branchcode,
1432 categorycode => $patron ? $patron->categorycode : undef,
1433 itemtype => $itemtype,
1436 'recalls_per_record',
1440 push( @recalls_allowed, $rule->{recalls_allowed} ) if $rule;
1441 push( @recalls_per_record, $rule->{recalls_per_record} ) if $rule;
1442 push( @on_shelf_recalls, $rule->{on_shelf_recalls} ) if $rule;
1444 my $recalls_allowed = (sort {$b <=> $a} @recalls_allowed)[0]; # take highest
1445 my $recalls_per_record = (sort {$b <=> $a} @recalls_per_record)[0]; # take highest
1446 my %on_shelf_recalls_count = ();
1447 foreach my $count ( @on_shelf_recalls ) {
1448 $on_shelf_recalls_count{$count}++;
1450 my $on_shelf_recalls = (sort {$on_shelf_recalls_count{$b} <=> $on_shelf_recalls_count{$a}} @on_shelf_recalls)[0]; # take most common
1452 # check recalls allowed has been set and is not zero
1453 return 0 if ( !defined($recalls_allowed) || $recalls_allowed == 0 );
1456 # check borrower has not reached open recalls allowed limit
1457 return 0 if ( $patron->recalls->filter_by_current->count >= $recalls_allowed );
1459 # check borrower has not reached open recalls allowed per record limit
1460 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $recalls_per_record );
1462 # check if any of the items under this biblio are already checked out by this borrower
1463 return 0 if ( Koha::Checkouts->search({ itemnumber => [ @all_itemnumbers ], borrowernumber => $patron->borrowernumber })->count > 0 );
1466 # check item availability
1467 my $checked_out_count = 0;
1469 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1472 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1473 return 0 if ( $on_shelf_recalls eq 'all' && $checked_out_count < scalar @items );
1475 # can't recall if no items have been checked out
1476 return 0 if ( $checked_out_count == 0 );
1482 =head2 Internal methods
1494 Kyle M Hall <kyle@bywatersolutions.com>