3 # Copyright ByWater Solutions 2014
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22 use List::MoreUtils qw( any );
24 use URI::Escape qw( uri_escape_utf8 );
26 use C4::Koha qw( GetNormalizedISBN );
29 use Koha::DateUtils qw( dt_from_string );
31 use base qw(Koha::Object);
33 use Koha::Acquisition::Orders;
34 use Koha::ArticleRequests;
35 use Koha::Biblio::Metadatas;
36 use Koha::Biblio::ItemGroups;
37 use Koha::Biblioitems;
38 use Koha::Cache::Memory::Lite;
40 use Koha::CirculationRules;
41 use Koha::Item::Transfer::Limits;
44 use Koha::Old::Checkouts;
46 use Koha::RecordProcessor;
47 use Koha::Suggestions;
48 use Koha::Subscriptions;
49 use Koha::SearchEngine;
50 use Koha::SearchEngine::Search;
51 use Koha::SearchEngine::QueryBuilder;
56 Koha::Biblio - Koha Biblio Object class
66 Overloaded I<store> method to set default values
73 $self->datecreated( dt_from_string ) unless $self->datecreated;
75 return $self->SUPER::store;
80 my $metadata = $biblio->metadata();
82 Returns a Koha::Biblio::Metadata object
89 my $metadata = $self->_result->metadata;
90 return Koha::Biblio::Metadata->_new_from_dbic($metadata);
95 my $record = $biblio->record();
97 Returns a Marc::Record object
104 return $self->metadata->record;
109 my $orders = $biblio->orders();
111 Returns a Koha::Acquisition::Orders object
118 my $orders = $self->_result->orders;
119 return Koha::Acquisition::Orders->_new_from_dbic($orders);
124 my $active_orders = $biblio->active_orders();
126 Returns the active acquisition orders related to this biblio.
127 An order is considered active when it is not cancelled (i.e. when datecancellation
135 return $self->orders->search({ datecancellationprinted => undef });
140 my $tickets = $biblio->tickets();
142 Returns all tickets linked to the biblio
148 my $rs = $self->_result->tickets;
149 return Koha::Tickets->_new_from_dbic( $rs );
154 my $item_groups = $biblio->item_groups();
156 Returns a Koha::Biblio::ItemGroups object
163 my $item_groups = $self->_result->item_groups;
164 return Koha::Biblio::ItemGroups->_new_from_dbic($item_groups);
167 =head3 can_article_request
169 my $bool = $biblio->can_article_request( $borrower );
171 Returns true if article requests can be made for this record
173 $borrower must be a Koha::Patron object
177 sub can_article_request {
178 my ( $self, $borrower ) = @_;
180 my $rule = $self->article_request_type($borrower);
181 return q{} if $rule eq 'item_only' && !$self->items()->count();
182 return 1 if $rule && $rule ne 'no';
187 =head3 can_be_transferred
189 $biblio->can_be_transferred({ to => $to_library, from => $from_library })
191 Checks if at least one item of a biblio can be transferred to given library.
193 This feature is controlled by two system preferences:
194 UseBranchTransferLimits to enable / disable the feature
195 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
196 for setting the limitations
198 Performance-wise, it is recommended to use this method for a biblio instead of
199 iterating each item of a biblio with Koha::Item->can_be_transferred().
201 Takes HASHref that can have the following parameters:
202 MANDATORY PARAMETERS:
205 $from : Koha::Library # if given, only items from that
206 # holdingbranch are considered
208 Returns 1 if at least one of the item of a biblio can be transferred
209 to $to_library, otherwise 0.
213 sub can_be_transferred {
214 my ($self, $params) = @_;
216 my $to = $params->{to};
217 my $from = $params->{from};
219 return 1 unless C4::Context->preference('UseBranchTransferLimits');
220 my $limittype = C4::Context->preference('BranchTransferLimitsType');
223 foreach my $item_of_bib ($self->items->as_list) {
224 next unless $item_of_bib->holdingbranch;
225 next if $from && $from->branchcode ne $item_of_bib->holdingbranch;
226 return 1 if $item_of_bib->holdingbranch eq $to->branchcode;
227 my $code = $limittype eq 'itemtype'
228 ? $item_of_bib->effective_itemtype
229 : $item_of_bib->ccode;
230 return 1 unless $code;
231 $items->{$code}->{$item_of_bib->holdingbranch} = 1;
234 # At this point we will have a HASHref containing each itemtype/ccode that
235 # this biblio has, inside which are all of the holdingbranches where those
236 # items are located at. Then, we will query Koha::Item::Transfer::Limits to
237 # find out whether a transfer limits for such $limittype from any of the
238 # listed holdingbranches to the given $to library exist. If at least one
239 # holdingbranch for that $limittype does not have a transfer limit to given
240 # $to library, then we know that the transfer is possible.
241 foreach my $code (keys %{$items}) {
242 my @holdingbranches = keys %{$items->{$code}};
243 return 1 if Koha::Item::Transfer::Limits->search({
244 toBranch => $to->branchcode,
245 fromBranch => { 'in' => \@holdingbranches },
248 group_by => [qw/fromBranch/]
249 })->count == scalar(@holdingbranches) ? 0 : 1;
256 =head3 pickup_locations
258 my $pickup_locations = $biblio->pickup_locations( {patron => $patron } );
260 Returns a Koha::Libraries set of possible pickup locations for this biblio's items,
261 according to patron's home library (if patron is defined and holds are allowed
262 only from hold groups) and if item can be transferred to each pickup location.
266 sub pickup_locations {
267 my ( $self, $params ) = @_;
269 my $patron = $params->{patron};
271 my $memory_cache = Koha::Cache::Memory::Lite->get_instance();
272 my @pickup_locations;
273 foreach my $item ( $self->items->as_list ) {
274 my $cache_key = sprintf "Pickup_locations:%s:%s:%s:%s:%s",
275 $item->itype,$item->homebranch,$item->holdingbranch,$item->ccode || "",$patron->branchcode||"" ;
276 my $item_pickup_locations = $memory_cache->get_from_cache( $cache_key );
277 unless( $item_pickup_locations ){
278 @{ $item_pickup_locations } = $item->pickup_locations( { patron => $patron } )->_resultset->get_column('branchcode')->all;
279 $memory_cache->set_in_cache( $cache_key, $item_pickup_locations );
281 push @pickup_locations, @{ $item_pickup_locations }
284 return Koha::Libraries->search(
285 { branchcode => { '-in' => \@pickup_locations } }, { order_by => ['branchname'] } );
288 =head3 hidden_in_opac
290 my $bool = $biblio->hidden_in_opac({ [ rules => $rules ] })
292 Returns true if the biblio matches the hidding criteria defined in $rules.
293 Returns false otherwise. It involves the I<OpacHiddenItems> and
294 I<OpacHiddenItemsHidesRecord> system preferences.
296 Takes HASHref that can have the following parameters:
298 $rules : { <field> => [ value_1, ... ], ... }
300 Note: $rules inherits its structure from the parsed YAML from reading
301 the I<OpacHiddenItems> system preference.
306 my ( $self, $params ) = @_;
308 my $rules = $params->{rules} // {};
310 my @items = $self->items->as_list;
312 return 0 unless @items; # Do not hide if there is no item
314 # Ok, there are items, don't even try the rules unless OpacHiddenItemsHidesRecord
315 return 0 unless C4::Context->preference('OpacHiddenItemsHidesRecord');
317 return !(any { !$_->hidden_in_opac({ rules => $rules }) } @items);
320 =head3 article_request_type
322 my $type = $biblio->article_request_type( $borrower );
324 Returns the article request type based on items, or on the record
325 itself if there are no items.
327 $borrower must be a Koha::Patron object
331 sub article_request_type {
332 my ( $self, $borrower ) = @_;
334 return q{} unless $borrower;
336 my $rule = $self->article_request_type_for_items( $borrower );
337 return $rule if $rule;
339 # If the record has no items that are requestable, go by the record itemtype
340 $rule = $self->article_request_type_for_bib($borrower);
341 return $rule if $rule;
346 =head3 article_request_type_for_bib
348 my $type = $biblio->article_request_type_for_bib
350 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record
354 sub article_request_type_for_bib {
355 my ( $self, $borrower ) = @_;
357 return q{} unless $borrower;
359 my $borrowertype = $borrower->categorycode;
360 my $itemtype = $self->itemtype();
362 my $rule = Koha::CirculationRules->get_effective_rule(
364 rule_name => 'article_requests',
365 categorycode => $borrowertype,
366 itemtype => $itemtype,
370 return q{} unless $rule;
371 return $rule->rule_value || q{}
374 =head3 article_request_type_for_items
376 my $type = $biblio->article_request_type_for_items
378 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record's items
380 If there is a conflict where some items are 'bib_only' and some are 'item_only', 'bib_only' will be returned.
384 sub article_request_type_for_items {
385 my ( $self, $borrower ) = @_;
388 foreach my $item ( $self->items()->as_list() ) {
389 my $rule = $item->article_request_type($borrower);
390 return $rule if $rule eq 'bib_only'; # we don't need to go any further
394 return 'item_only' if $counts->{item_only};
395 return 'yes' if $counts->{yes};
396 return 'no' if $counts->{no};
400 =head3 article_requests
402 my $article_requests = $biblio->article_requests
404 Returns the article requests associated with this biblio
408 sub article_requests {
411 return Koha::ArticleRequests->_new_from_dbic( scalar $self->_result->article_requests );
414 =head3 current_checkouts
416 my $current_checkouts = $biblio->current_checkouts
418 Returns the current checkouts associated with this biblio
422 sub current_checkouts {
425 return Koha::Checkouts->search( { "item.biblionumber" => $self->id },
426 { join => 'item' } );
431 my $old_checkouts = $biblio->old_checkouts
433 Returns the past checkouts associated with this biblio
440 return Koha::Old::Checkouts->search( { "item.biblionumber" => $self->id },
441 { join => 'item' } );
446 my $items = $biblio->items();
448 Returns the related Koha::Items object for this biblio
455 my $items_rs = $self->_result->items;
457 return Koha::Items->_new_from_dbic( $items_rs );
462 my $host_items = $biblio->host_items();
464 Return the host items (easy analytical record)
471 return Koha::Items->new->empty
472 unless C4::Context->preference('EasyAnalyticalRecords');
474 my $marcflavour = C4::Context->preference("marcflavour");
475 my $analyticfield = '773';
476 if ( $marcflavour eq 'MARC21' ) {
477 $analyticfield = '773';
479 elsif ( $marcflavour eq 'UNIMARC' ) {
480 $analyticfield = '461';
482 my $marc_record = $self->metadata->record;
484 foreach my $field ( $marc_record->field($analyticfield) ) {
485 push @itemnumbers, $field->subfield('9');
488 return Koha::Items->search( { itemnumber => { -in => \@itemnumbers } } );
493 my $itemtype = $biblio->itemtype();
495 Returns the itemtype for this record.
502 return $self->biblioitem()->itemtype();
507 my $holds = $biblio->holds();
509 return the current holds placed on this record
514 my ( $self, $params, $attributes ) = @_;
515 $attributes->{order_by} = 'priority' unless exists $attributes->{order_by};
516 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
517 return Koha::Holds->_new_from_dbic($hold_rs);
522 my $holds = $biblio->current_holds
524 Return the holds placed on this bibliographic record.
525 It does not include future holds.
531 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
533 { reservedate => { '<=' => $dtf->format_date(dt_from_string) } } );
538 my $field = $self->biblioitem()->itemtype
540 Returns the related Koha::Biblioitem object for this Biblio object
547 $self->{_biblioitem} ||= Koha::Biblioitems->find( { biblionumber => $self->biblionumber() } );
549 return $self->{_biblioitem};
554 my $suggestions = $self->suggestions
556 Returns the related Koha::Suggestions object for this Biblio object
563 my $suggestions_rs = $self->_result->suggestions;
564 return Koha::Suggestions->_new_from_dbic( $suggestions_rs );
567 =head3 get_marc_components
569 my $components = $self->get_marc_components();
571 Returns an array of search results data, which are component parts of
572 this object (MARC21 773 points to this)
576 sub get_marc_components {
577 my ($self, $max_results) = @_;
579 return [] if (C4::Context->preference('marcflavour') ne 'MARC21');
581 my ( $searchstr, $sort ) = $self->get_components_query;
584 if (defined($searchstr)) {
585 my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
586 my ( $error, $results, $facets );
588 ( $error, $results, $facets ) = $searcher->search_compat( $searchstr, undef, [$sort], ['biblioserver'], $max_results, 0, undef, undef, 'ccl', 0 );
593 warn "Warning from search_compat: '$error'";
597 message => 'component_search',
602 $components = $results->{biblioserver}->{RECORDS} if defined($results) && $results->{biblioserver}->{hits};
605 return $components // [];
608 =head2 get_components_query
610 Returns a query which can be used to search for all component parts of MARC21 biblios
614 sub get_components_query {
617 my $builder = Koha::SearchEngine::QueryBuilder->new(
618 { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
619 my $marc = $self->metadata->record;
620 my $component_sort_field = C4::Context->preference('ComponentSortField') // "title";
621 my $component_sort_order = C4::Context->preference('ComponentSortOrder') // "asc";
622 my $sort = $component_sort_field . "_" . $component_sort_order;
625 if ( C4::Context->preference('UseControlNumber') ) {
626 my $pf001 = $marc->field('001') || undef;
628 if ( defined($pf001) ) {
630 my $pf003 = $marc->field('003') || undef;
632 if ( !defined($pf003) ) {
633 # search for 773$w='Host001'
634 $searchstr .= "rcn:\"" . $pf001->data()."\"";
638 # search for (773$w='Host001' and 003='Host003') or 773$w='(Host003)Host001'
639 $searchstr .= "(rcn:\"" . $pf001->data() . "\" AND cni:\"" . $pf003->data() . "\")";
640 $searchstr .= " OR rcn:\"" . $pf003->data() . " " . $pf001->data() . "\"";
644 # limit to monograph and serial component part records
645 $searchstr .= " AND (bib-level:a OR bib-level:b)";
650 my $cleaned_title = $marc->subfield('245', "a");
651 $cleaned_title =~ tr|/||;
652 $cleaned_title = $builder->clean_search_term($cleaned_title);
653 $searchstr = qq#Host-item:("$cleaned_title")#;
655 my ($error, $query ,$query_str) = $builder->build_query_compat( undef, [$searchstr], undef, undef, [$sort], 0 );
661 return ($query, $query_str, $sort);
666 my $subscriptions = $self->subscriptions
668 Returns the related Koha::Subscriptions object for this Biblio object
675 $self->{_subscriptions} ||= Koha::Subscriptions->search( { biblionumber => $self->biblionumber } );
677 return $self->{_subscriptions};
680 =head3 has_items_waiting_or_intransit
682 my $itemsWaitingOrInTransit = $biblio->has_items_waiting_or_intransit
684 Tells if this bibliographic record has items waiting or in transit.
688 sub has_items_waiting_or_intransit {
691 if ( Koha::Holds->search({ biblionumber => $self->id,
692 found => ['W', 'T'] })->count ) {
696 foreach my $item ( $self->items->as_list ) {
697 return 1 if $item->get_transfer;
705 my $coins = $biblio->get_coins;
707 Returns the COinS (a span) which can be included in a biblio record
714 my $record = $self->metadata->record;
716 my $pos7 = substr $record->leader(), 7, 1;
717 my $pos6 = substr $record->leader(), 6, 1;
720 my ( $aulast, $aufirst ) = ( '', '' );
731 # For the purposes of generating COinS metadata, LDR/06-07 can be
732 # considered the same for UNIMARC and MARC21
741 'i' => 'audioRecording',
742 'j' => 'audioRecording',
745 'm' => 'computerProgram',
750 'a' => 'journalArticle',
754 $genre = $fmts6->{$pos6} ? $fmts6->{$pos6} : 'book';
756 if ( $genre eq 'book' ) {
757 $genre = $fmts7->{$pos7} if $fmts7->{$pos7};
760 ##### We must transform mtx to a valable mtx and document type ####
761 if ( $genre eq 'book' ) {
764 } elsif ( $genre eq 'journal' ) {
767 } elsif ( $genre eq 'journalArticle' ) {
775 if ( C4::Context->preference("marcflavour") eq "UNIMARC" ) {
778 $aulast = $record->subfield( '700', 'a' ) || '';
779 $aufirst = $record->subfield( '700', 'b' ) || '';
780 push @authors, "$aufirst $aulast" if ($aufirst or $aulast);
783 if ( $record->field('200') ) {
784 for my $au ( $record->field('200')->subfield('g') ) {
789 $title = $record->subfield( '200', 'a' );
790 my $subfield_210d = $record->subfield('210', 'd');
791 if ($subfield_210d and $subfield_210d =~ /(\d{4})/) {
794 $publisher = $record->subfield( '210', 'c' ) || '';
795 $isbn = $record->subfield( '010', 'a' ) || '';
796 $issn = $record->subfield( '011', 'a' ) || '';
799 # MARC21 need some improve
802 if ( $record->field('100') ) {
803 push @authors, $record->subfield( '100', 'a' );
807 if ( $record->field('700') ) {
808 for my $au ( $record->field('700')->subfield('a') ) {
812 $title = $record->field('245');
813 $title &&= $title->as_string('ab');
814 if ($titletype eq 'a') {
815 $pubyear = $record->field('008') || '';
816 $pubyear = substr($pubyear->data(), 7, 4) if $pubyear;
817 $isbn = $record->subfield( '773', 'z' ) || '';
818 $issn = $record->subfield( '773', 'x' ) || '';
819 $hosttitle = $record->subfield( '773', 't' ) || $record->subfield( '773', 'a') || q{};
820 my @rels = $record->subfield( '773', 'g' );
821 $pages = join(', ', @rels);
823 $pubyear = $record->subfield( '260', 'c' ) || '';
824 $publisher = $record->subfield( '260', 'b' ) || '';
825 $isbn = $record->subfield( '020', 'a' ) || '';
826 $issn = $record->subfield( '022', 'a' ) || '';
832 [ 'ctx_ver', 'Z39.88-2004' ],
833 [ 'rft_val_fmt', "info:ofi/fmt:kev:mtx:$mtx" ],
834 [ ($mtx eq 'dc' ? 'rft.type' : 'rft.genre'), $genre ],
835 [ "rft.${titletype}title", $title ],
838 # rft.title is authorized only once, so by checking $titletype
839 # we ensure that rft.title is not already in the list.
840 if ($hosttitle and $titletype) {
841 push @params, [ 'rft.title', $hosttitle ];
845 [ 'rft.isbn', $isbn ],
846 [ 'rft.issn', $issn ],
849 # If it's a subscription, these informations have no meaning.
850 if ($genre ne 'journal') {
852 [ 'rft.aulast', $aulast ],
853 [ 'rft.aufirst', $aufirst ],
854 (map { [ 'rft.au', $_ ] } @authors),
855 [ 'rft.pub', $publisher ],
856 [ 'rft.date', $pubyear ],
857 [ 'rft.pages', $pages ],
861 my $coins_value = join( '&',
862 map { $$_[1] ? $$_[0] . '=' . uri_escape_utf8( $$_[1] ) : () } @params );
869 my $url = $biblio->get_openurl;
871 Returns url for OpenURL resolver set in OpenURLResolverURL system preference
878 my $OpenURLResolverURL = C4::Context->preference('OpenURLResolverURL');
880 if ($OpenURLResolverURL) {
881 my $uri = URI->new($OpenURLResolverURL);
883 if (not defined $uri->query) {
884 $OpenURLResolverURL .= '?';
886 $OpenURLResolverURL .= '&';
888 $OpenURLResolverURL .= $self->get_coins;
891 return $OpenURLResolverURL;
896 my $serial = $biblio->is_serial
898 Return boolean true if this bibbliographic record is continuing resource
905 return 1 if $self->serial;
907 my $record = $self->metadata->record;
908 return 1 if substr($record->leader, 7, 1) eq 's';
913 =head3 custom_cover_image_url
915 my $image_url = $biblio->custom_cover_image_url
917 Return the specific url of the cover image for this bibliographic record.
918 It is built regaring the value of the system preference CustomCoverImagesURL
922 sub custom_cover_image_url {
924 my $url = C4::Context->preference('CustomCoverImagesURL');
925 if ( $url =~ m|{isbn}| ) {
926 my $isbn = $self->biblioitem->isbn;
928 $url =~ s|{isbn}|$isbn|g;
930 if ( $url =~ m|{normalized_isbn}| ) {
931 my $normalized_isbn = C4::Koha::GetNormalizedISBN($self->biblioitem->isbn);
932 return unless $normalized_isbn;
933 $url =~ s|{normalized_isbn}|$normalized_isbn|g;
935 if ( $url =~ m|{issn}| ) {
936 my $issn = $self->biblioitem->issn;
938 $url =~ s|{issn}|$issn|g;
941 my $re = qr|{(?<field>\d{3})(\$(?<subfield>.))?}|;
943 my $field = $+{field};
944 my $subfield = $+{subfield};
945 my $marc_record = $self->metadata->record;
948 $value = $marc_record->subfield( $field, $subfield );
950 my $controlfield = $marc_record->field($field);
951 $value = $controlfield->data() if $controlfield;
953 return unless $value;
954 $url =~ s|$re|$value|;
962 Return the cover images associated with this biblio.
969 my $cover_images_rs = $self->_result->cover_images;
970 return unless $cover_images_rs;
971 return Koha::CoverImages->_new_from_dbic($cover_images_rs);
974 =head3 get_marc_notes
976 $marcnotesarray = $biblio->get_marc_notes({ opac => 1 });
978 Get all notes from the MARC record and returns them in an array.
979 The notes are stored in different fields depending on MARC flavour.
980 MARC21 5XX $u subfields receive special attention as they are URIs.
985 my ( $self, $params ) = @_;
987 my $marcflavour = C4::Context->preference('marcflavour');
988 my $opac = $params->{opac} // '0';
989 my $interface = $params->{opac} ? 'opac' : 'intranet';
991 my $record = $params->{record} // $self->metadata->record;
992 my $record_processor = Koha::RecordProcessor->new(
994 filters => [ 'ViewPolicy', 'ExpandCodedFields' ],
996 interface => $interface,
997 frameworkcode => $self->frameworkcode
1001 $record_processor->process($record);
1003 my $scope = $marcflavour eq "UNIMARC"? '3..': '5..';
1004 #MARC21 specs indicate some notes should be private if first indicator 0
1005 my %maybe_private = (
1013 my %hiddenlist = map { $_ => 1 }
1014 split( /,/, C4::Context->preference('NotesToHide'));
1017 foreach my $field ( $record->field($scope) ) {
1018 my $tag = $field->tag();
1019 next if $hiddenlist{ $tag };
1020 next if $opac && $maybe_private{$tag} && !$field->indicator(1);
1021 if( $marcflavour ne 'UNIMARC' && $field->subfield('u') ) {
1022 # Field 5XX$u always contains URI
1023 # Examples: 505u, 506u, 510u, 514u, 520u, 530u, 538u, 540u, 542u, 552u, 555u, 561u, 563u, 583u
1024 # We first push the other subfields, then all $u's separately
1025 # Leave further actions to the template (see e.g. opac-detail)
1027 join '', ( 'a' .. 't', 'v' .. 'z', '0' .. '9' ); # excl 'u'
1028 push @marcnotes, { marcnote => $field->as_string($othersub) };
1029 foreach my $sub ( $field->subfield('u') ) {
1030 $sub =~ s/^\s+|\s+$//g; # trim
1031 push @marcnotes, { marcnote => $sub };
1034 push @marcnotes, { marcnote => $field->as_string() };
1040 =head3 _get_marc_authors
1042 Private method to return the list of authors contained in the MARC record.
1043 See get get_marc_contributors and get_marc_authors for the public methods.
1047 sub _get_marc_authors {
1048 my ( $self, $params ) = @_;
1050 my $fields_filter = $params->{fields_filter};
1051 my $mintag = $params->{mintag};
1052 my $maxtag = $params->{maxtag};
1054 my $AuthoritySeparator = C4::Context->preference('AuthoritySeparator');
1055 my $marcflavour = C4::Context->preference('marcflavour');
1057 # tagslib useful only for UNIMARC author responsibilities
1058 my $tagslib = $marcflavour eq "UNIMARC"
1059 ? C4::Biblio::GetMarcStructure( 1, $self->frameworkcode, { unsafe => 1 } )
1063 foreach my $field ( $self->metadata->record->field($fields_filter) ) {
1066 if $mintag && $field->tag() < $mintag
1067 || $maxtag && $field->tag() > $maxtag;
1071 my @subfields = $field->subfields();
1074 # if there is an authority link, build the link with Koha-Auth-Number: subfield9
1075 my $subfield9 = $field->subfield('9');
1077 my $linkvalue = $subfield9;
1078 $linkvalue =~ s/(\(|\))//g;
1079 @link_loop = ( { 'limit' => 'an', 'link' => $linkvalue } );
1084 for my $authors_subfield (@subfields) {
1085 next if ( $authors_subfield->[0] eq '9' );
1087 # unimarc3 contains the $3 of the author for UNIMARC.
1088 # For french academic libraries, it's the "ppn", and it's required for idref webservice
1089 $unimarc3 = $authors_subfield->[1] if $marcflavour eq 'UNIMARC' and $authors_subfield->[0] =~ /3/;
1091 # don't load unimarc subfields 3, 5
1092 next if ( $marcflavour eq 'UNIMARC' and ( $authors_subfield->[0] =~ /3|5/ ) );
1094 my $code = $authors_subfield->[0];
1095 my $value = $authors_subfield->[1];
1096 my $linkvalue = $value;
1097 $linkvalue =~ s/(\(|\))//g;
1098 # UNIMARC author responsibility
1099 if ( $marcflavour eq 'UNIMARC' and $code eq '4' ) {
1100 $value = C4::Biblio::GetAuthorisedValueDesc( $field->tag(), $code, $value, '', $tagslib );
1101 $linkvalue = "($value)";
1103 # if no authority link, build a search query
1104 unless ($subfield9) {
1107 'link' => $linkvalue,
1108 operator => (scalar @link_loop) ? ' AND ' : undef
1111 my @this_link_loop = @link_loop;
1113 unless ( $code eq '0') {
1114 push @subfields_loop, {
1115 tag => $field->tag(),
1118 link_loop => \@this_link_loop,
1119 separator => (scalar @subfields_loop) ? $AuthoritySeparator : ''
1123 push @marcauthors, {
1124 MARCAUTHOR_SUBFIELDS_LOOP => \@subfields_loop,
1125 authoritylink => $subfield9,
1126 unimarc3 => $unimarc3
1129 return \@marcauthors;
1132 =head3 get_marc_contributors
1134 my $contributors = $biblio->get_marc_contributors;
1136 Get all contributors (but first author) from the MARC record and returns them in an array.
1137 They are stored in different fields depending on MARC flavour (700..720 for MARC21)
1141 sub get_marc_contributors {
1142 my ( $self, $params ) = @_;
1144 my ( $mintag, $maxtag, $fields_filter );
1145 my $marcflavour = C4::Context->preference('marcflavour');
1147 if ( $marcflavour eq "UNIMARC" ) {
1150 $fields_filter = '7..';
1151 } else { # marc21/normarc
1154 $fields_filter = '7..';
1157 return $self->_get_marc_authors(
1159 fields_filter => $fields_filter,
1166 =head3 get_marc_authors
1168 my $authors = $biblio->get_marc_authors;
1170 Get all authors from the MARC record and returns them in an array.
1171 They are stored in different fields depending on MARC flavour
1172 (main author from 100 then secondary authors from 700..720).
1176 sub get_marc_authors {
1177 my ( $self, $params ) = @_;
1179 my ( $mintag, $maxtag, $fields_filter );
1180 my $marcflavour = C4::Context->preference('marcflavour');
1182 if ( $marcflavour eq "UNIMARC" ) {
1183 $fields_filter = '200';
1184 } else { # marc21/normarc
1185 $fields_filter = '100';
1188 my @first_authors = @{$self->_get_marc_authors(
1190 fields_filter => $fields_filter,
1196 my @other_authors = @{$self->get_marc_contributors};
1198 return [@first_authors, @other_authors];
1204 my $json = $biblio->to_api;
1206 Overloaded method that returns a JSON representation of the Koha::Biblio object,
1207 suitable for API output. The related Koha::Biblioitem object is merged as expected
1213 my ($self, $args) = @_;
1215 my $response = $self->SUPER::to_api( $args );
1216 my $biblioitem = $self->biblioitem->to_api;
1218 return { %$response, %$biblioitem };
1221 =head3 to_api_mapping
1223 This method returns the mapping for representing a Koha::Biblio object
1228 sub to_api_mapping {
1230 biblionumber => 'biblio_id',
1231 frameworkcode => 'framework_id',
1232 unititle => 'uniform_title',
1233 seriestitle => 'series_title',
1234 copyrightdate => 'copyright_date',
1235 datecreated => 'creation_date',
1236 deleted_on => undef,
1240 =head3 get_marc_host
1242 $host = $biblio->get_marc_host;
1244 ( $host, $relatedparts, $hostinfo ) = $biblio->get_marc_host;
1246 Returns host biblio record from MARC21 773 (undef if no 773 present).
1247 It looks at the first 773 field with MARCorgCode or only a control
1248 number. Complete $w or numeric part is used to search host record.
1249 The optional parameter no_items triggers a check if $biblio has items.
1250 If there are, the sub returns undef.
1251 Called in list context, it also returns 773$g (related parts).
1253 If there is no $w, we use $0 (host biblionumber) or $9 (host itemnumber)
1254 to search for the host record. If there is also no $0 and no $9, we search
1255 using author and title. Failing all of that, we return an undef host and
1256 form a concatenation of strings with 773$agt for host information,
1257 returned when called in list context.
1262 my ($self, $params) = @_;
1263 my $no_items = $params->{no_items};
1264 return if C4::Context->preference('marcflavour') eq 'UNIMARC'; # TODO
1265 return if $params->{no_items} && $self->items->count > 0;
1268 eval { $record = $self->metadata->record };
1271 # We pick the first $w with your MARCOrgCode or the first $w that has no
1272 # code (between parentheses) at all.
1273 my $orgcode = C4::Context->preference('MARCOrgCode') // q{};
1275 foreach my $f ( $record->field('773') ) {
1276 my $w = $f->subfield('w') or next;
1277 if( $w =~ /^\($orgcode\)\s*(\d+)/i or $w =~ /^\d+/ ) {
1283 my $engine = Koha::SearchEngine::Search->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1285 if ( !$hostfld and $record->subfield('773','t') ) {
1286 # not linked using $w
1287 my $unlinkedf = $record->field('773');
1289 if ( C4::Context->preference("EasyAnalyticalRecords") ) {
1290 if ( $unlinkedf->subfield('0') ) {
1291 # use 773$0 host biblionumber
1292 $bibno = $unlinkedf->subfield('0');
1293 } elsif ( $unlinkedf->subfield('9') ) {
1294 # use 773$9 host itemnumber
1295 my $linkeditemnumber = $unlinkedf->subfield('9');
1296 $bibno = Koha::Items->find( $linkeditemnumber )->biblionumber;
1300 my $host = Koha::Biblios->find($bibno) or return;
1301 return wantarray ? ( $host, $unlinkedf->subfield('g') ) : $host;
1303 # just return plaintext and no host record
1304 my $hostinfo = join( ", ", $unlinkedf->subfield('a'), $unlinkedf->subfield('t'), $unlinkedf->subfield('g') );
1305 return wantarray ? ( undef, $unlinkedf->subfield('g'), $hostinfo ) : undef;
1307 return if !$hostfld;
1308 my $rcn = $hostfld->subfield('w');
1310 # Look for control number with/without orgcode
1311 for my $try (1..2) {
1312 my ( $error, $results, $total_hits ) = $engine->simple_search_compat( 'Control-number='.$rcn, 0,1 );
1313 if( !$error and $total_hits == 1 ) {
1314 $bibno = $engine->extract_biblionumber( $results->[0] );
1317 # Add or remove orgcode for second try
1318 if( $try == 1 && $rcn =~ /\)\s*(\d+)/ ) {
1319 $rcn = $1; # number only
1320 } elsif( $try == 1 && $rcn =~ /^\d+/ ) {
1321 $rcn = "($orgcode)$rcn";
1327 my $host = Koha::Biblios->find($bibno) or return;
1328 return wantarray ? ( $host, $hostfld->subfield('g') ) : $host;
1332 =head3 get_marc_host_only
1334 my $host = $biblio->get_marc_host_only;
1340 sub get_marc_host_only {
1343 my ( $host ) = $self->get_marc_host;
1348 =head3 get_marc_relatedparts_only
1350 my $relatedparts = $biblio->get_marc_relatedparts_only;
1352 Return related parts only
1356 sub get_marc_relatedparts_only {
1359 my ( undef, $relatedparts ) = $self->get_marc_host;
1361 return $relatedparts;
1364 =head3 get_marc_hostinfo_only
1366 my $hostinfo = $biblio->get_marc_hostinfo_only;
1368 Return host info only
1372 sub get_marc_hostinfo_only {
1375 my ( $host, $relatedparts, $hostinfo ) = $self->get_marc_host;
1382 my $recalls = $biblio->recalls;
1384 Return recalls linked to this biblio
1390 return Koha::Recalls->_new_from_dbic( scalar $self->_result->recalls );
1393 =head3 can_be_recalled
1395 my @items_for_recall = $biblio->can_be_recalled({ patron => $patron_object });
1397 Does biblio-level checks and returns the items attached to this biblio that are available for recall
1401 sub can_be_recalled {
1402 my ( $self, $params ) = @_;
1404 return 0 if !( C4::Context->preference('UseRecalls') );
1406 my $patron = $params->{patron};
1408 my $branchcode = C4::Context->userenv->{'branch'};
1409 if ( C4::Context->preference('CircControl') eq 'PatronLibrary' and $patron ) {
1410 $branchcode = $patron->branchcode;
1413 my @all_items = Koha::Items->search({ biblionumber => $self->biblionumber })->as_list;
1415 # if there are no available items at all, no recall can be placed
1416 return 0 if ( scalar @all_items == 0 );
1421 my @all_itemnumbers;
1422 foreach my $item ( @all_items ) {
1423 push( @all_itemnumbers, $item->itemnumber );
1424 if ( $item->can_be_recalled({ patron => $patron }) ) {
1425 push( @itemtypes, $item->effective_itemtype );
1426 push( @itemnumbers, $item->itemnumber );
1427 push( @items, $item );
1431 # if there are no recallable items, no recall can be placed
1432 return 0 if ( scalar @items == 0 );
1434 # Check the circulation rule for each relevant itemtype for this biblio
1435 my ( @recalls_allowed, @recalls_per_record, @on_shelf_recalls );
1436 foreach my $itemtype ( @itemtypes ) {
1437 my $rule = Koha::CirculationRules->get_effective_rules({
1438 branchcode => $branchcode,
1439 categorycode => $patron ? $patron->categorycode : undef,
1440 itemtype => $itemtype,
1443 'recalls_per_record',
1447 push( @recalls_allowed, $rule->{recalls_allowed} ) if $rule;
1448 push( @recalls_per_record, $rule->{recalls_per_record} ) if $rule;
1449 push( @on_shelf_recalls, $rule->{on_shelf_recalls} ) if $rule;
1451 my $recalls_allowed = (sort {$b <=> $a} @recalls_allowed)[0]; # take highest
1452 my $recalls_per_record = (sort {$b <=> $a} @recalls_per_record)[0]; # take highest
1453 my %on_shelf_recalls_count = ();
1454 foreach my $count ( @on_shelf_recalls ) {
1455 $on_shelf_recalls_count{$count}++;
1457 my $on_shelf_recalls = (sort {$on_shelf_recalls_count{$b} <=> $on_shelf_recalls_count{$a}} @on_shelf_recalls)[0]; # take most common
1459 # check recalls allowed has been set and is not zero
1460 return 0 if ( !defined($recalls_allowed) || $recalls_allowed == 0 );
1463 # check borrower has not reached open recalls allowed limit
1464 return 0 if ( $patron->recalls->filter_by_current->count >= $recalls_allowed );
1466 # check borrower has not reached open recalls allowed per record limit
1467 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $recalls_per_record );
1469 # check if any of the items under this biblio are already checked out by this borrower
1470 return 0 if ( Koha::Checkouts->search({ itemnumber => [ @all_itemnumbers ], borrowernumber => $patron->borrowernumber })->count > 0 );
1473 # check item availability
1474 my $checked_out_count = 0;
1476 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1479 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1480 return 0 if ( $on_shelf_recalls eq 'all' && $checked_out_count < scalar @items );
1482 # can't recall if no items have been checked out
1483 return 0 if ( $checked_out_count == 0 );
1489 =head2 Internal methods
1501 Kyle M Hall <kyle@bywatersolutions.com>