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