Bug 32894: Remove incorrect caching from bundle_items
[koha.git] / Koha / Biblio.pm
1 package Koha::Biblio;
2
3 # Copyright ByWater Solutions 2014
4 #
5 # This file is part of Koha.
6 #
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.
11 #
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.
16 #
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>.
19
20 use Modern::Perl;
21
22 use List::MoreUtils qw( any );
23 use URI;
24 use URI::Escape qw( uri_escape_utf8 );
25
26 use C4::Koha qw( GetNormalizedISBN );
27
28 use Koha::Database;
29 use Koha::DateUtils qw( dt_from_string );
30
31 use base qw(Koha::Object);
32
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;
39 use Koha::Checkouts;
40 use Koha::CirculationRules;
41 use Koha::Exceptions;
42 use Koha::Illrequests;
43 use Koha::Item::Transfer::Limits;
44 use Koha::Items;
45 use Koha::Libraries;
46 use Koha::Old::Checkouts;
47 use Koha::Recalls;
48 use Koha::RecordProcessor;
49 use Koha::Suggestions;
50 use Koha::Subscriptions;
51 use Koha::SearchEngine;
52 use Koha::SearchEngine::Search;
53 use Koha::SearchEngine::QueryBuilder;
54
55 =head1 NAME
56
57 Koha::Biblio - Koha Biblio Object class
58
59 =head1 API
60
61 =head2 Class Methods
62
63 =cut
64
65 =head3 store
66
67 Overloaded I<store> method to set default values
68
69 =cut
70
71 sub store {
72     my ( $self ) = @_;
73
74     $self->datecreated( dt_from_string ) unless $self->datecreated;
75
76     return $self->SUPER::store;
77 }
78
79 =head3 metadata
80
81 my $metadata = $biblio->metadata();
82
83 Returns a Koha::Biblio::Metadata object
84
85 =cut
86
87 sub metadata {
88     my ( $self ) = @_;
89
90     my $metadata = $self->_result->metadata;
91     return Koha::Biblio::Metadata->_new_from_dbic($metadata);
92 }
93
94 =head3 record
95
96 my $record = $biblio->record();
97
98 Returns a Marc::Record object
99
100 =cut
101
102 sub record {
103     my ( $self ) = @_;
104
105     return $self->metadata->record;
106 }
107
108 =head3 orders
109
110 my $orders = $biblio->orders();
111
112 Returns a Koha::Acquisition::Orders object
113
114 =cut
115
116 sub orders {
117     my ( $self ) = @_;
118
119     my $orders = $self->_result->orders;
120     return Koha::Acquisition::Orders->_new_from_dbic($orders);
121 }
122
123 =head3 active_orders
124
125 my $active_orders = $biblio->active_orders();
126
127 Returns the active acquisition orders related to this biblio.
128 An order is considered active when it is not cancelled (i.e. when datecancellation
129 is not undef).
130
131 =cut
132
133 sub active_orders {
134     my ( $self ) = @_;
135
136     return $self->orders->search({ datecancellationprinted => undef });
137 }
138
139 =head3 tickets
140
141   my $tickets = $biblio->tickets();
142
143 Returns all tickets linked to the biblio
144
145 =cut
146
147 sub tickets {
148     my ( $self ) = @_;
149     my $rs = $self->_result->tickets;
150     return Koha::Tickets->_new_from_dbic( $rs );
151 }
152
153 =head3 ill_requests
154
155     my $ill_requests = $biblio->ill_requests();
156
157 Returns a Koha::Illrequests object
158
159 =cut
160
161 sub ill_requests {
162     my ( $self ) = @_;
163
164     my $ill_requests = $self->_result->ill_requests;
165     return Koha::Illrequests->_new_from_dbic($ill_requests);
166 }
167
168 =head3 item_groups
169
170 my $item_groups = $biblio->item_groups();
171
172 Returns a Koha::Biblio::ItemGroups object
173
174 =cut
175
176 sub item_groups {
177     my ( $self ) = @_;
178
179     my $item_groups = $self->_result->item_groups;
180     return Koha::Biblio::ItemGroups->_new_from_dbic($item_groups);
181 }
182
183 =head3 can_article_request
184
185 my $bool = $biblio->can_article_request( $borrower );
186
187 Returns true if article requests can be made for this record
188
189 $borrower must be a Koha::Patron object
190
191 =cut
192
193 sub can_article_request {
194     my ( $self, $borrower ) = @_;
195
196     my $rule = $self->article_request_type($borrower);
197     return q{} if $rule eq 'item_only' && !$self->items()->count();
198     return 1 if $rule && $rule ne 'no';
199
200     return q{};
201 }
202
203 =head3 can_be_transferred
204
205 $biblio->can_be_transferred({ to => $to_library, from => $from_library })
206
207 Checks if at least one item of a biblio can be transferred to given library.
208
209 This feature is controlled by two system preferences:
210 UseBranchTransferLimits to enable / disable the feature
211 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
212                          for setting the limitations
213
214 Performance-wise, it is recommended to use this method for a biblio instead of
215 iterating each item of a biblio with Koha::Item->can_be_transferred().
216
217 Takes HASHref that can have the following parameters:
218     MANDATORY PARAMETERS:
219     $to   : Koha::Library
220     OPTIONAL PARAMETERS:
221     $from : Koha::Library # if given, only items from that
222                           # holdingbranch are considered
223
224 Returns 1 if at least one of the item of a biblio can be transferred
225 to $to_library, otherwise 0.
226
227 =cut
228
229 sub can_be_transferred {
230     my ($self, $params) = @_;
231
232     my $to   = $params->{to};
233     my $from = $params->{from};
234
235     return 1 unless C4::Context->preference('UseBranchTransferLimits');
236     my $limittype = C4::Context->preference('BranchTransferLimitsType');
237
238     my $items;
239     foreach my $item_of_bib ($self->items->as_list) {
240         next unless $item_of_bib->holdingbranch;
241         next if $from && $from->branchcode ne $item_of_bib->holdingbranch;
242         return 1 if $item_of_bib->holdingbranch eq $to->branchcode;
243         my $code = $limittype eq 'itemtype'
244             ? $item_of_bib->effective_itemtype
245             : $item_of_bib->ccode;
246         return 1 unless $code;
247         $items->{$code}->{$item_of_bib->holdingbranch} = 1;
248     }
249
250     # At this point we will have a HASHref containing each itemtype/ccode that
251     # this biblio has, inside which are all of the holdingbranches where those
252     # items are located at. Then, we will query Koha::Item::Transfer::Limits to
253     # find out whether a transfer limits for such $limittype from any of the
254     # listed holdingbranches to the given $to library exist. If at least one
255     # holdingbranch for that $limittype does not have a transfer limit to given
256     # $to library, then we know that the transfer is possible.
257     foreach my $code (keys %{$items}) {
258         my @holdingbranches = keys %{$items->{$code}};
259         return 1 if Koha::Item::Transfer::Limits->search({
260             toBranch => $to->branchcode,
261             fromBranch => { 'in' => \@holdingbranches },
262             $limittype => $code
263         }, {
264             group_by => [qw/fromBranch/]
265         })->count == scalar(@holdingbranches) ? 0 : 1;
266     }
267
268     return 0;
269 }
270
271
272 =head3 pickup_locations
273
274     my $pickup_locations = $biblio->pickup_locations({ patron => $patron });
275
276 Returns a Koha::Libraries set of possible pickup locations for this biblio's items,
277 according to patron's home library and if item can be transferred to each pickup location.
278
279 Throws a I<Koha::Exceptions::MissingParameter> exception if the B<mandatory> parameter I<patron>
280 is not passed.
281
282 =cut
283
284 sub pickup_locations {
285     my ( $self, $params ) = @_;
286
287     Koha::Exceptions::MissingParameter->throw( parameter => 'patron' )
288       unless exists $params->{patron};
289
290     my $patron = $params->{patron};
291
292     my $memory_cache = Koha::Cache::Memory::Lite->get_instance();
293     my @pickup_locations;
294     foreach my $item ( $self->items->as_list ) {
295         my $cache_key = sprintf "Pickup_locations:%s:%s:%s:%s:%s",
296            $item->itype,$item->homebranch,$item->holdingbranch,$item->ccode || "",$patron->branchcode||"" ;
297         my $item_pickup_locations = $memory_cache->get_from_cache( $cache_key );
298         unless( $item_pickup_locations ){
299           @{ $item_pickup_locations } = $item->pickup_locations( { patron => $patron } )->_resultset->get_column('branchcode')->all;
300           $memory_cache->set_in_cache( $cache_key, $item_pickup_locations );
301         }
302         push @pickup_locations, @{ $item_pickup_locations }
303     }
304
305     return Koha::Libraries->search(
306         { branchcode => { '-in' => \@pickup_locations } }, { order_by => ['branchname'] } );
307 }
308
309 =head3 hidden_in_opac
310
311     my $bool = $biblio->hidden_in_opac({ [ rules => $rules ] })
312
313 Returns true if the biblio matches the hidding criteria defined in $rules.
314 Returns false otherwise. It involves the I<OpacHiddenItems> and
315 I<OpacHiddenItemsHidesRecord> system preferences.
316
317 Takes HASHref that can have the following parameters:
318     OPTIONAL PARAMETERS:
319     $rules : { <field> => [ value_1, ... ], ... }
320
321 Note: $rules inherits its structure from the parsed YAML from reading
322 the I<OpacHiddenItems> system preference.
323
324 =cut
325
326 sub hidden_in_opac {
327     my ( $self, $params ) = @_;
328
329     my $rules = $params->{rules} // {};
330
331     my @items = $self->items->as_list;
332
333     return 0 unless @items; # Do not hide if there is no item
334
335     # Ok, there are items, don't even try the rules unless OpacHiddenItemsHidesRecord
336     return 0 unless C4::Context->preference('OpacHiddenItemsHidesRecord');
337
338     return !(any { !$_->hidden_in_opac({ rules => $rules }) } @items);
339 }
340
341 =head3 article_request_type
342
343 my $type = $biblio->article_request_type( $borrower );
344
345 Returns the article request type based on items, or on the record
346 itself if there are no items.
347
348 $borrower must be a Koha::Patron object
349
350 =cut
351
352 sub article_request_type {
353     my ( $self, $borrower ) = @_;
354
355     return q{} unless $borrower;
356
357     my $rule = $self->article_request_type_for_items( $borrower );
358     return $rule if $rule;
359
360     # If the record has no items that are requestable, go by the record itemtype
361     $rule = $self->article_request_type_for_bib($borrower);
362     return $rule if $rule;
363
364     return q{};
365 }
366
367 =head3 article_request_type_for_bib
368
369 my $type = $biblio->article_request_type_for_bib
370
371 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record
372
373 =cut
374
375 sub article_request_type_for_bib {
376     my ( $self, $borrower ) = @_;
377
378     return q{} unless $borrower;
379
380     my $borrowertype = $borrower->categorycode;
381     my $itemtype     = $self->itemtype();
382
383     my $rule = Koha::CirculationRules->get_effective_rule(
384         {
385             rule_name    => 'article_requests',
386             categorycode => $borrowertype,
387             itemtype     => $itemtype,
388         }
389     );
390
391     return q{} unless $rule;
392     return $rule->rule_value || q{}
393 }
394
395 =head3 article_request_type_for_items
396
397 my $type = $biblio->article_request_type_for_items
398
399 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record's items
400
401 If there is a conflict where some items are 'bib_only' and some are 'item_only', 'bib_only' will be returned.
402
403 =cut
404
405 sub article_request_type_for_items {
406     my ( $self, $borrower ) = @_;
407
408     my $counts;
409     foreach my $item ( $self->items()->as_list() ) {
410         my $rule = $item->article_request_type($borrower);
411         return $rule if $rule eq 'bib_only';    # we don't need to go any further
412         $counts->{$rule}++;
413     }
414
415     return 'item_only' if $counts->{item_only};
416     return 'yes'       if $counts->{yes};
417     return 'no'        if $counts->{no};
418     return q{};
419 }
420
421 =head3 article_requests
422
423     my $article_requests = $biblio->article_requests
424
425 Returns the article requests associated with this biblio
426
427 =cut
428
429 sub article_requests {
430     my ( $self ) = @_;
431
432     return Koha::ArticleRequests->_new_from_dbic( scalar $self->_result->article_requests );
433 }
434
435 =head3 current_checkouts
436
437     my $current_checkouts = $biblio->current_checkouts
438
439 Returns the current checkouts associated with this biblio
440
441 =cut
442
443 sub current_checkouts {
444     my ($self) = @_;
445
446     return Koha::Checkouts->search( { "item.biblionumber" => $self->id },
447         { join => 'item' } );
448 }
449
450 =head3 old_checkouts
451
452     my $old_checkouts = $biblio->old_checkouts
453
454 Returns the past checkouts associated with this biblio
455
456 =cut
457
458 sub old_checkouts {
459     my ( $self ) = @_;
460
461     return Koha::Old::Checkouts->search( { "item.biblionumber" => $self->id },
462         { join => 'item' } );
463 }
464
465 =head3 items
466
467 my $items = $biblio->items();
468
469 Returns the related Koha::Items object for this biblio
470
471 =cut
472
473 sub items {
474     my ($self) = @_;
475
476     my $items_rs = $self->_result->items;
477
478     return Koha::Items->_new_from_dbic( $items_rs );
479 }
480
481 =head3 host_items
482
483 my $host_items = $biblio->host_items();
484
485 Return the host items (easy analytical record)
486
487 =cut
488
489 sub host_items {
490     my ($self) = @_;
491
492     return Koha::Items->new->empty
493       unless C4::Context->preference('EasyAnalyticalRecords');
494
495     my $marcflavour = C4::Context->preference("marcflavour");
496     my $analyticfield = '773';
497     if ( $marcflavour eq 'MARC21' ) {
498         $analyticfield = '773';
499     }
500     elsif ( $marcflavour eq 'UNIMARC' ) {
501         $analyticfield = '461';
502     }
503     my $marc_record = $self->metadata->record;
504     my @itemnumbers;
505     foreach my $field ( $marc_record->field($analyticfield) ) {
506         push @itemnumbers, $field->subfield('9');
507     }
508
509     return Koha::Items->search( { itemnumber => { -in => \@itemnumbers } } );
510 }
511
512 =head3 itemtype
513
514 my $itemtype = $biblio->itemtype();
515
516 Returns the itemtype for this record.
517
518 =cut
519
520 sub itemtype {
521     my ( $self ) = @_;
522
523     return $self->biblioitem()->itemtype();
524 }
525
526 =head3 holds
527
528 my $holds = $biblio->holds();
529
530 return the current holds placed on this record
531
532 =cut
533
534 sub holds {
535     my ( $self, $params, $attributes ) = @_;
536     $attributes->{order_by} = 'priority' unless exists $attributes->{order_by};
537     my $hold_rs = $self->_result->reserves->search( $params, $attributes );
538     return Koha::Holds->_new_from_dbic($hold_rs);
539 }
540
541 =head3 current_holds
542
543 my $holds = $biblio->current_holds
544
545 Return the holds placed on this bibliographic record.
546 It does not include future holds.
547
548 =cut
549
550 sub current_holds {
551     my ($self) = @_;
552     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
553     return $self->holds(
554         { reservedate => { '<=' => $dtf->format_date(dt_from_string) } } );
555 }
556
557 =head3 biblioitem
558
559 my $field = $self->biblioitem
560
561 Returns the related Koha::Biblioitem object for this Biblio object
562
563 =cut
564
565 sub biblioitem {
566     my ($self) = @_;
567     return Koha::Biblioitems->find( { biblionumber => $self->biblionumber } );
568 }
569
570 =head3 suggestions
571
572 my $suggestions = $self->suggestions
573
574 Returns the related Koha::Suggestions object for this Biblio object
575
576 =cut
577
578 sub suggestions {
579     my ($self) = @_;
580
581     my $suggestions_rs = $self->_result->suggestions;
582     return Koha::Suggestions->_new_from_dbic( $suggestions_rs );
583 }
584
585 =head3 get_marc_components
586
587   my $components = $self->get_marc_components();
588
589 Returns an array of search results data, which are component parts of
590 this object (MARC21 773 points to this)
591
592 =cut
593
594 sub get_marc_components {
595     my ($self, $max_results) = @_;
596
597     return [] if (C4::Context->preference('marcflavour') ne 'MARC21');
598
599     my ( $searchstr, $sort ) = $self->get_components_query;
600
601     my $components;
602     if (defined($searchstr)) {
603         my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
604         my ( $error, $results, $facets );
605         eval {
606             ( $error, $results, $facets ) = $searcher->search_compat( $searchstr, undef, [$sort], ['biblioserver'], $max_results, 0, undef, undef, 'ccl', 0 );
607         };
608         if( $error || $@ ) {
609             $error //= q{};
610             $error .= $@ if $@;
611             warn "Warning from search_compat: '$error'";
612             $self->add_message(
613                 {
614                     type    => 'error',
615                     message => 'component_search',
616                     payload => $error,
617                 }
618             );
619         }
620         $components = $results->{biblioserver}->{RECORDS} if defined($results) && $results->{biblioserver}->{hits};
621     }
622
623     return $components // [];
624 }
625
626 =head2 get_components_query
627
628 Returns a query which can be used to search for all component parts of MARC21 biblios
629
630 =cut
631
632 sub get_components_query {
633     my ($self) = @_;
634
635     my $builder = Koha::SearchEngine::QueryBuilder->new(
636         { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
637     my $marc = $self->metadata->record;
638     my $component_sort_field = C4::Context->preference('ComponentSortField') // "title";
639     my $component_sort_order = C4::Context->preference('ComponentSortOrder') // "asc";
640     my $sort = $component_sort_field . "_" . $component_sort_order;
641
642     my $searchstr;
643     if ( C4::Context->preference('UseControlNumber') ) {
644         my $pf001 = $marc->field('001') || undef;
645
646         if ( defined($pf001) ) {
647             $searchstr = "(";
648             my $pf003 = $marc->field('003') || undef;
649
650             if ( !defined($pf003) ) {
651                 # search for 773$w='Host001'
652                 $searchstr .= "rcn:\"" . $pf001->data()."\"";
653             }
654             else {
655                 $searchstr .= "(";
656                 # search for (773$w='Host001' and 003='Host003') or 773$w='(Host003)Host001'
657                 $searchstr .= "(rcn:\"" . $pf001->data() . "\" AND cni:\"" . $pf003->data() . "\")";
658                 $searchstr .= " OR rcn:\"" . $pf003->data() . " " . $pf001->data() . "\"";
659                 $searchstr .= ")";
660             }
661
662             # limit to monograph and serial component part records
663             $searchstr .= " AND (bib-level:a OR bib-level:b)";
664             $searchstr .= ")";
665         }
666     }
667     else {
668         my $cleaned_title = $marc->subfield('245', "a");
669         $cleaned_title =~ tr|/||;
670         $cleaned_title = $builder->clean_search_term($cleaned_title);
671         $searchstr = qq#Host-item:("$cleaned_title")#;
672     }
673     my ($error, $query ,$query_str) = $builder->build_query_compat( undef, [$searchstr], undef, undef, [$sort], 0 );
674     if( $error ){
675         warn $error;
676         return;
677     }
678
679     return ($query, $query_str, $sort);
680 }
681
682 =head3 subscriptions
683
684 my $subscriptions = $self->subscriptions
685
686 Returns the related Koha::Subscriptions object for this Biblio object
687
688 =cut
689
690 sub subscriptions {
691     my ($self) = @_;
692     my $rs = $self->_result->subscriptions;
693     return Koha::Subscriptions->_new_from_dbic($rs);
694 }
695
696 =head3 has_items_waiting_or_intransit
697
698 my $itemsWaitingOrInTransit = $biblio->has_items_waiting_or_intransit
699
700 Tells if this bibliographic record has items waiting or in transit.
701
702 =cut
703
704 sub has_items_waiting_or_intransit {
705     my ( $self ) = @_;
706
707     if ( Koha::Holds->search({ biblionumber => $self->id,
708                                found => ['W', 'T'] })->count ) {
709         return 1;
710     }
711
712     foreach my $item ( $self->items->as_list ) {
713         return 1 if $item->get_transfer;
714     }
715
716     return 0;
717 }
718
719 =head2 get_coins
720
721 my $coins = $biblio->get_coins;
722
723 Returns the COinS (a span) which can be included in a biblio record
724
725 =cut
726
727 sub get_coins {
728     my ( $self ) = @_;
729
730     my $record = $self->metadata->record;
731
732     my $pos7 = substr $record->leader(), 7, 1;
733     my $pos6 = substr $record->leader(), 6, 1;
734     my $mtx;
735     my $genre;
736     my ( $aulast, $aufirst ) = ( '', '' );
737     my @authors;
738     my $title;
739     my $hosttitle;
740     my $pubyear   = '';
741     my $isbn      = '';
742     my $issn      = '';
743     my $publisher = '';
744     my $pages     = '';
745     my $titletype = '';
746
747     # For the purposes of generating COinS metadata, LDR/06-07 can be
748     # considered the same for UNIMARC and MARC21
749     my $fmts6 = {
750         'a' => 'book',
751         'b' => 'manuscript',
752         'c' => 'book',
753         'd' => 'manuscript',
754         'e' => 'map',
755         'f' => 'map',
756         'g' => 'film',
757         'i' => 'audioRecording',
758         'j' => 'audioRecording',
759         'k' => 'artwork',
760         'l' => 'document',
761         'm' => 'computerProgram',
762         'o' => 'document',
763         'r' => 'document',
764     };
765     my $fmts7 = {
766         'a' => 'journalArticle',
767         's' => 'journal',
768     };
769
770     $genre = $fmts6->{$pos6} ? $fmts6->{$pos6} : 'book';
771
772     if ( $genre eq 'book' ) {
773             $genre = $fmts7->{$pos7} if $fmts7->{$pos7};
774     }
775
776     ##### We must transform mtx to a valable mtx and document type ####
777     if ( $genre eq 'book' ) {
778             $mtx = 'book';
779             $titletype = 'b';
780     } elsif ( $genre eq 'journal' ) {
781             $mtx = 'journal';
782             $titletype = 'j';
783     } elsif ( $genre eq 'journalArticle' ) {
784             $mtx   = 'journal';
785             $genre = 'article';
786             $titletype = 'a';
787     } else {
788             $mtx = 'dc';
789     }
790
791     if ( C4::Context->preference("marcflavour") eq "UNIMARC" ) {
792
793         # Setting datas
794         $aulast  = $record->subfield( '700', 'a' ) || '';
795         $aufirst = $record->subfield( '700', 'b' ) || '';
796         push @authors, "$aufirst $aulast" if ($aufirst or $aulast);
797
798         # others authors
799         if ( $record->field('200') ) {
800             for my $au ( $record->field('200')->subfield('g') ) {
801                 push @authors, $au;
802             }
803         }
804
805         $title     = $record->subfield( '200', 'a' );
806         my $subfield_210d = $record->subfield('210', 'd');
807         if ($subfield_210d and $subfield_210d =~ /(\d{4})/) {
808             $pubyear = $1;
809         }
810         $publisher = $record->subfield( '210', 'c' ) || '';
811         $isbn      = $record->subfield( '010', 'a' ) || '';
812         $issn      = $record->subfield( '011', 'a' ) || '';
813     } else {
814
815         # MARC21 need some improve
816
817         # Setting datas
818         if ( $record->field('100') ) {
819             push @authors, $record->subfield( '100', 'a' );
820         }
821
822         # others authors
823         if ( $record->field('700') ) {
824             for my $au ( $record->field('700')->subfield('a') ) {
825                 push @authors, $au;
826             }
827         }
828         $title = $record->field('245');
829         $title &&= $title->as_string('ab');
830         if ($titletype eq 'a') {
831             $pubyear   = $record->field('008') || '';
832             $pubyear   = substr($pubyear->data(), 7, 4) if $pubyear;
833             $isbn      = $record->subfield( '773', 'z' ) || '';
834             $issn      = $record->subfield( '773', 'x' ) || '';
835             $hosttitle = $record->subfield( '773', 't' ) || $record->subfield( '773', 'a') || q{};
836             my @rels = $record->subfield( '773', 'g' );
837             $pages = join(', ', @rels);
838         } else {
839             $pubyear   = $record->subfield( '260', 'c' ) || '';
840             $publisher = $record->subfield( '260', 'b' ) || '';
841             $isbn      = $record->subfield( '020', 'a' ) || '';
842             $issn      = $record->subfield( '022', 'a' ) || '';
843         }
844
845     }
846
847     my @params = (
848         [ 'ctx_ver', 'Z39.88-2004' ],
849         [ 'rft_val_fmt', "info:ofi/fmt:kev:mtx:$mtx" ],
850         [ ($mtx eq 'dc' ? 'rft.type' : 'rft.genre'), $genre ],
851         [ "rft.${titletype}title", $title ],
852     );
853
854     # rft.title is authorized only once, so by checking $titletype
855     # we ensure that rft.title is not already in the list.
856     if ($hosttitle and $titletype) {
857         push @params, [ 'rft.title', $hosttitle ];
858     }
859
860     push @params, (
861         [ 'rft.isbn', $isbn ],
862         [ 'rft.issn', $issn ],
863     );
864
865     # If it's a subscription, these informations have no meaning.
866     if ($genre ne 'journal') {
867         push @params, (
868             [ 'rft.aulast', $aulast ],
869             [ 'rft.aufirst', $aufirst ],
870             (map { [ 'rft.au', $_ ] } @authors),
871             [ 'rft.pub', $publisher ],
872             [ 'rft.date', $pubyear ],
873             [ 'rft.pages', $pages ],
874         );
875     }
876
877     my $coins_value = join( '&amp;',
878         map { $$_[1] ? $$_[0] . '=' . uri_escape_utf8( $$_[1] ) : () } @params );
879
880     return $coins_value;
881 }
882
883 =head2 get_openurl
884
885 my $url = $biblio->get_openurl;
886
887 Returns url for OpenURL resolver set in OpenURLResolverURL system preference
888
889 =cut
890
891 sub get_openurl {
892     my ( $self ) = @_;
893
894     my $OpenURLResolverURL = C4::Context->preference('OpenURLResolverURL');
895
896     if ($OpenURLResolverURL) {
897         my $uri = URI->new($OpenURLResolverURL);
898
899         if (not defined $uri->query) {
900             $OpenURLResolverURL .= '?';
901         } else {
902             $OpenURLResolverURL .= '&amp;';
903         }
904         $OpenURLResolverURL .= $self->get_coins;
905     }
906
907     return $OpenURLResolverURL;
908 }
909
910 =head3 is_serial
911
912 my $serial = $biblio->is_serial
913
914 Return boolean true if this bibbliographic record is continuing resource
915
916 =cut
917
918 sub is_serial {
919     my ( $self ) = @_;
920
921     return 1 if $self->serial;
922
923     my $record = $self->metadata->record;
924     return 1 if substr($record->leader, 7, 1) eq 's';
925
926     return 0;
927 }
928
929 =head3 custom_cover_image_url
930
931 my $image_url = $biblio->custom_cover_image_url
932
933 Return the specific url of the cover image for this bibliographic record.
934 It is built regaring the value of the system preference CustomCoverImagesURL
935
936 =cut
937
938 sub custom_cover_image_url {
939     my ( $self ) = @_;
940     my $url = C4::Context->preference('CustomCoverImagesURL');
941     if ( $url =~ m|{isbn}| ) {
942         my $isbn = $self->biblioitem->isbn;
943         return unless $isbn;
944         $url =~ s|{isbn}|$isbn|g;
945     }
946     if ( $url =~ m|{normalized_isbn}| ) {
947         my $normalized_isbn = C4::Koha::GetNormalizedISBN($self->biblioitem->isbn);
948         return unless $normalized_isbn;
949         $url =~ s|{normalized_isbn}|$normalized_isbn|g;
950     }
951     if ( $url =~ m|{issn}| ) {
952         my $issn = $self->biblioitem->issn;
953         return unless $issn;
954         $url =~ s|{issn}|$issn|g;
955     }
956
957     my $re = qr|{(?<field>\d{3})(\$(?<subfield>.))?}|;
958     if ( $url =~ $re ) {
959         my $field = $+{field};
960         my $subfield = $+{subfield};
961         my $marc_record = $self->metadata->record;
962         my $value;
963         if ( $subfield ) {
964             $value = $marc_record->subfield( $field, $subfield );
965         } else {
966             my $controlfield = $marc_record->field($field);
967             $value = $controlfield->data() if $controlfield;
968         }
969         return unless $value;
970         $url =~ s|$re|$value|;
971     }
972
973     return $url;
974 }
975
976 =head3 cover_images
977
978 Return the cover images associated with this biblio.
979
980 =cut
981
982 sub cover_images {
983     my ( $self ) = @_;
984
985     my $cover_images_rs = $self->_result->cover_images;
986     return unless $cover_images_rs;
987     return Koha::CoverImages->_new_from_dbic($cover_images_rs);
988 }
989
990 =head3 get_marc_notes
991
992     $marcnotesarray = $biblio->get_marc_notes({ opac => 1 });
993
994 Get all notes from the MARC record and returns them in an array.
995 The notes are stored in different fields depending on MARC flavour.
996 MARC21 5XX $u subfields receive special attention as they are URIs.
997
998 =cut
999
1000 sub get_marc_notes {
1001     my ( $self, $params ) = @_;
1002
1003     my $marcflavour = C4::Context->preference('marcflavour');
1004     my $opac = $params->{opac} // '0';
1005     my $interface = $params->{opac} ? 'opac' : 'intranet';
1006
1007     my $record = $params->{record} // $self->metadata->record;
1008     my $record_processor = Koha::RecordProcessor->new(
1009         {
1010             filters => [ 'ViewPolicy', 'ExpandCodedFields' ],
1011             options => {
1012                 interface     => $interface,
1013                 frameworkcode => $self->frameworkcode
1014             }
1015         }
1016     );
1017     $record_processor->process($record);
1018
1019     my $scope = $marcflavour eq "UNIMARC"? '3..': '5..';
1020     #MARC21 specs indicate some notes should be private if first indicator 0
1021     my %maybe_private = (
1022         541 => 1,
1023         542 => 1,
1024         561 => 1,
1025         583 => 1,
1026         590 => 1
1027     );
1028
1029     my %hiddenlist = map { $_ => 1 }
1030         split( /,/, C4::Context->preference('NotesToHide'));
1031
1032     my @marcnotes;
1033     foreach my $field ( $record->field($scope) ) {
1034         my $tag = $field->tag();
1035         next if $hiddenlist{ $tag };
1036         next if $opac && $maybe_private{$tag} && !$field->indicator(1);
1037         if( $marcflavour ne 'UNIMARC' && $field->subfield('u') ) {
1038             # Field 5XX$u always contains URI
1039             # Examples: 505u, 506u, 510u, 514u, 520u, 530u, 538u, 540u, 542u, 552u, 555u, 561u, 563u, 583u
1040             # We first push the other subfields, then all $u's separately
1041             # Leave further actions to the template (see e.g. opac-detail)
1042             my $othersub =
1043                 join '', ( 'a' .. 't', 'v' .. 'z', '0' .. '9' ); # excl 'u'
1044             push @marcnotes, { marcnote => $field->as_string($othersub) };
1045             foreach my $sub ( $field->subfield('u') ) {
1046                 $sub =~ s/^\s+|\s+$//g; # trim
1047                 push @marcnotes, { marcnote => $sub };
1048             }
1049         } else {
1050             push @marcnotes, { marcnote => $field->as_string() };
1051         }
1052     }
1053     return \@marcnotes;
1054 }
1055
1056 =head3 _get_marc_authors
1057
1058 Private method to return the list of authors contained in the MARC record.
1059 See get get_marc_contributors and get_marc_authors for the public methods.
1060
1061 =cut
1062
1063 sub _get_marc_authors {
1064     my ( $self, $params ) = @_;
1065
1066     my $fields_filter = $params->{fields_filter};
1067     my $mintag        = $params->{mintag};
1068     my $maxtag        = $params->{maxtag};
1069
1070     my $AuthoritySeparator = C4::Context->preference('AuthoritySeparator');
1071     my $marcflavour        = C4::Context->preference('marcflavour');
1072
1073     # tagslib useful only for UNIMARC author responsibilities
1074     my $tagslib = $marcflavour eq "UNIMARC"
1075       ? C4::Biblio::GetMarcStructure( 1, $self->frameworkcode, { unsafe => 1 } )
1076       : undef;
1077
1078     my @marcauthors;
1079     foreach my $field ( $self->metadata->record->field($fields_filter) ) {
1080
1081         next
1082           if $mintag && $field->tag() < $mintag
1083           || $maxtag && $field->tag() > $maxtag;
1084
1085         my @subfields_loop;
1086         my @link_loop;
1087         my @subfields  = $field->subfields();
1088         my $count_auth = 0;
1089
1090         # if there is an authority link, build the link with Koha-Auth-Number: subfield9
1091         my $subfield9 = $field->subfield('9');
1092         if ($subfield9) {
1093             my $linkvalue = $subfield9;
1094             $linkvalue =~ s/(\(|\))//g;
1095             @link_loop = ( { 'limit' => 'an', 'link' => $linkvalue } );
1096         }
1097
1098         # other subfields
1099         my $unimarc3;
1100         for my $authors_subfield (@subfields) {
1101             next if ( $authors_subfield->[0] eq '9' );
1102
1103             # unimarc3 contains the $3 of the author for UNIMARC.
1104             # For french academic libraries, it's the "ppn", and it's required for idref webservice
1105             $unimarc3 = $authors_subfield->[1] if $marcflavour eq 'UNIMARC' and $authors_subfield->[0] =~ /3/;
1106
1107             # don't load unimarc subfields 3, 5
1108             next if ( $marcflavour eq 'UNIMARC' and ( $authors_subfield->[0] =~ /3|5/ ) );
1109
1110             my $code = $authors_subfield->[0];
1111             my $value        = $authors_subfield->[1];
1112             my $linkvalue    = $value;
1113             $linkvalue =~ s/(\(|\))//g;
1114             # UNIMARC author responsibility
1115             if ( $marcflavour eq 'UNIMARC' and $code eq '4' ) {
1116                 $value = C4::Biblio::GetAuthorisedValueDesc( $field->tag(), $code, $value, '', $tagslib );
1117                 $linkvalue = "($value)";
1118             }
1119             # if no authority link, build a search query
1120             unless ($subfield9) {
1121                 push @link_loop, {
1122                     limit    => 'au',
1123                     'link'   => $linkvalue,
1124                     operator => (scalar @link_loop) ? ' AND ' : undef
1125                 };
1126             }
1127             my @this_link_loop = @link_loop;
1128             # do not display $0
1129             unless ( $code eq '0') {
1130                 push @subfields_loop, {
1131                     tag       => $field->tag(),
1132                     code      => $code,
1133                     value     => $value,
1134                     link_loop => \@this_link_loop,
1135                     separator => (scalar @subfields_loop) ? $AuthoritySeparator : ''
1136                 };
1137             }
1138         }
1139         push @marcauthors, {
1140             MARCAUTHOR_SUBFIELDS_LOOP => \@subfields_loop,
1141             authoritylink => $subfield9,
1142             unimarc3 => $unimarc3
1143         };
1144     }
1145     return \@marcauthors;
1146 }
1147
1148 =head3 get_marc_contributors
1149
1150     my $contributors = $biblio->get_marc_contributors;
1151
1152 Get all contributors (but first author) from the MARC record and returns them in an array.
1153 They are stored in different fields depending on MARC flavour (700..720 for MARC21)
1154
1155 =cut
1156
1157 sub get_marc_contributors {
1158     my ( $self, $params ) = @_;
1159
1160     my ( $mintag, $maxtag, $fields_filter );
1161     my $marcflavour = C4::Context->preference('marcflavour');
1162
1163     if ( $marcflavour eq "UNIMARC" ) {
1164         $mintag = "700";
1165         $maxtag = "712";
1166         $fields_filter = '7..';
1167     } else { # marc21/normarc
1168         $mintag = "700";
1169         $maxtag = "720";
1170         $fields_filter = '7..';
1171     }
1172
1173     return $self->_get_marc_authors(
1174         {
1175             fields_filter => $fields_filter,
1176             mintag       => $mintag,
1177             maxtag       => $maxtag
1178         }
1179     );
1180 }
1181
1182 =head3 get_marc_authors
1183
1184     my $authors = $biblio->get_marc_authors;
1185
1186 Get all authors from the MARC record and returns them in an array.
1187 They are stored in different fields depending on MARC flavour
1188 (main author from 100 then secondary authors from 700..720).
1189
1190 =cut
1191
1192 sub get_marc_authors {
1193     my ( $self, $params ) = @_;
1194
1195     my ( $mintag, $maxtag, $fields_filter );
1196     my $marcflavour = C4::Context->preference('marcflavour');
1197
1198     if ( $marcflavour eq "UNIMARC" ) {
1199         $fields_filter = '200';
1200     } else { # marc21/normarc
1201         $fields_filter = '100';
1202     }
1203
1204     my @first_authors = @{$self->_get_marc_authors(
1205         {
1206             fields_filter => $fields_filter,
1207             mintag       => $mintag,
1208             maxtag       => $maxtag
1209         }
1210     )};
1211
1212     my @other_authors = @{$self->get_marc_contributors};
1213
1214     return [@first_authors, @other_authors];
1215 }
1216
1217
1218 =head3 to_api
1219
1220     my $json = $biblio->to_api;
1221
1222 Overloaded method that returns a JSON representation of the Koha::Biblio object,
1223 suitable for API output. The related Koha::Biblioitem object is merged as expected
1224 on the API.
1225
1226 =cut
1227
1228 sub to_api {
1229     my ($self, $args) = @_;
1230
1231     my $response = $self->SUPER::to_api( $args );
1232     my $biblioitem = $self->biblioitem->to_api;
1233
1234     return { %$response, %$biblioitem };
1235 }
1236
1237 =head3 to_api_mapping
1238
1239 This method returns the mapping for representing a Koha::Biblio object
1240 on the API.
1241
1242 =cut
1243
1244 sub to_api_mapping {
1245     return {
1246         biblionumber     => 'biblio_id',
1247         frameworkcode    => 'framework_id',
1248         unititle         => 'uniform_title',
1249         seriestitle      => 'series_title',
1250         copyrightdate    => 'copyright_date',
1251         datecreated      => 'creation_date',
1252         deleted_on       => undef,
1253     };
1254 }
1255
1256 =head3 get_marc_host
1257
1258     $host = $biblio->get_marc_host;
1259     # OR:
1260     ( $host, $relatedparts, $hostinfo ) = $biblio->get_marc_host;
1261
1262     Returns host biblio record from MARC21 773 (undef if no 773 present).
1263     It looks at the first 773 field with MARCorgCode or only a control
1264     number. Complete $w or numeric part is used to search host record.
1265     The optional parameter no_items triggers a check if $biblio has items.
1266     If there are, the sub returns undef.
1267     Called in list context, it also returns 773$g (related parts).
1268
1269     If there is no $w, we use $0 (host biblionumber) or $9 (host itemnumber)
1270     to search for the host record. If there is also no $0 and no $9, we search
1271     using author and title. Failing all of that, we return an undef host and
1272     form a concatenation of strings with 773$agt for host information,
1273     returned when called in list context.
1274
1275 =cut
1276
1277 sub get_marc_host {
1278     my ($self, $params) = @_;
1279     my $no_items = $params->{no_items};
1280     return if C4::Context->preference('marcflavour') eq 'UNIMARC'; # TODO
1281     return if $params->{no_items} && $self->items->count > 0;
1282
1283     my $record;
1284     eval { $record = $self->metadata->record };
1285     return if !$record;
1286
1287     # We pick the first $w with your MARCOrgCode or the first $w that has no
1288     # code (between parentheses) at all.
1289     my $orgcode = C4::Context->preference('MARCOrgCode') // q{};
1290     my $hostfld;
1291     foreach my $f ( $record->field('773') ) {
1292         my $w = $f->subfield('w') or next;
1293         if( $w =~ /^\($orgcode\)\s*(\d+)/i or $w =~ /^\d+/ ) {
1294             $hostfld = $f;
1295             last;
1296         }
1297     }
1298
1299     my $engine = Koha::SearchEngine::Search->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1300     my $bibno;
1301     if ( !$hostfld and $record->subfield('773','t') ) {
1302         # not linked using $w
1303         my $unlinkedf = $record->field('773');
1304         my $host;
1305         if ( C4::Context->preference("EasyAnalyticalRecords") ) {
1306             if ( $unlinkedf->subfield('0') ) {
1307                 # use 773$0 host biblionumber
1308                 $bibno = $unlinkedf->subfield('0');
1309             } elsif ( $unlinkedf->subfield('9') ) {
1310                 # use 773$9 host itemnumber
1311                 my $linkeditemnumber = $unlinkedf->subfield('9');
1312                 $bibno = Koha::Items->find( $linkeditemnumber )->biblionumber;
1313             }
1314         }
1315         if ( $bibno ) {
1316             my $host = Koha::Biblios->find($bibno) or return;
1317             return wantarray ? ( $host, $unlinkedf->subfield('g') ) : $host;
1318         }
1319         # just return plaintext and no host record
1320         my $hostinfo = join( ", ", $unlinkedf->subfield('a'), $unlinkedf->subfield('t'), $unlinkedf->subfield('g') );
1321         return wantarray ? ( undef, $unlinkedf->subfield('g'), $hostinfo ) : undef;
1322     }
1323     return if !$hostfld;
1324     my $rcn = $hostfld->subfield('w');
1325
1326     # Look for control number with/without orgcode
1327     for my $try (1..2) {
1328         my ( $error, $results, $total_hits ) = $engine->simple_search_compat( 'Control-number='.$rcn, 0,1 );
1329         if( !$error and $total_hits == 1 ) {
1330             $bibno = $engine->extract_biblionumber( $results->[0] );
1331             last;
1332         }
1333         # Add or remove orgcode for second try
1334         if( $try == 1 && $rcn =~ /\)\s*(\d+)/ ) {
1335             $rcn = $1; # number only
1336         } elsif( $try == 1 && $rcn =~ /^\d+/ ) {
1337             $rcn = "($orgcode)$rcn";
1338         } else {
1339             last;
1340         }
1341     }
1342     if( $bibno ) {
1343         my $host = Koha::Biblios->find($bibno) or return;
1344         return wantarray ? ( $host, $hostfld->subfield('g') ) : $host;
1345     }
1346 }
1347
1348 =head3 recalls
1349
1350     my $recalls = $biblio->recalls;
1351
1352 Return recalls linked to this biblio
1353
1354 =cut
1355
1356 sub recalls {
1357     my ( $self ) = @_;
1358     return Koha::Recalls->_new_from_dbic( scalar $self->_result->recalls );
1359 }
1360
1361 =head3 can_be_recalled
1362
1363     my @items_for_recall = $biblio->can_be_recalled({ patron => $patron_object });
1364
1365 Does biblio-level checks and returns the items attached to this biblio that are available for recall
1366
1367 =cut
1368
1369 sub can_be_recalled {
1370     my ( $self, $params ) = @_;
1371
1372     return 0 if !( C4::Context->preference('UseRecalls') );
1373
1374     my $patron = $params->{patron};
1375
1376     my $branchcode = C4::Context->userenv->{'branch'};
1377     if ( C4::Context->preference('CircControl') eq 'PatronLibrary' and $patron ) {
1378         $branchcode = $patron->branchcode;
1379     }
1380
1381     my @all_items = Koha::Items->search({ biblionumber => $self->biblionumber })->as_list;
1382
1383     # if there are no available items at all, no recall can be placed
1384     return 0 if ( scalar @all_items == 0 );
1385
1386     my @itemtypes;
1387     my @itemnumbers;
1388     my @items;
1389     my @all_itemnumbers;
1390     foreach my $item ( @all_items ) {
1391         push( @all_itemnumbers, $item->itemnumber );
1392         if ( $item->can_be_recalled({ patron => $patron }) ) {
1393             push( @itemtypes, $item->effective_itemtype );
1394             push( @itemnumbers, $item->itemnumber );
1395             push( @items, $item );
1396         }
1397     }
1398
1399     # if there are no recallable items, no recall can be placed
1400     return 0 if ( scalar @items == 0 );
1401
1402     # Check the circulation rule for each relevant itemtype for this biblio
1403     my ( @recalls_allowed, @recalls_per_record, @on_shelf_recalls );
1404     foreach my $itemtype ( @itemtypes ) {
1405         my $rule = Koha::CirculationRules->get_effective_rules({
1406             branchcode => $branchcode,
1407             categorycode => $patron ? $patron->categorycode : undef,
1408             itemtype => $itemtype,
1409             rules => [
1410                 'recalls_allowed',
1411                 'recalls_per_record',
1412                 'on_shelf_recalls',
1413             ],
1414         });
1415         push( @recalls_allowed, $rule->{recalls_allowed} ) if $rule;
1416         push( @recalls_per_record, $rule->{recalls_per_record} ) if $rule;
1417         push( @on_shelf_recalls, $rule->{on_shelf_recalls} ) if $rule;
1418     }
1419     my $recalls_allowed = (sort {$b <=> $a} @recalls_allowed)[0]; # take highest
1420     my $recalls_per_record = (sort {$b <=> $a} @recalls_per_record)[0]; # take highest
1421     my %on_shelf_recalls_count = ();
1422     foreach my $count ( @on_shelf_recalls ) {
1423         $on_shelf_recalls_count{$count}++;
1424     }
1425     my $on_shelf_recalls = (sort {$on_shelf_recalls_count{$b} <=> $on_shelf_recalls_count{$a}} @on_shelf_recalls)[0]; # take most common
1426
1427     # check recalls allowed has been set and is not zero
1428     return 0 if ( !defined($recalls_allowed) || $recalls_allowed == 0 );
1429
1430     if ( $patron ) {
1431         # check borrower has not reached open recalls allowed limit
1432         return 0 if ( $patron->recalls->filter_by_current->count >= $recalls_allowed );
1433
1434         # check borrower has not reached open recalls allowed per record limit
1435         return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $recalls_per_record );
1436
1437         # check if any of the items under this biblio are already checked out by this borrower
1438         return 0 if ( Koha::Checkouts->search({ itemnumber => [ @all_itemnumbers ], borrowernumber => $patron->borrowernumber })->count > 0 );
1439     }
1440
1441     # check item availability
1442     my $checked_out_count = 0;
1443     foreach (@items) {
1444         if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1445     }
1446
1447     # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1448     return 0 if ( $on_shelf_recalls eq 'all' && $checked_out_count < scalar @items );
1449
1450     # can't recall if no items have been checked out
1451     return 0 if ( $checked_out_count == 0 );
1452
1453     # can recall
1454     return @items;
1455 }
1456
1457 =head2 Internal methods
1458
1459 =head3 type
1460
1461 =cut
1462
1463 sub _type {
1464     return 'Biblio';
1465 }
1466
1467 =head1 AUTHOR
1468
1469 Kyle M Hall <kyle@bywatersolutions.com>
1470
1471 =cut
1472
1473 1;