3 # Copyright ByWater Solutions 2014
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22 use List::MoreUtils qw( any );
24 use URI::Escape qw( uri_escape_utf8 );
26 use C4::Koha qw( GetNormalizedISBN );
29 use Koha::DateUtils qw( dt_from_string );
31 use base qw(Koha::Object);
33 use Koha::Acquisition::Orders;
34 use Koha::ArticleRequests;
35 use Koha::Biblio::Metadatas;
36 use Koha::Biblio::ItemGroups;
37 use Koha::Biblioitems;
38 use Koha::Cache::Memory::Lite;
40 use Koha::CirculationRules;
42 use Koha::Illrequests;
43 use Koha::Item::Transfer::Limits;
46 use Koha::Old::Checkouts;
48 use Koha::RecordProcessor;
49 use Koha::Suggestions;
50 use Koha::Subscriptions;
51 use Koha::SearchEngine;
52 use Koha::SearchEngine::Search;
53 use Koha::SearchEngine::QueryBuilder;
57 Koha::Biblio - Koha Biblio Object class
67 Overloaded I<store> method to set default values
74 $self->datecreated( dt_from_string ) unless $self->datecreated;
76 return $self->SUPER::store;
81 my $metadata = $biblio->metadata();
83 Returns a Koha::Biblio::Metadata object
90 my $metadata = $self->_result->metadata;
91 return Koha::Biblio::Metadata->_new_from_dbic($metadata);
96 my $record = $biblio->record();
98 Returns a Marc::Record object
105 return $self->metadata->record;
110 my $orders = $biblio->orders();
112 Returns a Koha::Acquisition::Orders object
119 my $orders = $self->_result->orders;
120 return Koha::Acquisition::Orders->_new_from_dbic($orders);
125 my $active_orders = $biblio->active_orders();
127 Returns the active acquisition orders related to this biblio.
128 An order is considered active when it is not cancelled (i.e. when datecancellation
136 return $self->orders->search({ datecancellationprinted => undef });
141 my $tickets = $biblio->tickets();
143 Returns all tickets linked to the biblio
149 my $rs = $self->_result->tickets;
150 return Koha::Tickets->_new_from_dbic( $rs );
155 my $ill_requests = $biblio->ill_requests();
157 Returns a Koha::Illrequests object
164 my $ill_requests = $self->_result->ill_requests;
165 return Koha::Illrequests->_new_from_dbic($ill_requests);
170 my $item_groups = $biblio->item_groups();
172 Returns a Koha::Biblio::ItemGroups object
179 my $item_groups = $self->_result->item_groups;
180 return Koha::Biblio::ItemGroups->_new_from_dbic($item_groups);
183 =head3 can_article_request
185 my $bool = $biblio->can_article_request( $borrower );
187 Returns true if article requests can be made for this record
189 $borrower must be a Koha::Patron object
193 sub can_article_request {
194 my ( $self, $borrower ) = @_;
196 my $rule = $self->article_request_type($borrower);
197 return q{} if $rule eq 'item_only' && !$self->items()->count();
198 return 1 if $rule && $rule ne 'no';
203 =head3 can_be_transferred
205 $biblio->can_be_transferred({ to => $to_library, from => $from_library })
207 Checks if at least one item of a biblio can be transferred to given library.
209 This feature is controlled by two system preferences:
210 UseBranchTransferLimits to enable / disable the feature
211 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
212 for setting the limitations
214 Performance-wise, it is recommended to use this method for a biblio instead of
215 iterating each item of a biblio with Koha::Item->can_be_transferred().
217 Takes HASHref that can have the following parameters:
218 MANDATORY PARAMETERS:
221 $from : Koha::Library # if given, only items from that
222 # holdingbranch are considered
224 Returns 1 if at least one of the item of a biblio can be transferred
225 to $to_library, otherwise 0.
229 sub can_be_transferred {
230 my ($self, $params) = @_;
232 my $to = $params->{to};
233 my $from = $params->{from};
235 return 1 unless C4::Context->preference('UseBranchTransferLimits');
236 my $limittype = C4::Context->preference('BranchTransferLimitsType');
239 foreach my $item_of_bib ($self->items->as_list) {
240 next unless $item_of_bib->holdingbranch;
241 next if $from && $from->branchcode ne $item_of_bib->holdingbranch;
242 return 1 if $item_of_bib->holdingbranch eq $to->branchcode;
243 my $code = $limittype eq 'itemtype'
244 ? $item_of_bib->effective_itemtype
245 : $item_of_bib->ccode;
246 return 1 unless $code;
247 $items->{$code}->{$item_of_bib->holdingbranch} = 1;
250 # At this point we will have a HASHref containing each itemtype/ccode that
251 # this biblio has, inside which are all of the holdingbranches where those
252 # items are located at. Then, we will query Koha::Item::Transfer::Limits to
253 # find out whether a transfer limits for such $limittype from any of the
254 # listed holdingbranches to the given $to library exist. If at least one
255 # holdingbranch for that $limittype does not have a transfer limit to given
256 # $to library, then we know that the transfer is possible.
257 foreach my $code (keys %{$items}) {
258 my @holdingbranches = keys %{$items->{$code}};
259 return 1 if Koha::Item::Transfer::Limits->search({
260 toBranch => $to->branchcode,
261 fromBranch => { 'in' => \@holdingbranches },
264 group_by => [qw/fromBranch/]
265 })->count == scalar(@holdingbranches) ? 0 : 1;
272 =head3 pickup_locations
274 my $pickup_locations = $biblio->pickup_locations({ patron => $patron });
276 Returns a Koha::Libraries set of possible pickup locations for this biblio's items,
277 according to patron's home library and if item can be transferred to each pickup location.
279 Throws a I<Koha::Exceptions::MissingParameter> exception if the B<mandatory> parameter I<patron>
284 sub pickup_locations {
285 my ( $self, $params ) = @_;
287 Koha::Exceptions::MissingParameter->throw( parameter => 'patron' )
288 unless exists $params->{patron};
290 my $patron = $params->{patron};
292 my $memory_cache = Koha::Cache::Memory::Lite->get_instance();
293 my @pickup_locations;
294 foreach my $item ( $self->items->as_list ) {
295 my $cache_key = sprintf "Pickup_locations:%s:%s:%s:%s:%s",
296 $item->itype,$item->homebranch,$item->holdingbranch,$item->ccode || "",$patron->branchcode||"" ;
297 my $item_pickup_locations = $memory_cache->get_from_cache( $cache_key );
298 unless( $item_pickup_locations ){
299 @{ $item_pickup_locations } = $item->pickup_locations( { patron => $patron } )->_resultset->get_column('branchcode')->all;
300 $memory_cache->set_in_cache( $cache_key, $item_pickup_locations );
302 push @pickup_locations, @{ $item_pickup_locations }
305 return Koha::Libraries->search(
306 { branchcode => { '-in' => \@pickup_locations } }, { order_by => ['branchname'] } );
309 =head3 hidden_in_opac
311 my $bool = $biblio->hidden_in_opac({ [ rules => $rules ] })
313 Returns true if the biblio matches the hidding criteria defined in $rules.
314 Returns false otherwise. It involves the I<OpacHiddenItems> and
315 I<OpacHiddenItemsHidesRecord> system preferences.
317 Takes HASHref that can have the following parameters:
319 $rules : { <field> => [ value_1, ... ], ... }
321 Note: $rules inherits its structure from the parsed YAML from reading
322 the I<OpacHiddenItems> system preference.
327 my ( $self, $params ) = @_;
329 my $rules = $params->{rules} // {};
331 my @items = $self->items->as_list;
333 return 0 unless @items; # Do not hide if there is no item
335 # Ok, there are items, don't even try the rules unless OpacHiddenItemsHidesRecord
336 return 0 unless C4::Context->preference('OpacHiddenItemsHidesRecord');
338 return !(any { !$_->hidden_in_opac({ rules => $rules }) } @items);
341 =head3 article_request_type
343 my $type = $biblio->article_request_type( $borrower );
345 Returns the article request type based on items, or on the record
346 itself if there are no items.
348 $borrower must be a Koha::Patron object
352 sub article_request_type {
353 my ( $self, $borrower ) = @_;
355 return q{} unless $borrower;
357 my $rule = $self->article_request_type_for_items( $borrower );
358 return $rule if $rule;
360 # If the record has no items that are requestable, go by the record itemtype
361 $rule = $self->article_request_type_for_bib($borrower);
362 return $rule if $rule;
367 =head3 article_request_type_for_bib
369 my $type = $biblio->article_request_type_for_bib
371 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record
375 sub article_request_type_for_bib {
376 my ( $self, $borrower ) = @_;
378 return q{} unless $borrower;
380 my $borrowertype = $borrower->categorycode;
381 my $itemtype = $self->itemtype();
383 my $rule = Koha::CirculationRules->get_effective_rule(
385 rule_name => 'article_requests',
386 categorycode => $borrowertype,
387 itemtype => $itemtype,
391 return q{} unless $rule;
392 return $rule->rule_value || q{}
395 =head3 article_request_type_for_items
397 my $type = $biblio->article_request_type_for_items
399 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record's items
401 If there is a conflict where some items are 'bib_only' and some are 'item_only', 'bib_only' will be returned.
405 sub article_request_type_for_items {
406 my ( $self, $borrower ) = @_;
409 foreach my $item ( $self->items()->as_list() ) {
410 my $rule = $item->article_request_type($borrower);
411 return $rule if $rule eq 'bib_only'; # we don't need to go any further
415 return 'item_only' if $counts->{item_only};
416 return 'yes' if $counts->{yes};
417 return 'no' if $counts->{no};
421 =head3 article_requests
423 my $article_requests = $biblio->article_requests
425 Returns the article requests associated with this biblio
429 sub article_requests {
432 return Koha::ArticleRequests->_new_from_dbic( scalar $self->_result->article_requests );
435 =head3 current_checkouts
437 my $current_checkouts = $biblio->current_checkouts
439 Returns the current checkouts associated with this biblio
443 sub current_checkouts {
446 return Koha::Checkouts->search( { "item.biblionumber" => $self->id },
447 { join => 'item' } );
452 my $old_checkouts = $biblio->old_checkouts
454 Returns the past checkouts associated with this biblio
461 return Koha::Old::Checkouts->search( { "item.biblionumber" => $self->id },
462 { join => 'item' } );
467 my $items = $biblio->items();
469 Returns the related Koha::Items object for this biblio
474 my ($self,$params) = @_;
476 my $items_rs = $self->_result->items;
478 return Koha::Items->_new_from_dbic( $items_rs ) unless $params->{host_items};
480 my $host_itemnumbers = $self->_host_itemnumbers();
481 my $params = { -or => [biblionumber => $self->id] };
482 push @{$params->{'-or'}}, itemnumber => { -in => $host_itemnumbers } if $host_itemnumbers;
484 return Koha::Items->search($params);
489 my $host_items = $biblio->host_items();
491 Return the host items (easy analytical record)
498 return Koha::Items->new->empty
499 unless C4::Context->preference('EasyAnalyticalRecords');
501 my $host_itemnumbers = $self->_host_itemnumbers;
503 return Koha::Items->search( { itemnumber => { -in => $host_itemnumbers } } );
506 =head3 _host_itemnumbers
508 my $host_itemnumber = $biblio->_host_itemnumbers();
510 Return the itemnumbers for analytical items on this record
514 sub _host_itemnumbers {
517 my $marcflavour = C4::Context->preference("marcflavour");
518 my $analyticfield = '773';
519 if ( $marcflavour eq 'UNIMARC' ) {
520 $analyticfield = '461';
522 my $marc_record = $self->metadata->record;
524 foreach my $field ( $marc_record->field($analyticfield) ) {
525 push @itemnumbers, $field->subfield('9');
527 return \@itemnumbers;
533 my $itemtype = $biblio->itemtype();
535 Returns the itemtype for this record.
542 return $self->biblioitem()->itemtype();
547 my $holds = $biblio->holds();
549 return the current holds placed on this record
554 my ( $self, $params, $attributes ) = @_;
555 $attributes->{order_by} = 'priority' unless exists $attributes->{order_by};
556 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
557 return Koha::Holds->_new_from_dbic($hold_rs);
562 my $holds = $biblio->current_holds
564 Return the holds placed on this bibliographic record.
565 It does not include future holds.
571 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
573 { reservedate => { '<=' => $dtf->format_date(dt_from_string) } } );
578 my $field = $self->biblioitem
580 Returns the related Koha::Biblioitem object for this Biblio object
586 return Koha::Biblioitems->find( { biblionumber => $self->biblionumber } );
591 my $suggestions = $self->suggestions
593 Returns the related Koha::Suggestions object for this Biblio object
600 my $suggestions_rs = $self->_result->suggestions;
601 return Koha::Suggestions->_new_from_dbic( $suggestions_rs );
604 =head3 get_marc_components
606 my $components = $self->get_marc_components();
608 Returns an array of search results data, which are component parts of
609 this object (MARC21 773 points to this)
613 sub get_marc_components {
614 my ($self, $max_results) = @_;
616 return [] if (C4::Context->preference('marcflavour') ne 'MARC21');
618 my ( $searchstr, $sort ) = $self->get_components_query;
621 if (defined($searchstr)) {
622 my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
623 my ( $error, $results, $facets );
625 ( $error, $results, $facets ) = $searcher->search_compat( $searchstr, undef, [$sort], ['biblioserver'], $max_results, 0, undef, undef, 'ccl', 0 );
630 warn "Warning from search_compat: '$error'";
634 message => 'component_search',
639 $components = $results->{biblioserver}->{RECORDS} if defined($results) && $results->{biblioserver}->{hits};
642 return $components // [];
645 =head2 get_components_query
647 Returns a query which can be used to search for all component parts of MARC21 biblios
651 sub get_components_query {
654 my $builder = Koha::SearchEngine::QueryBuilder->new(
655 { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
656 my $marc = $self->metadata->record;
657 my $component_sort_field = C4::Context->preference('ComponentSortField') // "title";
658 my $component_sort_order = C4::Context->preference('ComponentSortOrder') // "asc";
659 my $sort = $component_sort_field . "_" . $component_sort_order;
662 if ( C4::Context->preference('UseControlNumber') ) {
663 my $pf001 = $marc->field('001') || undef;
665 if ( defined($pf001) ) {
667 my $pf003 = $marc->field('003') || undef;
669 if ( !defined($pf003) ) {
670 # search for 773$w='Host001'
671 $searchstr .= "rcn:\"" . $pf001->data()."\"";
675 # search for (773$w='Host001' and 003='Host003') or 773$w='(Host003)Host001'
676 $searchstr .= "(rcn:\"" . $pf001->data() . "\" AND cni:\"" . $pf003->data() . "\")";
677 $searchstr .= " OR rcn:\"" . $pf003->data() . " " . $pf001->data() . "\"";
681 # limit to monograph and serial component part records
682 $searchstr .= " AND (bib-level:a OR bib-level:b)";
687 my $cleaned_title = $marc->subfield('245', "a");
688 $cleaned_title =~ tr|/||;
689 $cleaned_title = $builder->clean_search_term($cleaned_title);
690 $searchstr = qq#Host-item:("$cleaned_title")#;
692 my ($error, $query ,$query_str) = $builder->build_query_compat( undef, [$searchstr], undef, undef, [$sort], 0 );
698 return ($query, $query_str, $sort);
703 my $subscriptions = $self->subscriptions
705 Returns the related Koha::Subscriptions object for this Biblio object
711 my $rs = $self->_result->subscriptions;
712 return Koha::Subscriptions->_new_from_dbic($rs);
715 =head3 has_items_waiting_or_intransit
717 my $itemsWaitingOrInTransit = $biblio->has_items_waiting_or_intransit
719 Tells if this bibliographic record has items waiting or in transit.
723 sub has_items_waiting_or_intransit {
726 if ( Koha::Holds->search({ biblionumber => $self->id,
727 found => ['W', 'T'] })->count ) {
731 foreach my $item ( $self->items->as_list ) {
732 return 1 if $item->get_transfer;
740 my $coins = $biblio->get_coins;
742 Returns the COinS (a span) which can be included in a biblio record
749 my $record = $self->metadata->record;
751 my $pos7 = substr $record->leader(), 7, 1;
752 my $pos6 = substr $record->leader(), 6, 1;
755 my ( $aulast, $aufirst ) = ( '', '' );
766 # For the purposes of generating COinS metadata, LDR/06-07 can be
767 # considered the same for UNIMARC and MARC21
776 'i' => 'audioRecording',
777 'j' => 'audioRecording',
780 'm' => 'computerProgram',
785 'a' => 'journalArticle',
789 $genre = $fmts6->{$pos6} ? $fmts6->{$pos6} : 'book';
791 if ( $genre eq 'book' ) {
792 $genre = $fmts7->{$pos7} if $fmts7->{$pos7};
795 ##### We must transform mtx to a valable mtx and document type ####
796 if ( $genre eq 'book' ) {
799 } elsif ( $genre eq 'journal' ) {
802 } elsif ( $genre eq 'journalArticle' ) {
810 if ( C4::Context->preference("marcflavour") eq "UNIMARC" ) {
813 $aulast = $record->subfield( '700', 'a' ) || '';
814 $aufirst = $record->subfield( '700', 'b' ) || '';
815 push @authors, "$aufirst $aulast" if ($aufirst or $aulast);
818 if ( $record->field('200') ) {
819 for my $au ( $record->field('200')->subfield('g') ) {
824 $title = $record->subfield( '200', 'a' );
825 my $subfield_210d = $record->subfield('210', 'd');
826 if ($subfield_210d and $subfield_210d =~ /(\d{4})/) {
829 $publisher = $record->subfield( '210', 'c' ) || '';
830 $isbn = $record->subfield( '010', 'a' ) || '';
831 $issn = $record->subfield( '011', 'a' ) || '';
834 # MARC21 need some improve
837 if ( $record->field('100') ) {
838 push @authors, $record->subfield( '100', 'a' );
842 if ( $record->field('700') ) {
843 for my $au ( $record->field('700')->subfield('a') ) {
847 $title = $record->field('245');
848 $title &&= $title->as_string('ab');
849 if ($titletype eq 'a') {
850 $pubyear = $record->field('008') || '';
851 $pubyear = substr($pubyear->data(), 7, 4) if $pubyear;
852 $isbn = $record->subfield( '773', 'z' ) || '';
853 $issn = $record->subfield( '773', 'x' ) || '';
854 $hosttitle = $record->subfield( '773', 't' ) || $record->subfield( '773', 'a') || q{};
855 my @rels = $record->subfield( '773', 'g' );
856 $pages = join(', ', @rels);
858 $pubyear = $record->subfield( '260', 'c' ) || '';
859 $publisher = $record->subfield( '260', 'b' ) || '';
860 $isbn = $record->subfield( '020', 'a' ) || '';
861 $issn = $record->subfield( '022', 'a' ) || '';
867 [ 'ctx_ver', 'Z39.88-2004' ],
868 [ 'rft_val_fmt', "info:ofi/fmt:kev:mtx:$mtx" ],
869 [ ($mtx eq 'dc' ? 'rft.type' : 'rft.genre'), $genre ],
870 [ "rft.${titletype}title", $title ],
873 # rft.title is authorized only once, so by checking $titletype
874 # we ensure that rft.title is not already in the list.
875 if ($hosttitle and $titletype) {
876 push @params, [ 'rft.title', $hosttitle ];
880 [ 'rft.isbn', $isbn ],
881 [ 'rft.issn', $issn ],
884 # If it's a subscription, these informations have no meaning.
885 if ($genre ne 'journal') {
887 [ 'rft.aulast', $aulast ],
888 [ 'rft.aufirst', $aufirst ],
889 (map { [ 'rft.au', $_ ] } @authors),
890 [ 'rft.pub', $publisher ],
891 [ 'rft.date', $pubyear ],
892 [ 'rft.pages', $pages ],
896 my $coins_value = join( '&',
897 map { $$_[1] ? $$_[0] . '=' . uri_escape_utf8( $$_[1] ) : () } @params );
904 my $url = $biblio->get_openurl;
906 Returns url for OpenURL resolver set in OpenURLResolverURL system preference
913 my $OpenURLResolverURL = C4::Context->preference('OpenURLResolverURL');
915 if ($OpenURLResolverURL) {
916 my $uri = URI->new($OpenURLResolverURL);
918 if (not defined $uri->query) {
919 $OpenURLResolverURL .= '?';
921 $OpenURLResolverURL .= '&';
923 $OpenURLResolverURL .= $self->get_coins;
926 return $OpenURLResolverURL;
931 my $serial = $biblio->is_serial
933 Return boolean true if this bibbliographic record is continuing resource
940 return 1 if $self->serial;
942 my $record = $self->metadata->record;
943 return 1 if substr($record->leader, 7, 1) eq 's';
948 =head3 custom_cover_image_url
950 my $image_url = $biblio->custom_cover_image_url
952 Return the specific url of the cover image for this bibliographic record.
953 It is built regaring the value of the system preference CustomCoverImagesURL
957 sub custom_cover_image_url {
959 my $url = C4::Context->preference('CustomCoverImagesURL');
960 if ( $url =~ m|{isbn}| ) {
961 my $isbn = $self->biblioitem->isbn;
963 $url =~ s|{isbn}|$isbn|g;
965 if ( $url =~ m|{normalized_isbn}| ) {
966 my $normalized_isbn = C4::Koha::GetNormalizedISBN($self->biblioitem->isbn);
967 return unless $normalized_isbn;
968 $url =~ s|{normalized_isbn}|$normalized_isbn|g;
970 if ( $url =~ m|{issn}| ) {
971 my $issn = $self->biblioitem->issn;
973 $url =~ s|{issn}|$issn|g;
976 my $re = qr|{(?<field>\d{3})(\$(?<subfield>.))?}|;
978 my $field = $+{field};
979 my $subfield = $+{subfield};
980 my $marc_record = $self->metadata->record;
983 $value = $marc_record->subfield( $field, $subfield );
985 my $controlfield = $marc_record->field($field);
986 $value = $controlfield->data() if $controlfield;
988 return unless $value;
989 $url =~ s|$re|$value|;
997 Return the cover images associated with this biblio.
1004 my $cover_images_rs = $self->_result->cover_images;
1005 return unless $cover_images_rs;
1006 return Koha::CoverImages->_new_from_dbic($cover_images_rs);
1009 =head3 get_marc_notes
1011 $marcnotesarray = $biblio->get_marc_notes({ opac => 1 });
1013 Get all notes from the MARC record and returns them in an array.
1014 The notes are stored in different fields depending on MARC flavour.
1015 MARC21 5XX $u subfields receive special attention as they are URIs.
1019 sub get_marc_notes {
1020 my ( $self, $params ) = @_;
1022 my $marcflavour = C4::Context->preference('marcflavour');
1023 my $opac = $params->{opac} // '0';
1024 my $interface = $params->{opac} ? 'opac' : 'intranet';
1026 my $record = $params->{record} // $self->metadata->record;
1027 my $record_processor = Koha::RecordProcessor->new(
1029 filters => [ 'ViewPolicy', 'ExpandCodedFields' ],
1031 interface => $interface,
1032 frameworkcode => $self->frameworkcode
1036 $record_processor->process($record);
1038 my $scope = $marcflavour eq "UNIMARC"? '3..': '5..';
1039 #MARC21 specs indicate some notes should be private if first indicator 0
1040 my %maybe_private = (
1048 my %hiddenlist = map { $_ => 1 }
1049 split( /,/, C4::Context->preference('NotesToHide'));
1052 foreach my $field ( $record->field($scope) ) {
1053 my $tag = $field->tag();
1054 next if $hiddenlist{ $tag };
1055 next if $opac && $maybe_private{$tag} && !$field->indicator(1);
1056 if( $marcflavour ne 'UNIMARC' && $field->subfield('u') ) {
1057 # Field 5XX$u always contains URI
1058 # Examples: 505u, 506u, 510u, 514u, 520u, 530u, 538u, 540u, 542u, 552u, 555u, 561u, 563u, 583u
1059 # We first push the other subfields, then all $u's separately
1060 # Leave further actions to the template (see e.g. opac-detail)
1062 join '', ( 'a' .. 't', 'v' .. 'z', '0' .. '9' ); # excl 'u'
1063 push @marcnotes, { marcnote => $field->as_string($othersub) };
1064 foreach my $sub ( $field->subfield('u') ) {
1065 $sub =~ s/^\s+|\s+$//g; # trim
1066 push @marcnotes, { marcnote => $sub };
1069 push @marcnotes, { marcnote => $field->as_string() };
1075 =head3 _get_marc_authors
1077 Private method to return the list of authors contained in the MARC record.
1078 See get get_marc_contributors and get_marc_authors for the public methods.
1082 sub _get_marc_authors {
1083 my ( $self, $params ) = @_;
1085 my $fields_filter = $params->{fields_filter};
1086 my $mintag = $params->{mintag};
1087 my $maxtag = $params->{maxtag};
1089 my $AuthoritySeparator = C4::Context->preference('AuthoritySeparator');
1090 my $marcflavour = C4::Context->preference('marcflavour');
1092 # tagslib useful only for UNIMARC author responsibilities
1093 my $tagslib = $marcflavour eq "UNIMARC"
1094 ? C4::Biblio::GetMarcStructure( 1, $self->frameworkcode, { unsafe => 1 } )
1098 foreach my $field ( $self->metadata->record->field($fields_filter) ) {
1101 if $mintag && $field->tag() < $mintag
1102 || $maxtag && $field->tag() > $maxtag;
1106 my @subfields = $field->subfields();
1109 # if there is an authority link, build the link with Koha-Auth-Number: subfield9
1110 my $subfield9 = $field->subfield('9');
1112 my $linkvalue = $subfield9;
1113 $linkvalue =~ s/(\(|\))//g;
1114 @link_loop = ( { 'limit' => 'an', 'link' => $linkvalue } );
1119 for my $authors_subfield (@subfields) {
1120 next if ( $authors_subfield->[0] eq '9' );
1122 # unimarc3 contains the $3 of the author for UNIMARC.
1123 # For french academic libraries, it's the "ppn", and it's required for idref webservice
1124 $unimarc3 = $authors_subfield->[1] if $marcflavour eq 'UNIMARC' and $authors_subfield->[0] =~ /3/;
1126 # don't load unimarc subfields 3, 5
1127 next if ( $marcflavour eq 'UNIMARC' and ( $authors_subfield->[0] =~ /3|5/ ) );
1129 my $code = $authors_subfield->[0];
1130 my $value = $authors_subfield->[1];
1131 my $linkvalue = $value;
1132 $linkvalue =~ s/(\(|\))//g;
1133 # UNIMARC author responsibility
1134 if ( $marcflavour eq 'UNIMARC' and $code eq '4' ) {
1135 $value = C4::Biblio::GetAuthorisedValueDesc( $field->tag(), $code, $value, '', $tagslib );
1136 $linkvalue = "($value)";
1138 # if no authority link, build a search query
1139 unless ($subfield9) {
1142 'link' => $linkvalue,
1143 operator => (scalar @link_loop) ? ' AND ' : undef
1146 my @this_link_loop = @link_loop;
1148 unless ( $code eq '0') {
1149 push @subfields_loop, {
1150 tag => $field->tag(),
1153 link_loop => \@this_link_loop,
1154 separator => (scalar @subfields_loop) ? $AuthoritySeparator : ''
1158 push @marcauthors, {
1159 MARCAUTHOR_SUBFIELDS_LOOP => \@subfields_loop,
1160 authoritylink => $subfield9,
1161 unimarc3 => $unimarc3
1164 return \@marcauthors;
1167 =head3 get_marc_contributors
1169 my $contributors = $biblio->get_marc_contributors;
1171 Get all contributors (but first author) from the MARC record and returns them in an array.
1172 They are stored in different fields depending on MARC flavour (700..720 for MARC21)
1176 sub get_marc_contributors {
1177 my ( $self, $params ) = @_;
1179 my ( $mintag, $maxtag, $fields_filter );
1180 my $marcflavour = C4::Context->preference('marcflavour');
1182 if ( $marcflavour eq "UNIMARC" ) {
1185 $fields_filter = '7..';
1186 } else { # marc21/normarc
1189 $fields_filter = '7..';
1192 return $self->_get_marc_authors(
1194 fields_filter => $fields_filter,
1201 =head3 get_marc_authors
1203 my $authors = $biblio->get_marc_authors;
1205 Get all authors from the MARC record and returns them in an array.
1206 They are stored in different fields depending on MARC flavour
1207 (main author from 100 then secondary authors from 700..720).
1211 sub get_marc_authors {
1212 my ( $self, $params ) = @_;
1214 my ( $mintag, $maxtag, $fields_filter );
1215 my $marcflavour = C4::Context->preference('marcflavour');
1217 if ( $marcflavour eq "UNIMARC" ) {
1218 $fields_filter = '200';
1219 } else { # marc21/normarc
1220 $fields_filter = '100';
1223 my @first_authors = @{$self->_get_marc_authors(
1225 fields_filter => $fields_filter,
1231 my @other_authors = @{$self->get_marc_contributors};
1233 return [@first_authors, @other_authors];
1239 my $json = $biblio->to_api;
1241 Overloaded method that returns a JSON representation of the Koha::Biblio object,
1242 suitable for API output. The related Koha::Biblioitem object is merged as expected
1248 my ($self, $args) = @_;
1250 my $response = $self->SUPER::to_api( $args );
1251 my $biblioitem = $self->biblioitem->to_api;
1253 return { %$response, %$biblioitem };
1256 =head3 to_api_mapping
1258 This method returns the mapping for representing a Koha::Biblio object
1263 sub to_api_mapping {
1265 biblionumber => 'biblio_id',
1266 frameworkcode => 'framework_id',
1267 unititle => 'uniform_title',
1268 seriestitle => 'series_title',
1269 copyrightdate => 'copyright_date',
1270 datecreated => 'creation_date',
1271 deleted_on => undef,
1275 =head3 get_marc_host
1277 $host = $biblio->get_marc_host;
1279 ( $host, $relatedparts, $hostinfo ) = $biblio->get_marc_host;
1281 Returns host biblio record from MARC21 773 (undef if no 773 present).
1282 It looks at the first 773 field with MARCorgCode or only a control
1283 number. Complete $w or numeric part is used to search host record.
1284 The optional parameter no_items triggers a check if $biblio has items.
1285 If there are, the sub returns undef.
1286 Called in list context, it also returns 773$g (related parts).
1288 If there is no $w, we use $0 (host biblionumber) or $9 (host itemnumber)
1289 to search for the host record. If there is also no $0 and no $9, we search
1290 using author and title. Failing all of that, we return an undef host and
1291 form a concatenation of strings with 773$agt for host information,
1292 returned when called in list context.
1297 my ($self, $params) = @_;
1298 my $no_items = $params->{no_items};
1299 return if C4::Context->preference('marcflavour') eq 'UNIMARC'; # TODO
1300 return if $params->{no_items} && $self->items->count > 0;
1303 eval { $record = $self->metadata->record };
1306 # We pick the first $w with your MARCOrgCode or the first $w that has no
1307 # code (between parentheses) at all.
1308 my $orgcode = C4::Context->preference('MARCOrgCode') // q{};
1310 foreach my $f ( $record->field('773') ) {
1311 my $w = $f->subfield('w') or next;
1312 if( $w =~ /^\($orgcode\)\s*(\d+)/i or $w =~ /^\d+/ ) {
1318 my $engine = Koha::SearchEngine::Search->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1320 if ( !$hostfld and $record->subfield('773','t') ) {
1321 # not linked using $w
1322 my $unlinkedf = $record->field('773');
1324 if ( C4::Context->preference("EasyAnalyticalRecords") ) {
1325 if ( $unlinkedf->subfield('0') ) {
1326 # use 773$0 host biblionumber
1327 $bibno = $unlinkedf->subfield('0');
1328 } elsif ( $unlinkedf->subfield('9') ) {
1329 # use 773$9 host itemnumber
1330 my $linkeditemnumber = $unlinkedf->subfield('9');
1331 $bibno = Koha::Items->find( $linkeditemnumber )->biblionumber;
1335 my $host = Koha::Biblios->find($bibno) or return;
1336 return wantarray ? ( $host, $unlinkedf->subfield('g') ) : $host;
1338 # just return plaintext and no host record
1339 my $hostinfo = join( ", ", $unlinkedf->subfield('a'), $unlinkedf->subfield('t'), $unlinkedf->subfield('g') );
1340 return wantarray ? ( undef, $unlinkedf->subfield('g'), $hostinfo ) : undef;
1342 return if !$hostfld;
1343 my $rcn = $hostfld->subfield('w');
1345 # Look for control number with/without orgcode
1346 for my $try (1..2) {
1347 my ( $error, $results, $total_hits ) = $engine->simple_search_compat( 'Control-number='.$rcn, 0,1 );
1348 if( !$error and $total_hits == 1 ) {
1349 $bibno = $engine->extract_biblionumber( $results->[0] );
1352 # Add or remove orgcode for second try
1353 if( $try == 1 && $rcn =~ /\)\s*(\d+)/ ) {
1354 $rcn = $1; # number only
1355 } elsif( $try == 1 && $rcn =~ /^\d+/ ) {
1356 $rcn = "($orgcode)$rcn";
1362 my $host = Koha::Biblios->find($bibno) or return;
1363 return wantarray ? ( $host, $hostfld->subfield('g') ) : $host;
1369 my $recalls = $biblio->recalls;
1371 Return recalls linked to this biblio
1377 return Koha::Recalls->_new_from_dbic( scalar $self->_result->recalls );
1380 =head3 can_be_recalled
1382 my @items_for_recall = $biblio->can_be_recalled({ patron => $patron_object });
1384 Does biblio-level checks and returns the items attached to this biblio that are available for recall
1388 sub can_be_recalled {
1389 my ( $self, $params ) = @_;
1391 return 0 if !( C4::Context->preference('UseRecalls') );
1393 my $patron = $params->{patron};
1395 my $branchcode = C4::Context->userenv->{'branch'};
1396 if ( C4::Context->preference('CircControl') eq 'PatronLibrary' and $patron ) {
1397 $branchcode = $patron->branchcode;
1400 my @all_items = Koha::Items->search({ biblionumber => $self->biblionumber })->as_list;
1402 # if there are no available items at all, no recall can be placed
1403 return 0 if ( scalar @all_items == 0 );
1408 my @all_itemnumbers;
1409 foreach my $item ( @all_items ) {
1410 push( @all_itemnumbers, $item->itemnumber );
1411 if ( $item->can_be_recalled({ patron => $patron }) ) {
1412 push( @itemtypes, $item->effective_itemtype );
1413 push( @itemnumbers, $item->itemnumber );
1414 push( @items, $item );
1418 # if there are no recallable items, no recall can be placed
1419 return 0 if ( scalar @items == 0 );
1421 # Check the circulation rule for each relevant itemtype for this biblio
1422 my ( @recalls_allowed, @recalls_per_record, @on_shelf_recalls );
1423 foreach my $itemtype ( @itemtypes ) {
1424 my $rule = Koha::CirculationRules->get_effective_rules({
1425 branchcode => $branchcode,
1426 categorycode => $patron ? $patron->categorycode : undef,
1427 itemtype => $itemtype,
1430 'recalls_per_record',
1434 push( @recalls_allowed, $rule->{recalls_allowed} ) if $rule;
1435 push( @recalls_per_record, $rule->{recalls_per_record} ) if $rule;
1436 push( @on_shelf_recalls, $rule->{on_shelf_recalls} ) if $rule;
1438 my $recalls_allowed = (sort {$b <=> $a} @recalls_allowed)[0]; # take highest
1439 my $recalls_per_record = (sort {$b <=> $a} @recalls_per_record)[0]; # take highest
1440 my %on_shelf_recalls_count = ();
1441 foreach my $count ( @on_shelf_recalls ) {
1442 $on_shelf_recalls_count{$count}++;
1444 my $on_shelf_recalls = (sort {$on_shelf_recalls_count{$b} <=> $on_shelf_recalls_count{$a}} @on_shelf_recalls)[0]; # take most common
1446 # check recalls allowed has been set and is not zero
1447 return 0 if ( !defined($recalls_allowed) || $recalls_allowed == 0 );
1450 # check borrower has not reached open recalls allowed limit
1451 return 0 if ( $patron->recalls->filter_by_current->count >= $recalls_allowed );
1453 # check borrower has not reached open recalls allowed per record limit
1454 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $recalls_per_record );
1456 # check if any of the items under this biblio are already checked out by this borrower
1457 return 0 if ( Koha::Checkouts->search({ itemnumber => [ @all_itemnumbers ], borrowernumber => $patron->borrowernumber })->count > 0 );
1460 # check item availability
1461 my $checked_out_count = 0;
1463 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1466 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1467 return 0 if ( $on_shelf_recalls eq 'all' && $checked_out_count < scalar @items );
1469 # can't recall if no items have been checked out
1470 return 0 if ( $checked_out_count == 0 );
1476 =head2 Internal methods
1488 Kyle M Hall <kyle@bywatersolutions.com>