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