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;
41 use Koha::Item::Transfer::Limits;
44 use Koha::Old::Checkouts;
46 use Koha::RecordProcessor;
47 use Koha::Suggestions;
48 use Koha::Subscriptions;
49 use Koha::SearchEngine;
50 use Koha::SearchEngine::Search;
51 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 $item_groups = $biblio->item_groups();
141 Returns a Koha::Biblio::ItemGroups object
148 my $item_groups = $self->_result->item_groups;
149 return Koha::Biblio::ItemGroups->_new_from_dbic($item_groups);
152 =head3 can_article_request
154 my $bool = $biblio->can_article_request( $borrower );
156 Returns true if article requests can be made for this record
158 $borrower must be a Koha::Patron object
162 sub can_article_request {
163 my ( $self, $borrower ) = @_;
165 my $rule = $self->article_request_type($borrower);
166 return q{} if $rule eq 'item_only' && !$self->items()->count();
167 return 1 if $rule && $rule ne 'no';
172 =head3 can_be_transferred
174 $biblio->can_be_transferred({ to => $to_library, from => $from_library })
176 Checks if at least one item of a biblio can be transferred to given library.
178 This feature is controlled by two system preferences:
179 UseBranchTransferLimits to enable / disable the feature
180 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
181 for setting the limitations
183 Performance-wise, it is recommended to use this method for a biblio instead of
184 iterating each item of a biblio with Koha::Item->can_be_transferred().
186 Takes HASHref that can have the following parameters:
187 MANDATORY PARAMETERS:
190 $from : Koha::Library # if given, only items from that
191 # holdingbranch are considered
193 Returns 1 if at least one of the item of a biblio can be transferred
194 to $to_library, otherwise 0.
198 sub can_be_transferred {
199 my ($self, $params) = @_;
201 my $to = $params->{to};
202 my $from = $params->{from};
204 return 1 unless C4::Context->preference('UseBranchTransferLimits');
205 my $limittype = C4::Context->preference('BranchTransferLimitsType');
208 foreach my $item_of_bib ($self->items->as_list) {
209 next unless $item_of_bib->holdingbranch;
210 next if $from && $from->branchcode ne $item_of_bib->holdingbranch;
211 return 1 if $item_of_bib->holdingbranch eq $to->branchcode;
212 my $code = $limittype eq 'itemtype'
213 ? $item_of_bib->effective_itemtype
214 : $item_of_bib->ccode;
215 return 1 unless $code;
216 $items->{$code}->{$item_of_bib->holdingbranch} = 1;
219 # At this point we will have a HASHref containing each itemtype/ccode that
220 # this biblio has, inside which are all of the holdingbranches where those
221 # items are located at. Then, we will query Koha::Item::Transfer::Limits to
222 # find out whether a transfer limits for such $limittype from any of the
223 # listed holdingbranches to the given $to library exist. If at least one
224 # holdingbranch for that $limittype does not have a transfer limit to given
225 # $to library, then we know that the transfer is possible.
226 foreach my $code (keys %{$items}) {
227 my @holdingbranches = keys %{$items->{$code}};
228 return 1 if Koha::Item::Transfer::Limits->search({
229 toBranch => $to->branchcode,
230 fromBranch => { 'in' => \@holdingbranches },
233 group_by => [qw/fromBranch/]
234 })->count == scalar(@holdingbranches) ? 0 : 1;
241 =head3 pickup_locations
243 my $pickup_locations = $biblio->pickup_locations( {patron => $patron } );
245 Returns a Koha::Libraries set of possible pickup locations for this biblio's items,
246 according to patron's home library (if patron is defined and holds are allowed
247 only from hold groups) and if item can be transferred to each pickup location.
251 sub pickup_locations {
252 my ( $self, $params ) = @_;
254 my $patron = $params->{patron};
256 my $memory_cache = Koha::Cache::Memory::Lite->get_instance();
257 my @pickup_locations;
258 foreach my $item ( $self->items->as_list ) {
259 my $cache_key = sprintf "Pickup_locations:%s:%s:%s:%s:%s",
260 $item->itype,$item->homebranch,$item->holdingbranch,$item->ccode || "",$patron->branchcode||"" ;
261 my $item_pickup_locations = $memory_cache->get_from_cache( $cache_key );
262 unless( $item_pickup_locations ){
263 @{ $item_pickup_locations } = $item->pickup_locations( { patron => $patron } )->_resultset->get_column('branchcode')->all;
264 $memory_cache->set_in_cache( $cache_key, $item_pickup_locations );
266 push @pickup_locations, @{ $item_pickup_locations }
269 return Koha::Libraries->search(
270 { branchcode => { '-in' => \@pickup_locations } }, { order_by => ['branchname'] } );
273 =head3 hidden_in_opac
275 my $bool = $biblio->hidden_in_opac({ [ rules => $rules ] })
277 Returns true if the biblio matches the hidding criteria defined in $rules.
278 Returns false otherwise. It involves the I<OpacHiddenItems> and
279 I<OpacHiddenItemsHidesRecord> system preferences.
281 Takes HASHref that can have the following parameters:
283 $rules : { <field> => [ value_1, ... ], ... }
285 Note: $rules inherits its structure from the parsed YAML from reading
286 the I<OpacHiddenItems> system preference.
291 my ( $self, $params ) = @_;
293 my $rules = $params->{rules} // {};
295 my @items = $self->items->as_list;
297 return 0 unless @items; # Do not hide if there is no item
299 # Ok, there are items, don't even try the rules unless OpacHiddenItemsHidesRecord
300 return 0 unless C4::Context->preference('OpacHiddenItemsHidesRecord');
302 return !(any { !$_->hidden_in_opac({ rules => $rules }) } @items);
305 =head3 article_request_type
307 my $type = $biblio->article_request_type( $borrower );
309 Returns the article request type based on items, or on the record
310 itself if there are no items.
312 $borrower must be a Koha::Patron object
316 sub article_request_type {
317 my ( $self, $borrower ) = @_;
319 return q{} unless $borrower;
321 my $rule = $self->article_request_type_for_items( $borrower );
322 return $rule if $rule;
324 # If the record has no items that are requestable, go by the record itemtype
325 $rule = $self->article_request_type_for_bib($borrower);
326 return $rule if $rule;
331 =head3 article_request_type_for_bib
333 my $type = $biblio->article_request_type_for_bib
335 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record
339 sub article_request_type_for_bib {
340 my ( $self, $borrower ) = @_;
342 return q{} unless $borrower;
344 my $borrowertype = $borrower->categorycode;
345 my $itemtype = $self->itemtype();
347 my $rule = Koha::CirculationRules->get_effective_rule(
349 rule_name => 'article_requests',
350 categorycode => $borrowertype,
351 itemtype => $itemtype,
355 return q{} unless $rule;
356 return $rule->rule_value || q{}
359 =head3 article_request_type_for_items
361 my $type = $biblio->article_request_type_for_items
363 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record's items
365 If there is a conflict where some items are 'bib_only' and some are 'item_only', 'bib_only' will be returned.
369 sub article_request_type_for_items {
370 my ( $self, $borrower ) = @_;
373 foreach my $item ( $self->items()->as_list() ) {
374 my $rule = $item->article_request_type($borrower);
375 return $rule if $rule eq 'bib_only'; # we don't need to go any further
379 return 'item_only' if $counts->{item_only};
380 return 'yes' if $counts->{yes};
381 return 'no' if $counts->{no};
385 =head3 article_requests
387 my $article_requests = $biblio->article_requests
389 Returns the article requests associated with this biblio
393 sub article_requests {
396 return Koha::ArticleRequests->_new_from_dbic( scalar $self->_result->article_requests );
399 =head3 current_checkouts
401 my $current_checkouts = $biblio->current_checkouts
403 Returns the current checkouts associated with this biblio
407 sub current_checkouts {
410 return Koha::Checkouts->search( { "item.biblionumber" => $self->id },
411 { join => 'item' } );
416 my $old_checkouts = $biblio->old_checkouts
418 Returns the past checkouts associated with this biblio
425 return Koha::Old::Checkouts->search( { "item.biblionumber" => $self->id },
426 { join => 'item' } );
431 my $items = $biblio->items();
433 Returns the related Koha::Items object for this biblio
440 my $items_rs = $self->_result->items;
442 return Koha::Items->_new_from_dbic( $items_rs );
447 my $host_items = $biblio->host_items();
449 Return the host items (easy analytical record)
456 return Koha::Items->new->empty
457 unless C4::Context->preference('EasyAnalyticalRecords');
459 my $marcflavour = C4::Context->preference("marcflavour");
460 my $analyticfield = '773';
461 if ( $marcflavour eq 'MARC21' ) {
462 $analyticfield = '773';
464 elsif ( $marcflavour eq 'UNIMARC' ) {
465 $analyticfield = '461';
467 my $marc_record = $self->metadata->record;
469 foreach my $field ( $marc_record->field($analyticfield) ) {
470 push @itemnumbers, $field->subfield('9');
473 return Koha::Items->search( { itemnumber => { -in => \@itemnumbers } } );
478 my $itemtype = $biblio->itemtype();
480 Returns the itemtype for this record.
487 return $self->biblioitem()->itemtype();
492 my $holds = $biblio->holds();
494 return the current holds placed on this record
499 my ( $self, $params, $attributes ) = @_;
500 $attributes->{order_by} = 'priority' unless exists $attributes->{order_by};
501 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
502 return Koha::Holds->_new_from_dbic($hold_rs);
507 my $holds = $biblio->current_holds
509 Return the holds placed on this bibliographic record.
510 It does not include future holds.
516 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
518 { reservedate => { '<=' => $dtf->format_date(dt_from_string) } } );
523 my $field = $self->biblioitem()->itemtype
525 Returns the related Koha::Biblioitem object for this Biblio object
532 $self->{_biblioitem} ||= Koha::Biblioitems->find( { biblionumber => $self->biblionumber() } );
534 return $self->{_biblioitem};
539 my $suggestions = $self->suggestions
541 Returns the related Koha::Suggestions object for this Biblio object
548 my $suggestions_rs = $self->_result->suggestions;
549 return Koha::Suggestions->_new_from_dbic( $suggestions_rs );
552 =head3 get_marc_components
554 my $components = $self->get_marc_components();
556 Returns an array of search results data, which are component parts of
557 this object (MARC21 773 points to this)
561 sub get_marc_components {
562 my ($self, $max_results) = @_;
564 return [] if (C4::Context->preference('marcflavour') ne 'MARC21');
566 my ( $searchstr, $sort ) = $self->get_components_query;
569 if (defined($searchstr)) {
570 my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
571 my ( $error, $results, $facets );
573 ( $error, $results, $facets ) = $searcher->search_compat( $searchstr, undef, [$sort], ['biblioserver'], $max_results, 0, undef, undef, 'ccl', 0 );
578 warn "Warning from search_compat: '$error'";
582 message => 'component_search',
587 $components = $results->{biblioserver}->{RECORDS} if defined($results) && $results->{biblioserver}->{hits};
590 return $components // [];
593 =head2 get_components_query
595 Returns a query which can be used to search for all component parts of MARC21 biblios
599 sub get_components_query {
602 my $builder = Koha::SearchEngine::QueryBuilder->new(
603 { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
604 my $marc = $self->metadata->record;
605 my $component_sort_field = C4::Context->preference('ComponentSortField') // "title";
606 my $component_sort_order = C4::Context->preference('ComponentSortOrder') // "asc";
607 my $sort = $component_sort_field . "_" . $component_sort_order;
610 if ( C4::Context->preference('UseControlNumber') ) {
611 my $pf001 = $marc->field('001') || undef;
613 if ( defined($pf001) ) {
615 my $pf003 = $marc->field('003') || undef;
617 if ( !defined($pf003) ) {
618 # search for 773$w='Host001'
619 $searchstr .= "rcn:\"" . $pf001->data()."\"";
623 # search for (773$w='Host001' and 003='Host003') or 773$w='(Host003)Host001'
624 $searchstr .= "(rcn:\"" . $pf001->data() . "\" AND cni:\"" . $pf003->data() . "\")";
625 $searchstr .= " OR rcn:\"" . $pf003->data() . " " . $pf001->data() . "\"";
629 # limit to monograph and serial component part records
630 $searchstr .= " AND (bib-level:a OR bib-level:b)";
635 my $cleaned_title = $marc->subfield('245', "a");
636 $cleaned_title =~ tr|/||;
637 $cleaned_title = $builder->clean_search_term($cleaned_title);
638 $searchstr = qq#Host-item:("$cleaned_title")#;
640 my ($error, $query ,$query_str) = $builder->build_query_compat( undef, [$searchstr], undef, undef, [$sort], 0 );
646 return ($query, $query_str, $sort);
651 my $subscriptions = $self->subscriptions
653 Returns the related Koha::Subscriptions object for this Biblio object
660 $self->{_subscriptions} ||= Koha::Subscriptions->search( { biblionumber => $self->biblionumber } );
662 return $self->{_subscriptions};
665 =head3 has_items_waiting_or_intransit
667 my $itemsWaitingOrInTransit = $biblio->has_items_waiting_or_intransit
669 Tells if this bibliographic record has items waiting or in transit.
673 sub has_items_waiting_or_intransit {
676 if ( Koha::Holds->search({ biblionumber => $self->id,
677 found => ['W', 'T'] })->count ) {
681 foreach my $item ( $self->items->as_list ) {
682 return 1 if $item->get_transfer;
690 my $coins = $biblio->get_coins;
692 Returns the COinS (a span) which can be included in a biblio record
699 my $record = $self->metadata->record;
701 my $pos7 = substr $record->leader(), 7, 1;
702 my $pos6 = substr $record->leader(), 6, 1;
705 my ( $aulast, $aufirst ) = ( '', '' );
716 # For the purposes of generating COinS metadata, LDR/06-07 can be
717 # considered the same for UNIMARC and MARC21
726 'i' => 'audioRecording',
727 'j' => 'audioRecording',
730 'm' => 'computerProgram',
735 'a' => 'journalArticle',
739 $genre = $fmts6->{$pos6} ? $fmts6->{$pos6} : 'book';
741 if ( $genre eq 'book' ) {
742 $genre = $fmts7->{$pos7} if $fmts7->{$pos7};
745 ##### We must transform mtx to a valable mtx and document type ####
746 if ( $genre eq 'book' ) {
749 } elsif ( $genre eq 'journal' ) {
752 } elsif ( $genre eq 'journalArticle' ) {
760 if ( C4::Context->preference("marcflavour") eq "UNIMARC" ) {
763 $aulast = $record->subfield( '700', 'a' ) || '';
764 $aufirst = $record->subfield( '700', 'b' ) || '';
765 push @authors, "$aufirst $aulast" if ($aufirst or $aulast);
768 if ( $record->field('200') ) {
769 for my $au ( $record->field('200')->subfield('g') ) {
774 $title = $record->subfield( '200', 'a' );
775 my $subfield_210d = $record->subfield('210', 'd');
776 if ($subfield_210d and $subfield_210d =~ /(\d{4})/) {
779 $publisher = $record->subfield( '210', 'c' ) || '';
780 $isbn = $record->subfield( '010', 'a' ) || '';
781 $issn = $record->subfield( '011', 'a' ) || '';
784 # MARC21 need some improve
787 if ( $record->field('100') ) {
788 push @authors, $record->subfield( '100', 'a' );
792 if ( $record->field('700') ) {
793 for my $au ( $record->field('700')->subfield('a') ) {
797 $title = $record->field('245');
798 $title &&= $title->as_string('ab');
799 if ($titletype eq 'a') {
800 $pubyear = $record->field('008') || '';
801 $pubyear = substr($pubyear->data(), 7, 4) if $pubyear;
802 $isbn = $record->subfield( '773', 'z' ) || '';
803 $issn = $record->subfield( '773', 'x' ) || '';
804 $hosttitle = $record->subfield( '773', 't' ) || $record->subfield( '773', 'a') || q{};
805 my @rels = $record->subfield( '773', 'g' );
806 $pages = join(', ', @rels);
808 $pubyear = $record->subfield( '260', 'c' ) || '';
809 $publisher = $record->subfield( '260', 'b' ) || '';
810 $isbn = $record->subfield( '020', 'a' ) || '';
811 $issn = $record->subfield( '022', 'a' ) || '';
817 [ 'ctx_ver', 'Z39.88-2004' ],
818 [ 'rft_val_fmt', "info:ofi/fmt:kev:mtx:$mtx" ],
819 [ ($mtx eq 'dc' ? 'rft.type' : 'rft.genre'), $genre ],
820 [ "rft.${titletype}title", $title ],
823 # rft.title is authorized only once, so by checking $titletype
824 # we ensure that rft.title is not already in the list.
825 if ($hosttitle and $titletype) {
826 push @params, [ 'rft.title', $hosttitle ];
830 [ 'rft.isbn', $isbn ],
831 [ 'rft.issn', $issn ],
834 # If it's a subscription, these informations have no meaning.
835 if ($genre ne 'journal') {
837 [ 'rft.aulast', $aulast ],
838 [ 'rft.aufirst', $aufirst ],
839 (map { [ 'rft.au', $_ ] } @authors),
840 [ 'rft.pub', $publisher ],
841 [ 'rft.date', $pubyear ],
842 [ 'rft.pages', $pages ],
846 my $coins_value = join( '&',
847 map { $$_[1] ? $$_[0] . '=' . uri_escape_utf8( $$_[1] ) : () } @params );
854 my $url = $biblio->get_openurl;
856 Returns url for OpenURL resolver set in OpenURLResolverURL system preference
863 my $OpenURLResolverURL = C4::Context->preference('OpenURLResolverURL');
865 if ($OpenURLResolverURL) {
866 my $uri = URI->new($OpenURLResolverURL);
868 if (not defined $uri->query) {
869 $OpenURLResolverURL .= '?';
871 $OpenURLResolverURL .= '&';
873 $OpenURLResolverURL .= $self->get_coins;
876 return $OpenURLResolverURL;
881 my $serial = $biblio->is_serial
883 Return boolean true if this bibbliographic record is continuing resource
890 return 1 if $self->serial;
892 my $record = $self->metadata->record;
893 return 1 if substr($record->leader, 7, 1) eq 's';
898 =head3 custom_cover_image_url
900 my $image_url = $biblio->custom_cover_image_url
902 Return the specific url of the cover image for this bibliographic record.
903 It is built regaring the value of the system preference CustomCoverImagesURL
907 sub custom_cover_image_url {
909 my $url = C4::Context->preference('CustomCoverImagesURL');
910 if ( $url =~ m|{isbn}| ) {
911 my $isbn = $self->biblioitem->isbn;
913 $url =~ s|{isbn}|$isbn|g;
915 if ( $url =~ m|{normalized_isbn}| ) {
916 my $normalized_isbn = C4::Koha::GetNormalizedISBN($self->biblioitem->isbn);
917 return unless $normalized_isbn;
918 $url =~ s|{normalized_isbn}|$normalized_isbn|g;
920 if ( $url =~ m|{issn}| ) {
921 my $issn = $self->biblioitem->issn;
923 $url =~ s|{issn}|$issn|g;
926 my $re = qr|{(?<field>\d{3})(\$(?<subfield>.))?}|;
928 my $field = $+{field};
929 my $subfield = $+{subfield};
930 my $marc_record = $self->metadata->record;
933 $value = $marc_record->subfield( $field, $subfield );
935 my $controlfield = $marc_record->field($field);
936 $value = $controlfield->data() if $controlfield;
938 return unless $value;
939 $url =~ s|$re|$value|;
947 Return the cover images associated with this biblio.
954 my $cover_images_rs = $self->_result->cover_images;
955 return unless $cover_images_rs;
956 return Koha::CoverImages->_new_from_dbic($cover_images_rs);
959 =head3 get_marc_notes
961 $marcnotesarray = $biblio->get_marc_notes({ opac => 1 });
963 Get all notes from the MARC record and returns them in an array.
964 The notes are stored in different fields depending on MARC flavour.
965 MARC21 5XX $u subfields receive special attention as they are URIs.
970 my ( $self, $params ) = @_;
972 my $marcflavour = C4::Context->preference('marcflavour');
973 my $opac = $params->{opac} // '0';
974 my $interface = $params->{opac} ? 'opac' : 'intranet';
976 my $record = $params->{record} // $self->metadata->record;
977 my $record_processor = Koha::RecordProcessor->new(
979 filters => [ 'ViewPolicy', 'ExpandCodedFields' ],
981 interface => $interface,
982 frameworkcode => $self->frameworkcode
986 $record_processor->process($record);
988 my $scope = $marcflavour eq "UNIMARC"? '3..': '5..';
989 #MARC21 specs indicate some notes should be private if first indicator 0
990 my %maybe_private = (
998 my %hiddenlist = map { $_ => 1 }
999 split( /,/, C4::Context->preference('NotesToHide'));
1002 foreach my $field ( $record->field($scope) ) {
1003 my $tag = $field->tag();
1004 next if $hiddenlist{ $tag };
1005 next if $opac && $maybe_private{$tag} && !$field->indicator(1);
1006 if( $marcflavour ne 'UNIMARC' && $field->subfield('u') ) {
1007 # Field 5XX$u always contains URI
1008 # Examples: 505u, 506u, 510u, 514u, 520u, 530u, 538u, 540u, 542u, 552u, 555u, 561u, 563u, 583u
1009 # We first push the other subfields, then all $u's separately
1010 # Leave further actions to the template (see e.g. opac-detail)
1012 join '', ( 'a' .. 't', 'v' .. 'z', '0' .. '9' ); # excl 'u'
1013 push @marcnotes, { marcnote => $field->as_string($othersub) };
1014 foreach my $sub ( $field->subfield('u') ) {
1015 $sub =~ s/^\s+|\s+$//g; # trim
1016 push @marcnotes, { marcnote => $sub };
1019 push @marcnotes, { marcnote => $field->as_string() };
1025 =head3 _get_marc_authors
1027 Private method to return the list of authors contained in the MARC record.
1028 See get get_marc_contributors and get_marc_authors for the public methods.
1032 sub _get_marc_authors {
1033 my ( $self, $params ) = @_;
1035 my $fields_filter = $params->{fields_filter};
1036 my $mintag = $params->{mintag};
1037 my $maxtag = $params->{maxtag};
1039 my $AuthoritySeparator = C4::Context->preference('AuthoritySeparator');
1040 my $marcflavour = C4::Context->preference('marcflavour');
1042 # tagslib useful only for UNIMARC author responsibilities
1043 my $tagslib = $marcflavour eq "UNIMARC"
1044 ? C4::Biblio::GetMarcStructure( 1, $self->frameworkcode, { unsafe => 1 } )
1048 foreach my $field ( $self->metadata->record->field($fields_filter) ) {
1051 if $mintag && $field->tag() < $mintag
1052 || $maxtag && $field->tag() > $maxtag;
1056 my @subfields = $field->subfields();
1059 # if there is an authority link, build the link with Koha-Auth-Number: subfield9
1060 my $subfield9 = $field->subfield('9');
1062 my $linkvalue = $subfield9;
1063 $linkvalue =~ s/(\(|\))//g;
1064 @link_loop = ( { 'limit' => 'an', 'link' => $linkvalue } );
1069 for my $authors_subfield (@subfields) {
1070 next if ( $authors_subfield->[0] eq '9' );
1072 # unimarc3 contains the $3 of the author for UNIMARC.
1073 # For french academic libraries, it's the "ppn", and it's required for idref webservice
1074 $unimarc3 = $authors_subfield->[1] if $marcflavour eq 'UNIMARC' and $authors_subfield->[0] =~ /3/;
1076 # don't load unimarc subfields 3, 5
1077 next if ( $marcflavour eq 'UNIMARC' and ( $authors_subfield->[0] =~ /3|5/ ) );
1079 my $code = $authors_subfield->[0];
1080 my $value = $authors_subfield->[1];
1081 my $linkvalue = $value;
1082 $linkvalue =~ s/(\(|\))//g;
1083 # UNIMARC author responsibility
1084 if ( $marcflavour eq 'UNIMARC' and $code eq '4' ) {
1085 $value = C4::Biblio::GetAuthorisedValueDesc( $field->tag(), $code, $value, '', $tagslib );
1086 $linkvalue = "($value)";
1088 # if no authority link, build a search query
1089 unless ($subfield9) {
1092 'link' => $linkvalue,
1093 operator => (scalar @link_loop) ? ' AND ' : undef
1096 my @this_link_loop = @link_loop;
1098 unless ( $code eq '0') {
1099 push @subfields_loop, {
1100 tag => $field->tag(),
1103 link_loop => \@this_link_loop,
1104 separator => (scalar @subfields_loop) ? $AuthoritySeparator : ''
1108 push @marcauthors, {
1109 MARCAUTHOR_SUBFIELDS_LOOP => \@subfields_loop,
1110 authoritylink => $subfield9,
1111 unimarc3 => $unimarc3
1114 return \@marcauthors;
1117 =head3 get_marc_contributors
1119 my $contributors = $biblio->get_marc_contributors;
1121 Get all contributors (but first author) from the MARC record and returns them in an array.
1122 They are stored in different fields depending on MARC flavour (700..720 for MARC21)
1126 sub get_marc_contributors {
1127 my ( $self, $params ) = @_;
1129 my ( $mintag, $maxtag, $fields_filter );
1130 my $marcflavour = C4::Context->preference('marcflavour');
1132 if ( $marcflavour eq "UNIMARC" ) {
1135 $fields_filter = '7..';
1136 } else { # marc21/normarc
1139 $fields_filter = '7..';
1142 return $self->_get_marc_authors(
1144 fields_filter => $fields_filter,
1151 =head3 get_marc_authors
1153 my $authors = $biblio->get_marc_authors;
1155 Get all authors from the MARC record and returns them in an array.
1156 They are stored in different fields depending on MARC flavour
1157 (main author from 100 then secondary authors from 700..720).
1161 sub get_marc_authors {
1162 my ( $self, $params ) = @_;
1164 my ( $mintag, $maxtag, $fields_filter );
1165 my $marcflavour = C4::Context->preference('marcflavour');
1167 if ( $marcflavour eq "UNIMARC" ) {
1168 $fields_filter = '200';
1169 } else { # marc21/normarc
1170 $fields_filter = '100';
1173 my @first_authors = @{$self->_get_marc_authors(
1175 fields_filter => $fields_filter,
1181 my @other_authors = @{$self->get_marc_contributors};
1183 return [@first_authors, @other_authors];
1189 my $json = $biblio->to_api;
1191 Overloaded method that returns a JSON representation of the Koha::Biblio object,
1192 suitable for API output. The related Koha::Biblioitem object is merged as expected
1198 my ($self, $args) = @_;
1200 my $response = $self->SUPER::to_api( $args );
1201 my $biblioitem = $self->biblioitem->to_api;
1203 return { %$response, %$biblioitem };
1206 =head3 to_api_mapping
1208 This method returns the mapping for representing a Koha::Biblio object
1213 sub to_api_mapping {
1215 biblionumber => 'biblio_id',
1216 frameworkcode => 'framework_id',
1217 unititle => 'uniform_title',
1218 seriestitle => 'series_title',
1219 copyrightdate => 'copyright_date',
1220 datecreated => 'creation_date',
1221 deleted_on => undef,
1225 =head3 get_marc_host
1227 $host = $biblio->get_marc_host;
1229 ( $host, $relatedparts, $hostinfo ) = $biblio->get_marc_host;
1231 Returns host biblio record from MARC21 773 (undef if no 773 present).
1232 It looks at the first 773 field with MARCorgCode or only a control
1233 number. Complete $w or numeric part is used to search host record.
1234 The optional parameter no_items triggers a check if $biblio has items.
1235 If there are, the sub returns undef.
1236 Called in list context, it also returns 773$g (related parts).
1238 If there is no $w, we use $0 (host biblionumber) or $9 (host itemnumber)
1239 to search for the host record. If there is also no $0 and no $9, we search
1240 using author and title. Failing all of that, we return an undef host and
1241 form a concatenation of strings with 773$agt for host information,
1242 returned when called in list context.
1247 my ($self, $params) = @_;
1248 my $no_items = $params->{no_items};
1249 return if C4::Context->preference('marcflavour') eq 'UNIMARC'; # TODO
1250 return if $params->{no_items} && $self->items->count > 0;
1253 eval { $record = $self->metadata->record };
1256 # We pick the first $w with your MARCOrgCode or the first $w that has no
1257 # code (between parentheses) at all.
1258 my $orgcode = C4::Context->preference('MARCOrgCode') // q{};
1260 foreach my $f ( $record->field('773') ) {
1261 my $w = $f->subfield('w') or next;
1262 if( $w =~ /^\($orgcode\)\s*(\d+)/i or $w =~ /^\d+/ ) {
1268 my $engine = Koha::SearchEngine::Search->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1270 if ( !$hostfld and $record->subfield('773','t') ) {
1271 # not linked using $w
1272 my $unlinkedf = $record->field('773');
1274 if ( C4::Context->preference("EasyAnalyticalRecords") ) {
1275 if ( $unlinkedf->subfield('0') ) {
1276 # use 773$0 host biblionumber
1277 $bibno = $unlinkedf->subfield('0');
1278 } elsif ( $unlinkedf->subfield('9') ) {
1279 # use 773$9 host itemnumber
1280 my $linkeditemnumber = $unlinkedf->subfield('9');
1281 $bibno = Koha::Items->find( $linkeditemnumber )->biblionumber;
1285 my $host = Koha::Biblios->find($bibno) or return;
1286 return wantarray ? ( $host, $unlinkedf->subfield('g') ) : $host;
1288 # just return plaintext and no host record
1289 my $hostinfo = join( ", ", $unlinkedf->subfield('a'), $unlinkedf->subfield('t'), $unlinkedf->subfield('g') );
1290 return wantarray ? ( undef, $unlinkedf->subfield('g'), $hostinfo ) : undef;
1292 return if !$hostfld;
1293 my $rcn = $hostfld->subfield('w');
1295 # Look for control number with/without orgcode
1296 for my $try (1..2) {
1297 my ( $error, $results, $total_hits ) = $engine->simple_search_compat( 'Control-number='.$rcn, 0,1 );
1298 if( !$error and $total_hits == 1 ) {
1299 $bibno = $engine->extract_biblionumber( $results->[0] );
1302 # Add or remove orgcode for second try
1303 if( $try == 1 && $rcn =~ /\)\s*(\d+)/ ) {
1304 $rcn = $1; # number only
1305 } elsif( $try == 1 && $rcn =~ /^\d+/ ) {
1306 $rcn = "($orgcode)$rcn";
1312 my $host = Koha::Biblios->find($bibno) or return;
1313 return wantarray ? ( $host, $hostfld->subfield('g') ) : $host;
1319 my $recalls = $biblio->recalls;
1321 Return recalls linked to this biblio
1327 return Koha::Recalls->_new_from_dbic( scalar $self->_result->recalls );
1330 =head3 can_be_recalled
1332 my @items_for_recall = $biblio->can_be_recalled({ patron => $patron_object });
1334 Does biblio-level checks and returns the items attached to this biblio that are available for recall
1338 sub can_be_recalled {
1339 my ( $self, $params ) = @_;
1341 return 0 if !( C4::Context->preference('UseRecalls') );
1343 my $patron = $params->{patron};
1345 my $branchcode = C4::Context->userenv->{'branch'};
1346 if ( C4::Context->preference('CircControl') eq 'PatronLibrary' and $patron ) {
1347 $branchcode = $patron->branchcode;
1350 my @all_items = Koha::Items->search({ biblionumber => $self->biblionumber })->as_list;
1352 # if there are no available items at all, no recall can be placed
1353 return 0 if ( scalar @all_items == 0 );
1358 my @all_itemnumbers;
1359 foreach my $item ( @all_items ) {
1360 push( @all_itemnumbers, $item->itemnumber );
1361 if ( $item->can_be_recalled({ patron => $patron }) ) {
1362 push( @itemtypes, $item->effective_itemtype );
1363 push( @itemnumbers, $item->itemnumber );
1364 push( @items, $item );
1368 # if there are no recallable items, no recall can be placed
1369 return 0 if ( scalar @items == 0 );
1371 # Check the circulation rule for each relevant itemtype for this biblio
1372 my ( @recalls_allowed, @recalls_per_record, @on_shelf_recalls );
1373 foreach my $itemtype ( @itemtypes ) {
1374 my $rule = Koha::CirculationRules->get_effective_rules({
1375 branchcode => $branchcode,
1376 categorycode => $patron ? $patron->categorycode : undef,
1377 itemtype => $itemtype,
1380 'recalls_per_record',
1384 push( @recalls_allowed, $rule->{recalls_allowed} ) if $rule;
1385 push( @recalls_per_record, $rule->{recalls_per_record} ) if $rule;
1386 push( @on_shelf_recalls, $rule->{on_shelf_recalls} ) if $rule;
1388 my $recalls_allowed = (sort {$b <=> $a} @recalls_allowed)[0]; # take highest
1389 my $recalls_per_record = (sort {$b <=> $a} @recalls_per_record)[0]; # take highest
1390 my %on_shelf_recalls_count = ();
1391 foreach my $count ( @on_shelf_recalls ) {
1392 $on_shelf_recalls_count{$count}++;
1394 my $on_shelf_recalls = (sort {$on_shelf_recalls_count{$b} <=> $on_shelf_recalls_count{$a}} @on_shelf_recalls)[0]; # take most common
1396 # check recalls allowed has been set and is not zero
1397 return 0 if ( !defined($recalls_allowed) || $recalls_allowed == 0 );
1400 # check borrower has not reached open recalls allowed limit
1401 return 0 if ( $patron->recalls->filter_by_current->count >= $recalls_allowed );
1403 # check borrower has not reached open recalls allowed per record limit
1404 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $recalls_per_record );
1406 # check if any of the items under this biblio are already checked out by this borrower
1407 return 0 if ( Koha::Checkouts->search({ itemnumber => [ @all_itemnumbers ], borrowernumber => $patron->borrowernumber })->count > 0 );
1410 # check item availability
1411 my $checked_out_count = 0;
1413 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1416 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1417 return 0 if ( $on_shelf_recalls eq 'all' && $checked_out_count < scalar @items );
1419 # can't recall if no items have been checked out
1420 return 0 if ( $checked_out_count == 0 );
1426 =head2 Internal methods
1438 Kyle M Hall <kyle@bywatersolutions.com>