Bug 21983: Add Koha::Biblio->ill_requests
[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()->itemtype
560
561 Returns the related Koha::Biblioitem object for this Biblio object
562
563 =cut
564
565 sub biblioitem {
566     my ($self) = @_;
567
568     $self->{_biblioitem} ||= Koha::Biblioitems->find( { biblionumber => $self->biblionumber() } );
569
570     return $self->{_biblioitem};
571 }
572
573 =head3 suggestions
574
575 my $suggestions = $self->suggestions
576
577 Returns the related Koha::Suggestions object for this Biblio object
578
579 =cut
580
581 sub suggestions {
582     my ($self) = @_;
583
584     my $suggestions_rs = $self->_result->suggestions;
585     return Koha::Suggestions->_new_from_dbic( $suggestions_rs );
586 }
587
588 =head3 get_marc_components
589
590   my $components = $self->get_marc_components();
591
592 Returns an array of search results data, which are component parts of
593 this object (MARC21 773 points to this)
594
595 =cut
596
597 sub get_marc_components {
598     my ($self, $max_results) = @_;
599
600     return [] if (C4::Context->preference('marcflavour') ne 'MARC21');
601
602     my ( $searchstr, $sort ) = $self->get_components_query;
603
604     my $components;
605     if (defined($searchstr)) {
606         my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
607         my ( $error, $results, $facets );
608         eval {
609             ( $error, $results, $facets ) = $searcher->search_compat( $searchstr, undef, [$sort], ['biblioserver'], $max_results, 0, undef, undef, 'ccl', 0 );
610         };
611         if( $error || $@ ) {
612             $error //= q{};
613             $error .= $@ if $@;
614             warn "Warning from search_compat: '$error'";
615             $self->add_message(
616                 {
617                     type    => 'error',
618                     message => 'component_search',
619                     payload => $error,
620                 }
621             );
622         }
623         $components = $results->{biblioserver}->{RECORDS} if defined($results) && $results->{biblioserver}->{hits};
624     }
625
626     return $components // [];
627 }
628
629 =head2 get_components_query
630
631 Returns a query which can be used to search for all component parts of MARC21 biblios
632
633 =cut
634
635 sub get_components_query {
636     my ($self) = @_;
637
638     my $builder = Koha::SearchEngine::QueryBuilder->new(
639         { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
640     my $marc = $self->metadata->record;
641     my $component_sort_field = C4::Context->preference('ComponentSortField') // "title";
642     my $component_sort_order = C4::Context->preference('ComponentSortOrder') // "asc";
643     my $sort = $component_sort_field . "_" . $component_sort_order;
644
645     my $searchstr;
646     if ( C4::Context->preference('UseControlNumber') ) {
647         my $pf001 = $marc->field('001') || undef;
648
649         if ( defined($pf001) ) {
650             $searchstr = "(";
651             my $pf003 = $marc->field('003') || undef;
652
653             if ( !defined($pf003) ) {
654                 # search for 773$w='Host001'
655                 $searchstr .= "rcn:\"" . $pf001->data()."\"";
656             }
657             else {
658                 $searchstr .= "(";
659                 # search for (773$w='Host001' and 003='Host003') or 773$w='(Host003)Host001'
660                 $searchstr .= "(rcn:\"" . $pf001->data() . "\" AND cni:\"" . $pf003->data() . "\")";
661                 $searchstr .= " OR rcn:\"" . $pf003->data() . " " . $pf001->data() . "\"";
662                 $searchstr .= ")";
663             }
664
665             # limit to monograph and serial component part records
666             $searchstr .= " AND (bib-level:a OR bib-level:b)";
667             $searchstr .= ")";
668         }
669     }
670     else {
671         my $cleaned_title = $marc->subfield('245', "a");
672         $cleaned_title =~ tr|/||;
673         $cleaned_title = $builder->clean_search_term($cleaned_title);
674         $searchstr = qq#Host-item:("$cleaned_title")#;
675     }
676     my ($error, $query ,$query_str) = $builder->build_query_compat( undef, [$searchstr], undef, undef, [$sort], 0 );
677     if( $error ){
678         warn $error;
679         return;
680     }
681
682     return ($query, $query_str, $sort);
683 }
684
685 =head3 subscriptions
686
687 my $subscriptions = $self->subscriptions
688
689 Returns the related Koha::Subscriptions object for this Biblio object
690
691 =cut
692
693 sub subscriptions {
694     my ($self) = @_;
695
696     $self->{_subscriptions} ||= Koha::Subscriptions->search( { biblionumber => $self->biblionumber } );
697
698     return $self->{_subscriptions};
699 }
700
701 =head3 has_items_waiting_or_intransit
702
703 my $itemsWaitingOrInTransit = $biblio->has_items_waiting_or_intransit
704
705 Tells if this bibliographic record has items waiting or in transit.
706
707 =cut
708
709 sub has_items_waiting_or_intransit {
710     my ( $self ) = @_;
711
712     if ( Koha::Holds->search({ biblionumber => $self->id,
713                                found => ['W', 'T'] })->count ) {
714         return 1;
715     }
716
717     foreach my $item ( $self->items->as_list ) {
718         return 1 if $item->get_transfer;
719     }
720
721     return 0;
722 }
723
724 =head2 get_coins
725
726 my $coins = $biblio->get_coins;
727
728 Returns the COinS (a span) which can be included in a biblio record
729
730 =cut
731
732 sub get_coins {
733     my ( $self ) = @_;
734
735     my $record = $self->metadata->record;
736
737     my $pos7 = substr $record->leader(), 7, 1;
738     my $pos6 = substr $record->leader(), 6, 1;
739     my $mtx;
740     my $genre;
741     my ( $aulast, $aufirst ) = ( '', '' );
742     my @authors;
743     my $title;
744     my $hosttitle;
745     my $pubyear   = '';
746     my $isbn      = '';
747     my $issn      = '';
748     my $publisher = '';
749     my $pages     = '';
750     my $titletype = '';
751
752     # For the purposes of generating COinS metadata, LDR/06-07 can be
753     # considered the same for UNIMARC and MARC21
754     my $fmts6 = {
755         'a' => 'book',
756         'b' => 'manuscript',
757         'c' => 'book',
758         'd' => 'manuscript',
759         'e' => 'map',
760         'f' => 'map',
761         'g' => 'film',
762         'i' => 'audioRecording',
763         'j' => 'audioRecording',
764         'k' => 'artwork',
765         'l' => 'document',
766         'm' => 'computerProgram',
767         'o' => 'document',
768         'r' => 'document',
769     };
770     my $fmts7 = {
771         'a' => 'journalArticle',
772         's' => 'journal',
773     };
774
775     $genre = $fmts6->{$pos6} ? $fmts6->{$pos6} : 'book';
776
777     if ( $genre eq 'book' ) {
778             $genre = $fmts7->{$pos7} if $fmts7->{$pos7};
779     }
780
781     ##### We must transform mtx to a valable mtx and document type ####
782     if ( $genre eq 'book' ) {
783             $mtx = 'book';
784             $titletype = 'b';
785     } elsif ( $genre eq 'journal' ) {
786             $mtx = 'journal';
787             $titletype = 'j';
788     } elsif ( $genre eq 'journalArticle' ) {
789             $mtx   = 'journal';
790             $genre = 'article';
791             $titletype = 'a';
792     } else {
793             $mtx = 'dc';
794     }
795
796     if ( C4::Context->preference("marcflavour") eq "UNIMARC" ) {
797
798         # Setting datas
799         $aulast  = $record->subfield( '700', 'a' ) || '';
800         $aufirst = $record->subfield( '700', 'b' ) || '';
801         push @authors, "$aufirst $aulast" if ($aufirst or $aulast);
802
803         # others authors
804         if ( $record->field('200') ) {
805             for my $au ( $record->field('200')->subfield('g') ) {
806                 push @authors, $au;
807             }
808         }
809
810         $title     = $record->subfield( '200', 'a' );
811         my $subfield_210d = $record->subfield('210', 'd');
812         if ($subfield_210d and $subfield_210d =~ /(\d{4})/) {
813             $pubyear = $1;
814         }
815         $publisher = $record->subfield( '210', 'c' ) || '';
816         $isbn      = $record->subfield( '010', 'a' ) || '';
817         $issn      = $record->subfield( '011', 'a' ) || '';
818     } else {
819
820         # MARC21 need some improve
821
822         # Setting datas
823         if ( $record->field('100') ) {
824             push @authors, $record->subfield( '100', 'a' );
825         }
826
827         # others authors
828         if ( $record->field('700') ) {
829             for my $au ( $record->field('700')->subfield('a') ) {
830                 push @authors, $au;
831             }
832         }
833         $title = $record->field('245');
834         $title &&= $title->as_string('ab');
835         if ($titletype eq 'a') {
836             $pubyear   = $record->field('008') || '';
837             $pubyear   = substr($pubyear->data(), 7, 4) if $pubyear;
838             $isbn      = $record->subfield( '773', 'z' ) || '';
839             $issn      = $record->subfield( '773', 'x' ) || '';
840             $hosttitle = $record->subfield( '773', 't' ) || $record->subfield( '773', 'a') || q{};
841             my @rels = $record->subfield( '773', 'g' );
842             $pages = join(', ', @rels);
843         } else {
844             $pubyear   = $record->subfield( '260', 'c' ) || '';
845             $publisher = $record->subfield( '260', 'b' ) || '';
846             $isbn      = $record->subfield( '020', 'a' ) || '';
847             $issn      = $record->subfield( '022', 'a' ) || '';
848         }
849
850     }
851
852     my @params = (
853         [ 'ctx_ver', 'Z39.88-2004' ],
854         [ 'rft_val_fmt', "info:ofi/fmt:kev:mtx:$mtx" ],
855         [ ($mtx eq 'dc' ? 'rft.type' : 'rft.genre'), $genre ],
856         [ "rft.${titletype}title", $title ],
857     );
858
859     # rft.title is authorized only once, so by checking $titletype
860     # we ensure that rft.title is not already in the list.
861     if ($hosttitle and $titletype) {
862         push @params, [ 'rft.title', $hosttitle ];
863     }
864
865     push @params, (
866         [ 'rft.isbn', $isbn ],
867         [ 'rft.issn', $issn ],
868     );
869
870     # If it's a subscription, these informations have no meaning.
871     if ($genre ne 'journal') {
872         push @params, (
873             [ 'rft.aulast', $aulast ],
874             [ 'rft.aufirst', $aufirst ],
875             (map { [ 'rft.au', $_ ] } @authors),
876             [ 'rft.pub', $publisher ],
877             [ 'rft.date', $pubyear ],
878             [ 'rft.pages', $pages ],
879         );
880     }
881
882     my $coins_value = join( '&amp;',
883         map { $$_[1] ? $$_[0] . '=' . uri_escape_utf8( $$_[1] ) : () } @params );
884
885     return $coins_value;
886 }
887
888 =head2 get_openurl
889
890 my $url = $biblio->get_openurl;
891
892 Returns url for OpenURL resolver set in OpenURLResolverURL system preference
893
894 =cut
895
896 sub get_openurl {
897     my ( $self ) = @_;
898
899     my $OpenURLResolverURL = C4::Context->preference('OpenURLResolverURL');
900
901     if ($OpenURLResolverURL) {
902         my $uri = URI->new($OpenURLResolverURL);
903
904         if (not defined $uri->query) {
905             $OpenURLResolverURL .= '?';
906         } else {
907             $OpenURLResolverURL .= '&amp;';
908         }
909         $OpenURLResolverURL .= $self->get_coins;
910     }
911
912     return $OpenURLResolverURL;
913 }
914
915 =head3 is_serial
916
917 my $serial = $biblio->is_serial
918
919 Return boolean true if this bibbliographic record is continuing resource
920
921 =cut
922
923 sub is_serial {
924     my ( $self ) = @_;
925
926     return 1 if $self->serial;
927
928     my $record = $self->metadata->record;
929     return 1 if substr($record->leader, 7, 1) eq 's';
930
931     return 0;
932 }
933
934 =head3 custom_cover_image_url
935
936 my $image_url = $biblio->custom_cover_image_url
937
938 Return the specific url of the cover image for this bibliographic record.
939 It is built regaring the value of the system preference CustomCoverImagesURL
940
941 =cut
942
943 sub custom_cover_image_url {
944     my ( $self ) = @_;
945     my $url = C4::Context->preference('CustomCoverImagesURL');
946     if ( $url =~ m|{isbn}| ) {
947         my $isbn = $self->biblioitem->isbn;
948         return unless $isbn;
949         $url =~ s|{isbn}|$isbn|g;
950     }
951     if ( $url =~ m|{normalized_isbn}| ) {
952         my $normalized_isbn = C4::Koha::GetNormalizedISBN($self->biblioitem->isbn);
953         return unless $normalized_isbn;
954         $url =~ s|{normalized_isbn}|$normalized_isbn|g;
955     }
956     if ( $url =~ m|{issn}| ) {
957         my $issn = $self->biblioitem->issn;
958         return unless $issn;
959         $url =~ s|{issn}|$issn|g;
960     }
961
962     my $re = qr|{(?<field>\d{3})(\$(?<subfield>.))?}|;
963     if ( $url =~ $re ) {
964         my $field = $+{field};
965         my $subfield = $+{subfield};
966         my $marc_record = $self->metadata->record;
967         my $value;
968         if ( $subfield ) {
969             $value = $marc_record->subfield( $field, $subfield );
970         } else {
971             my $controlfield = $marc_record->field($field);
972             $value = $controlfield->data() if $controlfield;
973         }
974         return unless $value;
975         $url =~ s|$re|$value|;
976     }
977
978     return $url;
979 }
980
981 =head3 cover_images
982
983 Return the cover images associated with this biblio.
984
985 =cut
986
987 sub cover_images {
988     my ( $self ) = @_;
989
990     my $cover_images_rs = $self->_result->cover_images;
991     return unless $cover_images_rs;
992     return Koha::CoverImages->_new_from_dbic($cover_images_rs);
993 }
994
995 =head3 get_marc_notes
996
997     $marcnotesarray = $biblio->get_marc_notes({ opac => 1 });
998
999 Get all notes from the MARC record and returns them in an array.
1000 The notes are stored in different fields depending on MARC flavour.
1001 MARC21 5XX $u subfields receive special attention as they are URIs.
1002
1003 =cut
1004
1005 sub get_marc_notes {
1006     my ( $self, $params ) = @_;
1007
1008     my $marcflavour = C4::Context->preference('marcflavour');
1009     my $opac = $params->{opac} // '0';
1010     my $interface = $params->{opac} ? 'opac' : 'intranet';
1011
1012     my $record = $params->{record} // $self->metadata->record;
1013     my $record_processor = Koha::RecordProcessor->new(
1014         {
1015             filters => [ 'ViewPolicy', 'ExpandCodedFields' ],
1016             options => {
1017                 interface     => $interface,
1018                 frameworkcode => $self->frameworkcode
1019             }
1020         }
1021     );
1022     $record_processor->process($record);
1023
1024     my $scope = $marcflavour eq "UNIMARC"? '3..': '5..';
1025     #MARC21 specs indicate some notes should be private if first indicator 0
1026     my %maybe_private = (
1027         541 => 1,
1028         542 => 1,
1029         561 => 1,
1030         583 => 1,
1031         590 => 1
1032     );
1033
1034     my %hiddenlist = map { $_ => 1 }
1035         split( /,/, C4::Context->preference('NotesToHide'));
1036
1037     my @marcnotes;
1038     foreach my $field ( $record->field($scope) ) {
1039         my $tag = $field->tag();
1040         next if $hiddenlist{ $tag };
1041         next if $opac && $maybe_private{$tag} && !$field->indicator(1);
1042         if( $marcflavour ne 'UNIMARC' && $field->subfield('u') ) {
1043             # Field 5XX$u always contains URI
1044             # Examples: 505u, 506u, 510u, 514u, 520u, 530u, 538u, 540u, 542u, 552u, 555u, 561u, 563u, 583u
1045             # We first push the other subfields, then all $u's separately
1046             # Leave further actions to the template (see e.g. opac-detail)
1047             my $othersub =
1048                 join '', ( 'a' .. 't', 'v' .. 'z', '0' .. '9' ); # excl 'u'
1049             push @marcnotes, { marcnote => $field->as_string($othersub) };
1050             foreach my $sub ( $field->subfield('u') ) {
1051                 $sub =~ s/^\s+|\s+$//g; # trim
1052                 push @marcnotes, { marcnote => $sub };
1053             }
1054         } else {
1055             push @marcnotes, { marcnote => $field->as_string() };
1056         }
1057     }
1058     return \@marcnotes;
1059 }
1060
1061 =head3 _get_marc_authors
1062
1063 Private method to return the list of authors contained in the MARC record.
1064 See get get_marc_contributors and get_marc_authors for the public methods.
1065
1066 =cut
1067
1068 sub _get_marc_authors {
1069     my ( $self, $params ) = @_;
1070
1071     my $fields_filter = $params->{fields_filter};
1072     my $mintag        = $params->{mintag};
1073     my $maxtag        = $params->{maxtag};
1074
1075     my $AuthoritySeparator = C4::Context->preference('AuthoritySeparator');
1076     my $marcflavour        = C4::Context->preference('marcflavour');
1077
1078     # tagslib useful only for UNIMARC author responsibilities
1079     my $tagslib = $marcflavour eq "UNIMARC"
1080       ? C4::Biblio::GetMarcStructure( 1, $self->frameworkcode, { unsafe => 1 } )
1081       : undef;
1082
1083     my @marcauthors;
1084     foreach my $field ( $self->metadata->record->field($fields_filter) ) {
1085
1086         next
1087           if $mintag && $field->tag() < $mintag
1088           || $maxtag && $field->tag() > $maxtag;
1089
1090         my @subfields_loop;
1091         my @link_loop;
1092         my @subfields  = $field->subfields();
1093         my $count_auth = 0;
1094
1095         # if there is an authority link, build the link with Koha-Auth-Number: subfield9
1096         my $subfield9 = $field->subfield('9');
1097         if ($subfield9) {
1098             my $linkvalue = $subfield9;
1099             $linkvalue =~ s/(\(|\))//g;
1100             @link_loop = ( { 'limit' => 'an', 'link' => $linkvalue } );
1101         }
1102
1103         # other subfields
1104         my $unimarc3;
1105         for my $authors_subfield (@subfields) {
1106             next if ( $authors_subfield->[0] eq '9' );
1107
1108             # unimarc3 contains the $3 of the author for UNIMARC.
1109             # For french academic libraries, it's the "ppn", and it's required for idref webservice
1110             $unimarc3 = $authors_subfield->[1] if $marcflavour eq 'UNIMARC' and $authors_subfield->[0] =~ /3/;
1111
1112             # don't load unimarc subfields 3, 5
1113             next if ( $marcflavour eq 'UNIMARC' and ( $authors_subfield->[0] =~ /3|5/ ) );
1114
1115             my $code = $authors_subfield->[0];
1116             my $value        = $authors_subfield->[1];
1117             my $linkvalue    = $value;
1118             $linkvalue =~ s/(\(|\))//g;
1119             # UNIMARC author responsibility
1120             if ( $marcflavour eq 'UNIMARC' and $code eq '4' ) {
1121                 $value = C4::Biblio::GetAuthorisedValueDesc( $field->tag(), $code, $value, '', $tagslib );
1122                 $linkvalue = "($value)";
1123             }
1124             # if no authority link, build a search query
1125             unless ($subfield9) {
1126                 push @link_loop, {
1127                     limit    => 'au',
1128                     'link'   => $linkvalue,
1129                     operator => (scalar @link_loop) ? ' AND ' : undef
1130                 };
1131             }
1132             my @this_link_loop = @link_loop;
1133             # do not display $0
1134             unless ( $code eq '0') {
1135                 push @subfields_loop, {
1136                     tag       => $field->tag(),
1137                     code      => $code,
1138                     value     => $value,
1139                     link_loop => \@this_link_loop,
1140                     separator => (scalar @subfields_loop) ? $AuthoritySeparator : ''
1141                 };
1142             }
1143         }
1144         push @marcauthors, {
1145             MARCAUTHOR_SUBFIELDS_LOOP => \@subfields_loop,
1146             authoritylink => $subfield9,
1147             unimarc3 => $unimarc3
1148         };
1149     }
1150     return \@marcauthors;
1151 }
1152
1153 =head3 get_marc_contributors
1154
1155     my $contributors = $biblio->get_marc_contributors;
1156
1157 Get all contributors (but first author) from the MARC record and returns them in an array.
1158 They are stored in different fields depending on MARC flavour (700..720 for MARC21)
1159
1160 =cut
1161
1162 sub get_marc_contributors {
1163     my ( $self, $params ) = @_;
1164
1165     my ( $mintag, $maxtag, $fields_filter );
1166     my $marcflavour = C4::Context->preference('marcflavour');
1167
1168     if ( $marcflavour eq "UNIMARC" ) {
1169         $mintag = "700";
1170         $maxtag = "712";
1171         $fields_filter = '7..';
1172     } else { # marc21/normarc
1173         $mintag = "700";
1174         $maxtag = "720";
1175         $fields_filter = '7..';
1176     }
1177
1178     return $self->_get_marc_authors(
1179         {
1180             fields_filter => $fields_filter,
1181             mintag       => $mintag,
1182             maxtag       => $maxtag
1183         }
1184     );
1185 }
1186
1187 =head3 get_marc_authors
1188
1189     my $authors = $biblio->get_marc_authors;
1190
1191 Get all authors from the MARC record and returns them in an array.
1192 They are stored in different fields depending on MARC flavour
1193 (main author from 100 then secondary authors from 700..720).
1194
1195 =cut
1196
1197 sub get_marc_authors {
1198     my ( $self, $params ) = @_;
1199
1200     my ( $mintag, $maxtag, $fields_filter );
1201     my $marcflavour = C4::Context->preference('marcflavour');
1202
1203     if ( $marcflavour eq "UNIMARC" ) {
1204         $fields_filter = '200';
1205     } else { # marc21/normarc
1206         $fields_filter = '100';
1207     }
1208
1209     my @first_authors = @{$self->_get_marc_authors(
1210         {
1211             fields_filter => $fields_filter,
1212             mintag       => $mintag,
1213             maxtag       => $maxtag
1214         }
1215     )};
1216
1217     my @other_authors = @{$self->get_marc_contributors};
1218
1219     return [@first_authors, @other_authors];
1220 }
1221
1222
1223 =head3 to_api
1224
1225     my $json = $biblio->to_api;
1226
1227 Overloaded method that returns a JSON representation of the Koha::Biblio object,
1228 suitable for API output. The related Koha::Biblioitem object is merged as expected
1229 on the API.
1230
1231 =cut
1232
1233 sub to_api {
1234     my ($self, $args) = @_;
1235
1236     my $response = $self->SUPER::to_api( $args );
1237     my $biblioitem = $self->biblioitem->to_api;
1238
1239     return { %$response, %$biblioitem };
1240 }
1241
1242 =head3 to_api_mapping
1243
1244 This method returns the mapping for representing a Koha::Biblio object
1245 on the API.
1246
1247 =cut
1248
1249 sub to_api_mapping {
1250     return {
1251         biblionumber     => 'biblio_id',
1252         frameworkcode    => 'framework_id',
1253         unititle         => 'uniform_title',
1254         seriestitle      => 'series_title',
1255         copyrightdate    => 'copyright_date',
1256         datecreated      => 'creation_date',
1257         deleted_on       => undef,
1258     };
1259 }
1260
1261 =head3 get_marc_host
1262
1263     $host = $biblio->get_marc_host;
1264     # OR:
1265     ( $host, $relatedparts, $hostinfo ) = $biblio->get_marc_host;
1266
1267     Returns host biblio record from MARC21 773 (undef if no 773 present).
1268     It looks at the first 773 field with MARCorgCode or only a control
1269     number. Complete $w or numeric part is used to search host record.
1270     The optional parameter no_items triggers a check if $biblio has items.
1271     If there are, the sub returns undef.
1272     Called in list context, it also returns 773$g (related parts).
1273
1274     If there is no $w, we use $0 (host biblionumber) or $9 (host itemnumber)
1275     to search for the host record. If there is also no $0 and no $9, we search
1276     using author and title. Failing all of that, we return an undef host and
1277     form a concatenation of strings with 773$agt for host information,
1278     returned when called in list context.
1279
1280 =cut
1281
1282 sub get_marc_host {
1283     my ($self, $params) = @_;
1284     my $no_items = $params->{no_items};
1285     return if C4::Context->preference('marcflavour') eq 'UNIMARC'; # TODO
1286     return if $params->{no_items} && $self->items->count > 0;
1287
1288     my $record;
1289     eval { $record = $self->metadata->record };
1290     return if !$record;
1291
1292     # We pick the first $w with your MARCOrgCode or the first $w that has no
1293     # code (between parentheses) at all.
1294     my $orgcode = C4::Context->preference('MARCOrgCode') // q{};
1295     my $hostfld;
1296     foreach my $f ( $record->field('773') ) {
1297         my $w = $f->subfield('w') or next;
1298         if( $w =~ /^\($orgcode\)\s*(\d+)/i or $w =~ /^\d+/ ) {
1299             $hostfld = $f;
1300             last;
1301         }
1302     }
1303
1304     my $engine = Koha::SearchEngine::Search->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1305     my $bibno;
1306     if ( !$hostfld and $record->subfield('773','t') ) {
1307         # not linked using $w
1308         my $unlinkedf = $record->field('773');
1309         my $host;
1310         if ( C4::Context->preference("EasyAnalyticalRecords") ) {
1311             if ( $unlinkedf->subfield('0') ) {
1312                 # use 773$0 host biblionumber
1313                 $bibno = $unlinkedf->subfield('0');
1314             } elsif ( $unlinkedf->subfield('9') ) {
1315                 # use 773$9 host itemnumber
1316                 my $linkeditemnumber = $unlinkedf->subfield('9');
1317                 $bibno = Koha::Items->find( $linkeditemnumber )->biblionumber;
1318             }
1319         }
1320         if ( $bibno ) {
1321             my $host = Koha::Biblios->find($bibno) or return;
1322             return wantarray ? ( $host, $unlinkedf->subfield('g') ) : $host;
1323         }
1324         # just return plaintext and no host record
1325         my $hostinfo = join( ", ", $unlinkedf->subfield('a'), $unlinkedf->subfield('t'), $unlinkedf->subfield('g') );
1326         return wantarray ? ( undef, $unlinkedf->subfield('g'), $hostinfo ) : undef;
1327     }
1328     return if !$hostfld;
1329     my $rcn = $hostfld->subfield('w');
1330
1331     # Look for control number with/without orgcode
1332     for my $try (1..2) {
1333         my ( $error, $results, $total_hits ) = $engine->simple_search_compat( 'Control-number='.$rcn, 0,1 );
1334         if( !$error and $total_hits == 1 ) {
1335             $bibno = $engine->extract_biblionumber( $results->[0] );
1336             last;
1337         }
1338         # Add or remove orgcode for second try
1339         if( $try == 1 && $rcn =~ /\)\s*(\d+)/ ) {
1340             $rcn = $1; # number only
1341         } elsif( $try == 1 && $rcn =~ /^\d+/ ) {
1342             $rcn = "($orgcode)$rcn";
1343         } else {
1344             last;
1345         }
1346     }
1347     if( $bibno ) {
1348         my $host = Koha::Biblios->find($bibno) or return;
1349         return wantarray ? ( $host, $hostfld->subfield('g') ) : $host;
1350     }
1351 }
1352
1353 =head3 recalls
1354
1355     my $recalls = $biblio->recalls;
1356
1357 Return recalls linked to this biblio
1358
1359 =cut
1360
1361 sub recalls {
1362     my ( $self ) = @_;
1363     return Koha::Recalls->_new_from_dbic( scalar $self->_result->recalls );
1364 }
1365
1366 =head3 can_be_recalled
1367
1368     my @items_for_recall = $biblio->can_be_recalled({ patron => $patron_object });
1369
1370 Does biblio-level checks and returns the items attached to this biblio that are available for recall
1371
1372 =cut
1373
1374 sub can_be_recalled {
1375     my ( $self, $params ) = @_;
1376
1377     return 0 if !( C4::Context->preference('UseRecalls') );
1378
1379     my $patron = $params->{patron};
1380
1381     my $branchcode = C4::Context->userenv->{'branch'};
1382     if ( C4::Context->preference('CircControl') eq 'PatronLibrary' and $patron ) {
1383         $branchcode = $patron->branchcode;
1384     }
1385
1386     my @all_items = Koha::Items->search({ biblionumber => $self->biblionumber })->as_list;
1387
1388     # if there are no available items at all, no recall can be placed
1389     return 0 if ( scalar @all_items == 0 );
1390
1391     my @itemtypes;
1392     my @itemnumbers;
1393     my @items;
1394     my @all_itemnumbers;
1395     foreach my $item ( @all_items ) {
1396         push( @all_itemnumbers, $item->itemnumber );
1397         if ( $item->can_be_recalled({ patron => $patron }) ) {
1398             push( @itemtypes, $item->effective_itemtype );
1399             push( @itemnumbers, $item->itemnumber );
1400             push( @items, $item );
1401         }
1402     }
1403
1404     # if there are no recallable items, no recall can be placed
1405     return 0 if ( scalar @items == 0 );
1406
1407     # Check the circulation rule for each relevant itemtype for this biblio
1408     my ( @recalls_allowed, @recalls_per_record, @on_shelf_recalls );
1409     foreach my $itemtype ( @itemtypes ) {
1410         my $rule = Koha::CirculationRules->get_effective_rules({
1411             branchcode => $branchcode,
1412             categorycode => $patron ? $patron->categorycode : undef,
1413             itemtype => $itemtype,
1414             rules => [
1415                 'recalls_allowed',
1416                 'recalls_per_record',
1417                 'on_shelf_recalls',
1418             ],
1419         });
1420         push( @recalls_allowed, $rule->{recalls_allowed} ) if $rule;
1421         push( @recalls_per_record, $rule->{recalls_per_record} ) if $rule;
1422         push( @on_shelf_recalls, $rule->{on_shelf_recalls} ) if $rule;
1423     }
1424     my $recalls_allowed = (sort {$b <=> $a} @recalls_allowed)[0]; # take highest
1425     my $recalls_per_record = (sort {$b <=> $a} @recalls_per_record)[0]; # take highest
1426     my %on_shelf_recalls_count = ();
1427     foreach my $count ( @on_shelf_recalls ) {
1428         $on_shelf_recalls_count{$count}++;
1429     }
1430     my $on_shelf_recalls = (sort {$on_shelf_recalls_count{$b} <=> $on_shelf_recalls_count{$a}} @on_shelf_recalls)[0]; # take most common
1431
1432     # check recalls allowed has been set and is not zero
1433     return 0 if ( !defined($recalls_allowed) || $recalls_allowed == 0 );
1434
1435     if ( $patron ) {
1436         # check borrower has not reached open recalls allowed limit
1437         return 0 if ( $patron->recalls->filter_by_current->count >= $recalls_allowed );
1438
1439         # check borrower has not reached open recalls allowed per record limit
1440         return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $recalls_per_record );
1441
1442         # check if any of the items under this biblio are already checked out by this borrower
1443         return 0 if ( Koha::Checkouts->search({ itemnumber => [ @all_itemnumbers ], borrowernumber => $patron->borrowernumber })->count > 0 );
1444     }
1445
1446     # check item availability
1447     my $checked_out_count = 0;
1448     foreach (@items) {
1449         if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1450     }
1451
1452     # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1453     return 0 if ( $on_shelf_recalls eq 'all' && $checked_out_count < scalar @items );
1454
1455     # can't recall if no items have been checked out
1456     return 0 if ( $checked_out_count == 0 );
1457
1458     # can recall
1459     return @items;
1460 }
1461
1462 =head2 Internal methods
1463
1464 =head3 type
1465
1466 =cut
1467
1468 sub _type {
1469     return 'Biblio';
1470 }
1471
1472 =head1 AUTHOR
1473
1474 Kyle M Hall <kyle@bywatersolutions.com>
1475
1476 =cut
1477
1478 1;