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;
56 Koha::Biblio - Koha Biblio Object class
66 Overloaded I<store> method to set default values
73 $self->datecreated( dt_from_string ) unless $self->datecreated;
75 return $self->SUPER::store;
80 my $metadata = $biblio->metadata();
82 Returns a Koha::Biblio::Metadata object
89 my $metadata = $self->_result->metadata;
90 return Koha::Biblio::Metadata->_new_from_dbic($metadata);
95 my $record = $biblio->record();
97 Returns a Marc::Record object
104 return $self->metadata->record;
109 my $orders = $biblio->orders();
111 Returns a Koha::Acquisition::Orders object
118 my $orders = $self->_result->orders;
119 return Koha::Acquisition::Orders->_new_from_dbic($orders);
124 my $active_orders = $biblio->active_orders();
126 Returns the active acquisition orders related to this biblio.
127 An order is considered active when it is not cancelled (i.e. when datecancellation
135 return $self->orders->search({ datecancellationprinted => undef });
140 my $item_groups = $biblio->item_groups();
142 Returns a Koha::Biblio::ItemGroups object
149 my $item_groups = $self->_result->item_groups;
150 return Koha::Biblio::ItemGroups->_new_from_dbic($item_groups);
153 =head3 can_article_request
155 my $bool = $biblio->can_article_request( $borrower );
157 Returns true if article requests can be made for this record
159 $borrower must be a Koha::Patron object
163 sub can_article_request {
164 my ( $self, $borrower ) = @_;
166 my $rule = $self->article_request_type($borrower);
167 return q{} if $rule eq 'item_only' && !$self->items()->count();
168 return 1 if $rule && $rule ne 'no';
173 =head3 can_be_transferred
175 $biblio->can_be_transferred({ to => $to_library, from => $from_library })
177 Checks if at least one item of a biblio can be transferred to given library.
179 This feature is controlled by two system preferences:
180 UseBranchTransferLimits to enable / disable the feature
181 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
182 for setting the limitations
184 Performance-wise, it is recommended to use this method for a biblio instead of
185 iterating each item of a biblio with Koha::Item->can_be_transferred().
187 Takes HASHref that can have the following parameters:
188 MANDATORY PARAMETERS:
191 $from : Koha::Library # if given, only items from that
192 # holdingbranch are considered
194 Returns 1 if at least one of the item of a biblio can be transferred
195 to $to_library, otherwise 0.
199 sub can_be_transferred {
200 my ($self, $params) = @_;
202 my $to = $params->{to};
203 my $from = $params->{from};
205 return 1 unless C4::Context->preference('UseBranchTransferLimits');
206 my $limittype = C4::Context->preference('BranchTransferLimitsType');
209 foreach my $item_of_bib ($self->items->as_list) {
210 next unless $item_of_bib->holdingbranch;
211 next if $from && $from->branchcode ne $item_of_bib->holdingbranch;
212 return 1 if $item_of_bib->holdingbranch eq $to->branchcode;
213 my $code = $limittype eq 'itemtype'
214 ? $item_of_bib->effective_itemtype
215 : $item_of_bib->ccode;
216 return 1 unless $code;
217 $items->{$code}->{$item_of_bib->holdingbranch} = 1;
220 # At this point we will have a HASHref containing each itemtype/ccode that
221 # this biblio has, inside which are all of the holdingbranches where those
222 # items are located at. Then, we will query Koha::Item::Transfer::Limits to
223 # find out whether a transfer limits for such $limittype from any of the
224 # listed holdingbranches to the given $to library exist. If at least one
225 # holdingbranch for that $limittype does not have a transfer limit to given
226 # $to library, then we know that the transfer is possible.
227 foreach my $code (keys %{$items}) {
228 my @holdingbranches = keys %{$items->{$code}};
229 return 1 if Koha::Item::Transfer::Limits->search({
230 toBranch => $to->branchcode,
231 fromBranch => { 'in' => \@holdingbranches },
234 group_by => [qw/fromBranch/]
235 })->count == scalar(@holdingbranches) ? 0 : 1;
242 =head3 pickup_locations
244 my $pickup_locations = $biblio->pickup_locations({ patron => $patron });
246 Returns a Koha::Libraries set of possible pickup locations for this biblio's items,
247 according to patron's home library and if item can be transferred to each pickup location.
249 Throws a I<Koha::Exceptions::MissingParameter> exception if the B<mandatory> parameter I<patron>
254 sub pickup_locations {
255 my ( $self, $params ) = @_;
257 Koha::Exceptions::MissingParameter->throw( parameter => 'patron' )
258 unless exists $params->{patron};
260 my $patron = $params->{patron};
262 my $memory_cache = Koha::Cache::Memory::Lite->get_instance();
263 my @pickup_locations;
264 foreach my $item ( $self->items->as_list ) {
265 my $cache_key = sprintf "Pickup_locations:%s:%s:%s:%s:%s",
266 $item->itype,$item->homebranch,$item->holdingbranch,$item->ccode || "",$patron->branchcode||"" ;
267 my $item_pickup_locations = $memory_cache->get_from_cache( $cache_key );
268 unless( $item_pickup_locations ){
269 @{ $item_pickup_locations } = $item->pickup_locations( { patron => $patron } )->_resultset->get_column('branchcode')->all;
270 $memory_cache->set_in_cache( $cache_key, $item_pickup_locations );
272 push @pickup_locations, @{ $item_pickup_locations }
275 return Koha::Libraries->search(
276 { branchcode => { '-in' => \@pickup_locations } }, { order_by => ['branchname'] } );
279 =head3 hidden_in_opac
281 my $bool = $biblio->hidden_in_opac({ [ rules => $rules ] })
283 Returns true if the biblio matches the hidding criteria defined in $rules.
284 Returns false otherwise. It involves the I<OpacHiddenItems> and
285 I<OpacHiddenItemsHidesRecord> system preferences.
287 Takes HASHref that can have the following parameters:
289 $rules : { <field> => [ value_1, ... ], ... }
291 Note: $rules inherits its structure from the parsed YAML from reading
292 the I<OpacHiddenItems> system preference.
297 my ( $self, $params ) = @_;
299 my $rules = $params->{rules} // {};
301 my @items = $self->items->as_list;
303 return 0 unless @items; # Do not hide if there is no item
305 # Ok, there are items, don't even try the rules unless OpacHiddenItemsHidesRecord
306 return 0 unless C4::Context->preference('OpacHiddenItemsHidesRecord');
308 return !(any { !$_->hidden_in_opac({ rules => $rules }) } @items);
311 =head3 article_request_type
313 my $type = $biblio->article_request_type( $borrower );
315 Returns the article request type based on items, or on the record
316 itself if there are no items.
318 $borrower must be a Koha::Patron object
322 sub article_request_type {
323 my ( $self, $borrower ) = @_;
325 return q{} unless $borrower;
327 my $rule = $self->article_request_type_for_items( $borrower );
328 return $rule if $rule;
330 # If the record has no items that are requestable, go by the record itemtype
331 $rule = $self->article_request_type_for_bib($borrower);
332 return $rule if $rule;
337 =head3 article_request_type_for_bib
339 my $type = $biblio->article_request_type_for_bib
341 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record
345 sub article_request_type_for_bib {
346 my ( $self, $borrower ) = @_;
348 return q{} unless $borrower;
350 my $borrowertype = $borrower->categorycode;
351 my $itemtype = $self->itemtype();
353 my $rule = Koha::CirculationRules->get_effective_rule(
355 rule_name => 'article_requests',
356 categorycode => $borrowertype,
357 itemtype => $itemtype,
361 return q{} unless $rule;
362 return $rule->rule_value || q{}
365 =head3 article_request_type_for_items
367 my $type = $biblio->article_request_type_for_items
369 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record's items
371 If there is a conflict where some items are 'bib_only' and some are 'item_only', 'bib_only' will be returned.
375 sub article_request_type_for_items {
376 my ( $self, $borrower ) = @_;
379 foreach my $item ( $self->items()->as_list() ) {
380 my $rule = $item->article_request_type($borrower);
381 return $rule if $rule eq 'bib_only'; # we don't need to go any further
385 return 'item_only' if $counts->{item_only};
386 return 'yes' if $counts->{yes};
387 return 'no' if $counts->{no};
391 =head3 article_requests
393 my $article_requests = $biblio->article_requests
395 Returns the article requests associated with this biblio
399 sub article_requests {
402 return Koha::ArticleRequests->_new_from_dbic( scalar $self->_result->article_requests );
405 =head3 current_checkouts
407 my $current_checkouts = $biblio->current_checkouts
409 Returns the current checkouts associated with this biblio
413 sub current_checkouts {
416 return Koha::Checkouts->search( { "item.biblionumber" => $self->id },
417 { join => 'item' } );
422 my $old_checkouts = $biblio->old_checkouts
424 Returns the past checkouts associated with this biblio
431 return Koha::Old::Checkouts->search( { "item.biblionumber" => $self->id },
432 { join => 'item' } );
437 my $items = $biblio->items();
439 Returns the related Koha::Items object for this biblio
446 my $items_rs = $self->_result->items;
448 return Koha::Items->_new_from_dbic( $items_rs );
453 my $host_items = $biblio->host_items();
455 Return the host items (easy analytical record)
462 return Koha::Items->new->empty
463 unless C4::Context->preference('EasyAnalyticalRecords');
465 my $marcflavour = C4::Context->preference("marcflavour");
466 my $analyticfield = '773';
467 if ( $marcflavour eq 'MARC21' ) {
468 $analyticfield = '773';
470 elsif ( $marcflavour eq 'UNIMARC' ) {
471 $analyticfield = '461';
473 my $marc_record = $self->metadata->record;
475 foreach my $field ( $marc_record->field($analyticfield) ) {
476 push @itemnumbers, $field->subfield('9');
479 return Koha::Items->search( { itemnumber => { -in => \@itemnumbers } } );
484 my $itemtype = $biblio->itemtype();
486 Returns the itemtype for this record.
493 return $self->biblioitem()->itemtype();
498 my $holds = $biblio->holds();
500 return the current holds placed on this record
505 my ( $self, $params, $attributes ) = @_;
506 $attributes->{order_by} = 'priority' unless exists $attributes->{order_by};
507 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
508 return Koha::Holds->_new_from_dbic($hold_rs);
513 my $holds = $biblio->current_holds
515 Return the holds placed on this bibliographic record.
516 It does not include future holds.
522 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
524 { reservedate => { '<=' => $dtf->format_date(dt_from_string) } } );
529 my $field = $self->biblioitem()->itemtype
531 Returns the related Koha::Biblioitem object for this Biblio object
538 $self->{_biblioitem} ||= Koha::Biblioitems->find( { biblionumber => $self->biblionumber() } );
540 return $self->{_biblioitem};
545 my $suggestions = $self->suggestions
547 Returns the related Koha::Suggestions object for this Biblio object
554 my $suggestions_rs = $self->_result->suggestions;
555 return Koha::Suggestions->_new_from_dbic( $suggestions_rs );
558 =head3 get_marc_components
560 my $components = $self->get_marc_components();
562 Returns an array of search results data, which are component parts of
563 this object (MARC21 773 points to this)
567 sub get_marc_components {
568 my ($self, $max_results) = @_;
570 return [] if (C4::Context->preference('marcflavour') ne 'MARC21');
572 my ( $searchstr, $sort ) = $self->get_components_query;
575 if (defined($searchstr)) {
576 my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
577 my ( $error, $results, $facets );
579 ( $error, $results, $facets ) = $searcher->search_compat( $searchstr, undef, [$sort], ['biblioserver'], $max_results, 0, undef, undef, 'ccl', 0 );
584 warn "Warning from search_compat: '$error'";
588 message => 'component_search',
593 $components = $results->{biblioserver}->{RECORDS} if defined($results) && $results->{biblioserver}->{hits};
596 return $components // [];
599 =head2 get_components_query
601 Returns a query which can be used to search for all component parts of MARC21 biblios
605 sub get_components_query {
608 my $builder = Koha::SearchEngine::QueryBuilder->new(
609 { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
610 my $marc = $self->metadata->record;
611 my $component_sort_field = C4::Context->preference('ComponentSortField') // "title";
612 my $component_sort_order = C4::Context->preference('ComponentSortOrder') // "asc";
613 my $sort = $component_sort_field . "_" . $component_sort_order;
616 if ( C4::Context->preference('UseControlNumber') ) {
617 my $pf001 = $marc->field('001') || undef;
619 if ( defined($pf001) ) {
621 my $pf003 = $marc->field('003') || undef;
623 if ( !defined($pf003) ) {
624 # search for 773$w='Host001'
625 $searchstr .= "rcn:\"" . $pf001->data()."\"";
629 # search for (773$w='Host001' and 003='Host003') or 773$w='(Host003)Host001'
630 $searchstr .= "(rcn:\"" . $pf001->data() . "\" AND cni:\"" . $pf003->data() . "\")";
631 $searchstr .= " OR rcn:\"" . $pf003->data() . " " . $pf001->data() . "\"";
635 # limit to monograph and serial component part records
636 $searchstr .= " AND (bib-level:a OR bib-level:b)";
641 my $cleaned_title = $marc->subfield('245', "a");
642 $cleaned_title =~ tr|/||;
643 $cleaned_title = $builder->clean_search_term($cleaned_title);
644 $searchstr = qq#Host-item:("$cleaned_title")#;
646 my ($error, $query ,$query_str) = $builder->build_query_compat( undef, [$searchstr], undef, undef, [$sort], 0 );
652 return ($query, $query_str, $sort);
657 my $subscriptions = $self->subscriptions
659 Returns the related Koha::Subscriptions object for this Biblio object
666 $self->{_subscriptions} ||= Koha::Subscriptions->search( { biblionumber => $self->biblionumber } );
668 return $self->{_subscriptions};
671 =head3 has_items_waiting_or_intransit
673 my $itemsWaitingOrInTransit = $biblio->has_items_waiting_or_intransit
675 Tells if this bibliographic record has items waiting or in transit.
679 sub has_items_waiting_or_intransit {
682 if ( Koha::Holds->search({ biblionumber => $self->id,
683 found => ['W', 'T'] })->count ) {
687 foreach my $item ( $self->items->as_list ) {
688 return 1 if $item->get_transfer;
696 my $coins = $biblio->get_coins;
698 Returns the COinS (a span) which can be included in a biblio record
705 my $record = $self->metadata->record;
707 my $pos7 = substr $record->leader(), 7, 1;
708 my $pos6 = substr $record->leader(), 6, 1;
711 my ( $aulast, $aufirst ) = ( '', '' );
722 # For the purposes of generating COinS metadata, LDR/06-07 can be
723 # considered the same for UNIMARC and MARC21
732 'i' => 'audioRecording',
733 'j' => 'audioRecording',
736 'm' => 'computerProgram',
741 'a' => 'journalArticle',
745 $genre = $fmts6->{$pos6} ? $fmts6->{$pos6} : 'book';
747 if ( $genre eq 'book' ) {
748 $genre = $fmts7->{$pos7} if $fmts7->{$pos7};
751 ##### We must transform mtx to a valable mtx and document type ####
752 if ( $genre eq 'book' ) {
755 } elsif ( $genre eq 'journal' ) {
758 } elsif ( $genre eq 'journalArticle' ) {
766 if ( C4::Context->preference("marcflavour") eq "UNIMARC" ) {
769 $aulast = $record->subfield( '700', 'a' ) || '';
770 $aufirst = $record->subfield( '700', 'b' ) || '';
771 push @authors, "$aufirst $aulast" if ($aufirst or $aulast);
774 if ( $record->field('200') ) {
775 for my $au ( $record->field('200')->subfield('g') ) {
780 $title = $record->subfield( '200', 'a' );
781 my $subfield_210d = $record->subfield('210', 'd');
782 if ($subfield_210d and $subfield_210d =~ /(\d{4})/) {
785 $publisher = $record->subfield( '210', 'c' ) || '';
786 $isbn = $record->subfield( '010', 'a' ) || '';
787 $issn = $record->subfield( '011', 'a' ) || '';
790 # MARC21 need some improve
793 if ( $record->field('100') ) {
794 push @authors, $record->subfield( '100', 'a' );
798 if ( $record->field('700') ) {
799 for my $au ( $record->field('700')->subfield('a') ) {
803 $title = $record->field('245');
804 $title &&= $title->as_string('ab');
805 if ($titletype eq 'a') {
806 $pubyear = $record->field('008') || '';
807 $pubyear = substr($pubyear->data(), 7, 4) if $pubyear;
808 $isbn = $record->subfield( '773', 'z' ) || '';
809 $issn = $record->subfield( '773', 'x' ) || '';
810 $hosttitle = $record->subfield( '773', 't' ) || $record->subfield( '773', 'a') || q{};
811 my @rels = $record->subfield( '773', 'g' );
812 $pages = join(', ', @rels);
814 $pubyear = $record->subfield( '260', 'c' ) || '';
815 $publisher = $record->subfield( '260', 'b' ) || '';
816 $isbn = $record->subfield( '020', 'a' ) || '';
817 $issn = $record->subfield( '022', 'a' ) || '';
823 [ 'ctx_ver', 'Z39.88-2004' ],
824 [ 'rft_val_fmt', "info:ofi/fmt:kev:mtx:$mtx" ],
825 [ ($mtx eq 'dc' ? 'rft.type' : 'rft.genre'), $genre ],
826 [ "rft.${titletype}title", $title ],
829 # rft.title is authorized only once, so by checking $titletype
830 # we ensure that rft.title is not already in the list.
831 if ($hosttitle and $titletype) {
832 push @params, [ 'rft.title', $hosttitle ];
836 [ 'rft.isbn', $isbn ],
837 [ 'rft.issn', $issn ],
840 # If it's a subscription, these informations have no meaning.
841 if ($genre ne 'journal') {
843 [ 'rft.aulast', $aulast ],
844 [ 'rft.aufirst', $aufirst ],
845 (map { [ 'rft.au', $_ ] } @authors),
846 [ 'rft.pub', $publisher ],
847 [ 'rft.date', $pubyear ],
848 [ 'rft.pages', $pages ],
852 my $coins_value = join( '&',
853 map { $$_[1] ? $$_[0] . '=' . uri_escape_utf8( $$_[1] ) : () } @params );
860 my $url = $biblio->get_openurl;
862 Returns url for OpenURL resolver set in OpenURLResolverURL system preference
869 my $OpenURLResolverURL = C4::Context->preference('OpenURLResolverURL');
871 if ($OpenURLResolverURL) {
872 my $uri = URI->new($OpenURLResolverURL);
874 if (not defined $uri->query) {
875 $OpenURLResolverURL .= '?';
877 $OpenURLResolverURL .= '&';
879 $OpenURLResolverURL .= $self->get_coins;
882 return $OpenURLResolverURL;
887 my $serial = $biblio->is_serial
889 Return boolean true if this bibbliographic record is continuing resource
896 return 1 if $self->serial;
898 my $record = $self->metadata->record;
899 return 1 if substr($record->leader, 7, 1) eq 's';
904 =head3 custom_cover_image_url
906 my $image_url = $biblio->custom_cover_image_url
908 Return the specific url of the cover image for this bibliographic record.
909 It is built regaring the value of the system preference CustomCoverImagesURL
913 sub custom_cover_image_url {
915 my $url = C4::Context->preference('CustomCoverImagesURL');
916 if ( $url =~ m|{isbn}| ) {
917 my $isbn = $self->biblioitem->isbn;
919 $url =~ s|{isbn}|$isbn|g;
921 if ( $url =~ m|{normalized_isbn}| ) {
922 my $normalized_isbn = C4::Koha::GetNormalizedISBN($self->biblioitem->isbn);
923 return unless $normalized_isbn;
924 $url =~ s|{normalized_isbn}|$normalized_isbn|g;
926 if ( $url =~ m|{issn}| ) {
927 my $issn = $self->biblioitem->issn;
929 $url =~ s|{issn}|$issn|g;
932 my $re = qr|{(?<field>\d{3})(\$(?<subfield>.))?}|;
934 my $field = $+{field};
935 my $subfield = $+{subfield};
936 my $marc_record = $self->metadata->record;
939 $value = $marc_record->subfield( $field, $subfield );
941 my $controlfield = $marc_record->field($field);
942 $value = $controlfield->data() if $controlfield;
944 return unless $value;
945 $url =~ s|$re|$value|;
953 Return the cover images associated with this biblio.
960 my $cover_images_rs = $self->_result->cover_images;
961 return unless $cover_images_rs;
962 return Koha::CoverImages->_new_from_dbic($cover_images_rs);
965 =head3 get_marc_notes
967 $marcnotesarray = $biblio->get_marc_notes({ opac => 1 });
969 Get all notes from the MARC record and returns them in an array.
970 The notes are stored in different fields depending on MARC flavour.
971 MARC21 5XX $u subfields receive special attention as they are URIs.
976 my ( $self, $params ) = @_;
978 my $marcflavour = C4::Context->preference('marcflavour');
979 my $opac = $params->{opac} // '0';
980 my $interface = $params->{opac} ? 'opac' : 'intranet';
982 my $record = $params->{record} // $self->metadata->record;
983 my $record_processor = Koha::RecordProcessor->new(
985 filters => [ 'ViewPolicy', 'ExpandCodedFields' ],
987 interface => $interface,
988 frameworkcode => $self->frameworkcode
992 $record_processor->process($record);
994 my $scope = $marcflavour eq "UNIMARC"? '3..': '5..';
995 #MARC21 specs indicate some notes should be private if first indicator 0
996 my %maybe_private = (
1004 my %hiddenlist = map { $_ => 1 }
1005 split( /,/, C4::Context->preference('NotesToHide'));
1008 foreach my $field ( $record->field($scope) ) {
1009 my $tag = $field->tag();
1010 next if $hiddenlist{ $tag };
1011 next if $opac && $maybe_private{$tag} && !$field->indicator(1);
1012 if( $marcflavour ne 'UNIMARC' && $field->subfield('u') ) {
1013 # Field 5XX$u always contains URI
1014 # Examples: 505u, 506u, 510u, 514u, 520u, 530u, 538u, 540u, 542u, 552u, 555u, 561u, 563u, 583u
1015 # We first push the other subfields, then all $u's separately
1016 # Leave further actions to the template (see e.g. opac-detail)
1018 join '', ( 'a' .. 't', 'v' .. 'z', '0' .. '9' ); # excl 'u'
1019 push @marcnotes, { marcnote => $field->as_string($othersub) };
1020 foreach my $sub ( $field->subfield('u') ) {
1021 $sub =~ s/^\s+|\s+$//g; # trim
1022 push @marcnotes, { marcnote => $sub };
1025 push @marcnotes, { marcnote => $field->as_string() };
1031 =head3 _get_marc_authors
1033 Private method to return the list of authors contained in the MARC record.
1034 See get get_marc_contributors and get_marc_authors for the public methods.
1038 sub _get_marc_authors {
1039 my ( $self, $params ) = @_;
1041 my $fields_filter = $params->{fields_filter};
1042 my $mintag = $params->{mintag};
1043 my $maxtag = $params->{maxtag};
1045 my $AuthoritySeparator = C4::Context->preference('AuthoritySeparator');
1046 my $marcflavour = C4::Context->preference('marcflavour');
1048 # tagslib useful only for UNIMARC author responsibilities
1049 my $tagslib = $marcflavour eq "UNIMARC"
1050 ? C4::Biblio::GetMarcStructure( 1, $self->frameworkcode, { unsafe => 1 } )
1054 foreach my $field ( $self->metadata->record->field($fields_filter) ) {
1057 if $mintag && $field->tag() < $mintag
1058 || $maxtag && $field->tag() > $maxtag;
1062 my @subfields = $field->subfields();
1065 # if there is an authority link, build the link with Koha-Auth-Number: subfield9
1066 my $subfield9 = $field->subfield('9');
1068 my $linkvalue = $subfield9;
1069 $linkvalue =~ s/(\(|\))//g;
1070 @link_loop = ( { 'limit' => 'an', 'link' => $linkvalue } );
1075 for my $authors_subfield (@subfields) {
1076 next if ( $authors_subfield->[0] eq '9' );
1078 # unimarc3 contains the $3 of the author for UNIMARC.
1079 # For french academic libraries, it's the "ppn", and it's required for idref webservice
1080 $unimarc3 = $authors_subfield->[1] if $marcflavour eq 'UNIMARC' and $authors_subfield->[0] =~ /3/;
1082 # don't load unimarc subfields 3, 5
1083 next if ( $marcflavour eq 'UNIMARC' and ( $authors_subfield->[0] =~ /3|5/ ) );
1085 my $code = $authors_subfield->[0];
1086 my $value = $authors_subfield->[1];
1087 my $linkvalue = $value;
1088 $linkvalue =~ s/(\(|\))//g;
1089 # UNIMARC author responsibility
1090 if ( $marcflavour eq 'UNIMARC' and $code eq '4' ) {
1091 $value = C4::Biblio::GetAuthorisedValueDesc( $field->tag(), $code, $value, '', $tagslib );
1092 $linkvalue = "($value)";
1094 # if no authority link, build a search query
1095 unless ($subfield9) {
1098 'link' => $linkvalue,
1099 operator => (scalar @link_loop) ? ' AND ' : undef
1102 my @this_link_loop = @link_loop;
1104 unless ( $code eq '0') {
1105 push @subfields_loop, {
1106 tag => $field->tag(),
1109 link_loop => \@this_link_loop,
1110 separator => (scalar @subfields_loop) ? $AuthoritySeparator : ''
1114 push @marcauthors, {
1115 MARCAUTHOR_SUBFIELDS_LOOP => \@subfields_loop,
1116 authoritylink => $subfield9,
1117 unimarc3 => $unimarc3
1120 return \@marcauthors;
1123 =head3 get_marc_contributors
1125 my $contributors = $biblio->get_marc_contributors;
1127 Get all contributors (but first author) from the MARC record and returns them in an array.
1128 They are stored in different fields depending on MARC flavour (700..720 for MARC21)
1132 sub get_marc_contributors {
1133 my ( $self, $params ) = @_;
1135 my ( $mintag, $maxtag, $fields_filter );
1136 my $marcflavour = C4::Context->preference('marcflavour');
1138 if ( $marcflavour eq "UNIMARC" ) {
1141 $fields_filter = '7..';
1142 } else { # marc21/normarc
1145 $fields_filter = '7..';
1148 return $self->_get_marc_authors(
1150 fields_filter => $fields_filter,
1157 =head3 get_marc_authors
1159 my $authors = $biblio->get_marc_authors;
1161 Get all authors from the MARC record and returns them in an array.
1162 They are stored in different fields depending on MARC flavour
1163 (main author from 100 then secondary authors from 700..720).
1167 sub get_marc_authors {
1168 my ( $self, $params ) = @_;
1170 my ( $mintag, $maxtag, $fields_filter );
1171 my $marcflavour = C4::Context->preference('marcflavour');
1173 if ( $marcflavour eq "UNIMARC" ) {
1174 $fields_filter = '200';
1175 } else { # marc21/normarc
1176 $fields_filter = '100';
1179 my @first_authors = @{$self->_get_marc_authors(
1181 fields_filter => $fields_filter,
1187 my @other_authors = @{$self->get_marc_contributors};
1189 return [@first_authors, @other_authors];
1195 my $json = $biblio->to_api;
1197 Overloaded method that returns a JSON representation of the Koha::Biblio object,
1198 suitable for API output. The related Koha::Biblioitem object is merged as expected
1204 my ($self, $args) = @_;
1206 my $response = $self->SUPER::to_api( $args );
1207 my $biblioitem = $self->biblioitem->to_api;
1209 return { %$response, %$biblioitem };
1212 =head3 to_api_mapping
1214 This method returns the mapping for representing a Koha::Biblio object
1219 sub to_api_mapping {
1221 biblionumber => 'biblio_id',
1222 frameworkcode => 'framework_id',
1223 unititle => 'uniform_title',
1224 seriestitle => 'series_title',
1225 copyrightdate => 'copyright_date',
1226 datecreated => 'creation_date',
1227 deleted_on => undef,
1231 =head3 get_marc_host
1233 $host = $biblio->get_marc_host;
1235 ( $host, $relatedparts, $hostinfo ) = $biblio->get_marc_host;
1237 Returns host biblio record from MARC21 773 (undef if no 773 present).
1238 It looks at the first 773 field with MARCorgCode or only a control
1239 number. Complete $w or numeric part is used to search host record.
1240 The optional parameter no_items triggers a check if $biblio has items.
1241 If there are, the sub returns undef.
1242 Called in list context, it also returns 773$g (related parts).
1244 If there is no $w, we use $0 (host biblionumber) or $9 (host itemnumber)
1245 to search for the host record. If there is also no $0 and no $9, we search
1246 using author and title. Failing all of that, we return an undef host and
1247 form a concatenation of strings with 773$agt for host information,
1248 returned when called in list context.
1253 my ($self, $params) = @_;
1254 my $no_items = $params->{no_items};
1255 return if C4::Context->preference('marcflavour') eq 'UNIMARC'; # TODO
1256 return if $params->{no_items} && $self->items->count > 0;
1259 eval { $record = $self->metadata->record };
1262 # We pick the first $w with your MARCOrgCode or the first $w that has no
1263 # code (between parentheses) at all.
1264 my $orgcode = C4::Context->preference('MARCOrgCode') // q{};
1266 foreach my $f ( $record->field('773') ) {
1267 my $w = $f->subfield('w') or next;
1268 if( $w =~ /^\($orgcode\)\s*(\d+)/i or $w =~ /^\d+/ ) {
1274 my $engine = Koha::SearchEngine::Search->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1276 if ( !$hostfld and $record->subfield('773','t') ) {
1277 # not linked using $w
1278 my $unlinkedf = $record->field('773');
1280 if ( C4::Context->preference("EasyAnalyticalRecords") ) {
1281 if ( $unlinkedf->subfield('0') ) {
1282 # use 773$0 host biblionumber
1283 $bibno = $unlinkedf->subfield('0');
1284 } elsif ( $unlinkedf->subfield('9') ) {
1285 # use 773$9 host itemnumber
1286 my $linkeditemnumber = $unlinkedf->subfield('9');
1287 $bibno = Koha::Items->find( $linkeditemnumber )->biblionumber;
1291 my $host = Koha::Biblios->find($bibno) or return;
1292 return wantarray ? ( $host, $unlinkedf->subfield('g') ) : $host;
1294 # just return plaintext and no host record
1295 my $hostinfo = join( ", ", $unlinkedf->subfield('a'), $unlinkedf->subfield('t'), $unlinkedf->subfield('g') );
1296 return wantarray ? ( undef, $unlinkedf->subfield('g'), $hostinfo ) : undef;
1298 return if !$hostfld;
1299 my $rcn = $hostfld->subfield('w');
1301 # Look for control number with/without orgcode
1302 for my $try (1..2) {
1303 my ( $error, $results, $total_hits ) = $engine->simple_search_compat( 'Control-number='.$rcn, 0,1 );
1304 if( !$error and $total_hits == 1 ) {
1305 $bibno = $engine->extract_biblionumber( $results->[0] );
1308 # Add or remove orgcode for second try
1309 if( $try == 1 && $rcn =~ /\)\s*(\d+)/ ) {
1310 $rcn = $1; # number only
1311 } elsif( $try == 1 && $rcn =~ /^\d+/ ) {
1312 $rcn = "($orgcode)$rcn";
1318 my $host = Koha::Biblios->find($bibno) or return;
1319 return wantarray ? ( $host, $hostfld->subfield('g') ) : $host;
1325 my $recalls = $biblio->recalls;
1327 Return recalls linked to this biblio
1333 return Koha::Recalls->_new_from_dbic( scalar $self->_result->recalls );
1336 =head3 can_be_recalled
1338 my @items_for_recall = $biblio->can_be_recalled({ patron => $patron_object });
1340 Does biblio-level checks and returns the items attached to this biblio that are available for recall
1344 sub can_be_recalled {
1345 my ( $self, $params ) = @_;
1347 return 0 if !( C4::Context->preference('UseRecalls') );
1349 my $patron = $params->{patron};
1351 my $branchcode = C4::Context->userenv->{'branch'};
1352 if ( C4::Context->preference('CircControl') eq 'PatronLibrary' and $patron ) {
1353 $branchcode = $patron->branchcode;
1356 my @all_items = Koha::Items->search({ biblionumber => $self->biblionumber })->as_list;
1358 # if there are no available items at all, no recall can be placed
1359 return 0 if ( scalar @all_items == 0 );
1364 my @all_itemnumbers;
1365 foreach my $item ( @all_items ) {
1366 push( @all_itemnumbers, $item->itemnumber );
1367 if ( $item->can_be_recalled({ patron => $patron }) ) {
1368 push( @itemtypes, $item->effective_itemtype );
1369 push( @itemnumbers, $item->itemnumber );
1370 push( @items, $item );
1374 # if there are no recallable items, no recall can be placed
1375 return 0 if ( scalar @items == 0 );
1377 # Check the circulation rule for each relevant itemtype for this biblio
1378 my ( @recalls_allowed, @recalls_per_record, @on_shelf_recalls );
1379 foreach my $itemtype ( @itemtypes ) {
1380 my $rule = Koha::CirculationRules->get_effective_rules({
1381 branchcode => $branchcode,
1382 categorycode => $patron ? $patron->categorycode : undef,
1383 itemtype => $itemtype,
1386 'recalls_per_record',
1390 push( @recalls_allowed, $rule->{recalls_allowed} ) if $rule;
1391 push( @recalls_per_record, $rule->{recalls_per_record} ) if $rule;
1392 push( @on_shelf_recalls, $rule->{on_shelf_recalls} ) if $rule;
1394 my $recalls_allowed = (sort {$b <=> $a} @recalls_allowed)[0]; # take highest
1395 my $recalls_per_record = (sort {$b <=> $a} @recalls_per_record)[0]; # take highest
1396 my %on_shelf_recalls_count = ();
1397 foreach my $count ( @on_shelf_recalls ) {
1398 $on_shelf_recalls_count{$count}++;
1400 my $on_shelf_recalls = (sort {$on_shelf_recalls_count{$b} <=> $on_shelf_recalls_count{$a}} @on_shelf_recalls)[0]; # take most common
1402 # check recalls allowed has been set and is not zero
1403 return 0 if ( !defined($recalls_allowed) || $recalls_allowed == 0 );
1406 # check borrower has not reached open recalls allowed limit
1407 return 0 if ( $patron->recalls->filter_by_current->count >= $recalls_allowed );
1409 # check borrower has not reached open recalls allowed per record limit
1410 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $recalls_per_record );
1412 # check if any of the items under this biblio are already checked out by this borrower
1413 return 0 if ( Koha::Checkouts->search({ itemnumber => [ @all_itemnumbers ], borrowernumber => $patron->borrowernumber })->count > 0 );
1416 # check item availability
1417 my $checked_out_count = 0;
1419 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1422 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1423 return 0 if ( $on_shelf_recalls eq 'all' && $checked_out_count < scalar @items );
1425 # can't recall if no items have been checked out
1426 return 0 if ( $checked_out_count == 0 );
1432 =head2 Internal methods
1444 Kyle M Hall <kyle@bywatersolutions.com>