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