3 # Copyright ByWater Solutions 2014
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22 use List::MoreUtils qw( any );
24 use URI::Escape qw( uri_escape_utf8 );
26 use C4::Koha qw( GetNormalizedISBN );
29 use Koha::DateUtils qw( dt_from_string );
31 use base qw(Koha::Object);
33 use Koha::Acquisition::Orders;
34 use Koha::ArticleRequests;
35 use Koha::Biblio::Metadatas;
36 use Koha::Biblio::ItemGroups;
37 use Koha::Biblioitems;
38 use Koha::Cache::Memory::Lite;
40 use Koha::CirculationRules;
42 use Koha::Illrequests;
43 use Koha::Item::Transfer::Limits;
46 use Koha::Old::Checkouts;
48 use Koha::RecordProcessor;
49 use Koha::Suggestions;
50 use Koha::Subscriptions;
51 use Koha::SearchEngine;
52 use Koha::SearchEngine::Search;
53 use Koha::SearchEngine::QueryBuilder;
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 $orders = $biblio->orders();
112 Returns a Koha::Acquisition::Orders object
119 my $orders = $self->_result->orders;
120 return Koha::Acquisition::Orders->_new_from_dbic($orders);
125 my $active_orders = $biblio->active_orders();
127 Returns the active acquisition orders related to this biblio.
128 An order is considered active when it is not cancelled (i.e. when datecancellation
136 return $self->orders->search({ datecancellationprinted => undef });
141 my $tickets = $biblio->tickets();
143 Returns all tickets linked to the biblio
149 my $rs = $self->_result->tickets;
150 return Koha::Tickets->_new_from_dbic( $rs );
155 my $ill_requests = $biblio->ill_requests();
157 Returns a Koha::Illrequests object
164 my $ill_requests = $self->_result->ill_requests;
165 return Koha::Illrequests->_new_from_dbic($ill_requests);
170 my $item_groups = $biblio->item_groups();
172 Returns a Koha::Biblio::ItemGroups object
179 my $item_groups = $self->_result->item_groups;
180 return Koha::Biblio::ItemGroups->_new_from_dbic($item_groups);
183 =head3 can_article_request
185 my $bool = $biblio->can_article_request( $borrower );
187 Returns true if article requests can be made for this record
189 $borrower must be a Koha::Patron object
193 sub can_article_request {
194 my ( $self, $borrower ) = @_;
196 my $rule = $self->article_request_type($borrower);
197 return q{} if $rule eq 'item_only' && !$self->items()->count();
198 return 1 if $rule && $rule ne 'no';
203 =head3 can_be_transferred
205 $biblio->can_be_transferred({ to => $to_library, from => $from_library })
207 Checks if at least one item of a biblio can be transferred to given library.
209 This feature is controlled by two system preferences:
210 UseBranchTransferLimits to enable / disable the feature
211 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
212 for setting the limitations
214 Performance-wise, it is recommended to use this method for a biblio instead of
215 iterating each item of a biblio with Koha::Item->can_be_transferred().
217 Takes HASHref that can have the following parameters:
218 MANDATORY PARAMETERS:
221 $from : Koha::Library # if given, only items from that
222 # holdingbranch are considered
224 Returns 1 if at least one of the item of a biblio can be transferred
225 to $to_library, otherwise 0.
229 sub can_be_transferred {
230 my ($self, $params) = @_;
232 my $to = $params->{to};
233 my $from = $params->{from};
235 return 1 unless C4::Context->preference('UseBranchTransferLimits');
236 my $limittype = C4::Context->preference('BranchTransferLimitsType');
239 foreach my $item_of_bib ($self->items->as_list) {
240 next unless $item_of_bib->holdingbranch;
241 next if $from && $from->branchcode ne $item_of_bib->holdingbranch;
242 return 1 if $item_of_bib->holdingbranch eq $to->branchcode;
243 my $code = $limittype eq 'itemtype'
244 ? $item_of_bib->effective_itemtype
245 : $item_of_bib->ccode;
246 return 1 unless $code;
247 $items->{$code}->{$item_of_bib->holdingbranch} = 1;
250 # At this point we will have a HASHref containing each itemtype/ccode that
251 # this biblio has, inside which are all of the holdingbranches where those
252 # items are located at. Then, we will query Koha::Item::Transfer::Limits to
253 # find out whether a transfer limits for such $limittype from any of the
254 # listed holdingbranches to the given $to library exist. If at least one
255 # holdingbranch for that $limittype does not have a transfer limit to given
256 # $to library, then we know that the transfer is possible.
257 foreach my $code (keys %{$items}) {
258 my @holdingbranches = keys %{$items->{$code}};
259 return 1 if Koha::Item::Transfer::Limits->search({
260 toBranch => $to->branchcode,
261 fromBranch => { 'in' => \@holdingbranches },
264 group_by => [qw/fromBranch/]
265 })->count == scalar(@holdingbranches) ? 0 : 1;
272 =head3 pickup_locations
274 my $pickup_locations = $biblio->pickup_locations({ patron => $patron });
276 Returns a Koha::Libraries set of possible pickup locations for this biblio's items,
277 according to patron's home library and if item can be transferred to each pickup location.
279 Throws a I<Koha::Exceptions::MissingParameter> exception if the B<mandatory> parameter I<patron>
284 sub pickup_locations {
285 my ( $self, $params ) = @_;
287 Koha::Exceptions::MissingParameter->throw( parameter => 'patron' )
288 unless exists $params->{patron};
290 my $patron = $params->{patron};
292 my $memory_cache = Koha::Cache::Memory::Lite->get_instance();
293 my @pickup_locations;
294 foreach my $item ( $self->items->as_list ) {
295 my $cache_key = sprintf "Pickup_locations:%s:%s:%s:%s:%s",
296 $item->itype,$item->homebranch,$item->holdingbranch,$item->ccode || "",$patron->branchcode||"" ;
297 my $item_pickup_locations = $memory_cache->get_from_cache( $cache_key );
298 unless( $item_pickup_locations ){
299 @{ $item_pickup_locations } = $item->pickup_locations( { patron => $patron } )->_resultset->get_column('branchcode')->all;
300 $memory_cache->set_in_cache( $cache_key, $item_pickup_locations );
302 push @pickup_locations, @{ $item_pickup_locations }
305 return Koha::Libraries->search(
306 { branchcode => { '-in' => \@pickup_locations } }, { order_by => ['branchname'] } );
309 =head3 hidden_in_opac
311 my $bool = $biblio->hidden_in_opac({ [ rules => $rules ] })
313 Returns true if the biblio matches the hidding criteria defined in $rules.
314 Returns false otherwise. It involves the I<OpacHiddenItems> and
315 I<OpacHiddenItemsHidesRecord> system preferences.
317 Takes HASHref that can have the following parameters:
319 $rules : { <field> => [ value_1, ... ], ... }
321 Note: $rules inherits its structure from the parsed YAML from reading
322 the I<OpacHiddenItems> system preference.
327 my ( $self, $params ) = @_;
329 my $rules = $params->{rules} // {};
331 my @items = $self->items->as_list;
333 return 0 unless @items; # Do not hide if there is no item
335 # Ok, there are items, don't even try the rules unless OpacHiddenItemsHidesRecord
336 return 0 unless C4::Context->preference('OpacHiddenItemsHidesRecord');
338 return !(any { !$_->hidden_in_opac({ rules => $rules }) } @items);
341 =head3 article_request_type
343 my $type = $biblio->article_request_type( $borrower );
345 Returns the article request type based on items, or on the record
346 itself if there are no items.
348 $borrower must be a Koha::Patron object
352 sub article_request_type {
353 my ( $self, $borrower ) = @_;
355 return q{} unless $borrower;
357 my $rule = $self->article_request_type_for_items( $borrower );
358 return $rule if $rule;
360 # If the record has no items that are requestable, go by the record itemtype
361 $rule = $self->article_request_type_for_bib($borrower);
362 return $rule if $rule;
367 =head3 article_request_type_for_bib
369 my $type = $biblio->article_request_type_for_bib
371 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record
375 sub article_request_type_for_bib {
376 my ( $self, $borrower ) = @_;
378 return q{} unless $borrower;
380 my $borrowertype = $borrower->categorycode;
381 my $itemtype = $self->itemtype();
383 my $rule = Koha::CirculationRules->get_effective_rule(
385 rule_name => 'article_requests',
386 categorycode => $borrowertype,
387 itemtype => $itemtype,
391 return q{} unless $rule;
392 return $rule->rule_value || q{}
395 =head3 article_request_type_for_items
397 my $type = $biblio->article_request_type_for_items
399 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record's items
401 If there is a conflict where some items are 'bib_only' and some are 'item_only', 'bib_only' will be returned.
405 sub article_request_type_for_items {
406 my ( $self, $borrower ) = @_;
409 foreach my $item ( $self->items()->as_list() ) {
410 my $rule = $item->article_request_type($borrower);
411 return $rule if $rule eq 'bib_only'; # we don't need to go any further
415 return 'item_only' if $counts->{item_only};
416 return 'yes' if $counts->{yes};
417 return 'no' if $counts->{no};
421 =head3 article_requests
423 my $article_requests = $biblio->article_requests
425 Returns the article requests associated with this biblio
429 sub article_requests {
432 return Koha::ArticleRequests->_new_from_dbic( scalar $self->_result->article_requests );
435 =head3 current_checkouts
437 my $current_checkouts = $biblio->current_checkouts
439 Returns the current checkouts associated with this biblio
443 sub current_checkouts {
446 return Koha::Checkouts->search( { "item.biblionumber" => $self->id },
447 { join => 'item' } );
452 my $old_checkouts = $biblio->old_checkouts
454 Returns the past checkouts associated with this biblio
461 return Koha::Old::Checkouts->search( { "item.biblionumber" => $self->id },
462 { join => 'item' } );
467 my $items = $biblio->items({ [ host_items => 1 ] });
469 The optional param host_items allows you to include 'analytical' items.
471 Returns the related Koha::Items object for this biblio
476 my ($self,$params) = @_;
478 my $items_rs = $self->_result->items;
480 return Koha::Items->_new_from_dbic( $items_rs ) unless $params->{host_items};
482 my $host_itemnumbers = $self->_host_itemnumbers();
483 my $search_params = { -or => [biblionumber => $self->id] };
484 push @{$search_params->{'-or'}}, itemnumber => { -in => $host_itemnumbers } if $host_itemnumbers;
486 return Koha::Items->search($search_params);
491 my $host_items = $biblio->host_items();
493 Return the host items (easy analytical record)
500 return Koha::Items->new->empty
501 unless C4::Context->preference('EasyAnalyticalRecords');
503 my $host_itemnumbers = $self->_host_itemnumbers;
505 return Koha::Items->search( { itemnumber => { -in => $host_itemnumbers } } );
508 =head3 _host_itemnumbers
510 my $host_itemnumber = $biblio->_host_itemnumbers();
512 Return the itemnumbers for analytical items on this record
516 sub _host_itemnumbers {
519 my $marcflavour = C4::Context->preference("marcflavour");
520 my $analyticfield = '773';
521 if ( $marcflavour eq 'UNIMARC' ) {
522 $analyticfield = '461';
524 my $marc_record = $self->metadata->record;
526 foreach my $field ( $marc_record->field($analyticfield) ) {
527 push @itemnumbers, $field->subfield('9');
529 return \@itemnumbers;
535 my $itemtype = $biblio->itemtype();
537 Returns the itemtype for this record.
544 return $self->biblioitem()->itemtype();
549 my $holds = $biblio->holds();
551 return the current holds placed on this record
556 my ( $self, $params, $attributes ) = @_;
557 $attributes->{order_by} = 'priority' unless exists $attributes->{order_by};
558 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
559 return Koha::Holds->_new_from_dbic($hold_rs);
564 my $holds = $biblio->current_holds
566 Return the holds placed on this bibliographic record.
567 It does not include future holds.
573 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
575 { reservedate => { '<=' => $dtf->format_date(dt_from_string) } } );
580 my $field = $self->biblioitem
582 Returns the related Koha::Biblioitem object for this Biblio object
588 return Koha::Biblioitems->find( { biblionumber => $self->biblionumber } );
593 my $suggestions = $self->suggestions
595 Returns the related Koha::Suggestions object for this Biblio object
602 my $suggestions_rs = $self->_result->suggestions;
603 return Koha::Suggestions->_new_from_dbic( $suggestions_rs );
606 =head3 get_marc_components
608 my $components = $self->get_marc_components();
610 Returns an array of search results data, which are component parts of
611 this object (MARC21 773 points to this)
615 sub get_marc_components {
616 my ($self, $max_results) = @_;
618 return [] if (C4::Context->preference('marcflavour') ne 'MARC21');
620 my ( $searchstr, $sort ) = $self->get_components_query;
623 if (defined($searchstr)) {
624 my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
625 my ( $error, $results, $facets );
627 ( $error, $results, $facets ) = $searcher->search_compat( $searchstr, undef, [$sort], ['biblioserver'], $max_results, 0, undef, undef, 'ccl', 0 );
632 warn "Warning from search_compat: '$error'";
636 message => 'component_search',
641 $components = $results->{biblioserver}->{RECORDS} if defined($results) && $results->{biblioserver}->{hits};
644 return $components // [];
647 =head2 get_components_query
649 Returns a query which can be used to search for all component parts of MARC21 biblios
653 sub get_components_query {
656 my $builder = Koha::SearchEngine::QueryBuilder->new(
657 { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
658 my $marc = $self->metadata->record;
659 my $component_sort_field = C4::Context->preference('ComponentSortField') // "title";
660 my $component_sort_order = C4::Context->preference('ComponentSortOrder') // "asc";
661 my $sort = $component_sort_field . "_" . $component_sort_order;
664 if ( C4::Context->preference('UseControlNumber') ) {
665 my $pf001 = $marc->field('001') || undef;
667 if ( defined($pf001) ) {
669 my $pf003 = $marc->field('003') || undef;
671 if ( !defined($pf003) ) {
672 # search for 773$w='Host001'
673 $searchstr .= "rcn:\"" . $pf001->data()."\"";
677 # search for (773$w='Host001' and 003='Host003') or 773$w='(Host003)Host001'
678 $searchstr .= "(rcn:\"" . $pf001->data() . "\" AND cni:\"" . $pf003->data() . "\")";
679 $searchstr .= " OR rcn:\"" . $pf003->data() . " " . $pf001->data() . "\"";
683 # limit to monograph and serial component part records
684 $searchstr .= " AND (bib-level:a OR bib-level:b)";
689 my $cleaned_title = $marc->subfield('245', "a");
690 $cleaned_title =~ tr|/||;
691 $cleaned_title = $builder->clean_search_term($cleaned_title);
692 $searchstr = qq#Host-item:("$cleaned_title")#;
694 my ($error, $query ,$query_str) = $builder->build_query_compat( undef, [$searchstr], undef, undef, [$sort], 0 );
700 return ($query, $query_str, $sort);
705 my $subscriptions = $self->subscriptions
707 Returns the related Koha::Subscriptions object for this Biblio object
713 my $rs = $self->_result->subscriptions;
714 return Koha::Subscriptions->_new_from_dbic($rs);
717 =head3 has_items_waiting_or_intransit
719 my $itemsWaitingOrInTransit = $biblio->has_items_waiting_or_intransit
721 Tells if this bibliographic record has items waiting or in transit.
725 sub has_items_waiting_or_intransit {
728 if ( Koha::Holds->search({ biblionumber => $self->id,
729 found => ['W', 'T'] })->count ) {
733 foreach my $item ( $self->items->as_list ) {
734 return 1 if $item->get_transfer;
742 my $coins = $biblio->get_coins;
744 Returns the COinS (a span) which can be included in a biblio record
751 my $record = $self->metadata->record;
753 my $pos7 = substr $record->leader(), 7, 1;
754 my $pos6 = substr $record->leader(), 6, 1;
757 my ( $aulast, $aufirst ) = ( '', '' );
768 # For the purposes of generating COinS metadata, LDR/06-07 can be
769 # considered the same for UNIMARC and MARC21
778 'i' => 'audioRecording',
779 'j' => 'audioRecording',
782 'm' => 'computerProgram',
787 'a' => 'journalArticle',
791 $genre = $fmts6->{$pos6} ? $fmts6->{$pos6} : 'book';
793 if ( $genre eq 'book' ) {
794 $genre = $fmts7->{$pos7} if $fmts7->{$pos7};
797 ##### We must transform mtx to a valable mtx and document type ####
798 if ( $genre eq 'book' ) {
801 } elsif ( $genre eq 'journal' ) {
804 } elsif ( $genre eq 'journalArticle' ) {
812 if ( C4::Context->preference("marcflavour") eq "UNIMARC" ) {
815 $aulast = $record->subfield( '700', 'a' ) || '';
816 $aufirst = $record->subfield( '700', 'b' ) || '';
817 push @authors, "$aufirst $aulast" if ($aufirst or $aulast);
820 if ( $record->field('200') ) {
821 for my $au ( $record->field('200')->subfield('g') ) {
826 $title = $record->subfield( '200', 'a' );
827 my $subfield_210d = $record->subfield('210', 'd');
828 if ($subfield_210d and $subfield_210d =~ /(\d{4})/) {
831 $publisher = $record->subfield( '210', 'c' ) || '';
832 $isbn = $record->subfield( '010', 'a' ) || '';
833 $issn = $record->subfield( '011', 'a' ) || '';
836 # MARC21 need some improve
839 if ( $record->field('100') ) {
840 push @authors, $record->subfield( '100', 'a' );
844 if ( $record->field('700') ) {
845 for my $au ( $record->field('700')->subfield('a') ) {
849 $title = $record->field('245');
850 $title &&= $title->as_string('ab');
851 if ($titletype eq 'a') {
852 $pubyear = $record->field('008') || '';
853 $pubyear = substr($pubyear->data(), 7, 4) if $pubyear;
854 $isbn = $record->subfield( '773', 'z' ) || '';
855 $issn = $record->subfield( '773', 'x' ) || '';
856 $hosttitle = $record->subfield( '773', 't' ) || $record->subfield( '773', 'a') || q{};
857 my @rels = $record->subfield( '773', 'g' );
858 $pages = join(', ', @rels);
860 $pubyear = $record->subfield( '260', 'c' ) || '';
861 $publisher = $record->subfield( '260', 'b' ) || '';
862 $isbn = $record->subfield( '020', 'a' ) || '';
863 $issn = $record->subfield( '022', 'a' ) || '';
869 [ 'ctx_ver', 'Z39.88-2004' ],
870 [ 'rft_val_fmt', "info:ofi/fmt:kev:mtx:$mtx" ],
871 [ ($mtx eq 'dc' ? 'rft.type' : 'rft.genre'), $genre ],
872 [ "rft.${titletype}title", $title ],
875 # rft.title is authorized only once, so by checking $titletype
876 # we ensure that rft.title is not already in the list.
877 if ($hosttitle and $titletype) {
878 push @params, [ 'rft.title', $hosttitle ];
882 [ 'rft.isbn', $isbn ],
883 [ 'rft.issn', $issn ],
886 # If it's a subscription, these informations have no meaning.
887 if ($genre ne 'journal') {
889 [ 'rft.aulast', $aulast ],
890 [ 'rft.aufirst', $aufirst ],
891 (map { [ 'rft.au', $_ ] } @authors),
892 [ 'rft.pub', $publisher ],
893 [ 'rft.date', $pubyear ],
894 [ 'rft.pages', $pages ],
898 my $coins_value = join( '&',
899 map { $$_[1] ? $$_[0] . '=' . uri_escape_utf8( $$_[1] ) : () } @params );
906 my $url = $biblio->get_openurl;
908 Returns url for OpenURL resolver set in OpenURLResolverURL system preference
915 my $OpenURLResolverURL = C4::Context->preference('OpenURLResolverURL');
917 if ($OpenURLResolverURL) {
918 my $uri = URI->new($OpenURLResolverURL);
920 if (not defined $uri->query) {
921 $OpenURLResolverURL .= '?';
923 $OpenURLResolverURL .= '&';
925 $OpenURLResolverURL .= $self->get_coins;
928 return $OpenURLResolverURL;
933 my $serial = $biblio->is_serial
935 Return boolean true if this bibbliographic record is continuing resource
942 return 1 if $self->serial;
944 my $record = $self->metadata->record;
945 return 1 if substr($record->leader, 7, 1) eq 's';
950 =head3 custom_cover_image_url
952 my $image_url = $biblio->custom_cover_image_url
954 Return the specific url of the cover image for this bibliographic record.
955 It is built regaring the value of the system preference CustomCoverImagesURL
959 sub custom_cover_image_url {
961 my $url = C4::Context->preference('CustomCoverImagesURL');
962 if ( $url =~ m|{isbn}| ) {
963 my $isbn = $self->biblioitem->isbn;
965 $url =~ s|{isbn}|$isbn|g;
967 if ( $url =~ m|{normalized_isbn}| ) {
968 my $normalized_isbn = C4::Koha::GetNormalizedISBN($self->biblioitem->isbn);
969 return unless $normalized_isbn;
970 $url =~ s|{normalized_isbn}|$normalized_isbn|g;
972 if ( $url =~ m|{issn}| ) {
973 my $issn = $self->biblioitem->issn;
975 $url =~ s|{issn}|$issn|g;
978 my $re = qr|{(?<field>\d{3})(\$(?<subfield>.))?}|;
980 my $field = $+{field};
981 my $subfield = $+{subfield};
982 my $marc_record = $self->metadata->record;
985 $value = $marc_record->subfield( $field, $subfield );
987 my $controlfield = $marc_record->field($field);
988 $value = $controlfield->data() if $controlfield;
990 return unless $value;
991 $url =~ s|$re|$value|;
999 Return the cover images associated with this biblio.
1006 my $cover_images_rs = $self->_result->cover_images;
1007 return unless $cover_images_rs;
1008 return Koha::CoverImages->_new_from_dbic($cover_images_rs);
1011 =head3 get_marc_notes
1013 $marcnotesarray = $biblio->get_marc_notes({ opac => 1 });
1015 Get all notes from the MARC record and returns them in an array.
1016 The notes are stored in different fields depending on MARC flavour.
1017 MARC21 5XX $u subfields receive special attention as they are URIs.
1021 sub get_marc_notes {
1022 my ( $self, $params ) = @_;
1024 my $marcflavour = C4::Context->preference('marcflavour');
1025 my $opac = $params->{opac} // '0';
1026 my $interface = $params->{opac} ? 'opac' : 'intranet';
1028 my $record = $params->{record} // $self->metadata->record;
1029 my $record_processor = Koha::RecordProcessor->new(
1031 filters => [ 'ViewPolicy', 'ExpandCodedFields' ],
1033 interface => $interface,
1034 frameworkcode => $self->frameworkcode
1038 $record_processor->process($record);
1040 my $scope = $marcflavour eq "UNIMARC"? '3..': '5..';
1041 #MARC21 specs indicate some notes should be private if first indicator 0
1042 my %maybe_private = (
1050 my %hiddenlist = map { $_ => 1 }
1051 split( /,/, C4::Context->preference('NotesToHide'));
1054 foreach my $field ( $record->field($scope) ) {
1055 my $tag = $field->tag();
1056 next if $hiddenlist{ $tag };
1057 next if $opac && $maybe_private{$tag} && !$field->indicator(1);
1058 if( $marcflavour ne 'UNIMARC' && $field->subfield('u') ) {
1059 # Field 5XX$u always contains URI
1060 # Examples: 505u, 506u, 510u, 514u, 520u, 530u, 538u, 540u, 542u, 552u, 555u, 561u, 563u, 583u
1061 # We first push the other subfields, then all $u's separately
1062 # Leave further actions to the template (see e.g. opac-detail)
1064 join '', ( 'a' .. 't', 'v' .. 'z', '0' .. '9' ); # excl 'u'
1065 push @marcnotes, { marcnote => $field->as_string($othersub) };
1066 foreach my $sub ( $field->subfield('u') ) {
1067 $sub =~ s/^\s+|\s+$//g; # trim
1068 push @marcnotes, { marcnote => $sub };
1071 push @marcnotes, { marcnote => $field->as_string() };
1077 =head3 _get_marc_authors
1079 Private method to return the list of authors contained in the MARC record.
1080 See get get_marc_contributors and get_marc_authors for the public methods.
1084 sub _get_marc_authors {
1085 my ( $self, $params ) = @_;
1087 my $fields_filter = $params->{fields_filter};
1088 my $mintag = $params->{mintag};
1089 my $maxtag = $params->{maxtag};
1091 my $AuthoritySeparator = C4::Context->preference('AuthoritySeparator');
1092 my $marcflavour = C4::Context->preference('marcflavour');
1094 # tagslib useful only for UNIMARC author responsibilities
1095 my $tagslib = $marcflavour eq "UNIMARC"
1096 ? C4::Biblio::GetMarcStructure( 1, $self->frameworkcode, { unsafe => 1 } )
1100 foreach my $field ( $self->metadata->record->field($fields_filter) ) {
1103 if $mintag && $field->tag() < $mintag
1104 || $maxtag && $field->tag() > $maxtag;
1108 my @subfields = $field->subfields();
1111 # if there is an authority link, build the link with Koha-Auth-Number: subfield9
1112 my $subfield9 = $field->subfield('9');
1114 my $linkvalue = $subfield9;
1115 $linkvalue =~ s/(\(|\))//g;
1116 @link_loop = ( { 'limit' => 'an', 'link' => $linkvalue } );
1121 for my $authors_subfield (@subfields) {
1122 next if ( $authors_subfield->[0] eq '9' );
1124 # unimarc3 contains the $3 of the author for UNIMARC.
1125 # For french academic libraries, it's the "ppn", and it's required for idref webservice
1126 $unimarc3 = $authors_subfield->[1] if $marcflavour eq 'UNIMARC' and $authors_subfield->[0] =~ /3/;
1128 # don't load unimarc subfields 3, 5
1129 next if ( $marcflavour eq 'UNIMARC' and ( $authors_subfield->[0] =~ /3|5/ ) );
1131 my $code = $authors_subfield->[0];
1132 my $value = $authors_subfield->[1];
1133 my $linkvalue = $value;
1134 $linkvalue =~ s/(\(|\))//g;
1135 # UNIMARC author responsibility
1136 if ( $marcflavour eq 'UNIMARC' and $code eq '4' ) {
1137 $value = C4::Biblio::GetAuthorisedValueDesc( $field->tag(), $code, $value, '', $tagslib );
1138 $linkvalue = "($value)";
1140 # if no authority link, build a search query
1141 unless ($subfield9) {
1144 'link' => $linkvalue,
1145 operator => (scalar @link_loop) ? ' AND ' : undef
1148 my @this_link_loop = @link_loop;
1150 unless ( $code eq '0') {
1151 push @subfields_loop, {
1152 tag => $field->tag(),
1155 link_loop => \@this_link_loop,
1156 separator => (scalar @subfields_loop) ? $AuthoritySeparator : ''
1160 push @marcauthors, {
1161 MARCAUTHOR_SUBFIELDS_LOOP => \@subfields_loop,
1162 authoritylink => $subfield9,
1163 unimarc3 => $unimarc3
1166 return \@marcauthors;
1169 =head3 get_marc_contributors
1171 my $contributors = $biblio->get_marc_contributors;
1173 Get all contributors (but first author) from the MARC record and returns them in an array.
1174 They are stored in different fields depending on MARC flavour (700..720 for MARC21)
1178 sub get_marc_contributors {
1179 my ( $self, $params ) = @_;
1181 my ( $mintag, $maxtag, $fields_filter );
1182 my $marcflavour = C4::Context->preference('marcflavour');
1184 if ( $marcflavour eq "UNIMARC" ) {
1187 $fields_filter = '7..';
1188 } else { # marc21/normarc
1191 $fields_filter = '7..';
1194 return $self->_get_marc_authors(
1196 fields_filter => $fields_filter,
1203 =head3 get_marc_authors
1205 my $authors = $biblio->get_marc_authors;
1207 Get all authors from the MARC record and returns them in an array.
1208 They are stored in different fields depending on MARC flavour
1209 (main author from 100 then secondary authors from 700..720).
1213 sub get_marc_authors {
1214 my ( $self, $params ) = @_;
1216 my ( $mintag, $maxtag, $fields_filter );
1217 my $marcflavour = C4::Context->preference('marcflavour');
1219 if ( $marcflavour eq "UNIMARC" ) {
1220 $fields_filter = '200';
1221 } else { # marc21/normarc
1222 $fields_filter = '100';
1225 my @first_authors = @{$self->_get_marc_authors(
1227 fields_filter => $fields_filter,
1233 my @other_authors = @{$self->get_marc_contributors};
1235 return [@first_authors, @other_authors];
1241 my $json = $biblio->to_api;
1243 Overloaded method that returns a JSON representation of the Koha::Biblio object,
1244 suitable for API output. The related Koha::Biblioitem object is merged as expected
1250 my ($self, $args) = @_;
1252 my $response = $self->SUPER::to_api( $args );
1253 my $biblioitem = $self->biblioitem->to_api;
1255 return { %$response, %$biblioitem };
1258 =head3 to_api_mapping
1260 This method returns the mapping for representing a Koha::Biblio object
1265 sub to_api_mapping {
1267 biblionumber => 'biblio_id',
1268 frameworkcode => 'framework_id',
1269 unititle => 'uniform_title',
1270 seriestitle => 'series_title',
1271 copyrightdate => 'copyright_date',
1272 datecreated => 'creation_date',
1273 deleted_on => undef,
1277 =head3 get_marc_host
1279 $host = $biblio->get_marc_host;
1281 ( $host, $relatedparts, $hostinfo ) = $biblio->get_marc_host;
1283 Returns host biblio record from MARC21 773 (undef if no 773 present).
1284 It looks at the first 773 field with MARCorgCode or only a control
1285 number. Complete $w or numeric part is used to search host record.
1286 The optional parameter no_items triggers a check if $biblio has items.
1287 If there are, the sub returns undef.
1288 Called in list context, it also returns 773$g (related parts).
1290 If there is no $w, we use $0 (host biblionumber) or $9 (host itemnumber)
1291 to search for the host record. If there is also no $0 and no $9, we search
1292 using author and title. Failing all of that, we return an undef host and
1293 form a concatenation of strings with 773$agt for host information,
1294 returned when called in list context.
1299 my ($self, $params) = @_;
1300 my $no_items = $params->{no_items};
1301 return if C4::Context->preference('marcflavour') eq 'UNIMARC'; # TODO
1302 return if $params->{no_items} && $self->items->count > 0;
1305 eval { $record = $self->metadata->record };
1308 # We pick the first $w with your MARCOrgCode or the first $w that has no
1309 # code (between parentheses) at all.
1310 my $orgcode = C4::Context->preference('MARCOrgCode') // q{};
1312 foreach my $f ( $record->field('773') ) {
1313 my $w = $f->subfield('w') or next;
1314 if( $w =~ /^\($orgcode\)\s*(\d+)/i or $w =~ /^\d+/ ) {
1320 my $engine = Koha::SearchEngine::Search->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1322 if ( !$hostfld and $record->subfield('773','t') ) {
1323 # not linked using $w
1324 my $unlinkedf = $record->field('773');
1326 if ( C4::Context->preference("EasyAnalyticalRecords") ) {
1327 if ( $unlinkedf->subfield('0') ) {
1328 # use 773$0 host biblionumber
1329 $bibno = $unlinkedf->subfield('0');
1330 } elsif ( $unlinkedf->subfield('9') ) {
1331 # use 773$9 host itemnumber
1332 my $linkeditemnumber = $unlinkedf->subfield('9');
1333 $bibno = Koha::Items->find( $linkeditemnumber )->biblionumber;
1337 my $host = Koha::Biblios->find($bibno) or return;
1338 return wantarray ? ( $host, $unlinkedf->subfield('g') ) : $host;
1340 # just return plaintext and no host record
1341 my $hostinfo = join( ", ", $unlinkedf->subfield('a'), $unlinkedf->subfield('t'), $unlinkedf->subfield('g') );
1342 return wantarray ? ( undef, $unlinkedf->subfield('g'), $hostinfo ) : undef;
1344 return if !$hostfld;
1345 my $rcn = $hostfld->subfield('w');
1347 # Look for control number with/without orgcode
1348 for my $try (1..2) {
1349 my ( $error, $results, $total_hits ) = $engine->simple_search_compat( 'Control-number='.$rcn, 0,1 );
1350 if( !$error and $total_hits == 1 ) {
1351 $bibno = $engine->extract_biblionumber( $results->[0] );
1354 # Add or remove orgcode for second try
1355 if( $try == 1 && $rcn =~ /\)\s*(\d+)/ ) {
1356 $rcn = $1; # number only
1357 } elsif( $try == 1 && $rcn =~ /^\d+/ ) {
1358 $rcn = "($orgcode)$rcn";
1364 my $host = Koha::Biblios->find($bibno) or return;
1365 return wantarray ? ( $host, $hostfld->subfield('g') ) : $host;
1371 my $recalls = $biblio->recalls;
1373 Return recalls linked to this biblio
1379 return Koha::Recalls->_new_from_dbic( scalar $self->_result->recalls );
1382 =head3 can_be_recalled
1384 my @items_for_recall = $biblio->can_be_recalled({ patron => $patron_object });
1386 Does biblio-level checks and returns the items attached to this biblio that are available for recall
1390 sub can_be_recalled {
1391 my ( $self, $params ) = @_;
1393 return 0 if !( C4::Context->preference('UseRecalls') );
1395 my $patron = $params->{patron};
1397 my $branchcode = C4::Context->userenv->{'branch'};
1398 if ( C4::Context->preference('CircControl') eq 'PatronLibrary' and $patron ) {
1399 $branchcode = $patron->branchcode;
1402 my @all_items = Koha::Items->search({ biblionumber => $self->biblionumber })->as_list;
1404 # if there are no available items at all, no recall can be placed
1405 return 0 if ( scalar @all_items == 0 );
1410 my @all_itemnumbers;
1411 foreach my $item ( @all_items ) {
1412 push( @all_itemnumbers, $item->itemnumber );
1413 if ( $item->can_be_recalled({ patron => $patron }) ) {
1414 push( @itemtypes, $item->effective_itemtype );
1415 push( @itemnumbers, $item->itemnumber );
1416 push( @items, $item );
1420 # if there are no recallable items, no recall can be placed
1421 return 0 if ( scalar @items == 0 );
1423 # Check the circulation rule for each relevant itemtype for this biblio
1424 my ( @recalls_allowed, @recalls_per_record, @on_shelf_recalls );
1425 foreach my $itemtype ( @itemtypes ) {
1426 my $rule = Koha::CirculationRules->get_effective_rules({
1427 branchcode => $branchcode,
1428 categorycode => $patron ? $patron->categorycode : undef,
1429 itemtype => $itemtype,
1432 'recalls_per_record',
1436 push( @recalls_allowed, $rule->{recalls_allowed} ) if $rule;
1437 push( @recalls_per_record, $rule->{recalls_per_record} ) if $rule;
1438 push( @on_shelf_recalls, $rule->{on_shelf_recalls} ) if $rule;
1440 my $recalls_allowed = (sort {$b <=> $a} @recalls_allowed)[0]; # take highest
1441 my $recalls_per_record = (sort {$b <=> $a} @recalls_per_record)[0]; # take highest
1442 my %on_shelf_recalls_count = ();
1443 foreach my $count ( @on_shelf_recalls ) {
1444 $on_shelf_recalls_count{$count}++;
1446 my $on_shelf_recalls = (sort {$on_shelf_recalls_count{$b} <=> $on_shelf_recalls_count{$a}} @on_shelf_recalls)[0]; # take most common
1448 # check recalls allowed has been set and is not zero
1449 return 0 if ( !defined($recalls_allowed) || $recalls_allowed == 0 );
1452 # check borrower has not reached open recalls allowed limit
1453 return 0 if ( $patron->recalls->filter_by_current->count >= $recalls_allowed );
1455 # check borrower has not reached open recalls allowed per record limit
1456 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $recalls_per_record );
1458 # check if any of the items under this biblio are already checked out by this borrower
1459 return 0 if ( Koha::Checkouts->search({ itemnumber => [ @all_itemnumbers ], borrowernumber => $patron->borrowernumber })->count > 0 );
1462 # check item availability
1463 my $checked_out_count = 0;
1465 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1468 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1469 return 0 if ( $on_shelf_recalls eq 'all' && $checked_out_count < scalar @items );
1471 # can't recall if no items have been checked out
1472 return 0 if ( $checked_out_count == 0 );
1478 =head2 Internal methods
1490 Kyle M Hall <kyle@bywatersolutions.com>