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 GetNormalizedUPC GetNormalizedOCLCNumber );
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::Metadata::Extractor;
37 use Koha::Biblio::ItemGroups;
38 use Koha::Biblioitems;
39 use Koha::Cache::Memory::Lite;
42 use Koha::CirculationRules;
44 use Koha::Illrequests;
45 use Koha::Item::Transfer::Limits;
48 use Koha::Old::Checkouts;
51 use Koha::RecordProcessor;
52 use Koha::Suggestions;
53 use Koha::Subscriptions;
54 use Koha::SearchEngine;
55 use Koha::SearchEngine::Search;
56 use Koha::SearchEngine::QueryBuilder;
61 Koha::Biblio - Koha Biblio Object class
71 Overloaded I<store> method to set default values
78 $self->datecreated( dt_from_string ) unless $self->datecreated;
80 return $self->SUPER::store;
85 my $metadata = $biblio->metadata();
87 Returns a Koha::Biblio::Metadata object
94 my $metadata = $self->_result->metadata;
95 return Koha::Biblio::Metadata->_new_from_dbic($metadata);
100 my $record = $biblio->record();
102 Returns a Marc::Record object
109 return $self->metadata->record;
114 my $schema = $biblio->record_schema();
116 Returns the record schema (MARC21, USMARC or UNIMARC).
123 return $self->metadata->schema // C4::Context->preference("marcflavour");
128 my $orders = $biblio->orders();
130 Returns a Koha::Acquisition::Orders object
137 my $orders = $self->_result->orders;
138 return Koha::Acquisition::Orders->_new_from_dbic($orders);
143 my $active_orders = $biblio->active_orders();
145 Returns the active acquisition orders related to this biblio.
146 An order is considered active when it is not cancelled (i.e. when datecancellation
154 return $self->orders->search({ datecancellationprinted => undef });
159 my $tickets = $biblio->tickets();
161 Returns all tickets linked to the biblio
167 my $rs = $self->_result->tickets;
168 return Koha::Tickets->_new_from_dbic( $rs );
173 my $ill_requests = $biblio->ill_requests();
175 Returns a Koha::Illrequests object
182 my $ill_requests = $self->_result->ill_requests;
183 return Koha::Illrequests->_new_from_dbic($ill_requests);
188 my $item_groups = $biblio->item_groups();
190 Returns a Koha::Biblio::ItemGroups object
197 my $item_groups = $self->_result->item_groups;
198 return Koha::Biblio::ItemGroups->_new_from_dbic($item_groups);
201 =head3 can_article_request
203 my $bool = $biblio->can_article_request( $borrower );
205 Returns true if article requests can be made for this record
207 $borrower must be a Koha::Patron object
211 sub can_article_request {
212 my ( $self, $borrower ) = @_;
214 my $rule = $self->article_request_type($borrower);
215 return q{} if $rule eq 'item_only' && !$self->items()->count();
216 return 1 if $rule && $rule ne 'no';
226 $biblio->check_booking( { start_date => $datetime, end_date => $datetime, [ booking_id => $booking_id ] } );
228 Returns a boolean denoting whether the passed booking can be made without clashing.
230 Optionally, you may pass a booking id to exclude from the checks; This is helpful when you are updating an existing booking.
235 my ( $self, $params ) = @_;
237 my $start_date = dt_from_string( $params->{start_date} );
238 my $end_date = dt_from_string( $params->{end_date} );
239 my $booking_id = $params->{booking_id};
241 my $bookable_items = $self->bookable_items;
242 my $total_bookable = $bookable_items->count;
244 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
245 my $existing_bookings = $self->bookings(
249 $dtf->format_datetime($start_date),
250 $dtf->format_datetime($end_date)
255 $dtf->format_datetime($start_date),
256 $dtf->format_datetime($end_date)
260 start_date => { '<' => $dtf->format_datetime($start_date) },
261 end_date => { '>' => $dtf->format_datetime($end_date) }
268 ? $existing_bookings->search( { booking_id => { '!=' => $booking_id } } )->count
269 : $existing_bookings->count;
271 my $checkouts = $self->current_checkouts->search( { date_due => { '>=' => $dtf->format_datetime($start_date) } } );
272 $booked_count += $checkouts->count;
274 return ( ( $total_bookable - $booked_count ) > 0 ) ? 1 : 0;
277 =head3 assign_item_for_booking
281 sub assign_item_for_booking {
282 my ( $self, $params ) = @_;
284 my $start_date = dt_from_string( $params->{start_date} );
285 my $end_date = dt_from_string( $params->{end_date} );
287 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
289 my $existing_bookings = $self->bookings(
293 $dtf->format_datetime($start_date),
294 $dtf->format_datetime($end_date)
299 $dtf->format_datetime($start_date),
300 $dtf->format_datetime($end_date)
304 start_date => { '<' => $dtf->format_datetime($start_date) },
305 end_date => { '>' => $dtf->format_datetime($end_date) }
310 my $checkouts = $self->current_checkouts->search( { date_due => { '>=' => $dtf->format_datetime($start_date) } } );
312 my $bookable_items = $self->bookable_items->search(
315 '-and' => { '-not_in' => $existing_bookings->_resultset->get_column('item_id')->as_query },
316 { '-not_in' => $checkouts->_resultset->get_column('itemnumber')->as_query }
321 return $bookable_items->single->itemnumber;
324 =head3 can_be_transferred
326 $biblio->can_be_transferred({ to => $to_library, from => $from_library })
328 Checks if at least one item of a biblio can be transferred to given library.
330 This feature is controlled by two system preferences:
331 UseBranchTransferLimits to enable / disable the feature
332 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
333 for setting the limitations
335 Performance-wise, it is recommended to use this method for a biblio instead of
336 iterating each item of a biblio with Koha::Item->can_be_transferred().
338 Takes HASHref that can have the following parameters:
339 MANDATORY PARAMETERS:
342 $from : Koha::Library # if given, only items from that
343 # holdingbranch are considered
345 Returns 1 if at least one of the item of a biblio can be transferred
346 to $to_library, otherwise 0.
350 sub can_be_transferred {
351 my ($self, $params) = @_;
353 my $to = $params->{to};
354 my $from = $params->{from};
356 return 1 unless C4::Context->preference('UseBranchTransferLimits');
357 my $limittype = C4::Context->preference('BranchTransferLimitsType');
360 foreach my $item_of_bib ($self->items->as_list) {
361 next unless $item_of_bib->holdingbranch;
362 next if $from && $from->branchcode ne $item_of_bib->holdingbranch;
363 return 1 if $item_of_bib->holdingbranch eq $to->branchcode;
364 my $code = $limittype eq 'itemtype'
365 ? $item_of_bib->effective_itemtype
366 : $item_of_bib->ccode;
367 return 1 unless $code;
368 $items->{$code}->{$item_of_bib->holdingbranch} = 1;
371 # At this point we will have a HASHref containing each itemtype/ccode that
372 # this biblio has, inside which are all of the holdingbranches where those
373 # items are located at. Then, we will query Koha::Item::Transfer::Limits to
374 # find out whether a transfer limits for such $limittype from any of the
375 # listed holdingbranches to the given $to library exist. If at least one
376 # holdingbranch for that $limittype does not have a transfer limit to given
377 # $to library, then we know that the transfer is possible.
378 foreach my $code (keys %{$items}) {
379 my @holdingbranches = keys %{$items->{$code}};
380 return 1 if Koha::Item::Transfer::Limits->search({
381 toBranch => $to->branchcode,
382 fromBranch => { 'in' => \@holdingbranches },
385 group_by => [qw/fromBranch/]
386 })->count == scalar(@holdingbranches) ? 0 : 1;
393 =head3 pickup_locations
395 my $pickup_locations = $biblio->pickup_locations({ patron => $patron });
397 Returns a Koha::Libraries set of possible pickup locations for this biblio's items,
398 according to patron's home library and if item can be transferred to each pickup location.
400 Throws a I<Koha::Exceptions::MissingParameter> exception if the B<mandatory> parameter I<patron>
405 sub pickup_locations {
406 my ( $self, $params ) = @_;
408 Koha::Exceptions::MissingParameter->throw( parameter => 'patron' )
409 unless exists $params->{patron};
411 my $patron = $params->{patron};
413 my $memory_cache = Koha::Cache::Memory::Lite->get_instance();
414 my @pickup_locations;
415 foreach my $item ( $self->items->as_list ) {
416 my $cache_key = sprintf "Pickup_locations:%s:%s:%s:%s:%s",
417 $item->itype,$item->homebranch,$item->holdingbranch,$item->ccode || "",$patron->branchcode||"" ;
418 my $item_pickup_locations = $memory_cache->get_from_cache( $cache_key );
419 unless( $item_pickup_locations ){
420 @{ $item_pickup_locations } = $item->pickup_locations( { patron => $patron } )->_resultset->get_column('branchcode')->all;
421 $memory_cache->set_in_cache( $cache_key, $item_pickup_locations );
423 push @pickup_locations, @{ $item_pickup_locations }
426 return Koha::Libraries->search(
427 { branchcode => { '-in' => \@pickup_locations } }, { order_by => ['branchname'] } );
430 =head3 hidden_in_opac
432 my $bool = $biblio->hidden_in_opac({ [ rules => $rules ] })
434 Returns true if the biblio matches the hidding criteria defined in $rules.
435 Returns false otherwise. It involves the I<OpacHiddenItems> and
436 I<OpacHiddenItemsHidesRecord> system preferences.
438 Takes HASHref that can have the following parameters:
440 $rules : { <field> => [ value_1, ... ], ... }
442 Note: $rules inherits its structure from the parsed YAML from reading
443 the I<OpacHiddenItems> system preference.
448 my ( $self, $params ) = @_;
450 my $rules = $params->{rules} // {};
452 my @items = $self->items->as_list;
454 return 0 unless @items; # Do not hide if there is no item
456 # Ok, there are items, don't even try the rules unless OpacHiddenItemsHidesRecord
457 return 0 unless C4::Context->preference('OpacHiddenItemsHidesRecord');
459 return !(any { !$_->hidden_in_opac({ rules => $rules }) } @items);
462 =head3 article_request_type
464 my $type = $biblio->article_request_type( $borrower );
466 Returns the article request type based on items, or on the record
467 itself if there are no items.
469 $borrower must be a Koha::Patron object
473 sub article_request_type {
474 my ( $self, $borrower ) = @_;
476 return q{} unless $borrower;
478 my $rule = $self->article_request_type_for_items( $borrower );
479 return $rule if $rule;
481 # If the record has no items that are requestable, go by the record itemtype
482 $rule = $self->article_request_type_for_bib($borrower);
483 return $rule if $rule;
488 =head3 article_request_type_for_bib
490 my $type = $biblio->article_request_type_for_bib
492 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record
496 sub article_request_type_for_bib {
497 my ( $self, $borrower ) = @_;
499 return q{} unless $borrower;
501 my $borrowertype = $borrower->categorycode;
502 my $itemtype = $self->itemtype();
504 my $rule = Koha::CirculationRules->get_effective_rule(
506 rule_name => 'article_requests',
507 categorycode => $borrowertype,
508 itemtype => $itemtype,
512 return q{} unless $rule;
513 return $rule->rule_value || q{}
516 =head3 article_request_type_for_items
518 my $type = $biblio->article_request_type_for_items
520 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record's items
522 If there is a conflict where some items are 'bib_only' and some are 'item_only', 'bib_only' will be returned.
526 sub article_request_type_for_items {
527 my ( $self, $borrower ) = @_;
530 foreach my $item ( $self->items()->as_list() ) {
531 my $rule = $item->article_request_type($borrower);
532 return $rule if $rule eq 'bib_only'; # we don't need to go any further
536 return 'item_only' if $counts->{item_only};
537 return 'yes' if $counts->{yes};
538 return 'no' if $counts->{no};
542 =head3 article_requests
544 my $article_requests = $biblio->article_requests
546 Returns the article requests associated with this biblio
550 sub article_requests {
553 return Koha::ArticleRequests->_new_from_dbic( scalar $self->_result->article_requests );
556 =head3 current_checkouts
558 my $current_checkouts = $biblio->current_checkouts
560 Returns the current checkouts associated with this biblio
564 sub current_checkouts {
567 return Koha::Checkouts->search( { "item.biblionumber" => $self->id },
568 { join => 'item' } );
573 my $old_checkouts = $biblio->old_checkouts
575 Returns the past checkouts associated with this biblio
582 return Koha::Old::Checkouts->search( { "item.biblionumber" => $self->id },
583 { join => 'item' } );
588 my $items = $biblio->items({ [ host_items => 1 ] });
590 The optional param host_items allows you to include 'analytical' items.
592 Returns the related Koha::Items object for this biblio
597 my ($self,$params) = @_;
599 my $items_rs = $self->_result->items;
601 return Koha::Items->_new_from_dbic( $items_rs ) unless $params->{host_items};
603 my @itemnumbers = $items_rs->get_column('itemnumber')->all;
604 my $host_itemnumbers = $self->_host_itemnumbers();
605 push @itemnumbers, @{ $host_itemnumbers };
606 return Koha::Items->search({ "me.itemnumber" => { -in => \@itemnumbers } });
609 =head3 bookable_items
611 my $bookable_items = $biblio->bookable_items;
613 Returns the related Koha::Items resultset filtered to those items that can be booked.
619 return $self->items->filter_by_bookable;
624 my $host_items = $biblio->host_items();
626 Return the host items (easy analytical record)
633 return Koha::Items->new->empty
634 unless C4::Context->preference('EasyAnalyticalRecords');
636 my $host_itemnumbers = $self->_host_itemnumbers;
638 return Koha::Items->search( { itemnumber => { -in => $host_itemnumbers } } );
641 =head3 _host_itemnumbers
643 my $host_itemnumber = $biblio->_host_itemnumbers();
645 Return the itemnumbers for analytical items on this record
649 sub _host_itemnumbers {
652 my $marcflavour = C4::Context->preference("marcflavour");
653 my $analyticfield = '773';
654 if ( $marcflavour eq 'UNIMARC' ) {
655 $analyticfield = '461';
657 my $marc_record = $self->metadata->record;
659 foreach my $field ( $marc_record->field($analyticfield) ) {
660 push @itemnumbers, $field->subfield('9');
662 return \@itemnumbers;
668 my $itemtype = $biblio->itemtype();
670 Returns the itemtype for this record.
677 return $self->biblioitem()->itemtype();
682 my $holds = $biblio->holds();
684 return the current holds placed on this record
689 my ( $self, $params, $attributes ) = @_;
690 $attributes->{order_by} = 'priority' unless exists $attributes->{order_by};
691 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
692 return Koha::Holds->_new_from_dbic($hold_rs);
697 my $holds = $biblio->current_holds
699 Return the holds placed on this bibliographic record.
700 It does not include future holds.
706 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
708 { reservedate => { '<=' => $dtf->format_date(dt_from_string) } } );
713 my $field = $self->biblioitem
715 Returns the related Koha::Biblioitem object for this Biblio object
721 return Koha::Biblioitems->find( { biblionumber => $self->biblionumber } );
726 my $bookings = $item->bookings();
728 Returns the bookings attached to this biblio.
733 my ( $self, $params ) = @_;
734 my $bookings_rs = $self->_result->bookings->search($params);
735 return Koha::Bookings->_new_from_dbic($bookings_rs);
740 my $suggestions = $self->suggestions
742 Returns the related Koha::Suggestions object for this Biblio object
749 my $suggestions_rs = $self->_result->suggestions;
750 return Koha::Suggestions->_new_from_dbic( $suggestions_rs );
753 =head3 get_marc_components
755 my $components = $self->get_marc_components();
757 Returns an array of search results data, which are component parts of
758 this object (MARC21 773 points to this)
762 sub get_marc_components {
763 my ($self, $max_results) = @_;
765 return [] if (C4::Context->preference('marcflavour') ne 'MARC21');
767 my ( $searchstr, $sort ) = $self->get_components_query;
770 if (defined($searchstr)) {
771 my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
772 my ( $error, $results, $facets );
774 ( $error, $results, $facets ) = $searcher->search_compat( $searchstr, undef, [$sort], ['biblioserver'], $max_results, 0, undef, undef, 'ccl', 0 );
779 warn "Warning from search_compat: '$error'";
783 message => 'component_search',
788 $components = $results->{biblioserver}->{RECORDS} if defined($results) && $results->{biblioserver}->{hits};
791 return $components // [];
794 =head2 get_components_query
796 Returns a query which can be used to search for all component parts of MARC21 biblios
800 sub get_components_query {
803 my $builder = Koha::SearchEngine::QueryBuilder->new(
804 { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
805 my $marc = $self->metadata->record;
806 my $component_sort_field = C4::Context->preference('ComponentSortField') // "title";
807 my $component_sort_order = C4::Context->preference('ComponentSortOrder') // "asc";
808 my $sort = $component_sort_field . "_" . $component_sort_order;
811 if ( C4::Context->preference('UseControlNumber') ) {
812 my $pf001 = $marc->field('001') || undef;
814 if ( defined($pf001) ) {
816 my $pf003 = $marc->field('003') || undef;
818 if ( !defined($pf003) ) {
819 # search for 773$w='Host001'
820 $searchstr .= "rcn:\"" . $pf001->data()."\"";
824 # search for (773$w='Host001' and 003='Host003') or 773$w='(Host003)Host001'
825 $searchstr .= "(rcn:\"" . $pf001->data() . "\" AND cni:\"" . $pf003->data() . "\")";
826 $searchstr .= " OR rcn:\"" . $pf003->data() . " " . $pf001->data() . "\"";
830 # limit to monograph and serial component part records
831 $searchstr .= " AND (bib-level:a OR bib-level:b)";
836 my $cleaned_title = $marc->subfield('245', "a");
837 $cleaned_title =~ tr|/||;
838 $cleaned_title = $builder->clean_search_term($cleaned_title);
839 $searchstr = qq#Host-item:("$cleaned_title")#;
841 my ($error, $query ,$query_str) = $builder->build_query_compat( undef, [$searchstr], undef, undef, [$sort], 0 );
847 return ($query, $query_str, $sort);
850 =head3 get_marc_volumes
852 my $volumes = $self->get_marc_volumes();
854 Returns an array of MARCXML data, which are volumes parts of
855 this object (MARC21 773$w or 8xx$w point to this)
859 sub get_marc_volumes {
860 my ( $self, $max_results ) = @_;
862 return $self->{_volumes} if defined( $self->{_volumes} );
864 my $searchstr = $self->get_volumes_query;
866 if ( defined($searchstr) ) {
867 my $searcher = Koha::SearchEngine::Search->new( { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
868 my ( $errors, $results, $total_hits ) = $searcher->simple_search_compat( $searchstr, 0, $max_results );
870 ( defined($results) && scalar(@$results) ) ? $results : [];
872 $self->{_volumes} = [];
875 return $self->{_volumes};
878 =head2 get_volumes_query
880 Returns a query which can be used to search for all component parts of MARC21 biblios
884 sub get_volumes_query {
887 # MARC21 Only for now
888 return if ( C4::Context->preference('marcflavour') ne 'MARC21' );
890 my $marc = $self->metadata->record;
892 # Only build volumes query if we're in a 'Set' record
893 # or we have a monographic series.
894 # For monographic series the check on LDR 7 in (b or i or s) is omitted
895 my $leader19 = substr( $marc->leader, 19, 1 );
896 my $pf008 = $marc->field('008') || '';
897 my $mseries = ( $pf008 && substr( $pf008->data(), 21, 1 ) eq 'm' ) ? 1 : 0;
898 return unless ( $leader19 eq 'a' || $mseries );
900 my $builder = Koha::SearchEngine::QueryBuilder->new( { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
903 if ( C4::Context->preference('UseControlNumber') ) {
904 my $pf001 = $marc->field('001') || undef;
906 if ( defined($pf001) ) {
908 my $pf003 = $marc->field('003') || undef;
910 if ( !defined($pf003) ) {
912 # search for linking_field$w='Host001'
913 $searchstr .= "rcn:" . $pf001->data();
917 # search for (linking_field$w='Host001' and 003='Host003') or linking_field$w='(Host003)Host001'
918 $searchstr .= "(rcn:" . $pf001->data() . " AND cni:" . $pf003->data() . ")";
919 $searchstr .= " OR rcn:\"" . $pf003->data() . " " . $pf001->data() . "\"";
923 # exclude monograph and serial component part records
924 $searchstr .= " NOT (bib-level:a OR bib-level:b)";
928 my $cleaned_title = $marc->subfield( '245', "a" );
929 $cleaned_title =~ tr|/||;
930 $cleaned_title = $builder->clean_search_term($cleaned_title);
931 $searchstr = qq#(title-series,phr:("$cleaned_title") OR Host-item,phr:("$cleaned_title")#;
932 $searchstr .= " NOT (bib-level:a OR bib-level:b))";
940 my $subscriptions = $self->subscriptions
942 Returns the related Koha::Subscriptions object for this Biblio object
948 my $rs = $self->_result->subscriptions;
949 return Koha::Subscriptions->_new_from_dbic($rs);
952 =head3 has_items_waiting_or_intransit
954 my $itemsWaitingOrInTransit = $biblio->has_items_waiting_or_intransit
956 Tells if this bibliographic record has items waiting or in transit.
960 sub has_items_waiting_or_intransit {
963 if ( Koha::Holds->search({ biblionumber => $self->id,
964 found => ['W', 'T'] })->count ) {
968 foreach my $item ( $self->items->as_list ) {
969 return 1 if $item->get_transfer;
977 my $coins = $biblio->get_coins;
979 Returns the COinS (a span) which can be included in a biblio record
986 my $record = $self->metadata->record;
988 my $pos7 = substr $record->leader(), 7, 1;
989 my $pos6 = substr $record->leader(), 6, 1;
992 my ( $aulast, $aufirst ) = ( '', '' );
1003 # For the purposes of generating COinS metadata, LDR/06-07 can be
1004 # considered the same for UNIMARC and MARC21
1007 'b' => 'manuscript',
1009 'd' => 'manuscript',
1013 'i' => 'audioRecording',
1014 'j' => 'audioRecording',
1017 'm' => 'computerProgram',
1022 'a' => 'journalArticle',
1026 $genre = $fmts6->{$pos6} ? $fmts6->{$pos6} : 'book';
1028 if ( $genre eq 'book' ) {
1029 $genre = $fmts7->{$pos7} if $fmts7->{$pos7};
1032 ##### We must transform mtx to a valable mtx and document type ####
1033 if ( $genre eq 'book' ) {
1036 } elsif ( $genre eq 'journal' ) {
1039 } elsif ( $genre eq 'journalArticle' ) {
1047 if ( C4::Context->preference("marcflavour") eq "UNIMARC" ) {
1050 $aulast = $record->subfield( '700', 'a' ) || '';
1051 $aufirst = $record->subfield( '700', 'b' ) || '';
1052 push @authors, "$aufirst $aulast" if ($aufirst or $aulast);
1055 if ( $record->field('200') ) {
1056 for my $au ( $record->field('200')->subfield('g') ) {
1061 $title = $record->subfield( '200', 'a' );
1062 my $subfield_210d = $record->subfield('210', 'd');
1063 if ($subfield_210d and $subfield_210d =~ /(\d{4})/) {
1066 $publisher = $record->subfield( '210', 'c' ) || '';
1067 $isbn = $record->subfield( '010', 'a' ) || '';
1068 $issn = $record->subfield( '011', 'a' ) || '';
1071 # MARC21 need some improve
1074 if ( $record->field('100') ) {
1075 push @authors, $record->subfield( '100', 'a' );
1079 if ( $record->field('700') ) {
1080 for my $au ( $record->field('700')->subfield('a') ) {
1084 $title = $record->field('245');
1085 $title &&= $title->as_string('ab');
1086 if ($titletype eq 'a') {
1087 $pubyear = $record->field('008') || '';
1088 $pubyear = substr($pubyear->data(), 7, 4) if $pubyear;
1089 $isbn = $record->subfield( '773', 'z' ) || '';
1090 $issn = $record->subfield( '773', 'x' ) || '';
1091 $hosttitle = $record->subfield( '773', 't' ) || $record->subfield( '773', 'a') || q{};
1092 my @rels = $record->subfield( '773', 'g' );
1093 $pages = join(', ', @rels);
1095 $pubyear = $record->subfield( '260', 'c' ) || '';
1096 $publisher = $record->subfield( '260', 'b' ) || '';
1097 $isbn = $record->subfield( '020', 'a' ) || '';
1098 $issn = $record->subfield( '022', 'a' ) || '';
1104 [ 'ctx_ver', 'Z39.88-2004' ],
1105 [ 'rft_val_fmt', "info:ofi/fmt:kev:mtx:$mtx" ],
1106 [ ($mtx eq 'dc' ? 'rft.type' : 'rft.genre'), $genre ],
1107 [ "rft.${titletype}title", $title ],
1110 # rft.title is authorized only once, so by checking $titletype
1111 # we ensure that rft.title is not already in the list.
1112 if ($hosttitle and $titletype) {
1113 push @params, [ 'rft.title', $hosttitle ];
1117 [ 'rft.isbn', $isbn ],
1118 [ 'rft.issn', $issn ],
1121 # If it's a subscription, these informations have no meaning.
1122 if ($genre ne 'journal') {
1124 [ 'rft.aulast', $aulast ],
1125 [ 'rft.aufirst', $aufirst ],
1126 (map { [ 'rft.au', $_ ] } @authors),
1127 [ 'rft.pub', $publisher ],
1128 [ 'rft.date', $pubyear ],
1129 [ 'rft.pages', $pages ],
1133 my $coins_value = join( '&',
1134 map { $$_[1] ? $$_[0] . '=' . uri_escape_utf8( $$_[1] ) : () } @params );
1136 return $coins_value;
1141 my $url = $biblio->get_openurl;
1143 Returns url for OpenURL resolver set in OpenURLResolverURL system preference
1150 my $OpenURLResolverURL = C4::Context->preference('OpenURLResolverURL');
1152 if ($OpenURLResolverURL) {
1153 my $uri = URI->new($OpenURLResolverURL);
1155 if (not defined $uri->query) {
1156 $OpenURLResolverURL .= '?';
1158 $OpenURLResolverURL .= '&';
1160 $OpenURLResolverURL .= $self->get_coins;
1163 return $OpenURLResolverURL;
1168 my $serial = $biblio->is_serial
1170 Return boolean true if this bibbliographic record is continuing resource
1177 return 1 if $self->serial;
1179 my $record = $self->metadata->record;
1180 return 1 if substr($record->leader, 7, 1) eq 's';
1185 =head3 custom_cover_image_url
1187 my $image_url = $biblio->custom_cover_image_url
1189 Return the specific url of the cover image for this bibliographic record.
1190 It is built regaring the value of the system preference CustomCoverImagesURL
1194 sub custom_cover_image_url {
1196 my $url = C4::Context->preference('CustomCoverImagesURL');
1197 if ( $url =~ m|{isbn}| ) {
1198 my $isbn = $self->biblioitem->isbn;
1199 return unless $isbn;
1200 $url =~ s|{isbn}|$isbn|g;
1202 if ( $url =~ m|{normalized_isbn}| ) {
1203 my $normalized_isbn = $self->normalized_isbn;
1204 return unless $normalized_isbn;
1205 $url =~ s|{normalized_isbn}|$normalized_isbn|g;
1207 if ( $url =~ m|{issn}| ) {
1208 my $issn = $self->biblioitem->issn;
1209 return unless $issn;
1210 $url =~ s|{issn}|$issn|g;
1213 my $re = qr|{(?<field>\d{3})(\$(?<subfield>.))?}|;
1214 if ( $url =~ $re ) {
1215 my $field = $+{field};
1216 my $subfield = $+{subfield};
1217 my $marc_record = $self->metadata->record;
1220 $value = $marc_record->subfield( $field, $subfield );
1222 my $controlfield = $marc_record->field($field);
1223 $value = $controlfield->data() if $controlfield;
1225 return unless $value;
1226 $url =~ s|$re|$value|;
1234 Return the cover images associated with this biblio.
1241 my $cover_images_rs = $self->_result->cover_images;
1242 return unless $cover_images_rs;
1243 return Koha::CoverImages->_new_from_dbic($cover_images_rs);
1246 =head3 get_marc_notes
1248 $marcnotesarray = $biblio->get_marc_notes({ opac => 1 });
1250 Get all notes from the MARC record and returns them in an array.
1251 The notes are stored in different fields depending on MARC flavour.
1252 MARC21 5XX $u subfields receive special attention as they are URIs.
1256 sub get_marc_notes {
1257 my ( $self, $params ) = @_;
1259 my $marcflavour = C4::Context->preference('marcflavour');
1260 my $opac = $params->{opac} // '0';
1261 my $interface = $params->{opac} ? 'opac' : 'intranet';
1263 my $record = $params->{record} // $self->metadata->record;
1264 my $record_processor = Koha::RecordProcessor->new(
1266 filters => [ 'ViewPolicy', 'ExpandCodedFields' ],
1268 interface => $interface,
1269 frameworkcode => $self->frameworkcode
1273 $record_processor->process($record);
1275 my $scope = $marcflavour eq "UNIMARC"? '3..': '5..';
1276 #MARC21 specs indicate some notes should be private if first indicator 0
1277 my %maybe_private = (
1285 my %hiddenlist = map { $_ => 1 }
1286 split( /,/, C4::Context->preference('NotesToHide'));
1289 foreach my $field ( $record->field($scope) ) {
1290 my $tag = $field->tag();
1291 next if $hiddenlist{ $tag };
1292 next if $opac && $maybe_private{$tag} && !$field->indicator(1);
1293 if( $marcflavour ne 'UNIMARC' && $field->subfield('u') ) {
1294 # Field 5XX$u always contains URI
1295 # Examples: 505u, 506u, 510u, 514u, 520u, 530u, 538u, 540u, 542u, 552u, 555u, 561u, 563u, 583u
1296 # We first push the other subfields, then all $u's separately
1297 # Leave further actions to the template (see e.g. opac-detail)
1299 join '', ( 'a' .. 't', 'v' .. 'z', '0' .. '9' ); # excl 'u'
1300 push @marcnotes, { marcnote => $field->as_string($othersub) };
1301 foreach my $sub ( $field->subfield('u') ) {
1302 $sub =~ s/^\s+|\s+$//g; # trim
1303 push @marcnotes, { marcnote => $sub, tag => $tag };
1306 push @marcnotes, { marcnote => $field->as_string(), tag => $tag };
1312 =head3 _get_marc_authors
1314 Private method to return the list of authors contained in the MARC record.
1315 See get get_marc_contributors and get_marc_authors for the public methods.
1319 sub _get_marc_authors {
1320 my ( $self, $params ) = @_;
1322 my $fields_filter = $params->{fields_filter};
1323 my $mintag = $params->{mintag};
1324 my $maxtag = $params->{maxtag};
1326 my $AuthoritySeparator = C4::Context->preference('AuthoritySeparator');
1327 my $marcflavour = C4::Context->preference('marcflavour');
1329 # tagslib useful only for UNIMARC author responsibilities
1330 my $tagslib = $marcflavour eq "UNIMARC"
1331 ? C4::Biblio::GetMarcStructure( 1, $self->frameworkcode, { unsafe => 1 } )
1335 foreach my $field ( $self->metadata->record->field($fields_filter) ) {
1338 if $mintag && $field->tag() < $mintag
1339 || $maxtag && $field->tag() > $maxtag;
1343 my @subfields = $field->subfields();
1346 # if there is an authority link, build the link with Koha-Auth-Number: subfield9
1347 my $subfield9 = $field->subfield('9');
1349 my $linkvalue = $subfield9;
1350 $linkvalue =~ s/(\(|\))//g;
1351 @link_loop = ( { 'limit' => 'an', 'link' => $linkvalue } );
1356 for my $authors_subfield (@subfields) {
1357 next if ( $authors_subfield->[0] eq '9' );
1359 # unimarc3 contains the $3 of the author for UNIMARC.
1360 # For french academic libraries, it's the "ppn", and it's required for idref webservice
1361 $unimarc3 = $authors_subfield->[1] if $marcflavour eq 'UNIMARC' and $authors_subfield->[0] =~ /3/;
1363 # don't load unimarc subfields 3, 5
1364 next if ( $marcflavour eq 'UNIMARC' and ( $authors_subfield->[0] =~ /3|5/ ) );
1366 my $code = $authors_subfield->[0];
1367 my $value = $authors_subfield->[1];
1368 my $linkvalue = $value;
1369 $linkvalue =~ s/(\(|\))//g;
1370 # UNIMARC author responsibility
1371 if ( $marcflavour eq 'UNIMARC' and $code eq '4' ) {
1372 $value = C4::Biblio::GetAuthorisedValueDesc( $field->tag(), $code, $value, '', $tagslib );
1373 $linkvalue = "($value)";
1375 # if no authority link, build a search query
1376 unless ($subfield9) {
1379 'link' => $linkvalue,
1380 operator => (scalar @link_loop) ? ' AND ' : undef
1383 my @this_link_loop = @link_loop;
1385 unless ( $code eq '0') {
1386 push @subfields_loop, {
1387 tag => $field->tag(),
1390 link_loop => \@this_link_loop,
1391 separator => (scalar @subfields_loop) ? $AuthoritySeparator : ''
1395 push @marcauthors, {
1396 MARCAUTHOR_SUBFIELDS_LOOP => \@subfields_loop,
1397 authoritylink => $subfield9,
1398 unimarc3 => $unimarc3
1401 return \@marcauthors;
1404 =head3 get_marc_contributors
1406 my $contributors = $biblio->get_marc_contributors;
1408 Get all contributors (but first author) from the MARC record and returns them in an array.
1409 They are stored in different fields depending on MARC flavour (700..720 for MARC21)
1413 sub get_marc_contributors {
1414 my ( $self, $params ) = @_;
1416 my ( $mintag, $maxtag, $fields_filter );
1417 my $marcflavour = C4::Context->preference('marcflavour');
1419 if ( $marcflavour eq "UNIMARC" ) {
1422 $fields_filter = '7..';
1423 } else { # marc21/normarc
1426 $fields_filter = '7..';
1429 return $self->_get_marc_authors(
1431 fields_filter => $fields_filter,
1438 =head3 get_marc_authors
1440 my $authors = $biblio->get_marc_authors;
1442 Get all authors from the MARC record and returns them in an array.
1443 They are stored in different fields depending on MARC flavour
1444 (main author from 100 then secondary authors from 700..720).
1448 sub get_marc_authors {
1449 my ( $self, $params ) = @_;
1451 my ( $mintag, $maxtag, $fields_filter );
1452 my $marcflavour = C4::Context->preference('marcflavour');
1454 if ( $marcflavour eq "UNIMARC" ) {
1455 $fields_filter = '200';
1456 } else { # marc21/normarc
1457 $fields_filter = '100';
1460 my @first_authors = @{$self->_get_marc_authors(
1462 fields_filter => $fields_filter,
1468 my @other_authors = @{$self->get_marc_contributors};
1470 return [@first_authors, @other_authors];
1473 =head3 normalized_isbn
1475 my $normalized_isbn = $biblio->normalized_isbn
1477 Normalizes and returns the first valid ISBN found in the record.
1478 ISBN13 are converted into ISBN10. This is required to get some book cover images.
1482 sub normalized_isbn {
1484 return C4::Koha::GetNormalizedISBN($self->biblioitem->isbn);
1487 =head3 public_read_list
1489 This method returns the list of publicly readable database fields for both API and UI output purposes
1493 sub public_read_list {
1495 'biblionumber', 'frameworkcode', 'author',
1496 'title', 'medium', 'subtitle',
1497 'part_number', 'part_name', 'unititle',
1498 'notes', 'serial', 'seriestitle',
1499 'copyrightdate', 'abstract'
1503 =head3 metadata_extractor
1505 my $extractor = $biblio->metadata_extractor
1507 Return a Koha::Biblio::Metadata::Extractor object to use to extract data from the metadata (ie. MARC record for now)
1511 sub metadata_extractor {
1514 $self->{metadata_extractor} ||= Koha::Biblio::Metadata::Extractor->new( { biblio => $self } );
1516 return $self->{metadata_extractor};
1519 =head3 normalized_upc
1521 my $normalized_upc = $biblio->normalized_upc
1523 Normalizes and returns the UPC value found in the MARC record.
1527 sub normalized_upc {
1529 return $self->metadata_extractor->get_normalized_upc;
1532 =head3 normalized_oclc
1534 my $normalized_oclc = $biblio->normalized_oclc
1536 Normalizes and returns the OCLC number found in the MARC record.
1540 sub normalized_oclc {
1542 return $self->metadata_extractor->get_normalized_oclc;
1547 my $json = $biblio->to_api;
1549 Overloaded method that returns a JSON representation of the Koha::Biblio object,
1550 suitable for API output. The related Koha::Biblioitem object is merged as expected
1556 my ($self, $args) = @_;
1558 my $json_biblio = $self->SUPER::to_api( $args );
1559 return unless $json_biblio;
1561 $args = defined $args ? {%$args} : {};
1562 delete $args->{embed};
1564 my $json_biblioitem = $self->biblioitem->to_api( $args );
1565 return unless $json_biblioitem;
1567 return { %$json_biblio, %$json_biblioitem };
1570 =head3 to_api_mapping
1572 This method returns the mapping for representing a Koha::Biblio object
1577 sub to_api_mapping {
1579 biblionumber => 'biblio_id',
1580 frameworkcode => 'framework_id',
1581 unititle => 'uniform_title',
1582 seriestitle => 'series_title',
1583 copyrightdate => 'copyright_date',
1584 datecreated => 'creation_date',
1585 deleted_on => undef,
1589 =head3 get_marc_host
1591 $host = $biblio->get_marc_host;
1593 ( $host, $relatedparts, $hostinfo ) = $biblio->get_marc_host;
1595 Returns host biblio record from MARC21 773 (undef if no 773 present).
1596 It looks at the first 773 field with MARCorgCode or only a control
1597 number. Complete $w or numeric part is used to search host record.
1598 The optional parameter no_items triggers a check if $biblio has items.
1599 If there are, the sub returns undef.
1600 Called in list context, it also returns 773$g (related parts).
1602 If there is no $w, we use $0 (host biblionumber) or $9 (host itemnumber)
1603 to search for the host record. If there is also no $0 and no $9, we search
1604 using author and title. Failing all of that, we return an undef host and
1605 form a concatenation of strings with 773$agt for host information,
1606 returned when called in list context.
1611 my ($self, $params) = @_;
1612 my $no_items = $params->{no_items};
1613 return if C4::Context->preference('marcflavour') eq 'UNIMARC'; # TODO
1614 return if $params->{no_items} && $self->items->count > 0;
1617 eval { $record = $self->metadata->record };
1620 # We pick the first $w with your MARCOrgCode or the first $w that has no
1621 # code (between parentheses) at all.
1622 my $orgcode = C4::Context->preference('MARCOrgCode') // q{};
1624 foreach my $f ( $record->field('773') ) {
1625 my $w = $f->subfield('w') or next;
1626 if( $w =~ /^\($orgcode\)\s*(\d+)/i or $w =~ /^\d+/ ) {
1632 my $engine = Koha::SearchEngine::Search->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1634 if ( !$hostfld and $record->subfield('773','t') ) {
1635 # not linked using $w
1636 my $unlinkedf = $record->field('773');
1638 if ( C4::Context->preference("EasyAnalyticalRecords") ) {
1639 if ( $unlinkedf->subfield('0') ) {
1640 # use 773$0 host biblionumber
1641 $bibno = $unlinkedf->subfield('0');
1642 } elsif ( $unlinkedf->subfield('9') ) {
1643 # use 773$9 host itemnumber
1644 my $linkeditemnumber = $unlinkedf->subfield('9');
1645 $bibno = Koha::Items->find( $linkeditemnumber )->biblionumber;
1649 my $host = Koha::Biblios->find($bibno) or return;
1650 return wantarray ? ( $host, $unlinkedf->subfield('g') ) : $host;
1652 # just return plaintext and no host record
1653 my $hostinfo = join( ", ", $unlinkedf->subfield('a'), $unlinkedf->subfield('t'), $unlinkedf->subfield('g') );
1654 return wantarray ? ( undef, $unlinkedf->subfield('g'), $hostinfo ) : undef;
1656 return if !$hostfld;
1657 my $rcn = $hostfld->subfield('w');
1659 # Look for control number with/without orgcode
1660 for my $try (1..2) {
1661 my ( $error, $results, $total_hits ) = $engine->simple_search_compat( 'Control-number='.$rcn, 0,1 );
1662 if( !$error and $total_hits == 1 ) {
1663 $bibno = $engine->extract_biblionumber( $results->[0] );
1666 # Add or remove orgcode for second try
1667 if( $try == 1 && $rcn =~ /\)\s*(\d+)/ ) {
1668 $rcn = $1; # number only
1669 } elsif( $try == 1 && $rcn =~ /^\d+/ ) {
1670 $rcn = "($orgcode)$rcn";
1676 my $host = Koha::Biblios->find($bibno) or return;
1677 return wantarray ? ( $host, $hostfld->subfield('g') ) : $host;
1681 =head3 get_marc_host_only
1683 my $host = $biblio->get_marc_host_only;
1689 sub get_marc_host_only {
1692 my ( $host ) = $self->get_marc_host;
1697 =head3 get_marc_relatedparts_only
1699 my $relatedparts = $biblio->get_marc_relatedparts_only;
1701 Return related parts only
1705 sub get_marc_relatedparts_only {
1708 my ( undef, $relatedparts ) = $self->get_marc_host;
1710 return $relatedparts;
1713 =head3 get_marc_hostinfo_only
1715 my $hostinfo = $biblio->get_marc_hostinfo_only;
1717 Return host info only
1721 sub get_marc_hostinfo_only {
1724 my ( $host, $relatedparts, $hostinfo ) = $self->get_marc_host;
1731 my $recalls = $biblio->recalls;
1733 Return recalls linked to this biblio
1739 return Koha::Recalls->_new_from_dbic( scalar $self->_result->recalls );
1742 =head3 can_be_recalled
1744 my @items_for_recall = $biblio->can_be_recalled({ patron => $patron_object });
1746 Does biblio-level checks and returns the items attached to this biblio that are available for recall
1750 sub can_be_recalled {
1751 my ( $self, $params ) = @_;
1753 return 0 if !( C4::Context->preference('UseRecalls') );
1755 my $patron = $params->{patron};
1757 my $branchcode = C4::Context->userenv->{'branch'};
1758 if ( C4::Context->preference('CircControl') eq 'PatronLibrary' and $patron ) {
1759 $branchcode = $patron->branchcode;
1762 my @all_items = Koha::Items->search({ biblionumber => $self->biblionumber })->as_list;
1764 # if there are no available items at all, no recall can be placed
1765 return 0 if ( scalar @all_items == 0 );
1770 my @all_itemnumbers;
1771 foreach my $item ( @all_items ) {
1772 push( @all_itemnumbers, $item->itemnumber );
1773 if ( $item->can_be_recalled({ patron => $patron }) ) {
1774 push( @itemtypes, $item->effective_itemtype );
1775 push( @itemnumbers, $item->itemnumber );
1776 push( @items, $item );
1780 # if there are no recallable items, no recall can be placed
1781 return 0 if ( scalar @items == 0 );
1783 # Check the circulation rule for each relevant itemtype for this biblio
1784 my ( @recalls_allowed, @recalls_per_record, @on_shelf_recalls );
1785 foreach my $itemtype ( @itemtypes ) {
1786 my $rule = Koha::CirculationRules->get_effective_rules({
1787 branchcode => $branchcode,
1788 categorycode => $patron ? $patron->categorycode : undef,
1789 itemtype => $itemtype,
1792 'recalls_per_record',
1796 push( @recalls_allowed, $rule->{recalls_allowed} ) if $rule;
1797 push( @recalls_per_record, $rule->{recalls_per_record} ) if $rule;
1798 push( @on_shelf_recalls, $rule->{on_shelf_recalls} ) if $rule;
1800 my $recalls_allowed = (sort {$b <=> $a} @recalls_allowed)[0]; # take highest
1801 my $recalls_per_record = (sort {$b <=> $a} @recalls_per_record)[0]; # take highest
1802 my %on_shelf_recalls_count = ();
1803 foreach my $count ( @on_shelf_recalls ) {
1804 $on_shelf_recalls_count{$count}++;
1806 my $on_shelf_recalls = (sort {$on_shelf_recalls_count{$b} <=> $on_shelf_recalls_count{$a}} @on_shelf_recalls)[0]; # take most common
1808 # check recalls allowed has been set and is not zero
1809 return 0 if ( !defined($recalls_allowed) || $recalls_allowed == 0 );
1812 # check borrower has not reached open recalls allowed limit
1813 return 0 if ( $patron->recalls->filter_by_current->count >= $recalls_allowed );
1815 # check borrower has not reached open recalls allowed per record limit
1816 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $recalls_per_record );
1818 # check if any of the items under this biblio are already checked out by this borrower
1819 return 0 if ( Koha::Checkouts->search({ itemnumber => [ @all_itemnumbers ], borrowernumber => $patron->borrowernumber })->count > 0 );
1822 # check item availability
1823 my $checked_out_count = 0;
1825 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1828 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1829 return 0 if ( $on_shelf_recalls eq 'all' && $checked_out_count < scalar @items );
1831 # can't recall if no items have been checked out
1832 return 0 if ( $checked_out_count == 0 );
1840 my $ratings = $biblio->ratings
1842 Return a Koha::Ratings object representing the ratings of this bibliographic record
1848 my $rs = $self->_result->ratings;
1849 return Koha::Ratings->_new_from_dbic($rs);
1852 =head3 opac_summary_html
1854 my $summary_html = $biblio->opac_summary_html
1856 Based on the syspref OPACMySummaryHTML, returns a string representing the
1857 summary of this bibliographic record.
1858 {AUTHOR}, {TITLE}, {ISBN} and {BIBLIONUMBER} will be replaced.
1862 sub opac_summary_html {
1865 my $summary_html = C4::Context->preference('OPACMySummaryHTML');
1866 return q{} unless $summary_html;
1867 my $author = $self->author || q{};
1868 my $title = $self->title || q{};
1869 $title =~ s/\/+$//; # remove trailing slash
1870 $title =~ s/\s+$//; # remove trailing space
1871 my $normalized_isbn = $self->normalized_isbn || q{};
1872 my $biblionumber = $self->biblionumber;
1874 $summary_html =~ s/{AUTHOR}/$author/g;
1875 $summary_html =~ s/{TITLE}/$title/g;
1876 $summary_html =~ s/{ISBN}/$normalized_isbn/g;
1877 $summary_html =~ s/{BIBLIONUMBER}/$biblionumber/g;
1879 return $summary_html;
1882 =head2 Internal methods
1894 Kyle M Hall <kyle@bywatersolutions.com>