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