Bug 23974: Improve readability
[koha.git] / Koha / Biblio.pm
blob8c26db04fb6c69019f4e521a0a3c58ceee413099
1 package Koha::Biblio;
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 under the
8 # terms of the GNU General Public License as published by the Free Software
9 # Foundation; either version 3 of the License, or (at your option) any later
10 # version.
12 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License along
17 # with Koha; if not, write to the Free Software Foundation, Inc.,
18 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 use Modern::Perl;
22 use Carp;
23 use List::MoreUtils qw(any);
24 use URI;
25 use URI::Escape;
27 use C4::Koha;
28 use C4::Biblio qw();
30 use Koha::Database;
31 use Koha::DateUtils qw( dt_from_string );
33 use base qw(Koha::Object);
35 use Koha::ArticleRequest::Status;
36 use Koha::ArticleRequests;
37 use Koha::Biblio::Metadatas;
38 use Koha::Biblioitems;
39 use Koha::IssuingRules;
40 use Koha::Item::Transfer::Limits;
41 use Koha::Items;
42 use Koha::Libraries;
43 use Koha::Subscriptions;
45 =head1 NAME
47 Koha::Biblio - Koha Biblio Object class
49 =head1 API
51 =head2 Class Methods
53 =cut
55 =head3 store
57 Overloaded I<store> method to set default values
59 =cut
61 sub store {
62 my ( $self ) = @_;
64 $self->datecreated( dt_from_string ) unless $self->datecreated;
66 return $self->SUPER::store;
69 =head3 metadata
71 my $metadata = $biblio->metadata();
73 Returns a Koha::Biblio::Metadata object
75 =cut
77 sub metadata {
78 my ( $self ) = @_;
80 my $metadata = $self->_result->metadata;
81 return Koha::Biblio::Metadata->_new_from_dbic($metadata);
84 =head3 can_article_request
86 my $bool = $biblio->can_article_request( $borrower );
88 Returns true if article requests can be made for this record
90 $borrower must be a Koha::Patron object
92 =cut
94 sub can_article_request {
95 my ( $self, $borrower ) = @_;
97 my $rule = $self->article_request_type($borrower);
98 return q{} if $rule eq 'item_only' && !$self->items()->count();
99 return 1 if $rule && $rule ne 'no';
101 return q{};
104 =head3 can_be_transferred
106 $biblio->can_be_transferred({ to => $to_library, from => $from_library })
108 Checks if at least one item of a biblio can be transferred to given library.
110 This feature is controlled by two system preferences:
111 UseBranchTransferLimits to enable / disable the feature
112 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
113 for setting the limitations
115 Performance-wise, it is recommended to use this method for a biblio instead of
116 iterating each item of a biblio with Koha::Item->can_be_transferred().
118 Takes HASHref that can have the following parameters:
119 MANDATORY PARAMETERS:
120 $to : Koha::Library
121 OPTIONAL PARAMETERS:
122 $from : Koha::Library # if given, only items from that
123 # holdingbranch are considered
125 Returns 1 if at least one of the item of a biblio can be transferred
126 to $to_library, otherwise 0.
128 =cut
130 sub can_be_transferred {
131 my ($self, $params) = @_;
133 my $to = $params->{to};
134 my $from = $params->{from};
136 return 1 unless C4::Context->preference('UseBranchTransferLimits');
137 my $limittype = C4::Context->preference('BranchTransferLimitsType');
139 my $items;
140 foreach my $item_of_bib ($self->items->as_list) {
141 next unless $item_of_bib->holdingbranch;
142 next if $from && $from->branchcode ne $item_of_bib->holdingbranch;
143 return 1 if $item_of_bib->holdingbranch eq $to->branchcode;
144 my $code = $limittype eq 'itemtype'
145 ? $item_of_bib->effective_itemtype
146 : $item_of_bib->ccode;
147 return 1 unless $code;
148 $items->{$code}->{$item_of_bib->holdingbranch} = 1;
151 # At this point we will have a HASHref containing each itemtype/ccode that
152 # this biblio has, inside which are all of the holdingbranches where those
153 # items are located at. Then, we will query Koha::Item::Transfer::Limits to
154 # find out whether a transfer limits for such $limittype from any of the
155 # listed holdingbranches to the given $to library exist. If at least one
156 # holdingbranch for that $limittype does not have a transfer limit to given
157 # $to library, then we know that the transfer is possible.
158 foreach my $code (keys %{$items}) {
159 my @holdingbranches = keys %{$items->{$code}};
160 return 1 if Koha::Item::Transfer::Limits->search({
161 toBranch => $to->branchcode,
162 fromBranch => { 'in' => \@holdingbranches },
163 $limittype => $code
164 }, {
165 group_by => [qw/fromBranch/]
166 })->count == scalar(@holdingbranches) ? 0 : 1;
169 return 0;
173 =head3 pickup_locations
175 @pickup_locations = $biblio->pickup_locations( {patron => $patron } )
177 Returns possible pickup locations for this biblio items, according to patron's home library (if patron is defined and holds are allowed only from hold groups)
178 and if item can be transferred to each pickup location.
180 =cut
182 sub pickup_locations {
183 my ($self, $params) = @_;
185 my $patron = $params->{patron};
187 my @pickup_locations;
188 foreach my $item_of_bib ($self->items->as_list) {
189 push @pickup_locations, $item_of_bib->pickup_locations( {patron => $patron} );
192 my %seen;
193 @pickup_locations =
194 grep { !$seen{ $_->branchcode }++ } @pickup_locations;
196 return wantarray ? @pickup_locations : \@pickup_locations;
199 =head3 hidden_in_opac
201 my $bool = $biblio->hidden_in_opac({ [ rules => $rules ] })
203 Returns true if the biblio matches the hidding criteria defined in $rules.
204 Returns false otherwise.
206 Takes HASHref that can have the following parameters:
207 OPTIONAL PARAMETERS:
208 $rules : { <field> => [ value_1, ... ], ... }
210 Note: $rules inherits its structure from the parsed YAML from reading
211 the I<OpacHiddenItems> system preference.
213 =cut
215 sub hidden_in_opac {
216 my ( $self, $params ) = @_;
218 my $rules = $params->{rules} // {};
220 my @items = $self->items->as_list;
222 return 0 unless @items; # Do not hide if there is no item
224 return !(any { !$_->hidden_in_opac({ rules => $rules }) } @items);
227 =head3 article_request_type
229 my $type = $biblio->article_request_type( $borrower );
231 Returns the article request type based on items, or on the record
232 itself if there are no items.
234 $borrower must be a Koha::Patron object
236 =cut
238 sub article_request_type {
239 my ( $self, $borrower ) = @_;
241 return q{} unless $borrower;
243 my $rule = $self->article_request_type_for_items( $borrower );
244 return $rule if $rule;
246 # If the record has no items that are requestable, go by the record itemtype
247 $rule = $self->article_request_type_for_bib($borrower);
248 return $rule if $rule;
250 return q{};
253 =head3 article_request_type_for_bib
255 my $type = $biblio->article_request_type_for_bib
257 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record
259 =cut
261 sub article_request_type_for_bib {
262 my ( $self, $borrower ) = @_;
264 return q{} unless $borrower;
266 my $borrowertype = $borrower->categorycode;
267 my $itemtype = $self->itemtype();
269 my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule({ categorycode => $borrowertype, itemtype => $itemtype });
271 return q{} unless $issuing_rule;
272 return $issuing_rule->article_requests || q{}
275 =head3 article_request_type_for_items
277 my $type = $biblio->article_request_type_for_items
279 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record's items
281 If there is a conflict where some items are 'bib_only' and some are 'item_only', 'bib_only' will be returned.
283 =cut
285 sub article_request_type_for_items {
286 my ( $self, $borrower ) = @_;
288 my $counts;
289 foreach my $item ( $self->items()->as_list() ) {
290 my $rule = $item->article_request_type($borrower);
291 return $rule if $rule eq 'bib_only'; # we don't need to go any further
292 $counts->{$rule}++;
295 return 'item_only' if $counts->{item_only};
296 return 'yes' if $counts->{yes};
297 return 'no' if $counts->{no};
298 return q{};
301 =head3 article_requests
303 my @requests = $biblio->article_requests
305 Returns the article requests associated with this Biblio
307 =cut
309 sub article_requests {
310 my ( $self, $borrower ) = @_;
312 $self->{_article_requests} ||= Koha::ArticleRequests->search( { biblionumber => $self->biblionumber() } );
314 return wantarray ? $self->{_article_requests}->as_list : $self->{_article_requests};
317 =head3 article_requests_current
319 my @requests = $biblio->article_requests_current
321 Returns the article requests associated with this Biblio that are incomplete
323 =cut
325 sub article_requests_current {
326 my ( $self, $borrower ) = @_;
328 $self->{_article_requests_current} ||= Koha::ArticleRequests->search(
330 biblionumber => $self->biblionumber(),
331 -or => [
332 { status => Koha::ArticleRequest::Status::Pending },
333 { status => Koha::ArticleRequest::Status::Processing }
338 return wantarray ? $self->{_article_requests_current}->as_list : $self->{_article_requests_current};
341 =head3 article_requests_finished
343 my @requests = $biblio->article_requests_finished
345 Returns the article requests associated with this Biblio that are completed
347 =cut
349 sub article_requests_finished {
350 my ( $self, $borrower ) = @_;
352 $self->{_article_requests_finished} ||= Koha::ArticleRequests->search(
354 biblionumber => $self->biblionumber(),
355 -or => [
356 { status => Koha::ArticleRequest::Status::Completed },
357 { status => Koha::ArticleRequest::Status::Canceled }
362 return wantarray ? $self->{_article_requests_finished}->as_list : $self->{_article_requests_finished};
365 =head3 items
367 my $items = $biblio->items();
369 Returns the related Koha::Items object for this biblio
371 =cut
373 sub items {
374 my ($self) = @_;
376 my $items_rs = $self->_result->items;
378 return Koha::Items->_new_from_dbic( $items_rs );
381 =head3 itemtype
383 my $itemtype = $biblio->itemtype();
385 Returns the itemtype for this record.
387 =cut
389 sub itemtype {
390 my ( $self ) = @_;
392 return $self->biblioitem()->itemtype();
395 =head3 holds
397 my $holds = $biblio->holds();
399 return the current holds placed on this record
401 =cut
403 sub holds {
404 my ( $self, $params, $attributes ) = @_;
405 $attributes->{order_by} = 'priority' unless exists $attributes->{order_by};
406 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
407 return Koha::Holds->_new_from_dbic($hold_rs);
410 =head3 current_holds
412 my $holds = $biblio->current_holds
414 Return the holds placed on this bibliographic record.
415 It does not include future holds.
417 =cut
419 sub current_holds {
420 my ($self) = @_;
421 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
422 return $self->holds(
423 { reservedate => { '<=' => $dtf->format_date(dt_from_string) } } );
426 =head3 biblioitem
428 my $field = $self->biblioitem()->itemtype
430 Returns the related Koha::Biblioitem object for this Biblio object
432 =cut
434 sub biblioitem {
435 my ($self) = @_;
437 $self->{_biblioitem} ||= Koha::Biblioitems->find( { biblionumber => $self->biblionumber() } );
439 return $self->{_biblioitem};
442 =head3 subscriptions
444 my $subscriptions = $self->subscriptions
446 Returns the related Koha::Subscriptions object for this Biblio object
448 =cut
450 sub subscriptions {
451 my ($self) = @_;
453 $self->{_subscriptions} ||= Koha::Subscriptions->search( { biblionumber => $self->biblionumber } );
455 return $self->{_subscriptions};
458 =head3 has_items_waiting_or_intransit
460 my $itemsWaitingOrInTransit = $biblio->has_items_waiting_or_intransit
462 Tells if this bibliographic record has items waiting or in transit.
464 =cut
466 sub has_items_waiting_or_intransit {
467 my ( $self ) = @_;
469 if ( Koha::Holds->search({ biblionumber => $self->id,
470 found => ['W', 'T'] })->count ) {
471 return 1;
474 foreach my $item ( $self->items->as_list ) {
475 return 1 if $item->get_transfer;
478 return 0;
481 =head2 get_coins
483 my $coins = $biblio->get_coins;
485 Returns the COinS (a span) which can be included in a biblio record
487 =cut
489 sub get_coins {
490 my ( $self ) = @_;
492 my $record = $self->metadata->record;
494 my $pos7 = substr $record->leader(), 7, 1;
495 my $pos6 = substr $record->leader(), 6, 1;
496 my $mtx;
497 my $genre;
498 my ( $aulast, $aufirst ) = ( '', '' );
499 my @authors;
500 my $title;
501 my $hosttitle;
502 my $pubyear = '';
503 my $isbn = '';
504 my $issn = '';
505 my $publisher = '';
506 my $pages = '';
507 my $titletype = '';
509 # For the purposes of generating COinS metadata, LDR/06-07 can be
510 # considered the same for UNIMARC and MARC21
511 my $fmts6 = {
512 'a' => 'book',
513 'b' => 'manuscript',
514 'c' => 'book',
515 'd' => 'manuscript',
516 'e' => 'map',
517 'f' => 'map',
518 'g' => 'film',
519 'i' => 'audioRecording',
520 'j' => 'audioRecording',
521 'k' => 'artwork',
522 'l' => 'document',
523 'm' => 'computerProgram',
524 'o' => 'document',
525 'r' => 'document',
527 my $fmts7 = {
528 'a' => 'journalArticle',
529 's' => 'journal',
532 $genre = $fmts6->{$pos6} ? $fmts6->{$pos6} : 'book';
534 if ( $genre eq 'book' ) {
535 $genre = $fmts7->{$pos7} if $fmts7->{$pos7};
538 ##### We must transform mtx to a valable mtx and document type ####
539 if ( $genre eq 'book' ) {
540 $mtx = 'book';
541 $titletype = 'b';
542 } elsif ( $genre eq 'journal' ) {
543 $mtx = 'journal';
544 $titletype = 'j';
545 } elsif ( $genre eq 'journalArticle' ) {
546 $mtx = 'journal';
547 $genre = 'article';
548 $titletype = 'a';
549 } else {
550 $mtx = 'dc';
553 if ( C4::Context->preference("marcflavour") eq "UNIMARC" ) {
555 # Setting datas
556 $aulast = $record->subfield( '700', 'a' ) || '';
557 $aufirst = $record->subfield( '700', 'b' ) || '';
558 push @authors, "$aufirst $aulast" if ($aufirst or $aulast);
560 # others authors
561 if ( $record->field('200') ) {
562 for my $au ( $record->field('200')->subfield('g') ) {
563 push @authors, $au;
567 $title = $record->subfield( '200', 'a' );
568 my $subfield_210d = $record->subfield('210', 'd');
569 if ($subfield_210d and $subfield_210d =~ /(\d{4})/) {
570 $pubyear = $1;
572 $publisher = $record->subfield( '210', 'c' ) || '';
573 $isbn = $record->subfield( '010', 'a' ) || '';
574 $issn = $record->subfield( '011', 'a' ) || '';
575 } else {
577 # MARC21 need some improve
579 # Setting datas
580 if ( $record->field('100') ) {
581 push @authors, $record->subfield( '100', 'a' );
584 # others authors
585 if ( $record->field('700') ) {
586 for my $au ( $record->field('700')->subfield('a') ) {
587 push @authors, $au;
590 $title = $record->field('245')->as_string('ab');
591 if ($titletype eq 'a') {
592 $pubyear = $record->field('008') || '';
593 $pubyear = substr($pubyear->data(), 7, 4) if $pubyear;
594 $isbn = $record->subfield( '773', 'z' ) || '';
595 $issn = $record->subfield( '773', 'x' ) || '';
596 $hosttitle = $record->subfield( '773', 't' ) || $record->subfield( '773', 'a') || q{};
597 my @rels = $record->subfield( '773', 'g' );
598 $pages = join(', ', @rels);
599 } else {
600 $pubyear = $record->subfield( '260', 'c' ) || '';
601 $publisher = $record->subfield( '260', 'b' ) || '';
602 $isbn = $record->subfield( '020', 'a' ) || '';
603 $issn = $record->subfield( '022', 'a' ) || '';
608 my @params = (
609 [ 'ctx_ver', 'Z39.88-2004' ],
610 [ 'rft_val_fmt', "info:ofi/fmt:kev:mtx:$mtx" ],
611 [ ($mtx eq 'dc' ? 'rft.type' : 'rft.genre'), $genre ],
612 [ "rft.${titletype}title", $title ],
615 # rft.title is authorized only once, so by checking $titletype
616 # we ensure that rft.title is not already in the list.
617 if ($hosttitle and $titletype) {
618 push @params, [ 'rft.title', $hosttitle ];
621 push @params, (
622 [ 'rft.isbn', $isbn ],
623 [ 'rft.issn', $issn ],
626 # If it's a subscription, these informations have no meaning.
627 if ($genre ne 'journal') {
628 push @params, (
629 [ 'rft.aulast', $aulast ],
630 [ 'rft.aufirst', $aufirst ],
631 (map { [ 'rft.au', $_ ] } @authors),
632 [ 'rft.pub', $publisher ],
633 [ 'rft.date', $pubyear ],
634 [ 'rft.pages', $pages ],
638 my $coins_value = join( '&amp;',
639 map { $$_[1] ? $$_[0] . '=' . uri_escape_utf8( $$_[1] ) : () } @params );
641 return $coins_value;
644 =head2 get_openurl
646 my $url = $biblio->get_openurl;
648 Returns url for OpenURL resolver set in OpenURLResolverURL system preference
650 =cut
652 sub get_openurl {
653 my ( $self ) = @_;
655 my $OpenURLResolverURL = C4::Context->preference('OpenURLResolverURL');
657 if ($OpenURLResolverURL) {
658 my $uri = URI->new($OpenURLResolverURL);
660 if (not defined $uri->query) {
661 $OpenURLResolverURL .= '?';
662 } else {
663 $OpenURLResolverURL .= '&amp;';
665 $OpenURLResolverURL .= $self->get_coins;
668 return $OpenURLResolverURL;
671 =head3 is_serial
673 my $serial = $biblio->is_serial
675 Return boolean true if this bibbliographic record is continuing resource
677 =cut
679 sub is_serial {
680 my ( $self ) = @_;
682 return 1 if $self->serial;
684 my $record = $self->metadata->record;
685 return 1 if substr($record->leader, 7, 1) eq 's';
687 return 0;
690 =head3 custom_cover_image_url
692 my $image_url = $biblio->custom_cover_image_url
694 Return the specific url of the cover image for this bibliographic record.
695 It is built regaring the value of the system preference CustomCoverImagesURL
697 =cut
699 sub custom_cover_image_url {
700 my ( $self ) = @_;
701 my $url = C4::Context->preference('CustomCoverImagesURL');
702 if ( $url =~ m|{isbn}| ) {
703 my $isbn = $self->biblioitem->isbn;
704 $url =~ s|{isbn}|$isbn|g;
706 if ( $url =~ m|{normalized_isbn}| ) {
707 my $normalized_isbn = C4::Koha::GetNormalizedISBN($self->biblioitem->isbn);
708 $url =~ s|{normalized_isbn}|$normalized_isbn|g;
710 if ( $url =~ m|{issn}| ) {
711 my $issn = $self->biblioitem->issn;
712 $url =~ s|{issn}|$issn|g;
715 my $re = qr|{(?<field>\d{3})\$(?<subfield>.)}|;
716 if ( $url =~ $re ) {
717 my $field = $+{field};
718 my $subfield = $+{subfield};
719 my $marc_record = $self->metadata->record;
720 my $value = $marc_record->subfield($field, $subfield);
721 $url =~ s|$re|$value|;
724 return $url;
727 =head3 to_api_mapping
729 This method returns the mapping for representing a Koha::Biblio object
730 on the API.
732 =cut
734 sub to_api_mapping {
735 return {
736 biblionumber => 'biblio_id',
737 frameworkcode => 'framework_id',
738 unititle => 'uniform_title',
739 seriestitle => 'series_title',
740 copyrightdate => 'copyright_date',
741 datecreated => 'creation_date'
745 =head2 Internal methods
747 =head3 type
749 =cut
751 sub _type {
752 return 'Biblio';
755 =head1 AUTHOR
757 Kyle M Hall <kyle@bywatersolutions.com>
759 =cut