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
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.
23 use List
::MoreUtils
qw(any);
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
;
43 use Koha
::Subscriptions
;
47 Koha::Biblio - Koha Biblio Object class
57 Overloaded I<store> method to set default values
64 $self->datecreated( dt_from_string
) unless $self->datecreated;
66 return $self->SUPER::store
;
71 my $metadata = $biblio->metadata();
73 Returns a Koha::Biblio::Metadata object
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
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';
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:
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.
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');
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 },
165 group_by
=> [qw
/fromBranch/]
166 })->count == scalar(@holdingbranches) ?
0 : 1;
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.
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} );
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:
208 $rules : { <field> => [ value_1, ... ], ... }
210 Note: $rules inherits its structure from the parsed YAML from reading
211 the I<OpacHiddenItems> system preference.
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
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;
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
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.
285 sub article_request_type_for_items
{
286 my ( $self, $borrower ) = @_;
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
295 return 'item_only' if $counts->{item_only
};
296 return 'yes' if $counts->{yes
};
297 return 'no' if $counts->{no};
301 =head3 article_requests
303 my @requests = $biblio->article_requests
305 Returns the article requests associated with this Biblio
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
325 sub article_requests_current
{
326 my ( $self, $borrower ) = @_;
328 $self->{_article_requests_current
} ||= Koha
::ArticleRequests
->search(
330 biblionumber
=> $self->biblionumber(),
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
349 sub article_requests_finished
{
350 my ( $self, $borrower ) = @_;
352 $self->{_article_requests_finished
} ||= Koha
::ArticleRequests
->search(
354 biblionumber
=> $self->biblionumber(),
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
};
367 my $items = $biblio->items();
369 Returns the related Koha::Items object for this biblio
376 my $items_rs = $self->_result->items;
378 return Koha
::Items
->_new_from_dbic( $items_rs );
383 my $itemtype = $biblio->itemtype();
385 Returns the itemtype for this record.
392 return $self->biblioitem()->itemtype();
397 my $holds = $biblio->holds();
399 return the current holds placed on this record
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);
412 my $holds = $biblio->current_holds
414 Return the holds placed on this bibliographic record.
415 It does not include future holds.
421 my $dtf = Koha
::Database
->new->schema->storage->datetime_parser;
423 { reservedate
=> { '<=' => $dtf->format_date(dt_from_string
) } } );
428 my $field = $self->biblioitem()->itemtype
430 Returns the related Koha::Biblioitem object for this Biblio object
437 $self->{_biblioitem
} ||= Koha
::Biblioitems
->find( { biblionumber
=> $self->biblionumber() } );
439 return $self->{_biblioitem
};
444 my $subscriptions = $self->subscriptions
446 Returns the related Koha::Subscriptions object for this Biblio object
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.
466 sub has_items_waiting_or_intransit
{
469 if ( Koha
::Holds
->search({ biblionumber
=> $self->id,
470 found
=> ['W', 'T'] })->count ) {
474 foreach my $item ( $self->items->as_list ) {
475 return 1 if $item->get_transfer;
483 my $coins = $biblio->get_coins;
485 Returns the COinS (a span) which can be included in a biblio record
492 my $record = $self->metadata->record;
494 my $pos7 = substr $record->leader(), 7, 1;
495 my $pos6 = substr $record->leader(), 6, 1;
498 my ( $aulast, $aufirst ) = ( '', '' );
509 # For the purposes of generating COinS metadata, LDR/06-07 can be
510 # considered the same for UNIMARC and MARC21
519 'i' => 'audioRecording',
520 'j' => 'audioRecording',
523 'm' => 'computerProgram',
528 'a' => 'journalArticle',
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' ) {
542 } elsif ( $genre eq 'journal' ) {
545 } elsif ( $genre eq 'journalArticle' ) {
553 if ( C4
::Context
->preference("marcflavour") eq "UNIMARC" ) {
556 $aulast = $record->subfield( '700', 'a' ) || '';
557 $aufirst = $record->subfield( '700', 'b' ) || '';
558 push @authors, "$aufirst $aulast" if ($aufirst or $aulast);
561 if ( $record->field('200') ) {
562 for my $au ( $record->field('200')->subfield('g') ) {
567 $title = $record->subfield( '200', 'a' );
568 my $subfield_210d = $record->subfield('210', 'd');
569 if ($subfield_210d and $subfield_210d =~ /(\d{4})/) {
572 $publisher = $record->subfield( '210', 'c' ) || '';
573 $isbn = $record->subfield( '010', 'a' ) || '';
574 $issn = $record->subfield( '011', 'a' ) || '';
577 # MARC21 need some improve
580 if ( $record->field('100') ) {
581 push @authors, $record->subfield( '100', 'a' );
585 if ( $record->field('700') ) {
586 for my $au ( $record->field('700')->subfield('a') ) {
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);
600 $pubyear = $record->subfield( '260', 'c' ) || '';
601 $publisher = $record->subfield( '260', 'b' ) || '';
602 $isbn = $record->subfield( '020', 'a' ) || '';
603 $issn = $record->subfield( '022', 'a' ) || '';
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 ];
622 [ 'rft.isbn', $isbn ],
623 [ 'rft.issn', $issn ],
626 # If it's a subscription, these informations have no meaning.
627 if ($genre ne 'journal') {
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( '&',
639 map { $$_[1] ?
$$_[0] . '=' . uri_escape_utf8
( $$_[1] ) : () } @params );
646 my $url = $biblio->get_openurl;
648 Returns url for OpenURL resolver set in OpenURLResolverURL system preference
655 my $OpenURLResolverURL = C4
::Context
->preference('OpenURLResolverURL');
657 if ($OpenURLResolverURL) {
658 my $uri = URI
->new($OpenURLResolverURL);
660 if (not defined $uri->query) {
661 $OpenURLResolverURL .= '?';
663 $OpenURLResolverURL .= '&';
665 $OpenURLResolverURL .= $self->get_coins;
668 return $OpenURLResolverURL;
673 my $serial = $biblio->is_serial
675 Return boolean true if this bibbliographic record is continuing resource
682 return 1 if $self->serial;
684 my $record = $self->metadata->record;
685 return 1 if substr($record->leader, 7, 1) eq 's';
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
699 sub custom_cover_image_url
{
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
>.)}|;
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|;
727 =head3 to_api_mapping
729 This method returns the mapping for representing a Koha::Biblio object
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
757 Kyle M Hall <kyle@bywatersolutions.com>