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;
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 $tickets = $biblio->tickets();
142 Returns all tickets linked to the biblio
148 my $rs = $self->_result->tickets;
149 return Koha::Tickets->_new_from_dbic( $rs );
154 my $item_groups = $biblio->item_groups();
156 Returns a Koha::Biblio::ItemGroups object
163 my $item_groups = $self->_result->item_groups;
164 return Koha::Biblio::ItemGroups->_new_from_dbic($item_groups);
167 =head3 can_article_request
169 my $bool = $biblio->can_article_request( $borrower );
171 Returns true if article requests can be made for this record
173 $borrower must be a Koha::Patron object
177 sub can_article_request {
178 my ( $self, $borrower ) = @_;
180 my $rule = $self->article_request_type($borrower);
181 return q{} if $rule eq 'item_only' && !$self->items()->count();
182 return 1 if $rule && $rule ne 'no';
187 =head3 can_be_transferred
189 $biblio->can_be_transferred({ to => $to_library, from => $from_library })
191 Checks if at least one item of a biblio can be transferred to given library.
193 This feature is controlled by two system preferences:
194 UseBranchTransferLimits to enable / disable the feature
195 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
196 for setting the limitations
198 Performance-wise, it is recommended to use this method for a biblio instead of
199 iterating each item of a biblio with Koha::Item->can_be_transferred().
201 Takes HASHref that can have the following parameters:
202 MANDATORY PARAMETERS:
205 $from : Koha::Library # if given, only items from that
206 # holdingbranch are considered
208 Returns 1 if at least one of the item of a biblio can be transferred
209 to $to_library, otherwise 0.
213 sub can_be_transferred {
214 my ($self, $params) = @_;
216 my $to = $params->{to};
217 my $from = $params->{from};
219 return 1 unless C4::Context->preference('UseBranchTransferLimits');
220 my $limittype = C4::Context->preference('BranchTransferLimitsType');
223 foreach my $item_of_bib ($self->items->as_list) {
224 next unless $item_of_bib->holdingbranch;
225 next if $from && $from->branchcode ne $item_of_bib->holdingbranch;
226 return 1 if $item_of_bib->holdingbranch eq $to->branchcode;
227 my $code = $limittype eq 'itemtype'
228 ? $item_of_bib->effective_itemtype
229 : $item_of_bib->ccode;
230 return 1 unless $code;
231 $items->{$code}->{$item_of_bib->holdingbranch} = 1;
234 # At this point we will have a HASHref containing each itemtype/ccode that
235 # this biblio has, inside which are all of the holdingbranches where those
236 # items are located at. Then, we will query Koha::Item::Transfer::Limits to
237 # find out whether a transfer limits for such $limittype from any of the
238 # listed holdingbranches to the given $to library exist. If at least one
239 # holdingbranch for that $limittype does not have a transfer limit to given
240 # $to library, then we know that the transfer is possible.
241 foreach my $code (keys %{$items}) {
242 my @holdingbranches = keys %{$items->{$code}};
243 return 1 if Koha::Item::Transfer::Limits->search({
244 toBranch => $to->branchcode,
245 fromBranch => { 'in' => \@holdingbranches },
248 group_by => [qw/fromBranch/]
249 })->count == scalar(@holdingbranches) ? 0 : 1;
256 =head3 pickup_locations
258 my $pickup_locations = $biblio->pickup_locations( { patron => $patron } );
260 Returns a Koha::Libraries set of possible pickup locations for this biblio's items,
261 according to patron's home library and if item can be transferred to each pickup location.
263 Patron is a required parameter.
267 sub pickup_locations {
268 my ( $self, $params ) = @_;
270 my $patron = $params->{patron};
272 my $memory_cache = Koha::Cache::Memory::Lite->get_instance();
273 my @pickup_locations;
274 foreach my $item ( $self->items->as_list ) {
275 my $cache_key = sprintf "Pickup_locations:%s:%s:%s:%s:%s",
276 $item->itype,$item->homebranch,$item->holdingbranch,$item->ccode || "",$patron->branchcode||"" ;
277 my $item_pickup_locations = $memory_cache->get_from_cache( $cache_key );
278 unless( $item_pickup_locations ){
279 @{ $item_pickup_locations } = $item->pickup_locations( { patron => $patron } )->_resultset->get_column('branchcode')->all;
280 $memory_cache->set_in_cache( $cache_key, $item_pickup_locations );
282 push @pickup_locations, @{ $item_pickup_locations }
285 return Koha::Libraries->search(
286 { branchcode => { '-in' => \@pickup_locations } }, { order_by => ['branchname'] } );
289 =head3 hidden_in_opac
291 my $bool = $biblio->hidden_in_opac({ [ rules => $rules ] })
293 Returns true if the biblio matches the hidding criteria defined in $rules.
294 Returns false otherwise. It involves the I<OpacHiddenItems> and
295 I<OpacHiddenItemsHidesRecord> system preferences.
297 Takes HASHref that can have the following parameters:
299 $rules : { <field> => [ value_1, ... ], ... }
301 Note: $rules inherits its structure from the parsed YAML from reading
302 the I<OpacHiddenItems> system preference.
307 my ( $self, $params ) = @_;
309 my $rules = $params->{rules} // {};
311 my @items = $self->items->as_list;
313 return 0 unless @items; # Do not hide if there is no item
315 # Ok, there are items, don't even try the rules unless OpacHiddenItemsHidesRecord
316 return 0 unless C4::Context->preference('OpacHiddenItemsHidesRecord');
318 return !(any { !$_->hidden_in_opac({ rules => $rules }) } @items);
321 =head3 article_request_type
323 my $type = $biblio->article_request_type( $borrower );
325 Returns the article request type based on items, or on the record
326 itself if there are no items.
328 $borrower must be a Koha::Patron object
332 sub article_request_type {
333 my ( $self, $borrower ) = @_;
335 return q{} unless $borrower;
337 my $rule = $self->article_request_type_for_items( $borrower );
338 return $rule if $rule;
340 # If the record has no items that are requestable, go by the record itemtype
341 $rule = $self->article_request_type_for_bib($borrower);
342 return $rule if $rule;
347 =head3 article_request_type_for_bib
349 my $type = $biblio->article_request_type_for_bib
351 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record
355 sub article_request_type_for_bib {
356 my ( $self, $borrower ) = @_;
358 return q{} unless $borrower;
360 my $borrowertype = $borrower->categorycode;
361 my $itemtype = $self->itemtype();
363 my $rule = Koha::CirculationRules->get_effective_rule(
365 rule_name => 'article_requests',
366 categorycode => $borrowertype,
367 itemtype => $itemtype,
371 return q{} unless $rule;
372 return $rule->rule_value || q{}
375 =head3 article_request_type_for_items
377 my $type = $biblio->article_request_type_for_items
379 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record's items
381 If there is a conflict where some items are 'bib_only' and some are 'item_only', 'bib_only' will be returned.
385 sub article_request_type_for_items {
386 my ( $self, $borrower ) = @_;
389 foreach my $item ( $self->items()->as_list() ) {
390 my $rule = $item->article_request_type($borrower);
391 return $rule if $rule eq 'bib_only'; # we don't need to go any further
395 return 'item_only' if $counts->{item_only};
396 return 'yes' if $counts->{yes};
397 return 'no' if $counts->{no};
401 =head3 article_requests
403 my $article_requests = $biblio->article_requests
405 Returns the article requests associated with this biblio
409 sub article_requests {
412 return Koha::ArticleRequests->_new_from_dbic( scalar $self->_result->article_requests );
415 =head3 current_checkouts
417 my $current_checkouts = $biblio->current_checkouts
419 Returns the current checkouts associated with this biblio
423 sub current_checkouts {
426 return Koha::Checkouts->search( { "item.biblionumber" => $self->id },
427 { join => 'item' } );
432 my $old_checkouts = $biblio->old_checkouts
434 Returns the past checkouts associated with this biblio
441 return Koha::Old::Checkouts->search( { "item.biblionumber" => $self->id },
442 { join => 'item' } );
447 my $items = $biblio->items();
449 Returns the related Koha::Items object for this biblio
456 my $items_rs = $self->_result->items;
458 return Koha::Items->_new_from_dbic( $items_rs );
463 my $host_items = $biblio->host_items();
465 Return the host items (easy analytical record)
472 return Koha::Items->new->empty
473 unless C4::Context->preference('EasyAnalyticalRecords');
475 my $marcflavour = C4::Context->preference("marcflavour");
476 my $analyticfield = '773';
477 if ( $marcflavour eq 'MARC21' ) {
478 $analyticfield = '773';
480 elsif ( $marcflavour eq 'UNIMARC' ) {
481 $analyticfield = '461';
483 my $marc_record = $self->metadata->record;
485 foreach my $field ( $marc_record->field($analyticfield) ) {
486 push @itemnumbers, $field->subfield('9');
489 return Koha::Items->search( { itemnumber => { -in => \@itemnumbers } } );
494 my $itemtype = $biblio->itemtype();
496 Returns the itemtype for this record.
503 return $self->biblioitem()->itemtype();
508 my $holds = $biblio->holds();
510 return the current holds placed on this record
515 my ( $self, $params, $attributes ) = @_;
516 $attributes->{order_by} = 'priority' unless exists $attributes->{order_by};
517 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
518 return Koha::Holds->_new_from_dbic($hold_rs);
523 my $holds = $biblio->current_holds
525 Return the holds placed on this bibliographic record.
526 It does not include future holds.
532 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
534 { reservedate => { '<=' => $dtf->format_date(dt_from_string) } } );
539 my $field = $self->biblioitem()->itemtype
541 Returns the related Koha::Biblioitem object for this Biblio object
548 $self->{_biblioitem} ||= Koha::Biblioitems->find( { biblionumber => $self->biblionumber() } );
550 return $self->{_biblioitem};
555 my $suggestions = $self->suggestions
557 Returns the related Koha::Suggestions object for this Biblio object
564 my $suggestions_rs = $self->_result->suggestions;
565 return Koha::Suggestions->_new_from_dbic( $suggestions_rs );
568 =head3 get_marc_components
570 my $components = $self->get_marc_components();
572 Returns an array of search results data, which are component parts of
573 this object (MARC21 773 points to this)
577 sub get_marc_components {
578 my ($self, $max_results) = @_;
580 return [] if (C4::Context->preference('marcflavour') ne 'MARC21');
582 my ( $searchstr, $sort ) = $self->get_components_query;
585 if (defined($searchstr)) {
586 my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
587 my ( $error, $results, $facets );
589 ( $error, $results, $facets ) = $searcher->search_compat( $searchstr, undef, [$sort], ['biblioserver'], $max_results, 0, undef, undef, 'ccl', 0 );
594 warn "Warning from search_compat: '$error'";
598 message => 'component_search',
603 $components = $results->{biblioserver}->{RECORDS} if defined($results) && $results->{biblioserver}->{hits};
606 return $components // [];
609 =head2 get_components_query
611 Returns a query which can be used to search for all component parts of MARC21 biblios
615 sub get_components_query {
618 my $builder = Koha::SearchEngine::QueryBuilder->new(
619 { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
620 my $marc = $self->metadata->record;
621 my $component_sort_field = C4::Context->preference('ComponentSortField') // "title";
622 my $component_sort_order = C4::Context->preference('ComponentSortOrder') // "asc";
623 my $sort = $component_sort_field . "_" . $component_sort_order;
626 if ( C4::Context->preference('UseControlNumber') ) {
627 my $pf001 = $marc->field('001') || undef;
629 if ( defined($pf001) ) {
631 my $pf003 = $marc->field('003') || undef;
633 if ( !defined($pf003) ) {
634 # search for 773$w='Host001'
635 $searchstr .= "rcn:\"" . $pf001->data()."\"";
639 # search for (773$w='Host001' and 003='Host003') or 773$w='(Host003)Host001'
640 $searchstr .= "(rcn:\"" . $pf001->data() . "\" AND cni:\"" . $pf003->data() . "\")";
641 $searchstr .= " OR rcn:\"" . $pf003->data() . " " . $pf001->data() . "\"";
645 # limit to monograph and serial component part records
646 $searchstr .= " AND (bib-level:a OR bib-level:b)";
651 my $cleaned_title = $marc->subfield('245', "a");
652 $cleaned_title =~ tr|/||;
653 $cleaned_title = $builder->clean_search_term($cleaned_title);
654 $searchstr = qq#Host-item:("$cleaned_title")#;
656 my ($error, $query ,$query_str) = $builder->build_query_compat( undef, [$searchstr], undef, undef, [$sort], 0 );
662 return ($query, $query_str, $sort);
667 my $subscriptions = $self->subscriptions
669 Returns the related Koha::Subscriptions object for this Biblio object
676 $self->{_subscriptions} ||= Koha::Subscriptions->search( { biblionumber => $self->biblionumber } );
678 return $self->{_subscriptions};
681 =head3 has_items_waiting_or_intransit
683 my $itemsWaitingOrInTransit = $biblio->has_items_waiting_or_intransit
685 Tells if this bibliographic record has items waiting or in transit.
689 sub has_items_waiting_or_intransit {
692 if ( Koha::Holds->search({ biblionumber => $self->id,
693 found => ['W', 'T'] })->count ) {
697 foreach my $item ( $self->items->as_list ) {
698 return 1 if $item->get_transfer;
706 my $coins = $biblio->get_coins;
708 Returns the COinS (a span) which can be included in a biblio record
715 my $record = $self->metadata->record;
717 my $pos7 = substr $record->leader(), 7, 1;
718 my $pos6 = substr $record->leader(), 6, 1;
721 my ( $aulast, $aufirst ) = ( '', '' );
732 # For the purposes of generating COinS metadata, LDR/06-07 can be
733 # considered the same for UNIMARC and MARC21
742 'i' => 'audioRecording',
743 'j' => 'audioRecording',
746 'm' => 'computerProgram',
751 'a' => 'journalArticle',
755 $genre = $fmts6->{$pos6} ? $fmts6->{$pos6} : 'book';
757 if ( $genre eq 'book' ) {
758 $genre = $fmts7->{$pos7} if $fmts7->{$pos7};
761 ##### We must transform mtx to a valable mtx and document type ####
762 if ( $genre eq 'book' ) {
765 } elsif ( $genre eq 'journal' ) {
768 } elsif ( $genre eq 'journalArticle' ) {
776 if ( C4::Context->preference("marcflavour") eq "UNIMARC" ) {
779 $aulast = $record->subfield( '700', 'a' ) || '';
780 $aufirst = $record->subfield( '700', 'b' ) || '';
781 push @authors, "$aufirst $aulast" if ($aufirst or $aulast);
784 if ( $record->field('200') ) {
785 for my $au ( $record->field('200')->subfield('g') ) {
790 $title = $record->subfield( '200', 'a' );
791 my $subfield_210d = $record->subfield('210', 'd');
792 if ($subfield_210d and $subfield_210d =~ /(\d{4})/) {
795 $publisher = $record->subfield( '210', 'c' ) || '';
796 $isbn = $record->subfield( '010', 'a' ) || '';
797 $issn = $record->subfield( '011', 'a' ) || '';
800 # MARC21 need some improve
803 if ( $record->field('100') ) {
804 push @authors, $record->subfield( '100', 'a' );
808 if ( $record->field('700') ) {
809 for my $au ( $record->field('700')->subfield('a') ) {
813 $title = $record->field('245');
814 $title &&= $title->as_string('ab');
815 if ($titletype eq 'a') {
816 $pubyear = $record->field('008') || '';
817 $pubyear = substr($pubyear->data(), 7, 4) if $pubyear;
818 $isbn = $record->subfield( '773', 'z' ) || '';
819 $issn = $record->subfield( '773', 'x' ) || '';
820 $hosttitle = $record->subfield( '773', 't' ) || $record->subfield( '773', 'a') || q{};
821 my @rels = $record->subfield( '773', 'g' );
822 $pages = join(', ', @rels);
824 $pubyear = $record->subfield( '260', 'c' ) || '';
825 $publisher = $record->subfield( '260', 'b' ) || '';
826 $isbn = $record->subfield( '020', 'a' ) || '';
827 $issn = $record->subfield( '022', 'a' ) || '';
833 [ 'ctx_ver', 'Z39.88-2004' ],
834 [ 'rft_val_fmt', "info:ofi/fmt:kev:mtx:$mtx" ],
835 [ ($mtx eq 'dc' ? 'rft.type' : 'rft.genre'), $genre ],
836 [ "rft.${titletype}title", $title ],
839 # rft.title is authorized only once, so by checking $titletype
840 # we ensure that rft.title is not already in the list.
841 if ($hosttitle and $titletype) {
842 push @params, [ 'rft.title', $hosttitle ];
846 [ 'rft.isbn', $isbn ],
847 [ 'rft.issn', $issn ],
850 # If it's a subscription, these informations have no meaning.
851 if ($genre ne 'journal') {
853 [ 'rft.aulast', $aulast ],
854 [ 'rft.aufirst', $aufirst ],
855 (map { [ 'rft.au', $_ ] } @authors),
856 [ 'rft.pub', $publisher ],
857 [ 'rft.date', $pubyear ],
858 [ 'rft.pages', $pages ],
862 my $coins_value = join( '&',
863 map { $$_[1] ? $$_[0] . '=' . uri_escape_utf8( $$_[1] ) : () } @params );
870 my $url = $biblio->get_openurl;
872 Returns url for OpenURL resolver set in OpenURLResolverURL system preference
879 my $OpenURLResolverURL = C4::Context->preference('OpenURLResolverURL');
881 if ($OpenURLResolverURL) {
882 my $uri = URI->new($OpenURLResolverURL);
884 if (not defined $uri->query) {
885 $OpenURLResolverURL .= '?';
887 $OpenURLResolverURL .= '&';
889 $OpenURLResolverURL .= $self->get_coins;
892 return $OpenURLResolverURL;
897 my $serial = $biblio->is_serial
899 Return boolean true if this bibbliographic record is continuing resource
906 return 1 if $self->serial;
908 my $record = $self->metadata->record;
909 return 1 if substr($record->leader, 7, 1) eq 's';
914 =head3 custom_cover_image_url
916 my $image_url = $biblio->custom_cover_image_url
918 Return the specific url of the cover image for this bibliographic record.
919 It is built regaring the value of the system preference CustomCoverImagesURL
923 sub custom_cover_image_url {
925 my $url = C4::Context->preference('CustomCoverImagesURL');
926 if ( $url =~ m|{isbn}| ) {
927 my $isbn = $self->biblioitem->isbn;
929 $url =~ s|{isbn}|$isbn|g;
931 if ( $url =~ m|{normalized_isbn}| ) {
932 my $normalized_isbn = C4::Koha::GetNormalizedISBN($self->biblioitem->isbn);
933 return unless $normalized_isbn;
934 $url =~ s|{normalized_isbn}|$normalized_isbn|g;
936 if ( $url =~ m|{issn}| ) {
937 my $issn = $self->biblioitem->issn;
939 $url =~ s|{issn}|$issn|g;
942 my $re = qr|{(?<field>\d{3})(\$(?<subfield>.))?}|;
944 my $field = $+{field};
945 my $subfield = $+{subfield};
946 my $marc_record = $self->metadata->record;
949 $value = $marc_record->subfield( $field, $subfield );
951 my $controlfield = $marc_record->field($field);
952 $value = $controlfield->data() if $controlfield;
954 return unless $value;
955 $url =~ s|$re|$value|;
963 Return the cover images associated with this biblio.
970 my $cover_images_rs = $self->_result->cover_images;
971 return unless $cover_images_rs;
972 return Koha::CoverImages->_new_from_dbic($cover_images_rs);
975 =head3 get_marc_notes
977 $marcnotesarray = $biblio->get_marc_notes({ opac => 1 });
979 Get all notes from the MARC record and returns them in an array.
980 The notes are stored in different fields depending on MARC flavour.
981 MARC21 5XX $u subfields receive special attention as they are URIs.
986 my ( $self, $params ) = @_;
988 my $marcflavour = C4::Context->preference('marcflavour');
989 my $opac = $params->{opac} // '0';
990 my $interface = $params->{opac} ? 'opac' : 'intranet';
992 my $record = $params->{record} // $self->metadata->record;
993 my $record_processor = Koha::RecordProcessor->new(
995 filters => [ 'ViewPolicy', 'ExpandCodedFields' ],
997 interface => $interface,
998 frameworkcode => $self->frameworkcode
1002 $record_processor->process($record);
1004 my $scope = $marcflavour eq "UNIMARC"? '3..': '5..';
1005 #MARC21 specs indicate some notes should be private if first indicator 0
1006 my %maybe_private = (
1014 my %hiddenlist = map { $_ => 1 }
1015 split( /,/, C4::Context->preference('NotesToHide'));
1018 foreach my $field ( $record->field($scope) ) {
1019 my $tag = $field->tag();
1020 next if $hiddenlist{ $tag };
1021 next if $opac && $maybe_private{$tag} && !$field->indicator(1);
1022 if( $marcflavour ne 'UNIMARC' && $field->subfield('u') ) {
1023 # Field 5XX$u always contains URI
1024 # Examples: 505u, 506u, 510u, 514u, 520u, 530u, 538u, 540u, 542u, 552u, 555u, 561u, 563u, 583u
1025 # We first push the other subfields, then all $u's separately
1026 # Leave further actions to the template (see e.g. opac-detail)
1028 join '', ( 'a' .. 't', 'v' .. 'z', '0' .. '9' ); # excl 'u'
1029 push @marcnotes, { marcnote => $field->as_string($othersub) };
1030 foreach my $sub ( $field->subfield('u') ) {
1031 $sub =~ s/^\s+|\s+$//g; # trim
1032 push @marcnotes, { marcnote => $sub };
1035 push @marcnotes, { marcnote => $field->as_string() };
1041 =head3 _get_marc_authors
1043 Private method to return the list of authors contained in the MARC record.
1044 See get get_marc_contributors and get_marc_authors for the public methods.
1048 sub _get_marc_authors {
1049 my ( $self, $params ) = @_;
1051 my $fields_filter = $params->{fields_filter};
1052 my $mintag = $params->{mintag};
1053 my $maxtag = $params->{maxtag};
1055 my $AuthoritySeparator = C4::Context->preference('AuthoritySeparator');
1056 my $marcflavour = C4::Context->preference('marcflavour');
1058 # tagslib useful only for UNIMARC author responsibilities
1059 my $tagslib = $marcflavour eq "UNIMARC"
1060 ? C4::Biblio::GetMarcStructure( 1, $self->frameworkcode, { unsafe => 1 } )
1064 foreach my $field ( $self->metadata->record->field($fields_filter) ) {
1067 if $mintag && $field->tag() < $mintag
1068 || $maxtag && $field->tag() > $maxtag;
1072 my @subfields = $field->subfields();
1075 # if there is an authority link, build the link with Koha-Auth-Number: subfield9
1076 my $subfield9 = $field->subfield('9');
1078 my $linkvalue = $subfield9;
1079 $linkvalue =~ s/(\(|\))//g;
1080 @link_loop = ( { 'limit' => 'an', 'link' => $linkvalue } );
1085 for my $authors_subfield (@subfields) {
1086 next if ( $authors_subfield->[0] eq '9' );
1088 # unimarc3 contains the $3 of the author for UNIMARC.
1089 # For french academic libraries, it's the "ppn", and it's required for idref webservice
1090 $unimarc3 = $authors_subfield->[1] if $marcflavour eq 'UNIMARC' and $authors_subfield->[0] =~ /3/;
1092 # don't load unimarc subfields 3, 5
1093 next if ( $marcflavour eq 'UNIMARC' and ( $authors_subfield->[0] =~ /3|5/ ) );
1095 my $code = $authors_subfield->[0];
1096 my $value = $authors_subfield->[1];
1097 my $linkvalue = $value;
1098 $linkvalue =~ s/(\(|\))//g;
1099 # UNIMARC author responsibility
1100 if ( $marcflavour eq 'UNIMARC' and $code eq '4' ) {
1101 $value = C4::Biblio::GetAuthorisedValueDesc( $field->tag(), $code, $value, '', $tagslib );
1102 $linkvalue = "($value)";
1104 # if no authority link, build a search query
1105 unless ($subfield9) {
1108 'link' => $linkvalue,
1109 operator => (scalar @link_loop) ? ' AND ' : undef
1112 my @this_link_loop = @link_loop;
1114 unless ( $code eq '0') {
1115 push @subfields_loop, {
1116 tag => $field->tag(),
1119 link_loop => \@this_link_loop,
1120 separator => (scalar @subfields_loop) ? $AuthoritySeparator : ''
1124 push @marcauthors, {
1125 MARCAUTHOR_SUBFIELDS_LOOP => \@subfields_loop,
1126 authoritylink => $subfield9,
1127 unimarc3 => $unimarc3
1130 return \@marcauthors;
1133 =head3 get_marc_contributors
1135 my $contributors = $biblio->get_marc_contributors;
1137 Get all contributors (but first author) from the MARC record and returns them in an array.
1138 They are stored in different fields depending on MARC flavour (700..720 for MARC21)
1142 sub get_marc_contributors {
1143 my ( $self, $params ) = @_;
1145 my ( $mintag, $maxtag, $fields_filter );
1146 my $marcflavour = C4::Context->preference('marcflavour');
1148 if ( $marcflavour eq "UNIMARC" ) {
1151 $fields_filter = '7..';
1152 } else { # marc21/normarc
1155 $fields_filter = '7..';
1158 return $self->_get_marc_authors(
1160 fields_filter => $fields_filter,
1167 =head3 get_marc_authors
1169 my $authors = $biblio->get_marc_authors;
1171 Get all authors from the MARC record and returns them in an array.
1172 They are stored in different fields depending on MARC flavour
1173 (main author from 100 then secondary authors from 700..720).
1177 sub get_marc_authors {
1178 my ( $self, $params ) = @_;
1180 my ( $mintag, $maxtag, $fields_filter );
1181 my $marcflavour = C4::Context->preference('marcflavour');
1183 if ( $marcflavour eq "UNIMARC" ) {
1184 $fields_filter = '200';
1185 } else { # marc21/normarc
1186 $fields_filter = '100';
1189 my @first_authors = @{$self->_get_marc_authors(
1191 fields_filter => $fields_filter,
1197 my @other_authors = @{$self->get_marc_contributors};
1199 return [@first_authors, @other_authors];
1205 my $json = $biblio->to_api;
1207 Overloaded method that returns a JSON representation of the Koha::Biblio object,
1208 suitable for API output. The related Koha::Biblioitem object is merged as expected
1214 my ($self, $args) = @_;
1216 my $response = $self->SUPER::to_api( $args );
1217 my $biblioitem = $self->biblioitem->to_api;
1219 return { %$response, %$biblioitem };
1222 =head3 to_api_mapping
1224 This method returns the mapping for representing a Koha::Biblio object
1229 sub to_api_mapping {
1231 biblionumber => 'biblio_id',
1232 frameworkcode => 'framework_id',
1233 unititle => 'uniform_title',
1234 seriestitle => 'series_title',
1235 copyrightdate => 'copyright_date',
1236 datecreated => 'creation_date',
1237 deleted_on => undef,
1241 =head3 get_marc_host
1243 $host = $biblio->get_marc_host;
1245 ( $host, $relatedparts, $hostinfo ) = $biblio->get_marc_host;
1247 Returns host biblio record from MARC21 773 (undef if no 773 present).
1248 It looks at the first 773 field with MARCorgCode or only a control
1249 number. Complete $w or numeric part is used to search host record.
1250 The optional parameter no_items triggers a check if $biblio has items.
1251 If there are, the sub returns undef.
1252 Called in list context, it also returns 773$g (related parts).
1254 If there is no $w, we use $0 (host biblionumber) or $9 (host itemnumber)
1255 to search for the host record. If there is also no $0 and no $9, we search
1256 using author and title. Failing all of that, we return an undef host and
1257 form a concatenation of strings with 773$agt for host information,
1258 returned when called in list context.
1263 my ($self, $params) = @_;
1264 my $no_items = $params->{no_items};
1265 return if C4::Context->preference('marcflavour') eq 'UNIMARC'; # TODO
1266 return if $params->{no_items} && $self->items->count > 0;
1269 eval { $record = $self->metadata->record };
1272 # We pick the first $w with your MARCOrgCode or the first $w that has no
1273 # code (between parentheses) at all.
1274 my $orgcode = C4::Context->preference('MARCOrgCode') // q{};
1276 foreach my $f ( $record->field('773') ) {
1277 my $w = $f->subfield('w') or next;
1278 if( $w =~ /^\($orgcode\)\s*(\d+)/i or $w =~ /^\d+/ ) {
1284 my $engine = Koha::SearchEngine::Search->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1286 if ( !$hostfld and $record->subfield('773','t') ) {
1287 # not linked using $w
1288 my $unlinkedf = $record->field('773');
1290 if ( C4::Context->preference("EasyAnalyticalRecords") ) {
1291 if ( $unlinkedf->subfield('0') ) {
1292 # use 773$0 host biblionumber
1293 $bibno = $unlinkedf->subfield('0');
1294 } elsif ( $unlinkedf->subfield('9') ) {
1295 # use 773$9 host itemnumber
1296 my $linkeditemnumber = $unlinkedf->subfield('9');
1297 $bibno = Koha::Items->find( $linkeditemnumber )->biblionumber;
1301 my $host = Koha::Biblios->find($bibno) or return;
1302 return wantarray ? ( $host, $unlinkedf->subfield('g') ) : $host;
1304 # just return plaintext and no host record
1305 my $hostinfo = join( ", ", $unlinkedf->subfield('a'), $unlinkedf->subfield('t'), $unlinkedf->subfield('g') );
1306 return wantarray ? ( undef, $unlinkedf->subfield('g'), $hostinfo ) : undef;
1308 return if !$hostfld;
1309 my $rcn = $hostfld->subfield('w');
1311 # Look for control number with/without orgcode
1312 for my $try (1..2) {
1313 my ( $error, $results, $total_hits ) = $engine->simple_search_compat( 'Control-number='.$rcn, 0,1 );
1314 if( !$error and $total_hits == 1 ) {
1315 $bibno = $engine->extract_biblionumber( $results->[0] );
1318 # Add or remove orgcode for second try
1319 if( $try == 1 && $rcn =~ /\)\s*(\d+)/ ) {
1320 $rcn = $1; # number only
1321 } elsif( $try == 1 && $rcn =~ /^\d+/ ) {
1322 $rcn = "($orgcode)$rcn";
1328 my $host = Koha::Biblios->find($bibno) or return;
1329 return wantarray ? ( $host, $hostfld->subfield('g') ) : $host;
1333 =head3 get_marc_host_only
1335 my $host = $biblio->get_marc_host_only;
1341 sub get_marc_host_only {
1344 my ( $host ) = $self->get_marc_host;
1349 =head3 get_marc_relatedparts_only
1351 my $relatedparts = $biblio->get_marc_relatedparts_only;
1353 Return related parts only
1357 sub get_marc_relatedparts_only {
1360 my ( undef, $relatedparts ) = $self->get_marc_host;
1362 return $relatedparts;
1365 =head3 get_marc_hostinfo_only
1367 my $hostinfo = $biblio->get_marc_hostinfo_only;
1369 Return host info only
1373 sub get_marc_hostinfo_only {
1376 my ( $host, $relatedparts, $hostinfo ) = $self->get_marc_host;
1383 my $recalls = $biblio->recalls;
1385 Return recalls linked to this biblio
1391 return Koha::Recalls->_new_from_dbic( scalar $self->_result->recalls );
1394 =head3 can_be_recalled
1396 my @items_for_recall = $biblio->can_be_recalled({ patron => $patron_object });
1398 Does biblio-level checks and returns the items attached to this biblio that are available for recall
1402 sub can_be_recalled {
1403 my ( $self, $params ) = @_;
1405 return 0 if !( C4::Context->preference('UseRecalls') );
1407 my $patron = $params->{patron};
1409 my $branchcode = C4::Context->userenv->{'branch'};
1410 if ( C4::Context->preference('CircControl') eq 'PatronLibrary' and $patron ) {
1411 $branchcode = $patron->branchcode;
1414 my @all_items = Koha::Items->search({ biblionumber => $self->biblionumber })->as_list;
1416 # if there are no available items at all, no recall can be placed
1417 return 0 if ( scalar @all_items == 0 );
1422 my @all_itemnumbers;
1423 foreach my $item ( @all_items ) {
1424 push( @all_itemnumbers, $item->itemnumber );
1425 if ( $item->can_be_recalled({ patron => $patron }) ) {
1426 push( @itemtypes, $item->effective_itemtype );
1427 push( @itemnumbers, $item->itemnumber );
1428 push( @items, $item );
1432 # if there are no recallable items, no recall can be placed
1433 return 0 if ( scalar @items == 0 );
1435 # Check the circulation rule for each relevant itemtype for this biblio
1436 my ( @recalls_allowed, @recalls_per_record, @on_shelf_recalls );
1437 foreach my $itemtype ( @itemtypes ) {
1438 my $rule = Koha::CirculationRules->get_effective_rules({
1439 branchcode => $branchcode,
1440 categorycode => $patron ? $patron->categorycode : undef,
1441 itemtype => $itemtype,
1444 'recalls_per_record',
1448 push( @recalls_allowed, $rule->{recalls_allowed} ) if $rule;
1449 push( @recalls_per_record, $rule->{recalls_per_record} ) if $rule;
1450 push( @on_shelf_recalls, $rule->{on_shelf_recalls} ) if $rule;
1452 my $recalls_allowed = (sort {$b <=> $a} @recalls_allowed)[0]; # take highest
1453 my $recalls_per_record = (sort {$b <=> $a} @recalls_per_record)[0]; # take highest
1454 my %on_shelf_recalls_count = ();
1455 foreach my $count ( @on_shelf_recalls ) {
1456 $on_shelf_recalls_count{$count}++;
1458 my $on_shelf_recalls = (sort {$on_shelf_recalls_count{$b} <=> $on_shelf_recalls_count{$a}} @on_shelf_recalls)[0]; # take most common
1460 # check recalls allowed has been set and is not zero
1461 return 0 if ( !defined($recalls_allowed) || $recalls_allowed == 0 );
1464 # check borrower has not reached open recalls allowed limit
1465 return 0 if ( $patron->recalls->filter_by_current->count >= $recalls_allowed );
1467 # check borrower has not reached open recalls allowed per record limit
1468 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $recalls_per_record );
1470 # check if any of the items under this biblio are already checked out by this borrower
1471 return 0 if ( Koha::Checkouts->search({ itemnumber => [ @all_itemnumbers ], borrowernumber => $patron->borrowernumber })->count > 0 );
1474 # check item availability
1475 my $checked_out_count = 0;
1477 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1480 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1481 return 0 if ( $on_shelf_recalls eq 'all' && $checked_out_count < scalar @items );
1483 # can't recall if no items have been checked out
1484 return 0 if ( $checked_out_count == 0 );
1490 =head2 Internal methods
1502 Kyle M Hall <kyle@bywatersolutions.com>