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 );
27 use C4::XSLT qw( transformMARCXML4XSLT );
30 use Koha::DateUtils qw( dt_from_string );
32 use base qw(Koha::Object);
34 use Koha::Acquisition::Orders;
35 use Koha::ArticleRequest::Status;
36 use Koha::ArticleRequests;
37 use Koha::Biblio::Metadatas;
38 use Koha::Biblioitems;
39 use Koha::CirculationRules;
40 use Koha::Item::Transfer::Limits;
43 use Koha::Suggestions;
44 use Koha::Subscriptions;
48 Koha::Biblio - Koha Biblio Object class
58 Overloaded I<store> method to set default values
65 $self->datecreated( dt_from_string ) unless $self->datecreated;
67 return $self->SUPER::store;
72 my $metadata = $biblio->metadata();
74 Returns a Koha::Biblio::Metadata object
81 my $metadata = $self->_result->metadata;
82 return Koha::Biblio::Metadata->_new_from_dbic($metadata);
87 my $orders = $biblio->orders();
89 Returns a Koha::Acquisition::Orders object
96 my $orders = $self->_result->orders;
97 return Koha::Acquisition::Orders->_new_from_dbic($orders);
102 my $active_orders = $biblio->active_orders();
104 Returns the active acquisition orders related to this biblio.
105 An order is considered active when it is not cancelled (i.e. when datecancellation
113 return $self->orders->search({ datecancellationprinted => undef });
116 =head3 can_article_request
118 my $bool = $biblio->can_article_request( $borrower );
120 Returns true if article requests can be made for this record
122 $borrower must be a Koha::Patron object
126 sub can_article_request {
127 my ( $self, $borrower ) = @_;
129 my $rule = $self->article_request_type($borrower);
130 return q{} if $rule eq 'item_only' && !$self->items()->count();
131 return 1 if $rule && $rule ne 'no';
136 =head3 can_be_transferred
138 $biblio->can_be_transferred({ to => $to_library, from => $from_library })
140 Checks if at least one item of a biblio can be transferred to given library.
142 This feature is controlled by two system preferences:
143 UseBranchTransferLimits to enable / disable the feature
144 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
145 for setting the limitations
147 Performance-wise, it is recommended to use this method for a biblio instead of
148 iterating each item of a biblio with Koha::Item->can_be_transferred().
150 Takes HASHref that can have the following parameters:
151 MANDATORY PARAMETERS:
154 $from : Koha::Library # if given, only items from that
155 # holdingbranch are considered
157 Returns 1 if at least one of the item of a biblio can be transferred
158 to $to_library, otherwise 0.
162 sub can_be_transferred {
163 my ($self, $params) = @_;
165 my $to = $params->{to};
166 my $from = $params->{from};
168 return 1 unless C4::Context->preference('UseBranchTransferLimits');
169 my $limittype = C4::Context->preference('BranchTransferLimitsType');
172 foreach my $item_of_bib ($self->items->as_list) {
173 next unless $item_of_bib->holdingbranch;
174 next if $from && $from->branchcode ne $item_of_bib->holdingbranch;
175 return 1 if $item_of_bib->holdingbranch eq $to->branchcode;
176 my $code = $limittype eq 'itemtype'
177 ? $item_of_bib->effective_itemtype
178 : $item_of_bib->ccode;
179 return 1 unless $code;
180 $items->{$code}->{$item_of_bib->holdingbranch} = 1;
183 # At this point we will have a HASHref containing each itemtype/ccode that
184 # this biblio has, inside which are all of the holdingbranches where those
185 # items are located at. Then, we will query Koha::Item::Transfer::Limits to
186 # find out whether a transfer limits for such $limittype from any of the
187 # listed holdingbranches to the given $to library exist. If at least one
188 # holdingbranch for that $limittype does not have a transfer limit to given
189 # $to library, then we know that the transfer is possible.
190 foreach my $code (keys %{$items}) {
191 my @holdingbranches = keys %{$items->{$code}};
192 return 1 if Koha::Item::Transfer::Limits->search({
193 toBranch => $to->branchcode,
194 fromBranch => { 'in' => \@holdingbranches },
197 group_by => [qw/fromBranch/]
198 })->count == scalar(@holdingbranches) ? 0 : 1;
205 =head3 pickup_locations
207 my $pickup_locations = $biblio->pickup_locations( {patron => $patron } );
209 Returns a Koha::Libraries set of possible pickup locations for this biblio's items,
210 according to patron's home library (if patron is defined and holds are allowed
211 only from hold groups) and if item can be transferred to each pickup location.
215 sub pickup_locations {
216 my ( $self, $params ) = @_;
218 my $patron = $params->{patron};
220 my @pickup_locations;
221 foreach my $item_of_bib ( $self->items->as_list ) {
222 push @pickup_locations,
223 $item_of_bib->pickup_locations( { patron => $patron } )
224 ->_resultset->get_column('branchcode')->all;
227 return Koha::Libraries->search(
228 { branchcode => { '-in' => \@pickup_locations } }, { order_by => ['branchname'] } );
231 =head3 hidden_in_opac
233 my $bool = $biblio->hidden_in_opac({ [ rules => $rules ] })
235 Returns true if the biblio matches the hidding criteria defined in $rules.
236 Returns false otherwise. It involves the I<OpacHiddenItems> and
237 I<OpacHiddenItemsHidesRecord> system preferences.
239 Takes HASHref that can have the following parameters:
241 $rules : { <field> => [ value_1, ... ], ... }
243 Note: $rules inherits its structure from the parsed YAML from reading
244 the I<OpacHiddenItems> system preference.
249 my ( $self, $params ) = @_;
251 my $rules = $params->{rules} // {};
253 my @items = $self->items->as_list;
255 return 0 unless @items; # Do not hide if there is no item
257 # Ok, there are items, don't even try the rules unless OpacHiddenItemsHidesRecord
258 return 0 unless C4::Context->preference('OpacHiddenItemsHidesRecord');
260 return !(any { !$_->hidden_in_opac({ rules => $rules }) } @items);
263 =head3 article_request_type
265 my $type = $biblio->article_request_type( $borrower );
267 Returns the article request type based on items, or on the record
268 itself if there are no items.
270 $borrower must be a Koha::Patron object
274 sub article_request_type {
275 my ( $self, $borrower ) = @_;
277 return q{} unless $borrower;
279 my $rule = $self->article_request_type_for_items( $borrower );
280 return $rule if $rule;
282 # If the record has no items that are requestable, go by the record itemtype
283 $rule = $self->article_request_type_for_bib($borrower);
284 return $rule if $rule;
289 =head3 article_request_type_for_bib
291 my $type = $biblio->article_request_type_for_bib
293 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record
297 sub article_request_type_for_bib {
298 my ( $self, $borrower ) = @_;
300 return q{} unless $borrower;
302 my $borrowertype = $borrower->categorycode;
303 my $itemtype = $self->itemtype();
305 my $rule = Koha::CirculationRules->get_effective_rule(
307 rule_name => 'article_requests',
308 categorycode => $borrowertype,
309 itemtype => $itemtype,
313 return q{} unless $rule;
314 return $rule->rule_value || q{}
317 =head3 article_request_type_for_items
319 my $type = $biblio->article_request_type_for_items
321 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record's items
323 If there is a conflict where some items are 'bib_only' and some are 'item_only', 'bib_only' will be returned.
327 sub article_request_type_for_items {
328 my ( $self, $borrower ) = @_;
331 foreach my $item ( $self->items()->as_list() ) {
332 my $rule = $item->article_request_type($borrower);
333 return $rule if $rule eq 'bib_only'; # we don't need to go any further
337 return 'item_only' if $counts->{item_only};
338 return 'yes' if $counts->{yes};
339 return 'no' if $counts->{no};
343 =head3 article_requests
345 my @requests = $biblio->article_requests
347 Returns the article requests associated with this Biblio
351 sub article_requests {
352 my ( $self, $borrower ) = @_;
354 $self->{_article_requests} ||= Koha::ArticleRequests->search( { biblionumber => $self->biblionumber() } );
356 return wantarray ? $self->{_article_requests}->as_list : $self->{_article_requests};
359 =head3 article_requests_current
361 my @requests = $biblio->article_requests_current
363 Returns the article requests associated with this Biblio that are incomplete
367 sub article_requests_current {
368 my ( $self, $borrower ) = @_;
370 $self->{_article_requests_current} ||= Koha::ArticleRequests->search(
372 biblionumber => $self->biblionumber(),
374 { status => Koha::ArticleRequest::Status::Requested },
375 { status => Koha::ArticleRequest::Status::Pending },
376 { status => Koha::ArticleRequest::Status::Processing }
381 return wantarray ? $self->{_article_requests_current}->as_list : $self->{_article_requests_current};
384 =head3 article_requests_finished
386 my @requests = $biblio->article_requests_finished
388 Returns the article requests associated with this Biblio that are completed
392 sub article_requests_finished {
393 my ( $self, $borrower ) = @_;
395 $self->{_article_requests_finished} ||= Koha::ArticleRequests->search(
397 biblionumber => $self->biblionumber(),
399 { status => Koha::ArticleRequest::Status::Completed },
400 { status => Koha::ArticleRequest::Status::Canceled }
405 return wantarray ? $self->{_article_requests_finished}->as_list : $self->{_article_requests_finished};
410 my $items = $biblio->items();
412 Returns the related Koha::Items object for this biblio
419 my $items_rs = $self->_result->items;
421 return Koha::Items->_new_from_dbic( $items_rs );
426 my $host_items = $biblio->host_items();
428 Return the host items (easy analytical record)
435 return Koha::Items->new->empty
436 unless C4::Context->preference('EasyAnalyticalRecords');
438 my $marcflavour = C4::Context->preference("marcflavour");
439 my $analyticfield = '773';
440 if ( $marcflavour eq 'MARC21' || $marcflavour eq 'NORMARC' ) {
441 $analyticfield = '773';
443 elsif ( $marcflavour eq 'UNIMARC' ) {
444 $analyticfield = '461';
446 my $marc_record = $self->metadata->record;
448 foreach my $field ( $marc_record->field($analyticfield) ) {
449 push @itemnumbers, $field->subfield('9');
452 return Koha::Items->search( { itemnumber => { -in => \@itemnumbers } } );
457 my $itemtype = $biblio->itemtype();
459 Returns the itemtype for this record.
466 return $self->biblioitem()->itemtype();
471 my $holds = $biblio->holds();
473 return the current holds placed on this record
478 my ( $self, $params, $attributes ) = @_;
479 $attributes->{order_by} = 'priority' unless exists $attributes->{order_by};
480 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
481 return Koha::Holds->_new_from_dbic($hold_rs);
486 my $holds = $biblio->current_holds
488 Return the holds placed on this bibliographic record.
489 It does not include future holds.
495 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
497 { reservedate => { '<=' => $dtf->format_date(dt_from_string) } } );
502 my $field = $self->biblioitem()->itemtype
504 Returns the related Koha::Biblioitem object for this Biblio object
511 $self->{_biblioitem} ||= Koha::Biblioitems->find( { biblionumber => $self->biblionumber() } );
513 return $self->{_biblioitem};
518 my $suggestions = $self->suggestions
520 Returns the related Koha::Suggestions object for this Biblio object
527 my $suggestions_rs = $self->_result->suggestions;
528 return Koha::Suggestions->_new_from_dbic( $suggestions_rs );
533 my $subscriptions = $self->subscriptions
535 Returns the related Koha::Subscriptions object for this Biblio object
542 $self->{_subscriptions} ||= Koha::Subscriptions->search( { biblionumber => $self->biblionumber } );
544 return $self->{_subscriptions};
547 =head3 has_items_waiting_or_intransit
549 my $itemsWaitingOrInTransit = $biblio->has_items_waiting_or_intransit
551 Tells if this bibliographic record has items waiting or in transit.
555 sub has_items_waiting_or_intransit {
558 if ( Koha::Holds->search({ biblionumber => $self->id,
559 found => ['W', 'T'] })->count ) {
563 foreach my $item ( $self->items->as_list ) {
564 return 1 if $item->get_transfer;
572 my $coins = $biblio->get_coins;
574 Returns the COinS (a span) which can be included in a biblio record
581 my $record = $self->metadata->record;
583 my $pos7 = substr $record->leader(), 7, 1;
584 my $pos6 = substr $record->leader(), 6, 1;
587 my ( $aulast, $aufirst ) = ( '', '' );
598 # For the purposes of generating COinS metadata, LDR/06-07 can be
599 # considered the same for UNIMARC and MARC21
608 'i' => 'audioRecording',
609 'j' => 'audioRecording',
612 'm' => 'computerProgram',
617 'a' => 'journalArticle',
621 $genre = $fmts6->{$pos6} ? $fmts6->{$pos6} : 'book';
623 if ( $genre eq 'book' ) {
624 $genre = $fmts7->{$pos7} if $fmts7->{$pos7};
627 ##### We must transform mtx to a valable mtx and document type ####
628 if ( $genre eq 'book' ) {
631 } elsif ( $genre eq 'journal' ) {
634 } elsif ( $genre eq 'journalArticle' ) {
642 if ( C4::Context->preference("marcflavour") eq "UNIMARC" ) {
645 $aulast = $record->subfield( '700', 'a' ) || '';
646 $aufirst = $record->subfield( '700', 'b' ) || '';
647 push @authors, "$aufirst $aulast" if ($aufirst or $aulast);
650 if ( $record->field('200') ) {
651 for my $au ( $record->field('200')->subfield('g') ) {
656 $title = $record->subfield( '200', 'a' );
657 my $subfield_210d = $record->subfield('210', 'd');
658 if ($subfield_210d and $subfield_210d =~ /(\d{4})/) {
661 $publisher = $record->subfield( '210', 'c' ) || '';
662 $isbn = $record->subfield( '010', 'a' ) || '';
663 $issn = $record->subfield( '011', 'a' ) || '';
666 # MARC21 need some improve
669 if ( $record->field('100') ) {
670 push @authors, $record->subfield( '100', 'a' );
674 if ( $record->field('700') ) {
675 for my $au ( $record->field('700')->subfield('a') ) {
679 $title = $record->field('245');
680 $title &&= $title->as_string('ab');
681 if ($titletype eq 'a') {
682 $pubyear = $record->field('008') || '';
683 $pubyear = substr($pubyear->data(), 7, 4) if $pubyear;
684 $isbn = $record->subfield( '773', 'z' ) || '';
685 $issn = $record->subfield( '773', 'x' ) || '';
686 $hosttitle = $record->subfield( '773', 't' ) || $record->subfield( '773', 'a') || q{};
687 my @rels = $record->subfield( '773', 'g' );
688 $pages = join(', ', @rels);
690 $pubyear = $record->subfield( '260', 'c' ) || '';
691 $publisher = $record->subfield( '260', 'b' ) || '';
692 $isbn = $record->subfield( '020', 'a' ) || '';
693 $issn = $record->subfield( '022', 'a' ) || '';
699 [ 'ctx_ver', 'Z39.88-2004' ],
700 [ 'rft_val_fmt', "info:ofi/fmt:kev:mtx:$mtx" ],
701 [ ($mtx eq 'dc' ? 'rft.type' : 'rft.genre'), $genre ],
702 [ "rft.${titletype}title", $title ],
705 # rft.title is authorized only once, so by checking $titletype
706 # we ensure that rft.title is not already in the list.
707 if ($hosttitle and $titletype) {
708 push @params, [ 'rft.title', $hosttitle ];
712 [ 'rft.isbn', $isbn ],
713 [ 'rft.issn', $issn ],
716 # If it's a subscription, these informations have no meaning.
717 if ($genre ne 'journal') {
719 [ 'rft.aulast', $aulast ],
720 [ 'rft.aufirst', $aufirst ],
721 (map { [ 'rft.au', $_ ] } @authors),
722 [ 'rft.pub', $publisher ],
723 [ 'rft.date', $pubyear ],
724 [ 'rft.pages', $pages ],
728 my $coins_value = join( '&',
729 map { $$_[1] ? $$_[0] . '=' . uri_escape_utf8( $$_[1] ) : () } @params );
736 my $url = $biblio->get_openurl;
738 Returns url for OpenURL resolver set in OpenURLResolverURL system preference
745 my $OpenURLResolverURL = C4::Context->preference('OpenURLResolverURL');
747 if ($OpenURLResolverURL) {
748 my $uri = URI->new($OpenURLResolverURL);
750 if (not defined $uri->query) {
751 $OpenURLResolverURL .= '?';
753 $OpenURLResolverURL .= '&';
755 $OpenURLResolverURL .= $self->get_coins;
758 return $OpenURLResolverURL;
763 my $serial = $biblio->is_serial
765 Return boolean true if this bibbliographic record is continuing resource
772 return 1 if $self->serial;
774 my $record = $self->metadata->record;
775 return 1 if substr($record->leader, 7, 1) eq 's';
780 =head3 custom_cover_image_url
782 my $image_url = $biblio->custom_cover_image_url
784 Return the specific url of the cover image for this bibliographic record.
785 It is built regaring the value of the system preference CustomCoverImagesURL
789 sub custom_cover_image_url {
791 my $url = C4::Context->preference('CustomCoverImagesURL');
792 if ( $url =~ m|{isbn}| ) {
793 my $isbn = $self->biblioitem->isbn;
795 $url =~ s|{isbn}|$isbn|g;
797 if ( $url =~ m|{normalized_isbn}| ) {
798 my $normalized_isbn = C4::Koha::GetNormalizedISBN($self->biblioitem->isbn);
799 return unless $normalized_isbn;
800 $url =~ s|{normalized_isbn}|$normalized_isbn|g;
802 if ( $url =~ m|{issn}| ) {
803 my $issn = $self->biblioitem->issn;
805 $url =~ s|{issn}|$issn|g;
808 my $re = qr|{(?<field>\d{3})\$(?<subfield>.)}|;
810 my $field = $+{field};
811 my $subfield = $+{subfield};
812 my $marc_record = $self->metadata->record;
813 my $value = $marc_record->subfield($field, $subfield);
814 return unless $value;
815 $url =~ s|$re|$value|;
823 Return the cover images associated with this biblio.
830 my $cover_images_rs = $self->_result->cover_images;
831 return unless $cover_images_rs;
832 return Koha::CoverImages->_new_from_dbic($cover_images_rs);
835 =head3 get_marc_notes
837 $marcnotesarray = $biblio->get_marc_notes({ marcflavour => $marcflavour });
839 Get all notes from the MARC record and returns them in an array.
840 The notes are stored in different fields depending on MARC flavour.
841 MARC21 5XX $u subfields receive special attention as they are URIs.
846 my ( $self, $params ) = @_;
848 my $marcflavour = $params->{marcflavour};
849 my $opac = $params->{opac};
851 my $scope = $marcflavour eq "UNIMARC"? '3..': '5..';
854 #MARC21 specs indicate some notes should be private if first indicator 0
855 my %maybe_private = (
863 my %hiddenlist = map { $_ => 1 }
864 split( /,/, C4::Context->preference('NotesToHide'));
865 my $record = $self->metadata->record;
866 $record = transformMARCXML4XSLT( $self->biblionumber, $record, $opac );
868 foreach my $field ( $record->field($scope) ) {
869 my $tag = $field->tag();
870 next if $hiddenlist{ $tag };
871 next if $opac && $maybe_private{$tag} && !$field->indicator(1);
872 if( $marcflavour ne 'UNIMARC' && $field->subfield('u') ) {
873 # Field 5XX$u always contains URI
874 # Examples: 505u, 506u, 510u, 514u, 520u, 530u, 538u, 540u, 542u, 552u, 555u, 561u, 563u, 583u
875 # We first push the other subfields, then all $u's separately
876 # Leave further actions to the template (see e.g. opac-detail)
878 join '', ( 'a' .. 't', 'v' .. 'z', '0' .. '9' ); # excl 'u'
879 push @marcnotes, { marcnote => $field->as_string($othersub) };
880 foreach my $sub ( $field->subfield('u') ) {
881 $sub =~ s/^\s+|\s+$//g; # trim
882 push @marcnotes, { marcnote => $sub };
885 push @marcnotes, { marcnote => $field->as_string() };
893 my $json = $biblio->to_api;
895 Overloaded method that returns a JSON representation of the Koha::Biblio object,
896 suitable for API output. The related Koha::Biblioitem object is merged as expected
902 my ($self, $args) = @_;
904 my $response = $self->SUPER::to_api( $args );
905 my $biblioitem = $self->biblioitem->to_api;
907 return { %$response, %$biblioitem };
910 =head3 to_api_mapping
912 This method returns the mapping for representing a Koha::Biblio object
919 biblionumber => 'biblio_id',
920 frameworkcode => 'framework_id',
921 unititle => 'uniform_title',
922 seriestitle => 'series_title',
923 copyrightdate => 'copyright_date',
924 datecreated => 'creation_date'
930 $host = $biblio->get_marc_host;
932 ( $host, $relatedparts ) = $biblio->get_marc_host;
934 Returns host biblio record from MARC21 773 (undef if no 773 present).
935 It looks at the first 773 field with MARCorgCode or only a control
936 number. Complete $w or numeric part is used to search host record.
937 The optional parameter no_items triggers a check if $biblio has items.
938 If there are, the sub returns undef.
939 Called in list context, it also returns 773$g (related parts).
944 my ($self, $params) = @_;
945 my $no_items = $params->{no_items};
946 return if C4::Context->preference('marcflavour') eq 'UNIMARC'; # TODO
947 return if $params->{no_items} && $self->items->count > 0;
950 eval { $record = $self->metadata->record };
953 # We pick the first $w with your MARCOrgCode or the first $w that has no
954 # code (between parentheses) at all.
955 my $orgcode = C4::Context->preference('MARCOrgCode') // q{};
957 foreach my $f ( $record->field('773') ) {
958 my $w = $f->subfield('w') or next;
959 if( $w =~ /^\($orgcode\)\s*(\d+)/i or $w =~ /^\d+/ ) {
965 my $rcn = $hostfld->subfield('w');
967 # Look for control number with/without orgcode
968 my $engine = Koha::SearchEngine::Search->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
971 my ( $error, $results, $total_hits ) = $engine->simple_search_compat( 'Control-number='.$rcn, 0,1 );
972 if( !$error and $total_hits == 1 ) {
973 $bibno = $engine->extract_biblionumber( $results->[0] );
976 # Add or remove orgcode for second try
977 if( $try == 1 && $rcn =~ /\)\s*(\d+)/ ) {
978 $rcn = $1; # number only
979 } elsif( $try == 1 && $rcn =~ /^\d+/ ) {
980 $rcn = "($orgcode)$rcn";
986 my $host = Koha::Biblios->find($bibno) or return;
987 return wantarray ? ( $host, $hostfld->subfield('g') ) : $host;
991 =head2 Internal methods
1003 Kyle M Hall <kyle@bywatersolutions.com>