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';
225 $biblio->check_booking( { start_date => $datetime, end_date => $datetime, [ booking_id => $booking_id ] } );
227 Returns a boolean denoting whether the passed booking can be made without clashing.
229 Optionally, you may pass a booking id to exclude from the checks; This is helpful when you are updating an existing booking.
234 my ( $self, $params ) = @_;
236 my $start_date = dt_from_string( $params->{start_date} );
237 my $end_date = dt_from_string( $params->{end_date} );
238 my $booking_id = $params->{booking_id};
240 my $bookable_items = $self->bookable_items;
241 my $total_bookable = $bookable_items->count;
243 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
244 my $existing_bookings = $self->bookings(
248 $dtf->format_datetime($start_date),
249 $dtf->format_datetime($end_date)
254 $dtf->format_datetime($start_date),
255 $dtf->format_datetime($end_date)
259 start_date => { '<' => $dtf->format_datetime($start_date) },
260 end_date => { '>' => $dtf->format_datetime($end_date) }
267 ? $existing_bookings->search( { booking_id => { '!=' => $booking_id } } )
269 : $existing_bookings->count;
270 return ( ( $total_bookable - $booked_count ) > 0 ) ? 1 : 0;
273 =head3 assign_item_for_booking
277 sub assign_item_for_booking {
278 my ( $self, $params ) = @_;
280 my $start_date = dt_from_string( $params->{start_date} );
281 my $end_date = dt_from_string( $params->{end_date} );
283 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
284 my $existing_bookings = $self->bookings(
288 $dtf->format_datetime($start_date),
289 $dtf->format_datetime($end_date)
294 $dtf->format_datetime($start_date),
295 $dtf->format_datetime($end_date)
299 start_date => { '<' => $dtf->format_datetime($start_date) },
300 end_date => { '>' => $dtf->format_datetime($end_date) }
305 my $bookable_items = $self->bookable_items->search(
306 { itemnumber => { '-not_in' => $existing_bookings->_resultset->get_column('item_id')->as_query } },
309 return $bookable_items->single->itemnumber;
314 my $booking = $biblio->place_booking(
317 start_date => $datetime,
318 end_date => $datetime
322 Add a booking for this item for the dates passed.
324 Returns the Koha::Booking object or throws an exception if the item cannot be booked for the given dates.
329 my ( $self, $params ) = @_;
331 # check for mandatory params
332 my @mandatory = ( 'start_date', 'end_date', 'patron' );
333 for my $param (@mandatory) {
334 unless ( defined( $params->{$param} ) ) {
335 Koha::Exceptions::MissingParameter->throw(
336 error => "The $param parameter is mandatory" );
339 my $patron = $params->{patron};
342 my $booking = Koha::Booking->new(
344 start_date => $params->{start_date},
345 end_date => $params->{end_date},
346 patron_id => $patron->borrowernumber,
347 biblio_id => $self->biblionumber
353 =head3 can_be_transferred
355 $biblio->can_be_transferred({ to => $to_library, from => $from_library })
357 Checks if at least one item of a biblio can be transferred to given library.
359 This feature is controlled by two system preferences:
360 UseBranchTransferLimits to enable / disable the feature
361 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
362 for setting the limitations
364 Performance-wise, it is recommended to use this method for a biblio instead of
365 iterating each item of a biblio with Koha::Item->can_be_transferred().
367 Takes HASHref that can have the following parameters:
368 MANDATORY PARAMETERS:
371 $from : Koha::Library # if given, only items from that
372 # holdingbranch are considered
374 Returns 1 if at least one of the item of a biblio can be transferred
375 to $to_library, otherwise 0.
379 sub can_be_transferred {
380 my ($self, $params) = @_;
382 my $to = $params->{to};
383 my $from = $params->{from};
385 return 1 unless C4::Context->preference('UseBranchTransferLimits');
386 my $limittype = C4::Context->preference('BranchTransferLimitsType');
389 foreach my $item_of_bib ($self->items->as_list) {
390 next unless $item_of_bib->holdingbranch;
391 next if $from && $from->branchcode ne $item_of_bib->holdingbranch;
392 return 1 if $item_of_bib->holdingbranch eq $to->branchcode;
393 my $code = $limittype eq 'itemtype'
394 ? $item_of_bib->effective_itemtype
395 : $item_of_bib->ccode;
396 return 1 unless $code;
397 $items->{$code}->{$item_of_bib->holdingbranch} = 1;
400 # At this point we will have a HASHref containing each itemtype/ccode that
401 # this biblio has, inside which are all of the holdingbranches where those
402 # items are located at. Then, we will query Koha::Item::Transfer::Limits to
403 # find out whether a transfer limits for such $limittype from any of the
404 # listed holdingbranches to the given $to library exist. If at least one
405 # holdingbranch for that $limittype does not have a transfer limit to given
406 # $to library, then we know that the transfer is possible.
407 foreach my $code (keys %{$items}) {
408 my @holdingbranches = keys %{$items->{$code}};
409 return 1 if Koha::Item::Transfer::Limits->search({
410 toBranch => $to->branchcode,
411 fromBranch => { 'in' => \@holdingbranches },
414 group_by => [qw/fromBranch/]
415 })->count == scalar(@holdingbranches) ? 0 : 1;
422 =head3 pickup_locations
424 my $pickup_locations = $biblio->pickup_locations({ patron => $patron });
426 Returns a Koha::Libraries set of possible pickup locations for this biblio's items,
427 according to patron's home library and if item can be transferred to each pickup location.
429 Throws a I<Koha::Exceptions::MissingParameter> exception if the B<mandatory> parameter I<patron>
434 sub pickup_locations {
435 my ( $self, $params ) = @_;
437 Koha::Exceptions::MissingParameter->throw( parameter => 'patron' )
438 unless exists $params->{patron};
440 my $patron = $params->{patron};
442 my $memory_cache = Koha::Cache::Memory::Lite->get_instance();
443 my @pickup_locations;
444 foreach my $item ( $self->items->as_list ) {
445 my $cache_key = sprintf "Pickup_locations:%s:%s:%s:%s:%s",
446 $item->itype,$item->homebranch,$item->holdingbranch,$item->ccode || "",$patron->branchcode||"" ;
447 my $item_pickup_locations = $memory_cache->get_from_cache( $cache_key );
448 unless( $item_pickup_locations ){
449 @{ $item_pickup_locations } = $item->pickup_locations( { patron => $patron } )->_resultset->get_column('branchcode')->all;
450 $memory_cache->set_in_cache( $cache_key, $item_pickup_locations );
452 push @pickup_locations, @{ $item_pickup_locations }
455 return Koha::Libraries->search(
456 { branchcode => { '-in' => \@pickup_locations } }, { order_by => ['branchname'] } );
459 =head3 hidden_in_opac
461 my $bool = $biblio->hidden_in_opac({ [ rules => $rules ] })
463 Returns true if the biblio matches the hidding criteria defined in $rules.
464 Returns false otherwise. It involves the I<OpacHiddenItems> and
465 I<OpacHiddenItemsHidesRecord> system preferences.
467 Takes HASHref that can have the following parameters:
469 $rules : { <field> => [ value_1, ... ], ... }
471 Note: $rules inherits its structure from the parsed YAML from reading
472 the I<OpacHiddenItems> system preference.
477 my ( $self, $params ) = @_;
479 my $rules = $params->{rules} // {};
481 my @items = $self->items->as_list;
483 return 0 unless @items; # Do not hide if there is no item
485 # Ok, there are items, don't even try the rules unless OpacHiddenItemsHidesRecord
486 return 0 unless C4::Context->preference('OpacHiddenItemsHidesRecord');
488 return !(any { !$_->hidden_in_opac({ rules => $rules }) } @items);
491 =head3 article_request_type
493 my $type = $biblio->article_request_type( $borrower );
495 Returns the article request type based on items, or on the record
496 itself if there are no items.
498 $borrower must be a Koha::Patron object
502 sub article_request_type {
503 my ( $self, $borrower ) = @_;
505 return q{} unless $borrower;
507 my $rule = $self->article_request_type_for_items( $borrower );
508 return $rule if $rule;
510 # If the record has no items that are requestable, go by the record itemtype
511 $rule = $self->article_request_type_for_bib($borrower);
512 return $rule if $rule;
517 =head3 article_request_type_for_bib
519 my $type = $biblio->article_request_type_for_bib
521 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record
525 sub article_request_type_for_bib {
526 my ( $self, $borrower ) = @_;
528 return q{} unless $borrower;
530 my $borrowertype = $borrower->categorycode;
531 my $itemtype = $self->itemtype();
533 my $rule = Koha::CirculationRules->get_effective_rule(
535 rule_name => 'article_requests',
536 categorycode => $borrowertype,
537 itemtype => $itemtype,
541 return q{} unless $rule;
542 return $rule->rule_value || q{}
545 =head3 article_request_type_for_items
547 my $type = $biblio->article_request_type_for_items
549 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record's items
551 If there is a conflict where some items are 'bib_only' and some are 'item_only', 'bib_only' will be returned.
555 sub article_request_type_for_items {
556 my ( $self, $borrower ) = @_;
559 foreach my $item ( $self->items()->as_list() ) {
560 my $rule = $item->article_request_type($borrower);
561 return $rule if $rule eq 'bib_only'; # we don't need to go any further
565 return 'item_only' if $counts->{item_only};
566 return 'yes' if $counts->{yes};
567 return 'no' if $counts->{no};
571 =head3 article_requests
573 my $article_requests = $biblio->article_requests
575 Returns the article requests associated with this biblio
579 sub article_requests {
582 return Koha::ArticleRequests->_new_from_dbic( scalar $self->_result->article_requests );
585 =head3 current_checkouts
587 my $current_checkouts = $biblio->current_checkouts
589 Returns the current checkouts associated with this biblio
593 sub current_checkouts {
596 return Koha::Checkouts->search( { "item.biblionumber" => $self->id },
597 { join => 'item' } );
602 my $old_checkouts = $biblio->old_checkouts
604 Returns the past checkouts associated with this biblio
611 return Koha::Old::Checkouts->search( { "item.biblionumber" => $self->id },
612 { join => 'item' } );
617 my $items = $biblio->items({ [ host_items => 1 ] });
619 The optional param host_items allows you to include 'analytical' items.
621 Returns the related Koha::Items object for this biblio
626 my ($self,$params) = @_;
628 my $items_rs = $self->_result->items;
630 return Koha::Items->_new_from_dbic( $items_rs ) unless $params->{host_items};
632 my @itemnumbers = $items_rs->get_column('itemnumber')->all;
633 my $host_itemnumbers = $self->_host_itemnumbers();
634 push @itemnumbers, @{ $host_itemnumbers };
635 return Koha::Items->search({ "me.itemnumber" => { -in => \@itemnumbers } });
638 =head3 bookable_items
640 my $bookable_items = $biblio->bookable_items;
642 Returns the related Koha::Items resultset filtered to those items that can be booked.
648 return $self->items->filter_by_bookable;
654 my $host_items = $biblio->host_items();
656 Return the host items (easy analytical record)
663 return Koha::Items->new->empty
664 unless C4::Context->preference('EasyAnalyticalRecords');
666 my $host_itemnumbers = $self->_host_itemnumbers;
668 return Koha::Items->search( { itemnumber => { -in => $host_itemnumbers } } );
671 =head3 _host_itemnumbers
673 my $host_itemnumber = $biblio->_host_itemnumbers();
675 Return the itemnumbers for analytical items on this record
679 sub _host_itemnumbers {
682 my $marcflavour = C4::Context->preference("marcflavour");
683 my $analyticfield = '773';
684 if ( $marcflavour eq 'UNIMARC' ) {
685 $analyticfield = '461';
687 my $marc_record = $self->metadata->record;
689 foreach my $field ( $marc_record->field($analyticfield) ) {
690 push @itemnumbers, $field->subfield('9');
692 return \@itemnumbers;
698 my $itemtype = $biblio->itemtype();
700 Returns the itemtype for this record.
707 return $self->biblioitem()->itemtype();
712 my $holds = $biblio->holds();
714 return the current holds placed on this record
719 my ( $self, $params, $attributes ) = @_;
720 $attributes->{order_by} = 'priority' unless exists $attributes->{order_by};
721 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
722 return Koha::Holds->_new_from_dbic($hold_rs);
727 my $holds = $biblio->current_holds
729 Return the holds placed on this bibliographic record.
730 It does not include future holds.
736 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
738 { reservedate => { '<=' => $dtf->format_date(dt_from_string) } } );
743 my $field = $self->biblioitem
745 Returns the related Koha::Biblioitem object for this Biblio object
751 return Koha::Biblioitems->find( { biblionumber => $self->biblionumber } );
756 my $bookings = $item->bookings();
758 Returns the bookings attached to this biblio.
763 my ( $self, $params ) = @_;
764 my $bookings_rs = $self->_result->bookings->search($params);
765 return Koha::Bookings->_new_from_dbic( $bookings_rs );
770 my $suggestions = $self->suggestions
772 Returns the related Koha::Suggestions object for this Biblio object
779 my $suggestions_rs = $self->_result->suggestions;
780 return Koha::Suggestions->_new_from_dbic( $suggestions_rs );
783 =head3 get_marc_components
785 my $components = $self->get_marc_components();
787 Returns an array of search results data, which are component parts of
788 this object (MARC21 773 points to this)
792 sub get_marc_components {
793 my ($self, $max_results) = @_;
795 return [] if (C4::Context->preference('marcflavour') ne 'MARC21');
797 my ( $searchstr, $sort ) = $self->get_components_query;
800 if (defined($searchstr)) {
801 my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
802 my ( $error, $results, $facets );
804 ( $error, $results, $facets ) = $searcher->search_compat( $searchstr, undef, [$sort], ['biblioserver'], $max_results, 0, undef, undef, 'ccl', 0 );
809 warn "Warning from search_compat: '$error'";
813 message => 'component_search',
818 $components = $results->{biblioserver}->{RECORDS} if defined($results) && $results->{biblioserver}->{hits};
821 return $components // [];
824 =head2 get_components_query
826 Returns a query which can be used to search for all component parts of MARC21 biblios
830 sub get_components_query {
833 my $builder = Koha::SearchEngine::QueryBuilder->new(
834 { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
835 my $marc = $self->metadata->record;
836 my $component_sort_field = C4::Context->preference('ComponentSortField') // "title";
837 my $component_sort_order = C4::Context->preference('ComponentSortOrder') // "asc";
838 my $sort = $component_sort_field . "_" . $component_sort_order;
841 if ( C4::Context->preference('UseControlNumber') ) {
842 my $pf001 = $marc->field('001') || undef;
844 if ( defined($pf001) ) {
846 my $pf003 = $marc->field('003') || undef;
848 if ( !defined($pf003) ) {
849 # search for 773$w='Host001'
850 $searchstr .= "rcn:\"" . $pf001->data()."\"";
854 # search for (773$w='Host001' and 003='Host003') or 773$w='(Host003)Host001'
855 $searchstr .= "(rcn:\"" . $pf001->data() . "\" AND cni:\"" . $pf003->data() . "\")";
856 $searchstr .= " OR rcn:\"" . $pf003->data() . " " . $pf001->data() . "\"";
860 # limit to monograph and serial component part records
861 $searchstr .= " AND (bib-level:a OR bib-level:b)";
866 my $cleaned_title = $marc->subfield('245', "a");
867 $cleaned_title =~ tr|/||;
868 $cleaned_title = $builder->clean_search_term($cleaned_title);
869 $searchstr = qq#Host-item:("$cleaned_title")#;
871 my ($error, $query ,$query_str) = $builder->build_query_compat( undef, [$searchstr], undef, undef, [$sort], 0 );
877 return ($query, $query_str, $sort);
880 =head3 get_marc_volumes
882 my $volumes = $self->get_marc_volumes();
884 Returns an array of MARCXML data, which are volumes parts of
885 this object (MARC21 773$w or 8xx$w point to this)
889 sub get_marc_volumes {
890 my ( $self, $max_results ) = @_;
892 return $self->{_volumes} if defined( $self->{_volumes} );
894 my $searchstr = $self->get_volumes_query;
896 if ( defined($searchstr) ) {
897 my $searcher = Koha::SearchEngine::Search->new( { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
898 my ( $errors, $results, $total_hits ) = $searcher->simple_search_compat( $searchstr, 0, $max_results );
900 ( defined($results) && scalar(@$results) ) ? $results : [];
902 $self->{_volumes} = [];
905 return $self->{_volumes};
908 =head2 get_volumes_query
910 Returns a query which can be used to search for all component parts of MARC21 biblios
914 sub get_volumes_query {
917 # MARC21 Only for now
918 return if ( C4::Context->preference('marcflavour') ne 'MARC21' );
920 my $marc = $self->metadata->record;
922 # Only build volumes query if we're in a 'Set' record
923 # or we have a monographic series.
924 # For monographic series the check on LDR 7 in (b or i or s) is omitted
925 my $leader19 = substr( $marc->leader, 19, 1 );
926 my $pf008 = $marc->field('008') || '';
927 my $mseries = ( $pf008 && substr( $pf008->data(), 21, 1 ) eq 'm' ) ? 1 : 0;
928 return unless ( $leader19 eq 'a' || $mseries );
930 my $builder = Koha::SearchEngine::QueryBuilder->new( { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
933 if ( C4::Context->preference('UseControlNumber') ) {
934 my $pf001 = $marc->field('001') || undef;
936 if ( defined($pf001) ) {
938 my $pf003 = $marc->field('003') || undef;
940 if ( !defined($pf003) ) {
942 # search for linking_field$w='Host001'
943 $searchstr .= "rcn:" . $pf001->data();
947 # search for (linking_field$w='Host001' and 003='Host003') or linking_field$w='(Host003)Host001'
948 $searchstr .= "(rcn:" . $pf001->data() . " AND cni:" . $pf003->data() . ")";
949 $searchstr .= " OR rcn:\"" . $pf003->data() . " " . $pf001->data() . "\"";
953 # exclude monograph and serial component part records
954 $searchstr .= " NOT (bib-level:a OR bib-level:b)";
958 my $cleaned_title = $marc->subfield( '245', "a" );
959 $cleaned_title =~ tr|/||;
960 $cleaned_title = $builder->clean_search_term($cleaned_title);
961 $searchstr = qq#(title-series,phr:("$cleaned_title") OR Host-item,phr:("$cleaned_title")#;
962 $searchstr .= " NOT (bib-level:a OR bib-level:b))";
970 my $subscriptions = $self->subscriptions
972 Returns the related Koha::Subscriptions object for this Biblio object
978 my $rs = $self->_result->subscriptions;
979 return Koha::Subscriptions->_new_from_dbic($rs);
982 =head3 has_items_waiting_or_intransit
984 my $itemsWaitingOrInTransit = $biblio->has_items_waiting_or_intransit
986 Tells if this bibliographic record has items waiting or in transit.
990 sub has_items_waiting_or_intransit {
993 if ( Koha::Holds->search({ biblionumber => $self->id,
994 found => ['W', 'T'] })->count ) {
998 foreach my $item ( $self->items->as_list ) {
999 return 1 if $item->get_transfer;
1007 my $coins = $biblio->get_coins;
1009 Returns the COinS (a span) which can be included in a biblio record
1016 my $record = $self->metadata->record;
1018 my $pos7 = substr $record->leader(), 7, 1;
1019 my $pos6 = substr $record->leader(), 6, 1;
1022 my ( $aulast, $aufirst ) = ( '', '' );
1033 # For the purposes of generating COinS metadata, LDR/06-07 can be
1034 # considered the same for UNIMARC and MARC21
1037 'b' => 'manuscript',
1039 'd' => 'manuscript',
1043 'i' => 'audioRecording',
1044 'j' => 'audioRecording',
1047 'm' => 'computerProgram',
1052 'a' => 'journalArticle',
1056 $genre = $fmts6->{$pos6} ? $fmts6->{$pos6} : 'book';
1058 if ( $genre eq 'book' ) {
1059 $genre = $fmts7->{$pos7} if $fmts7->{$pos7};
1062 ##### We must transform mtx to a valable mtx and document type ####
1063 if ( $genre eq 'book' ) {
1066 } elsif ( $genre eq 'journal' ) {
1069 } elsif ( $genre eq 'journalArticle' ) {
1077 if ( C4::Context->preference("marcflavour") eq "UNIMARC" ) {
1080 $aulast = $record->subfield( '700', 'a' ) || '';
1081 $aufirst = $record->subfield( '700', 'b' ) || '';
1082 push @authors, "$aufirst $aulast" if ($aufirst or $aulast);
1085 if ( $record->field('200') ) {
1086 for my $au ( $record->field('200')->subfield('g') ) {
1091 $title = $record->subfield( '200', 'a' );
1092 my $subfield_210d = $record->subfield('210', 'd');
1093 if ($subfield_210d and $subfield_210d =~ /(\d{4})/) {
1096 $publisher = $record->subfield( '210', 'c' ) || '';
1097 $isbn = $record->subfield( '010', 'a' ) || '';
1098 $issn = $record->subfield( '011', 'a' ) || '';
1101 # MARC21 need some improve
1104 if ( $record->field('100') ) {
1105 push @authors, $record->subfield( '100', 'a' );
1109 if ( $record->field('700') ) {
1110 for my $au ( $record->field('700')->subfield('a') ) {
1114 $title = $record->field('245');
1115 $title &&= $title->as_string('ab');
1116 if ($titletype eq 'a') {
1117 $pubyear = $record->field('008') || '';
1118 $pubyear = substr($pubyear->data(), 7, 4) if $pubyear;
1119 $isbn = $record->subfield( '773', 'z' ) || '';
1120 $issn = $record->subfield( '773', 'x' ) || '';
1121 $hosttitle = $record->subfield( '773', 't' ) || $record->subfield( '773', 'a') || q{};
1122 my @rels = $record->subfield( '773', 'g' );
1123 $pages = join(', ', @rels);
1125 $pubyear = $record->subfield( '260', 'c' ) || '';
1126 $publisher = $record->subfield( '260', 'b' ) || '';
1127 $isbn = $record->subfield( '020', 'a' ) || '';
1128 $issn = $record->subfield( '022', 'a' ) || '';
1134 [ 'ctx_ver', 'Z39.88-2004' ],
1135 [ 'rft_val_fmt', "info:ofi/fmt:kev:mtx:$mtx" ],
1136 [ ($mtx eq 'dc' ? 'rft.type' : 'rft.genre'), $genre ],
1137 [ "rft.${titletype}title", $title ],
1140 # rft.title is authorized only once, so by checking $titletype
1141 # we ensure that rft.title is not already in the list.
1142 if ($hosttitle and $titletype) {
1143 push @params, [ 'rft.title', $hosttitle ];
1147 [ 'rft.isbn', $isbn ],
1148 [ 'rft.issn', $issn ],
1151 # If it's a subscription, these informations have no meaning.
1152 if ($genre ne 'journal') {
1154 [ 'rft.aulast', $aulast ],
1155 [ 'rft.aufirst', $aufirst ],
1156 (map { [ 'rft.au', $_ ] } @authors),
1157 [ 'rft.pub', $publisher ],
1158 [ 'rft.date', $pubyear ],
1159 [ 'rft.pages', $pages ],
1163 my $coins_value = join( '&',
1164 map { $$_[1] ? $$_[0] . '=' . uri_escape_utf8( $$_[1] ) : () } @params );
1166 return $coins_value;
1171 my $url = $biblio->get_openurl;
1173 Returns url for OpenURL resolver set in OpenURLResolverURL system preference
1180 my $OpenURLResolverURL = C4::Context->preference('OpenURLResolverURL');
1182 if ($OpenURLResolverURL) {
1183 my $uri = URI->new($OpenURLResolverURL);
1185 if (not defined $uri->query) {
1186 $OpenURLResolverURL .= '?';
1188 $OpenURLResolverURL .= '&';
1190 $OpenURLResolverURL .= $self->get_coins;
1193 return $OpenURLResolverURL;
1198 my $serial = $biblio->is_serial
1200 Return boolean true if this bibbliographic record is continuing resource
1207 return 1 if $self->serial;
1209 my $record = $self->metadata->record;
1210 return 1 if substr($record->leader, 7, 1) eq 's';
1215 =head3 custom_cover_image_url
1217 my $image_url = $biblio->custom_cover_image_url
1219 Return the specific url of the cover image for this bibliographic record.
1220 It is built regaring the value of the system preference CustomCoverImagesURL
1224 sub custom_cover_image_url {
1226 my $url = C4::Context->preference('CustomCoverImagesURL');
1227 if ( $url =~ m|{isbn}| ) {
1228 my $isbn = $self->biblioitem->isbn;
1229 return unless $isbn;
1230 $url =~ s|{isbn}|$isbn|g;
1232 if ( $url =~ m|{normalized_isbn}| ) {
1233 my $normalized_isbn = $self->normalized_isbn;
1234 return unless $normalized_isbn;
1235 $url =~ s|{normalized_isbn}|$normalized_isbn|g;
1237 if ( $url =~ m|{issn}| ) {
1238 my $issn = $self->biblioitem->issn;
1239 return unless $issn;
1240 $url =~ s|{issn}|$issn|g;
1243 my $re = qr|{(?<field>\d{3})(\$(?<subfield>.))?}|;
1244 if ( $url =~ $re ) {
1245 my $field = $+{field};
1246 my $subfield = $+{subfield};
1247 my $marc_record = $self->metadata->record;
1250 $value = $marc_record->subfield( $field, $subfield );
1252 my $controlfield = $marc_record->field($field);
1253 $value = $controlfield->data() if $controlfield;
1255 return unless $value;
1256 $url =~ s|$re|$value|;
1264 Return the cover images associated with this biblio.
1271 my $cover_images_rs = $self->_result->cover_images;
1272 return unless $cover_images_rs;
1273 return Koha::CoverImages->_new_from_dbic($cover_images_rs);
1276 =head3 get_marc_notes
1278 $marcnotesarray = $biblio->get_marc_notes({ opac => 1 });
1280 Get all notes from the MARC record and returns them in an array.
1281 The notes are stored in different fields depending on MARC flavour.
1282 MARC21 5XX $u subfields receive special attention as they are URIs.
1286 sub get_marc_notes {
1287 my ( $self, $params ) = @_;
1289 my $marcflavour = C4::Context->preference('marcflavour');
1290 my $opac = $params->{opac} // '0';
1291 my $interface = $params->{opac} ? 'opac' : 'intranet';
1293 my $record = $params->{record} // $self->metadata->record;
1294 my $record_processor = Koha::RecordProcessor->new(
1296 filters => [ 'ViewPolicy', 'ExpandCodedFields' ],
1298 interface => $interface,
1299 frameworkcode => $self->frameworkcode
1303 $record_processor->process($record);
1305 my $scope = $marcflavour eq "UNIMARC"? '3..': '5..';
1306 #MARC21 specs indicate some notes should be private if first indicator 0
1307 my %maybe_private = (
1315 my %hiddenlist = map { $_ => 1 }
1316 split( /,/, C4::Context->preference('NotesToHide'));
1319 foreach my $field ( $record->field($scope) ) {
1320 my $tag = $field->tag();
1321 next if $hiddenlist{ $tag };
1322 next if $opac && $maybe_private{$tag} && !$field->indicator(1);
1323 if( $marcflavour ne 'UNIMARC' && $field->subfield('u') ) {
1324 # Field 5XX$u always contains URI
1325 # Examples: 505u, 506u, 510u, 514u, 520u, 530u, 538u, 540u, 542u, 552u, 555u, 561u, 563u, 583u
1326 # We first push the other subfields, then all $u's separately
1327 # Leave further actions to the template (see e.g. opac-detail)
1329 join '', ( 'a' .. 't', 'v' .. 'z', '0' .. '9' ); # excl 'u'
1330 push @marcnotes, { marcnote => $field->as_string($othersub) };
1331 foreach my $sub ( $field->subfield('u') ) {
1332 $sub =~ s/^\s+|\s+$//g; # trim
1333 push @marcnotes, { marcnote => $sub, tag => $tag };
1336 push @marcnotes, { marcnote => $field->as_string(), tag => $tag };
1342 =head3 _get_marc_authors
1344 Private method to return the list of authors contained in the MARC record.
1345 See get get_marc_contributors and get_marc_authors for the public methods.
1349 sub _get_marc_authors {
1350 my ( $self, $params ) = @_;
1352 my $fields_filter = $params->{fields_filter};
1353 my $mintag = $params->{mintag};
1354 my $maxtag = $params->{maxtag};
1356 my $AuthoritySeparator = C4::Context->preference('AuthoritySeparator');
1357 my $marcflavour = C4::Context->preference('marcflavour');
1359 # tagslib useful only for UNIMARC author responsibilities
1360 my $tagslib = $marcflavour eq "UNIMARC"
1361 ? C4::Biblio::GetMarcStructure( 1, $self->frameworkcode, { unsafe => 1 } )
1365 foreach my $field ( $self->metadata->record->field($fields_filter) ) {
1368 if $mintag && $field->tag() < $mintag
1369 || $maxtag && $field->tag() > $maxtag;
1373 my @subfields = $field->subfields();
1376 # if there is an authority link, build the link with Koha-Auth-Number: subfield9
1377 my $subfield9 = $field->subfield('9');
1379 my $linkvalue = $subfield9;
1380 $linkvalue =~ s/(\(|\))//g;
1381 @link_loop = ( { 'limit' => 'an', 'link' => $linkvalue } );
1386 for my $authors_subfield (@subfields) {
1387 next if ( $authors_subfield->[0] eq '9' );
1389 # unimarc3 contains the $3 of the author for UNIMARC.
1390 # For french academic libraries, it's the "ppn", and it's required for idref webservice
1391 $unimarc3 = $authors_subfield->[1] if $marcflavour eq 'UNIMARC' and $authors_subfield->[0] =~ /3/;
1393 # don't load unimarc subfields 3, 5
1394 next if ( $marcflavour eq 'UNIMARC' and ( $authors_subfield->[0] =~ /3|5/ ) );
1396 my $code = $authors_subfield->[0];
1397 my $value = $authors_subfield->[1];
1398 my $linkvalue = $value;
1399 $linkvalue =~ s/(\(|\))//g;
1400 # UNIMARC author responsibility
1401 if ( $marcflavour eq 'UNIMARC' and $code eq '4' ) {
1402 $value = C4::Biblio::GetAuthorisedValueDesc( $field->tag(), $code, $value, '', $tagslib );
1403 $linkvalue = "($value)";
1405 # if no authority link, build a search query
1406 unless ($subfield9) {
1409 'link' => $linkvalue,
1410 operator => (scalar @link_loop) ? ' AND ' : undef
1413 my @this_link_loop = @link_loop;
1415 unless ( $code eq '0') {
1416 push @subfields_loop, {
1417 tag => $field->tag(),
1420 link_loop => \@this_link_loop,
1421 separator => (scalar @subfields_loop) ? $AuthoritySeparator : ''
1425 push @marcauthors, {
1426 MARCAUTHOR_SUBFIELDS_LOOP => \@subfields_loop,
1427 authoritylink => $subfield9,
1428 unimarc3 => $unimarc3
1431 return \@marcauthors;
1434 =head3 get_marc_contributors
1436 my $contributors = $biblio->get_marc_contributors;
1438 Get all contributors (but first author) from the MARC record and returns them in an array.
1439 They are stored in different fields depending on MARC flavour (700..720 for MARC21)
1443 sub get_marc_contributors {
1444 my ( $self, $params ) = @_;
1446 my ( $mintag, $maxtag, $fields_filter );
1447 my $marcflavour = C4::Context->preference('marcflavour');
1449 if ( $marcflavour eq "UNIMARC" ) {
1452 $fields_filter = '7..';
1453 } else { # marc21/normarc
1456 $fields_filter = '7..';
1459 return $self->_get_marc_authors(
1461 fields_filter => $fields_filter,
1468 =head3 get_marc_authors
1470 my $authors = $biblio->get_marc_authors;
1472 Get all authors from the MARC record and returns them in an array.
1473 They are stored in different fields depending on MARC flavour
1474 (main author from 100 then secondary authors from 700..720).
1478 sub get_marc_authors {
1479 my ( $self, $params ) = @_;
1481 my ( $mintag, $maxtag, $fields_filter );
1482 my $marcflavour = C4::Context->preference('marcflavour');
1484 if ( $marcflavour eq "UNIMARC" ) {
1485 $fields_filter = '200';
1486 } else { # marc21/normarc
1487 $fields_filter = '100';
1490 my @first_authors = @{$self->_get_marc_authors(
1492 fields_filter => $fields_filter,
1498 my @other_authors = @{$self->get_marc_contributors};
1500 return [@first_authors, @other_authors];
1503 =head3 normalized_isbn
1505 my $normalized_isbn = $biblio->normalized_isbn
1507 Normalizes and returns the first valid ISBN found in the record.
1508 ISBN13 are converted into ISBN10. This is required to get some book cover images.
1512 sub normalized_isbn {
1514 return C4::Koha::GetNormalizedISBN($self->biblioitem->isbn);
1517 =head3 public_read_list
1519 This method returns the list of publicly readable database fields for both API and UI output purposes
1523 sub public_read_list {
1525 'biblionumber', 'frameworkcode', 'author',
1526 'title', 'medium', 'subtitle',
1527 'part_number', 'part_name', 'unititle',
1528 'notes', 'serial', 'seriestitle',
1529 'copyrightdate', 'abstract'
1533 =head3 metadata_extractor
1535 my $extractor = $biblio->metadata_extractor
1537 Return a Koha::Biblio::Metadata::Extractor object to use to extract data from the metadata (ie. MARC record for now)
1541 sub metadata_extractor {
1544 $self->{metadata_extractor} ||= Koha::Biblio::Metadata::Extractor->new( { biblio => $self } );
1546 return $self->{metadata_extractor};
1549 =head3 normalized_upc
1551 my $normalized_upc = $biblio->normalized_upc
1553 Normalizes and returns the UPC value found in the MARC record.
1557 sub normalized_upc {
1559 return $self->metadata_extractor->get_normalized_upc;
1562 =head3 normalized_oclc
1564 my $normalized_oclc = $biblio->normalized_oclc
1566 Normalizes and returns the OCLC number found in the MARC record.
1570 sub normalized_oclc {
1572 return $self->metadata_extractor->get_normalized_oclc;
1577 my $json = $biblio->to_api;
1579 Overloaded method that returns a JSON representation of the Koha::Biblio object,
1580 suitable for API output. The related Koha::Biblioitem object is merged as expected
1586 my ($self, $args) = @_;
1588 my $json_biblio = $self->SUPER::to_api( $args );
1589 return unless $json_biblio;
1591 $args = defined $args ? {%$args} : {};
1592 delete $args->{embed};
1594 my $json_biblioitem = $self->biblioitem->to_api( $args );
1595 return unless $json_biblioitem;
1597 return { %$json_biblio, %$json_biblioitem };
1600 =head3 to_api_mapping
1602 This method returns the mapping for representing a Koha::Biblio object
1607 sub to_api_mapping {
1609 biblionumber => 'biblio_id',
1610 frameworkcode => 'framework_id',
1611 unititle => 'uniform_title',
1612 seriestitle => 'series_title',
1613 copyrightdate => 'copyright_date',
1614 datecreated => 'creation_date',
1615 deleted_on => undef,
1619 =head3 get_marc_host
1621 $host = $biblio->get_marc_host;
1623 ( $host, $relatedparts, $hostinfo ) = $biblio->get_marc_host;
1625 Returns host biblio record from MARC21 773 (undef if no 773 present).
1626 It looks at the first 773 field with MARCorgCode or only a control
1627 number. Complete $w or numeric part is used to search host record.
1628 The optional parameter no_items triggers a check if $biblio has items.
1629 If there are, the sub returns undef.
1630 Called in list context, it also returns 773$g (related parts).
1632 If there is no $w, we use $0 (host biblionumber) or $9 (host itemnumber)
1633 to search for the host record. If there is also no $0 and no $9, we search
1634 using author and title. Failing all of that, we return an undef host and
1635 form a concatenation of strings with 773$agt for host information,
1636 returned when called in list context.
1641 my ($self, $params) = @_;
1642 my $no_items = $params->{no_items};
1643 return if C4::Context->preference('marcflavour') eq 'UNIMARC'; # TODO
1644 return if $params->{no_items} && $self->items->count > 0;
1647 eval { $record = $self->metadata->record };
1650 # We pick the first $w with your MARCOrgCode or the first $w that has no
1651 # code (between parentheses) at all.
1652 my $orgcode = C4::Context->preference('MARCOrgCode') // q{};
1654 foreach my $f ( $record->field('773') ) {
1655 my $w = $f->subfield('w') or next;
1656 if( $w =~ /^\($orgcode\)\s*(\d+)/i or $w =~ /^\d+/ ) {
1662 my $engine = Koha::SearchEngine::Search->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1664 if ( !$hostfld and $record->subfield('773','t') ) {
1665 # not linked using $w
1666 my $unlinkedf = $record->field('773');
1668 if ( C4::Context->preference("EasyAnalyticalRecords") ) {
1669 if ( $unlinkedf->subfield('0') ) {
1670 # use 773$0 host biblionumber
1671 $bibno = $unlinkedf->subfield('0');
1672 } elsif ( $unlinkedf->subfield('9') ) {
1673 # use 773$9 host itemnumber
1674 my $linkeditemnumber = $unlinkedf->subfield('9');
1675 $bibno = Koha::Items->find( $linkeditemnumber )->biblionumber;
1679 my $host = Koha::Biblios->find($bibno) or return;
1680 return wantarray ? ( $host, $unlinkedf->subfield('g') ) : $host;
1682 # just return plaintext and no host record
1683 my $hostinfo = join( ", ", $unlinkedf->subfield('a'), $unlinkedf->subfield('t'), $unlinkedf->subfield('g') );
1684 return wantarray ? ( undef, $unlinkedf->subfield('g'), $hostinfo ) : undef;
1686 return if !$hostfld;
1687 my $rcn = $hostfld->subfield('w');
1689 # Look for control number with/without orgcode
1690 for my $try (1..2) {
1691 my ( $error, $results, $total_hits ) = $engine->simple_search_compat( 'Control-number='.$rcn, 0,1 );
1692 if( !$error and $total_hits == 1 ) {
1693 $bibno = $engine->extract_biblionumber( $results->[0] );
1696 # Add or remove orgcode for second try
1697 if( $try == 1 && $rcn =~ /\)\s*(\d+)/ ) {
1698 $rcn = $1; # number only
1699 } elsif( $try == 1 && $rcn =~ /^\d+/ ) {
1700 $rcn = "($orgcode)$rcn";
1706 my $host = Koha::Biblios->find($bibno) or return;
1707 return wantarray ? ( $host, $hostfld->subfield('g') ) : $host;
1711 =head3 get_marc_host_only
1713 my $host = $biblio->get_marc_host_only;
1719 sub get_marc_host_only {
1722 my ( $host ) = $self->get_marc_host;
1727 =head3 get_marc_relatedparts_only
1729 my $relatedparts = $biblio->get_marc_relatedparts_only;
1731 Return related parts only
1735 sub get_marc_relatedparts_only {
1738 my ( undef, $relatedparts ) = $self->get_marc_host;
1740 return $relatedparts;
1743 =head3 get_marc_hostinfo_only
1745 my $hostinfo = $biblio->get_marc_hostinfo_only;
1747 Return host info only
1751 sub get_marc_hostinfo_only {
1754 my ( $host, $relatedparts, $hostinfo ) = $self->get_marc_host;
1761 my $recalls = $biblio->recalls;
1763 Return recalls linked to this biblio
1769 return Koha::Recalls->_new_from_dbic( scalar $self->_result->recalls );
1772 =head3 can_be_recalled
1774 my @items_for_recall = $biblio->can_be_recalled({ patron => $patron_object });
1776 Does biblio-level checks and returns the items attached to this biblio that are available for recall
1780 sub can_be_recalled {
1781 my ( $self, $params ) = @_;
1783 return 0 if !( C4::Context->preference('UseRecalls') );
1785 my $patron = $params->{patron};
1787 my $branchcode = C4::Context->userenv->{'branch'};
1788 if ( C4::Context->preference('CircControl') eq 'PatronLibrary' and $patron ) {
1789 $branchcode = $patron->branchcode;
1792 my @all_items = Koha::Items->search({ biblionumber => $self->biblionumber })->as_list;
1794 # if there are no available items at all, no recall can be placed
1795 return 0 if ( scalar @all_items == 0 );
1800 my @all_itemnumbers;
1801 foreach my $item ( @all_items ) {
1802 push( @all_itemnumbers, $item->itemnumber );
1803 if ( $item->can_be_recalled({ patron => $patron }) ) {
1804 push( @itemtypes, $item->effective_itemtype );
1805 push( @itemnumbers, $item->itemnumber );
1806 push( @items, $item );
1810 # if there are no recallable items, no recall can be placed
1811 return 0 if ( scalar @items == 0 );
1813 # Check the circulation rule for each relevant itemtype for this biblio
1814 my ( @recalls_allowed, @recalls_per_record, @on_shelf_recalls );
1815 foreach my $itemtype ( @itemtypes ) {
1816 my $rule = Koha::CirculationRules->get_effective_rules({
1817 branchcode => $branchcode,
1818 categorycode => $patron ? $patron->categorycode : undef,
1819 itemtype => $itemtype,
1822 'recalls_per_record',
1826 push( @recalls_allowed, $rule->{recalls_allowed} ) if $rule;
1827 push( @recalls_per_record, $rule->{recalls_per_record} ) if $rule;
1828 push( @on_shelf_recalls, $rule->{on_shelf_recalls} ) if $rule;
1830 my $recalls_allowed = (sort {$b <=> $a} @recalls_allowed)[0]; # take highest
1831 my $recalls_per_record = (sort {$b <=> $a} @recalls_per_record)[0]; # take highest
1832 my %on_shelf_recalls_count = ();
1833 foreach my $count ( @on_shelf_recalls ) {
1834 $on_shelf_recalls_count{$count}++;
1836 my $on_shelf_recalls = (sort {$on_shelf_recalls_count{$b} <=> $on_shelf_recalls_count{$a}} @on_shelf_recalls)[0]; # take most common
1838 # check recalls allowed has been set and is not zero
1839 return 0 if ( !defined($recalls_allowed) || $recalls_allowed == 0 );
1842 # check borrower has not reached open recalls allowed limit
1843 return 0 if ( $patron->recalls->filter_by_current->count >= $recalls_allowed );
1845 # check borrower has not reached open recalls allowed per record limit
1846 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $recalls_per_record );
1848 # check if any of the items under this biblio are already checked out by this borrower
1849 return 0 if ( Koha::Checkouts->search({ itemnumber => [ @all_itemnumbers ], borrowernumber => $patron->borrowernumber })->count > 0 );
1852 # check item availability
1853 my $checked_out_count = 0;
1855 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1858 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1859 return 0 if ( $on_shelf_recalls eq 'all' && $checked_out_count < scalar @items );
1861 # can't recall if no items have been checked out
1862 return 0 if ( $checked_out_count == 0 );
1870 my $ratings = $biblio->ratings
1872 Return a Koha::Ratings object representing the ratings of this bibliographic record
1878 my $rs = $self->_result->ratings;
1879 return Koha::Ratings->_new_from_dbic($rs);
1882 =head3 opac_summary_html
1884 my $summary_html = $biblio->opac_summary_html
1886 Based on the syspref OPACMySummaryHTML, returns a string representing the
1887 summary of this bibliographic record.
1888 {AUTHOR}, {TITLE}, {ISBN} and {BIBLIONUMBER} will be replaced.
1892 sub opac_summary_html {
1895 my $summary_html = C4::Context->preference('OPACMySummaryHTML');
1896 return q{} unless $summary_html;
1897 my $author = $self->author || q{};
1898 my $title = $self->title || q{};
1899 $title =~ s/\/+$//; # remove trailing slash
1900 $title =~ s/\s+$//; # remove trailing space
1901 my $normalized_isbn = $self->normalized_isbn || q{};
1902 my $biblionumber = $self->biblionumber;
1904 $summary_html =~ s/{AUTHOR}/$author/g;
1905 $summary_html =~ s/{TITLE}/$title/g;
1906 $summary_html =~ s/{ISBN}/$normalized_isbn/g;
1907 $summary_html =~ s/{BIBLIONUMBER}/$biblionumber/g;
1909 return $summary_html;
1912 =head2 Internal methods
1924 Kyle M Hall <kyle@bywatersolutions.com>