3 # Copyright ByWater Solutions 2014
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22 use List::MoreUtils qw( any );
24 use URI::Escape qw( uri_escape_utf8 );
26 use C4::Koha qw( GetNormalizedISBN );
29 use Koha::DateUtils qw( dt_from_string );
31 use base qw(Koha::Object);
33 use Koha::Acquisition::Orders;
34 use Koha::ArticleRequests;
35 use Koha::Biblio::Metadatas;
36 use Koha::Biblio::ItemGroups;
37 use Koha::Biblioitems;
38 use Koha::Cache::Memory::Lite;
40 use Koha::CirculationRules;
42 use Koha::Item::Transfer::Limits;
45 use Koha::Old::Checkouts;
47 use Koha::RecordProcessor;
48 use Koha::Suggestions;
49 use Koha::Subscriptions;
50 use Koha::SearchEngine;
51 use Koha::SearchEngine::Search;
52 use Koha::SearchEngine::QueryBuilder;
57 Koha::Biblio - Koha Biblio Object class
67 Overloaded I<store> method to set default values
74 $self->datecreated( dt_from_string ) unless $self->datecreated;
76 return $self->SUPER::store;
81 my $metadata = $biblio->metadata();
83 Returns a Koha::Biblio::Metadata object
90 my $metadata = $self->_result->metadata;
91 return Koha::Biblio::Metadata->_new_from_dbic($metadata);
96 my $record = $biblio->record();
98 Returns a Marc::Record object
105 return $self->metadata->record;
110 my $schema = $biblio->record_schema();
112 Returns the record schema (MARC21, USMARC or UNIMARC).
119 return $self->metadata->schema // C4::Context->preference("marcflavour");
124 my $orders = $biblio->orders();
126 Returns a Koha::Acquisition::Orders object
133 my $orders = $self->_result->orders;
134 return Koha::Acquisition::Orders->_new_from_dbic($orders);
139 my $active_orders = $biblio->active_orders();
141 Returns the active acquisition orders related to this biblio.
142 An order is considered active when it is not cancelled (i.e. when datecancellation
150 return $self->orders->search({ datecancellationprinted => undef });
155 my $tickets = $biblio->tickets();
157 Returns all tickets linked to the biblio
163 my $rs = $self->_result->tickets;
164 return Koha::Tickets->_new_from_dbic( $rs );
169 my $item_groups = $biblio->item_groups();
171 Returns a Koha::Biblio::ItemGroups object
178 my $item_groups = $self->_result->item_groups;
179 return Koha::Biblio::ItemGroups->_new_from_dbic($item_groups);
182 =head3 can_article_request
184 my $bool = $biblio->can_article_request( $borrower );
186 Returns true if article requests can be made for this record
188 $borrower must be a Koha::Patron object
192 sub can_article_request {
193 my ( $self, $borrower ) = @_;
195 my $rule = $self->article_request_type($borrower);
196 return q{} if $rule eq 'item_only' && !$self->items()->count();
197 return 1 if $rule && $rule ne 'no';
202 =head3 can_be_transferred
204 $biblio->can_be_transferred({ to => $to_library, from => $from_library })
206 Checks if at least one item of a biblio can be transferred to given library.
208 This feature is controlled by two system preferences:
209 UseBranchTransferLimits to enable / disable the feature
210 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
211 for setting the limitations
213 Performance-wise, it is recommended to use this method for a biblio instead of
214 iterating each item of a biblio with Koha::Item->can_be_transferred().
216 Takes HASHref that can have the following parameters:
217 MANDATORY PARAMETERS:
220 $from : Koha::Library # if given, only items from that
221 # holdingbranch are considered
223 Returns 1 if at least one of the item of a biblio can be transferred
224 to $to_library, otherwise 0.
228 sub can_be_transferred {
229 my ($self, $params) = @_;
231 my $to = $params->{to};
232 my $from = $params->{from};
234 return 1 unless C4::Context->preference('UseBranchTransferLimits');
235 my $limittype = C4::Context->preference('BranchTransferLimitsType');
238 foreach my $item_of_bib ($self->items->as_list) {
239 next unless $item_of_bib->holdingbranch;
240 next if $from && $from->branchcode ne $item_of_bib->holdingbranch;
241 return 1 if $item_of_bib->holdingbranch eq $to->branchcode;
242 my $code = $limittype eq 'itemtype'
243 ? $item_of_bib->effective_itemtype
244 : $item_of_bib->ccode;
245 return 1 unless $code;
246 $items->{$code}->{$item_of_bib->holdingbranch} = 1;
249 # At this point we will have a HASHref containing each itemtype/ccode that
250 # this biblio has, inside which are all of the holdingbranches where those
251 # items are located at. Then, we will query Koha::Item::Transfer::Limits to
252 # find out whether a transfer limits for such $limittype from any of the
253 # listed holdingbranches to the given $to library exist. If at least one
254 # holdingbranch for that $limittype does not have a transfer limit to given
255 # $to library, then we know that the transfer is possible.
256 foreach my $code (keys %{$items}) {
257 my @holdingbranches = keys %{$items->{$code}};
258 return 1 if Koha::Item::Transfer::Limits->search({
259 toBranch => $to->branchcode,
260 fromBranch => { 'in' => \@holdingbranches },
263 group_by => [qw/fromBranch/]
264 })->count == scalar(@holdingbranches) ? 0 : 1;
271 =head3 pickup_locations
273 my $pickup_locations = $biblio->pickup_locations({ patron => $patron });
275 Returns a Koha::Libraries set of possible pickup locations for this biblio's items,
276 according to patron's home library and if item can be transferred to each pickup location.
278 Throws a I<Koha::Exceptions::MissingParameter> exception if the B<mandatory> parameter I<patron>
283 sub pickup_locations {
284 my ( $self, $params ) = @_;
286 Koha::Exceptions::MissingParameter->throw( parameter => 'patron' )
287 unless exists $params->{patron};
289 my $patron = $params->{patron};
291 my $memory_cache = Koha::Cache::Memory::Lite->get_instance();
292 my @pickup_locations;
293 foreach my $item ( $self->items->as_list ) {
294 my $cache_key = sprintf "Pickup_locations:%s:%s:%s:%s:%s",
295 $item->itype,$item->homebranch,$item->holdingbranch,$item->ccode || "",$patron->branchcode||"" ;
296 my $item_pickup_locations = $memory_cache->get_from_cache( $cache_key );
297 unless( $item_pickup_locations ){
298 @{ $item_pickup_locations } = $item->pickup_locations( { patron => $patron } )->_resultset->get_column('branchcode')->all;
299 $memory_cache->set_in_cache( $cache_key, $item_pickup_locations );
301 push @pickup_locations, @{ $item_pickup_locations }
304 return Koha::Libraries->search(
305 { branchcode => { '-in' => \@pickup_locations } }, { order_by => ['branchname'] } );
308 =head3 hidden_in_opac
310 my $bool = $biblio->hidden_in_opac({ [ rules => $rules ] })
312 Returns true if the biblio matches the hidding criteria defined in $rules.
313 Returns false otherwise. It involves the I<OpacHiddenItems> and
314 I<OpacHiddenItemsHidesRecord> system preferences.
316 Takes HASHref that can have the following parameters:
318 $rules : { <field> => [ value_1, ... ], ... }
320 Note: $rules inherits its structure from the parsed YAML from reading
321 the I<OpacHiddenItems> system preference.
326 my ( $self, $params ) = @_;
328 my $rules = $params->{rules} // {};
330 my @items = $self->items->as_list;
332 return 0 unless @items; # Do not hide if there is no item
334 # Ok, there are items, don't even try the rules unless OpacHiddenItemsHidesRecord
335 return 0 unless C4::Context->preference('OpacHiddenItemsHidesRecord');
337 return !(any { !$_->hidden_in_opac({ rules => $rules }) } @items);
340 =head3 article_request_type
342 my $type = $biblio->article_request_type( $borrower );
344 Returns the article request type based on items, or on the record
345 itself if there are no items.
347 $borrower must be a Koha::Patron object
351 sub article_request_type {
352 my ( $self, $borrower ) = @_;
354 return q{} unless $borrower;
356 my $rule = $self->article_request_type_for_items( $borrower );
357 return $rule if $rule;
359 # If the record has no items that are requestable, go by the record itemtype
360 $rule = $self->article_request_type_for_bib($borrower);
361 return $rule if $rule;
366 =head3 article_request_type_for_bib
368 my $type = $biblio->article_request_type_for_bib
370 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record
374 sub article_request_type_for_bib {
375 my ( $self, $borrower ) = @_;
377 return q{} unless $borrower;
379 my $borrowertype = $borrower->categorycode;
380 my $itemtype = $self->itemtype();
382 my $rule = Koha::CirculationRules->get_effective_rule(
384 rule_name => 'article_requests',
385 categorycode => $borrowertype,
386 itemtype => $itemtype,
390 return q{} unless $rule;
391 return $rule->rule_value || q{}
394 =head3 article_request_type_for_items
396 my $type = $biblio->article_request_type_for_items
398 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record's items
400 If there is a conflict where some items are 'bib_only' and some are 'item_only', 'bib_only' will be returned.
404 sub article_request_type_for_items {
405 my ( $self, $borrower ) = @_;
408 foreach my $item ( $self->items()->as_list() ) {
409 my $rule = $item->article_request_type($borrower);
410 return $rule if $rule eq 'bib_only'; # we don't need to go any further
414 return 'item_only' if $counts->{item_only};
415 return 'yes' if $counts->{yes};
416 return 'no' if $counts->{no};
420 =head3 article_requests
422 my $article_requests = $biblio->article_requests
424 Returns the article requests associated with this biblio
428 sub article_requests {
431 return Koha::ArticleRequests->_new_from_dbic( scalar $self->_result->article_requests );
434 =head3 current_checkouts
436 my $current_checkouts = $biblio->current_checkouts
438 Returns the current checkouts associated with this biblio
442 sub current_checkouts {
445 return Koha::Checkouts->search( { "item.biblionumber" => $self->id },
446 { join => 'item' } );
451 my $old_checkouts = $biblio->old_checkouts
453 Returns the past checkouts associated with this biblio
460 return Koha::Old::Checkouts->search( { "item.biblionumber" => $self->id },
461 { join => 'item' } );
466 my $items = $biblio->items();
468 Returns the related Koha::Items object for this biblio
475 my $items_rs = $self->_result->items;
477 return Koha::Items->_new_from_dbic( $items_rs );
482 my $host_items = $biblio->host_items();
484 Return the host items (easy analytical record)
491 return Koha::Items->new->empty
492 unless C4::Context->preference('EasyAnalyticalRecords');
494 my $marcflavour = C4::Context->preference("marcflavour");
495 my $analyticfield = '773';
496 if ( $marcflavour eq 'MARC21' ) {
497 $analyticfield = '773';
499 elsif ( $marcflavour eq 'UNIMARC' ) {
500 $analyticfield = '461';
502 my $marc_record = $self->metadata->record;
504 foreach my $field ( $marc_record->field($analyticfield) ) {
505 push @itemnumbers, $field->subfield('9');
508 return Koha::Items->search( { itemnumber => { -in => \@itemnumbers } } );
513 my $itemtype = $biblio->itemtype();
515 Returns the itemtype for this record.
522 return $self->biblioitem()->itemtype();
527 my $holds = $biblio->holds();
529 return the current holds placed on this record
534 my ( $self, $params, $attributes ) = @_;
535 $attributes->{order_by} = 'priority' unless exists $attributes->{order_by};
536 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
537 return Koha::Holds->_new_from_dbic($hold_rs);
542 my $holds = $biblio->current_holds
544 Return the holds placed on this bibliographic record.
545 It does not include future holds.
551 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
553 { reservedate => { '<=' => $dtf->format_date(dt_from_string) } } );
558 my $field = $self->biblioitem()->itemtype
560 Returns the related Koha::Biblioitem object for this Biblio object
567 $self->{_biblioitem} ||= Koha::Biblioitems->find( { biblionumber => $self->biblionumber() } );
569 return $self->{_biblioitem};
574 my $suggestions = $self->suggestions
576 Returns the related Koha::Suggestions object for this Biblio object
583 my $suggestions_rs = $self->_result->suggestions;
584 return Koha::Suggestions->_new_from_dbic( $suggestions_rs );
587 =head3 get_marc_components
589 my $components = $self->get_marc_components();
591 Returns an array of search results data, which are component parts of
592 this object (MARC21 773 points to this)
596 sub get_marc_components {
597 my ($self, $max_results) = @_;
599 return [] if (C4::Context->preference('marcflavour') ne 'MARC21');
601 my ( $searchstr, $sort ) = $self->get_components_query;
604 if (defined($searchstr)) {
605 my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
606 my ( $error, $results, $facets );
608 ( $error, $results, $facets ) = $searcher->search_compat( $searchstr, undef, [$sort], ['biblioserver'], $max_results, 0, undef, undef, 'ccl', 0 );
613 warn "Warning from search_compat: '$error'";
617 message => 'component_search',
622 $components = $results->{biblioserver}->{RECORDS} if defined($results) && $results->{biblioserver}->{hits};
625 return $components // [];
628 =head2 get_components_query
630 Returns a query which can be used to search for all component parts of MARC21 biblios
634 sub get_components_query {
637 my $builder = Koha::SearchEngine::QueryBuilder->new(
638 { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
639 my $marc = $self->metadata->record;
640 my $component_sort_field = C4::Context->preference('ComponentSortField') // "title";
641 my $component_sort_order = C4::Context->preference('ComponentSortOrder') // "asc";
642 my $sort = $component_sort_field . "_" . $component_sort_order;
645 if ( C4::Context->preference('UseControlNumber') ) {
646 my $pf001 = $marc->field('001') || undef;
648 if ( defined($pf001) ) {
650 my $pf003 = $marc->field('003') || undef;
652 if ( !defined($pf003) ) {
653 # search for 773$w='Host001'
654 $searchstr .= "rcn:\"" . $pf001->data()."\"";
658 # search for (773$w='Host001' and 003='Host003') or 773$w='(Host003)Host001'
659 $searchstr .= "(rcn:\"" . $pf001->data() . "\" AND cni:\"" . $pf003->data() . "\")";
660 $searchstr .= " OR rcn:\"" . $pf003->data() . " " . $pf001->data() . "\"";
664 # limit to monograph and serial component part records
665 $searchstr .= " AND (bib-level:a OR bib-level:b)";
670 my $cleaned_title = $marc->subfield('245', "a");
671 $cleaned_title =~ tr|/||;
672 $cleaned_title = $builder->clean_search_term($cleaned_title);
673 $searchstr = qq#Host-item:("$cleaned_title")#;
675 my ($error, $query ,$query_str) = $builder->build_query_compat( undef, [$searchstr], undef, undef, [$sort], 0 );
681 return ($query, $query_str, $sort);
686 my $subscriptions = $self->subscriptions
688 Returns the related Koha::Subscriptions object for this Biblio object
695 $self->{_subscriptions} ||= Koha::Subscriptions->search( { biblionumber => $self->biblionumber } );
697 return $self->{_subscriptions};
700 =head3 has_items_waiting_or_intransit
702 my $itemsWaitingOrInTransit = $biblio->has_items_waiting_or_intransit
704 Tells if this bibliographic record has items waiting or in transit.
708 sub has_items_waiting_or_intransit {
711 if ( Koha::Holds->search({ biblionumber => $self->id,
712 found => ['W', 'T'] })->count ) {
716 foreach my $item ( $self->items->as_list ) {
717 return 1 if $item->get_transfer;
725 my $coins = $biblio->get_coins;
727 Returns the COinS (a span) which can be included in a biblio record
734 my $record = $self->metadata->record;
736 my $pos7 = substr $record->leader(), 7, 1;
737 my $pos6 = substr $record->leader(), 6, 1;
740 my ( $aulast, $aufirst ) = ( '', '' );
751 # For the purposes of generating COinS metadata, LDR/06-07 can be
752 # considered the same for UNIMARC and MARC21
761 'i' => 'audioRecording',
762 'j' => 'audioRecording',
765 'm' => 'computerProgram',
770 'a' => 'journalArticle',
774 $genre = $fmts6->{$pos6} ? $fmts6->{$pos6} : 'book';
776 if ( $genre eq 'book' ) {
777 $genre = $fmts7->{$pos7} if $fmts7->{$pos7};
780 ##### We must transform mtx to a valable mtx and document type ####
781 if ( $genre eq 'book' ) {
784 } elsif ( $genre eq 'journal' ) {
787 } elsif ( $genre eq 'journalArticle' ) {
795 if ( C4::Context->preference("marcflavour") eq "UNIMARC" ) {
798 $aulast = $record->subfield( '700', 'a' ) || '';
799 $aufirst = $record->subfield( '700', 'b' ) || '';
800 push @authors, "$aufirst $aulast" if ($aufirst or $aulast);
803 if ( $record->field('200') ) {
804 for my $au ( $record->field('200')->subfield('g') ) {
809 $title = $record->subfield( '200', 'a' );
810 my $subfield_210d = $record->subfield('210', 'd');
811 if ($subfield_210d and $subfield_210d =~ /(\d{4})/) {
814 $publisher = $record->subfield( '210', 'c' ) || '';
815 $isbn = $record->subfield( '010', 'a' ) || '';
816 $issn = $record->subfield( '011', 'a' ) || '';
819 # MARC21 need some improve
822 if ( $record->field('100') ) {
823 push @authors, $record->subfield( '100', 'a' );
827 if ( $record->field('700') ) {
828 for my $au ( $record->field('700')->subfield('a') ) {
832 $title = $record->field('245');
833 $title &&= $title->as_string('ab');
834 if ($titletype eq 'a') {
835 $pubyear = $record->field('008') || '';
836 $pubyear = substr($pubyear->data(), 7, 4) if $pubyear;
837 $isbn = $record->subfield( '773', 'z' ) || '';
838 $issn = $record->subfield( '773', 'x' ) || '';
839 $hosttitle = $record->subfield( '773', 't' ) || $record->subfield( '773', 'a') || q{};
840 my @rels = $record->subfield( '773', 'g' );
841 $pages = join(', ', @rels);
843 $pubyear = $record->subfield( '260', 'c' ) || '';
844 $publisher = $record->subfield( '260', 'b' ) || '';
845 $isbn = $record->subfield( '020', 'a' ) || '';
846 $issn = $record->subfield( '022', 'a' ) || '';
852 [ 'ctx_ver', 'Z39.88-2004' ],
853 [ 'rft_val_fmt', "info:ofi/fmt:kev:mtx:$mtx" ],
854 [ ($mtx eq 'dc' ? 'rft.type' : 'rft.genre'), $genre ],
855 [ "rft.${titletype}title", $title ],
858 # rft.title is authorized only once, so by checking $titletype
859 # we ensure that rft.title is not already in the list.
860 if ($hosttitle and $titletype) {
861 push @params, [ 'rft.title', $hosttitle ];
865 [ 'rft.isbn', $isbn ],
866 [ 'rft.issn', $issn ],
869 # If it's a subscription, these informations have no meaning.
870 if ($genre ne 'journal') {
872 [ 'rft.aulast', $aulast ],
873 [ 'rft.aufirst', $aufirst ],
874 (map { [ 'rft.au', $_ ] } @authors),
875 [ 'rft.pub', $publisher ],
876 [ 'rft.date', $pubyear ],
877 [ 'rft.pages', $pages ],
881 my $coins_value = join( '&',
882 map { $$_[1] ? $$_[0] . '=' . uri_escape_utf8( $$_[1] ) : () } @params );
889 my $url = $biblio->get_openurl;
891 Returns url for OpenURL resolver set in OpenURLResolverURL system preference
898 my $OpenURLResolverURL = C4::Context->preference('OpenURLResolverURL');
900 if ($OpenURLResolverURL) {
901 my $uri = URI->new($OpenURLResolverURL);
903 if (not defined $uri->query) {
904 $OpenURLResolverURL .= '?';
906 $OpenURLResolverURL .= '&';
908 $OpenURLResolverURL .= $self->get_coins;
911 return $OpenURLResolverURL;
916 my $serial = $biblio->is_serial
918 Return boolean true if this bibbliographic record is continuing resource
925 return 1 if $self->serial;
927 my $record = $self->metadata->record;
928 return 1 if substr($record->leader, 7, 1) eq 's';
933 =head3 custom_cover_image_url
935 my $image_url = $biblio->custom_cover_image_url
937 Return the specific url of the cover image for this bibliographic record.
938 It is built regaring the value of the system preference CustomCoverImagesURL
942 sub custom_cover_image_url {
944 my $url = C4::Context->preference('CustomCoverImagesURL');
945 if ( $url =~ m|{isbn}| ) {
946 my $isbn = $self->biblioitem->isbn;
948 $url =~ s|{isbn}|$isbn|g;
950 if ( $url =~ m|{normalized_isbn}| ) {
951 my $normalized_isbn = C4::Koha::GetNormalizedISBN($self->biblioitem->isbn);
952 return unless $normalized_isbn;
953 $url =~ s|{normalized_isbn}|$normalized_isbn|g;
955 if ( $url =~ m|{issn}| ) {
956 my $issn = $self->biblioitem->issn;
958 $url =~ s|{issn}|$issn|g;
961 my $re = qr|{(?<field>\d{3})(\$(?<subfield>.))?}|;
963 my $field = $+{field};
964 my $subfield = $+{subfield};
965 my $marc_record = $self->metadata->record;
968 $value = $marc_record->subfield( $field, $subfield );
970 my $controlfield = $marc_record->field($field);
971 $value = $controlfield->data() if $controlfield;
973 return unless $value;
974 $url =~ s|$re|$value|;
982 Return the cover images associated with this biblio.
989 my $cover_images_rs = $self->_result->cover_images;
990 return unless $cover_images_rs;
991 return Koha::CoverImages->_new_from_dbic($cover_images_rs);
994 =head3 get_marc_notes
996 $marcnotesarray = $biblio->get_marc_notes({ opac => 1 });
998 Get all notes from the MARC record and returns them in an array.
999 The notes are stored in different fields depending on MARC flavour.
1000 MARC21 5XX $u subfields receive special attention as they are URIs.
1004 sub get_marc_notes {
1005 my ( $self, $params ) = @_;
1007 my $marcflavour = C4::Context->preference('marcflavour');
1008 my $opac = $params->{opac} // '0';
1009 my $interface = $params->{opac} ? 'opac' : 'intranet';
1011 my $record = $params->{record} // $self->metadata->record;
1012 my $record_processor = Koha::RecordProcessor->new(
1014 filters => [ 'ViewPolicy', 'ExpandCodedFields' ],
1016 interface => $interface,
1017 frameworkcode => $self->frameworkcode
1021 $record_processor->process($record);
1023 my $scope = $marcflavour eq "UNIMARC"? '3..': '5..';
1024 #MARC21 specs indicate some notes should be private if first indicator 0
1025 my %maybe_private = (
1033 my %hiddenlist = map { $_ => 1 }
1034 split( /,/, C4::Context->preference('NotesToHide'));
1037 foreach my $field ( $record->field($scope) ) {
1038 my $tag = $field->tag();
1039 next if $hiddenlist{ $tag };
1040 next if $opac && $maybe_private{$tag} && !$field->indicator(1);
1041 if( $marcflavour ne 'UNIMARC' && $field->subfield('u') ) {
1042 # Field 5XX$u always contains URI
1043 # Examples: 505u, 506u, 510u, 514u, 520u, 530u, 538u, 540u, 542u, 552u, 555u, 561u, 563u, 583u
1044 # We first push the other subfields, then all $u's separately
1045 # Leave further actions to the template (see e.g. opac-detail)
1047 join '', ( 'a' .. 't', 'v' .. 'z', '0' .. '9' ); # excl 'u'
1048 push @marcnotes, { marcnote => $field->as_string($othersub) };
1049 foreach my $sub ( $field->subfield('u') ) {
1050 $sub =~ s/^\s+|\s+$//g; # trim
1051 push @marcnotes, { marcnote => $sub };
1054 push @marcnotes, { marcnote => $field->as_string() };
1060 =head3 _get_marc_authors
1062 Private method to return the list of authors contained in the MARC record.
1063 See get get_marc_contributors and get_marc_authors for the public methods.
1067 sub _get_marc_authors {
1068 my ( $self, $params ) = @_;
1070 my $fields_filter = $params->{fields_filter};
1071 my $mintag = $params->{mintag};
1072 my $maxtag = $params->{maxtag};
1074 my $AuthoritySeparator = C4::Context->preference('AuthoritySeparator');
1075 my $marcflavour = C4::Context->preference('marcflavour');
1077 # tagslib useful only for UNIMARC author responsibilities
1078 my $tagslib = $marcflavour eq "UNIMARC"
1079 ? C4::Biblio::GetMarcStructure( 1, $self->frameworkcode, { unsafe => 1 } )
1083 foreach my $field ( $self->metadata->record->field($fields_filter) ) {
1086 if $mintag && $field->tag() < $mintag
1087 || $maxtag && $field->tag() > $maxtag;
1091 my @subfields = $field->subfields();
1094 # if there is an authority link, build the link with Koha-Auth-Number: subfield9
1095 my $subfield9 = $field->subfield('9');
1097 my $linkvalue = $subfield9;
1098 $linkvalue =~ s/(\(|\))//g;
1099 @link_loop = ( { 'limit' => 'an', 'link' => $linkvalue } );
1104 for my $authors_subfield (@subfields) {
1105 next if ( $authors_subfield->[0] eq '9' );
1107 # unimarc3 contains the $3 of the author for UNIMARC.
1108 # For french academic libraries, it's the "ppn", and it's required for idref webservice
1109 $unimarc3 = $authors_subfield->[1] if $marcflavour eq 'UNIMARC' and $authors_subfield->[0] =~ /3/;
1111 # don't load unimarc subfields 3, 5
1112 next if ( $marcflavour eq 'UNIMARC' and ( $authors_subfield->[0] =~ /3|5/ ) );
1114 my $code = $authors_subfield->[0];
1115 my $value = $authors_subfield->[1];
1116 my $linkvalue = $value;
1117 $linkvalue =~ s/(\(|\))//g;
1118 # UNIMARC author responsibility
1119 if ( $marcflavour eq 'UNIMARC' and $code eq '4' ) {
1120 $value = C4::Biblio::GetAuthorisedValueDesc( $field->tag(), $code, $value, '', $tagslib );
1121 $linkvalue = "($value)";
1123 # if no authority link, build a search query
1124 unless ($subfield9) {
1127 'link' => $linkvalue,
1128 operator => (scalar @link_loop) ? ' AND ' : undef
1131 my @this_link_loop = @link_loop;
1133 unless ( $code eq '0') {
1134 push @subfields_loop, {
1135 tag => $field->tag(),
1138 link_loop => \@this_link_loop,
1139 separator => (scalar @subfields_loop) ? $AuthoritySeparator : ''
1143 push @marcauthors, {
1144 MARCAUTHOR_SUBFIELDS_LOOP => \@subfields_loop,
1145 authoritylink => $subfield9,
1146 unimarc3 => $unimarc3
1149 return \@marcauthors;
1152 =head3 get_marc_contributors
1154 my $contributors = $biblio->get_marc_contributors;
1156 Get all contributors (but first author) from the MARC record and returns them in an array.
1157 They are stored in different fields depending on MARC flavour (700..720 for MARC21)
1161 sub get_marc_contributors {
1162 my ( $self, $params ) = @_;
1164 my ( $mintag, $maxtag, $fields_filter );
1165 my $marcflavour = C4::Context->preference('marcflavour');
1167 if ( $marcflavour eq "UNIMARC" ) {
1170 $fields_filter = '7..';
1171 } else { # marc21/normarc
1174 $fields_filter = '7..';
1177 return $self->_get_marc_authors(
1179 fields_filter => $fields_filter,
1186 =head3 get_marc_authors
1188 my $authors = $biblio->get_marc_authors;
1190 Get all authors from the MARC record and returns them in an array.
1191 They are stored in different fields depending on MARC flavour
1192 (main author from 100 then secondary authors from 700..720).
1196 sub get_marc_authors {
1197 my ( $self, $params ) = @_;
1199 my ( $mintag, $maxtag, $fields_filter );
1200 my $marcflavour = C4::Context->preference('marcflavour');
1202 if ( $marcflavour eq "UNIMARC" ) {
1203 $fields_filter = '200';
1204 } else { # marc21/normarc
1205 $fields_filter = '100';
1208 my @first_authors = @{$self->_get_marc_authors(
1210 fields_filter => $fields_filter,
1216 my @other_authors = @{$self->get_marc_contributors};
1218 return [@first_authors, @other_authors];
1224 my $json = $biblio->to_api;
1226 Overloaded method that returns a JSON representation of the Koha::Biblio object,
1227 suitable for API output. The related Koha::Biblioitem object is merged as expected
1233 my ($self, $args) = @_;
1235 my $response = $self->SUPER::to_api( $args );
1236 my $biblioitem = $self->biblioitem->to_api;
1238 return { %$response, %$biblioitem };
1241 =head3 to_api_mapping
1243 This method returns the mapping for representing a Koha::Biblio object
1248 sub to_api_mapping {
1250 biblionumber => 'biblio_id',
1251 frameworkcode => 'framework_id',
1252 unititle => 'uniform_title',
1253 seriestitle => 'series_title',
1254 copyrightdate => 'copyright_date',
1255 datecreated => 'creation_date',
1256 deleted_on => undef,
1260 =head3 get_marc_host
1262 $host = $biblio->get_marc_host;
1264 ( $host, $relatedparts, $hostinfo ) = $biblio->get_marc_host;
1266 Returns host biblio record from MARC21 773 (undef if no 773 present).
1267 It looks at the first 773 field with MARCorgCode or only a control
1268 number. Complete $w or numeric part is used to search host record.
1269 The optional parameter no_items triggers a check if $biblio has items.
1270 If there are, the sub returns undef.
1271 Called in list context, it also returns 773$g (related parts).
1273 If there is no $w, we use $0 (host biblionumber) or $9 (host itemnumber)
1274 to search for the host record. If there is also no $0 and no $9, we search
1275 using author and title. Failing all of that, we return an undef host and
1276 form a concatenation of strings with 773$agt for host information,
1277 returned when called in list context.
1282 my ($self, $params) = @_;
1283 my $no_items = $params->{no_items};
1284 return if C4::Context->preference('marcflavour') eq 'UNIMARC'; # TODO
1285 return if $params->{no_items} && $self->items->count > 0;
1288 eval { $record = $self->metadata->record };
1291 # We pick the first $w with your MARCOrgCode or the first $w that has no
1292 # code (between parentheses) at all.
1293 my $orgcode = C4::Context->preference('MARCOrgCode') // q{};
1295 foreach my $f ( $record->field('773') ) {
1296 my $w = $f->subfield('w') or next;
1297 if( $w =~ /^\($orgcode\)\s*(\d+)/i or $w =~ /^\d+/ ) {
1303 my $engine = Koha::SearchEngine::Search->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1305 if ( !$hostfld and $record->subfield('773','t') ) {
1306 # not linked using $w
1307 my $unlinkedf = $record->field('773');
1309 if ( C4::Context->preference("EasyAnalyticalRecords") ) {
1310 if ( $unlinkedf->subfield('0') ) {
1311 # use 773$0 host biblionumber
1312 $bibno = $unlinkedf->subfield('0');
1313 } elsif ( $unlinkedf->subfield('9') ) {
1314 # use 773$9 host itemnumber
1315 my $linkeditemnumber = $unlinkedf->subfield('9');
1316 $bibno = Koha::Items->find( $linkeditemnumber )->biblionumber;
1320 my $host = Koha::Biblios->find($bibno) or return;
1321 return wantarray ? ( $host, $unlinkedf->subfield('g') ) : $host;
1323 # just return plaintext and no host record
1324 my $hostinfo = join( ", ", $unlinkedf->subfield('a'), $unlinkedf->subfield('t'), $unlinkedf->subfield('g') );
1325 return wantarray ? ( undef, $unlinkedf->subfield('g'), $hostinfo ) : undef;
1327 return if !$hostfld;
1328 my $rcn = $hostfld->subfield('w');
1330 # Look for control number with/without orgcode
1331 for my $try (1..2) {
1332 my ( $error, $results, $total_hits ) = $engine->simple_search_compat( 'Control-number='.$rcn, 0,1 );
1333 if( !$error and $total_hits == 1 ) {
1334 $bibno = $engine->extract_biblionumber( $results->[0] );
1337 # Add or remove orgcode for second try
1338 if( $try == 1 && $rcn =~ /\)\s*(\d+)/ ) {
1339 $rcn = $1; # number only
1340 } elsif( $try == 1 && $rcn =~ /^\d+/ ) {
1341 $rcn = "($orgcode)$rcn";
1347 my $host = Koha::Biblios->find($bibno) or return;
1348 return wantarray ? ( $host, $hostfld->subfield('g') ) : $host;
1352 =head3 get_marc_host_only
1354 my $host = $biblio->get_marc_host_only;
1360 sub get_marc_host_only {
1363 my ( $host ) = $self->get_marc_host;
1368 =head3 get_marc_relatedparts_only
1370 my $relatedparts = $biblio->get_marc_relatedparts_only;
1372 Return related parts only
1376 sub get_marc_relatedparts_only {
1379 my ( undef, $relatedparts ) = $self->get_marc_host;
1381 return $relatedparts;
1384 =head3 get_marc_hostinfo_only
1386 my $hostinfo = $biblio->get_marc_hostinfo_only;
1388 Return host info only
1392 sub get_marc_hostinfo_only {
1395 my ( $host, $relatedparts, $hostinfo ) = $self->get_marc_host;
1402 my $recalls = $biblio->recalls;
1404 Return recalls linked to this biblio
1410 return Koha::Recalls->_new_from_dbic( scalar $self->_result->recalls );
1413 =head3 can_be_recalled
1415 my @items_for_recall = $biblio->can_be_recalled({ patron => $patron_object });
1417 Does biblio-level checks and returns the items attached to this biblio that are available for recall
1421 sub can_be_recalled {
1422 my ( $self, $params ) = @_;
1424 return 0 if !( C4::Context->preference('UseRecalls') );
1426 my $patron = $params->{patron};
1428 my $branchcode = C4::Context->userenv->{'branch'};
1429 if ( C4::Context->preference('CircControl') eq 'PatronLibrary' and $patron ) {
1430 $branchcode = $patron->branchcode;
1433 my @all_items = Koha::Items->search({ biblionumber => $self->biblionumber })->as_list;
1435 # if there are no available items at all, no recall can be placed
1436 return 0 if ( scalar @all_items == 0 );
1441 my @all_itemnumbers;
1442 foreach my $item ( @all_items ) {
1443 push( @all_itemnumbers, $item->itemnumber );
1444 if ( $item->can_be_recalled({ patron => $patron }) ) {
1445 push( @itemtypes, $item->effective_itemtype );
1446 push( @itemnumbers, $item->itemnumber );
1447 push( @items, $item );
1451 # if there are no recallable items, no recall can be placed
1452 return 0 if ( scalar @items == 0 );
1454 # Check the circulation rule for each relevant itemtype for this biblio
1455 my ( @recalls_allowed, @recalls_per_record, @on_shelf_recalls );
1456 foreach my $itemtype ( @itemtypes ) {
1457 my $rule = Koha::CirculationRules->get_effective_rules({
1458 branchcode => $branchcode,
1459 categorycode => $patron ? $patron->categorycode : undef,
1460 itemtype => $itemtype,
1463 'recalls_per_record',
1467 push( @recalls_allowed, $rule->{recalls_allowed} ) if $rule;
1468 push( @recalls_per_record, $rule->{recalls_per_record} ) if $rule;
1469 push( @on_shelf_recalls, $rule->{on_shelf_recalls} ) if $rule;
1471 my $recalls_allowed = (sort {$b <=> $a} @recalls_allowed)[0]; # take highest
1472 my $recalls_per_record = (sort {$b <=> $a} @recalls_per_record)[0]; # take highest
1473 my %on_shelf_recalls_count = ();
1474 foreach my $count ( @on_shelf_recalls ) {
1475 $on_shelf_recalls_count{$count}++;
1477 my $on_shelf_recalls = (sort {$on_shelf_recalls_count{$b} <=> $on_shelf_recalls_count{$a}} @on_shelf_recalls)[0]; # take most common
1479 # check recalls allowed has been set and is not zero
1480 return 0 if ( !defined($recalls_allowed) || $recalls_allowed == 0 );
1483 # check borrower has not reached open recalls allowed limit
1484 return 0 if ( $patron->recalls->filter_by_current->count >= $recalls_allowed );
1486 # check borrower has not reached open recalls allowed per record limit
1487 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $recalls_per_record );
1489 # check if any of the items under this biblio are already checked out by this borrower
1490 return 0 if ( Koha::Checkouts->search({ itemnumber => [ @all_itemnumbers ], borrowernumber => $patron->borrowernumber })->count > 0 );
1493 # check item availability
1494 my $checked_out_count = 0;
1496 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1499 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1500 return 0 if ( $on_shelf_recalls eq 'all' && $checked_out_count < scalar @items );
1502 # can't recall if no items have been checked out
1503 return 0 if ( $checked_out_count == 0 );
1509 =head2 Internal methods
1521 Kyle M Hall <kyle@bywatersolutions.com>