Bug 29486: _koha_marc_update_bib_ids no longer needed for GetMarcBiblio
[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::Biblioitems;
37 use Koha::Checkouts;
38 use Koha::CirculationRules;
39 use Koha::Item::Transfer::Limits;
40 use Koha::Items;
41 use Koha::Libraries;
42 use Koha::Old::Checkouts;
43 use Koha::RecordProcessor;
44 use Koha::Suggestions;
45 use Koha::Subscriptions;
46 use Koha::SearchEngine;
47 use Koha::SearchEngine::Search;
48 use Koha::SearchEngine::QueryBuilder;
49
50 =head1 NAME
51
52 Koha::Biblio - Koha Biblio Object class
53
54 =head1 API
55
56 =head2 Class Methods
57
58 =cut
59
60 =head3 store
61
62 Overloaded I<store> method to set default values
63
64 =cut
65
66 sub store {
67     my ( $self ) = @_;
68
69     $self->datecreated( dt_from_string ) unless $self->datecreated;
70
71     return $self->SUPER::store;
72 }
73
74 =head3 metadata
75
76 my $metadata = $biblio->metadata();
77
78 Returns a Koha::Biblio::Metadata object
79
80 =cut
81
82 sub metadata {
83     my ( $self ) = @_;
84
85     my $metadata = $self->_result->metadata;
86     return Koha::Biblio::Metadata->_new_from_dbic($metadata);
87 }
88
89 =head3 orders
90
91 my $orders = $biblio->orders();
92
93 Returns a Koha::Acquisition::Orders object
94
95 =cut
96
97 sub orders {
98     my ( $self ) = @_;
99
100     my $orders = $self->_result->orders;
101     return Koha::Acquisition::Orders->_new_from_dbic($orders);
102 }
103
104 =head3 active_orders
105
106 my $active_orders = $biblio->active_orders();
107
108 Returns the active acquisition orders related to this biblio.
109 An order is considered active when it is not cancelled (i.e. when datecancellation
110 is not undef).
111
112 =cut
113
114 sub active_orders {
115     my ( $self ) = @_;
116
117     return $self->orders->search({ datecancellationprinted => undef });
118 }
119
120 =head3 can_article_request
121
122 my $bool = $biblio->can_article_request( $borrower );
123
124 Returns true if article requests can be made for this record
125
126 $borrower must be a Koha::Patron object
127
128 =cut
129
130 sub can_article_request {
131     my ( $self, $borrower ) = @_;
132
133     my $rule = $self->article_request_type($borrower);
134     return q{} if $rule eq 'item_only' && !$self->items()->count();
135     return 1 if $rule && $rule ne 'no';
136
137     return q{};
138 }
139
140 =head3 can_be_transferred
141
142 $biblio->can_be_transferred({ to => $to_library, from => $from_library })
143
144 Checks if at least one item of a biblio can be transferred to given library.
145
146 This feature is controlled by two system preferences:
147 UseBranchTransferLimits to enable / disable the feature
148 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
149                          for setting the limitations
150
151 Performance-wise, it is recommended to use this method for a biblio instead of
152 iterating each item of a biblio with Koha::Item->can_be_transferred().
153
154 Takes HASHref that can have the following parameters:
155     MANDATORY PARAMETERS:
156     $to   : Koha::Library
157     OPTIONAL PARAMETERS:
158     $from : Koha::Library # if given, only items from that
159                           # holdingbranch are considered
160
161 Returns 1 if at least one of the item of a biblio can be transferred
162 to $to_library, otherwise 0.
163
164 =cut
165
166 sub can_be_transferred {
167     my ($self, $params) = @_;
168
169     my $to   = $params->{to};
170     my $from = $params->{from};
171
172     return 1 unless C4::Context->preference('UseBranchTransferLimits');
173     my $limittype = C4::Context->preference('BranchTransferLimitsType');
174
175     my $items;
176     foreach my $item_of_bib ($self->items->as_list) {
177         next unless $item_of_bib->holdingbranch;
178         next if $from && $from->branchcode ne $item_of_bib->holdingbranch;
179         return 1 if $item_of_bib->holdingbranch eq $to->branchcode;
180         my $code = $limittype eq 'itemtype'
181             ? $item_of_bib->effective_itemtype
182             : $item_of_bib->ccode;
183         return 1 unless $code;
184         $items->{$code}->{$item_of_bib->holdingbranch} = 1;
185     }
186
187     # At this point we will have a HASHref containing each itemtype/ccode that
188     # this biblio has, inside which are all of the holdingbranches where those
189     # items are located at. Then, we will query Koha::Item::Transfer::Limits to
190     # find out whether a transfer limits for such $limittype from any of the
191     # listed holdingbranches to the given $to library exist. If at least one
192     # holdingbranch for that $limittype does not have a transfer limit to given
193     # $to library, then we know that the transfer is possible.
194     foreach my $code (keys %{$items}) {
195         my @holdingbranches = keys %{$items->{$code}};
196         return 1 if Koha::Item::Transfer::Limits->search({
197             toBranch => $to->branchcode,
198             fromBranch => { 'in' => \@holdingbranches },
199             $limittype => $code
200         }, {
201             group_by => [qw/fromBranch/]
202         })->count == scalar(@holdingbranches) ? 0 : 1;
203     }
204
205     return 0;
206 }
207
208
209 =head3 pickup_locations
210
211     my $pickup_locations = $biblio->pickup_locations( {patron => $patron } );
212
213 Returns a Koha::Libraries set of possible pickup locations for this biblio's items,
214 according to patron's home library (if patron is defined and holds are allowed
215 only from hold groups) and if item can be transferred to each pickup location.
216
217 =cut
218
219 sub pickup_locations {
220     my ( $self, $params ) = @_;
221
222     my $patron = $params->{patron};
223
224     my @pickup_locations;
225     foreach my $item_of_bib ( $self->items->as_list ) {
226         push @pickup_locations,
227           $item_of_bib->pickup_locations( { patron => $patron } )
228           ->_resultset->get_column('branchcode')->all;
229     }
230
231     return Koha::Libraries->search(
232         { branchcode => { '-in' => \@pickup_locations } }, { order_by => ['branchname'] } );
233 }
234
235 =head3 hidden_in_opac
236
237     my $bool = $biblio->hidden_in_opac({ [ rules => $rules ] })
238
239 Returns true if the biblio matches the hidding criteria defined in $rules.
240 Returns false otherwise. It involves the I<OpacHiddenItems> and
241 I<OpacHiddenItemsHidesRecord> system preferences.
242
243 Takes HASHref that can have the following parameters:
244     OPTIONAL PARAMETERS:
245     $rules : { <field> => [ value_1, ... ], ... }
246
247 Note: $rules inherits its structure from the parsed YAML from reading
248 the I<OpacHiddenItems> system preference.
249
250 =cut
251
252 sub hidden_in_opac {
253     my ( $self, $params ) = @_;
254
255     my $rules = $params->{rules} // {};
256
257     my @items = $self->items->as_list;
258
259     return 0 unless @items; # Do not hide if there is no item
260
261     # Ok, there are items, don't even try the rules unless OpacHiddenItemsHidesRecord
262     return 0 unless C4::Context->preference('OpacHiddenItemsHidesRecord');
263
264     return !(any { !$_->hidden_in_opac({ rules => $rules }) } @items);
265 }
266
267 =head3 article_request_type
268
269 my $type = $biblio->article_request_type( $borrower );
270
271 Returns the article request type based on items, or on the record
272 itself if there are no items.
273
274 $borrower must be a Koha::Patron object
275
276 =cut
277
278 sub article_request_type {
279     my ( $self, $borrower ) = @_;
280
281     return q{} unless $borrower;
282
283     my $rule = $self->article_request_type_for_items( $borrower );
284     return $rule if $rule;
285
286     # If the record has no items that are requestable, go by the record itemtype
287     $rule = $self->article_request_type_for_bib($borrower);
288     return $rule if $rule;
289
290     return q{};
291 }
292
293 =head3 article_request_type_for_bib
294
295 my $type = $biblio->article_request_type_for_bib
296
297 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record
298
299 =cut
300
301 sub article_request_type_for_bib {
302     my ( $self, $borrower ) = @_;
303
304     return q{} unless $borrower;
305
306     my $borrowertype = $borrower->categorycode;
307     my $itemtype     = $self->itemtype();
308
309     my $rule = Koha::CirculationRules->get_effective_rule(
310         {
311             rule_name    => 'article_requests',
312             categorycode => $borrowertype,
313             itemtype     => $itemtype,
314         }
315     );
316
317     return q{} unless $rule;
318     return $rule->rule_value || q{}
319 }
320
321 =head3 article_request_type_for_items
322
323 my $type = $biblio->article_request_type_for_items
324
325 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record's items
326
327 If there is a conflict where some items are 'bib_only' and some are 'item_only', 'bib_only' will be returned.
328
329 =cut
330
331 sub article_request_type_for_items {
332     my ( $self, $borrower ) = @_;
333
334     my $counts;
335     foreach my $item ( $self->items()->as_list() ) {
336         my $rule = $item->article_request_type($borrower);
337         return $rule if $rule eq 'bib_only';    # we don't need to go any further
338         $counts->{$rule}++;
339     }
340
341     return 'item_only' if $counts->{item_only};
342     return 'yes'       if $counts->{yes};
343     return 'no'        if $counts->{no};
344     return q{};
345 }
346
347 =head3 article_requests
348
349     my $article_requests = $biblio->article_requests
350
351 Returns the article requests associated with this biblio
352
353 =cut
354
355 sub article_requests {
356     my ( $self ) = @_;
357
358     return Koha::ArticleRequests->_new_from_dbic( scalar $self->_result->article_requests );
359 }
360
361 =head3 current_checkouts
362
363     my $current_checkouts = $biblio->current_checkouts
364
365 Returns the current checkouts associated with this biblio
366
367 =cut
368
369 sub current_checkouts {
370     my ($self) = @_;
371
372     return Koha::Checkouts->search( { "item.biblionumber" => $self->id },
373         { join => 'item' } );
374 }
375
376 =head3 old_checkouts
377
378     my $old_checkouts = $biblio->old_checkouts
379
380 Returns the past checkouts associated with this biblio
381
382 =cut
383
384 sub old_checkouts {
385     my ( $self ) = @_;
386
387     return Koha::Old::Checkouts->search( { "item.biblionumber" => $self->id },
388         { join => 'item' } );
389 }
390
391 =head3 items
392
393 my $items = $biblio->items();
394
395 Returns the related Koha::Items object for this biblio
396
397 =cut
398
399 sub items {
400     my ($self) = @_;
401
402     my $items_rs = $self->_result->items;
403
404     return Koha::Items->_new_from_dbic( $items_rs );
405 }
406
407 =head3 host_items
408
409 my $host_items = $biblio->host_items();
410
411 Return the host items (easy analytical record)
412
413 =cut
414
415 sub host_items {
416     my ($self) = @_;
417
418     return Koha::Items->new->empty
419       unless C4::Context->preference('EasyAnalyticalRecords');
420
421     my $marcflavour = C4::Context->preference("marcflavour");
422     my $analyticfield = '773';
423     if ( $marcflavour eq 'MARC21' ) {
424         $analyticfield = '773';
425     }
426     elsif ( $marcflavour eq 'UNIMARC' ) {
427         $analyticfield = '461';
428     }
429     my $marc_record = $self->metadata->record;
430     my @itemnumbers;
431     foreach my $field ( $marc_record->field($analyticfield) ) {
432         push @itemnumbers, $field->subfield('9');
433     }
434
435     return Koha::Items->search( { itemnumber => { -in => \@itemnumbers } } );
436 }
437
438 =head3 itemtype
439
440 my $itemtype = $biblio->itemtype();
441
442 Returns the itemtype for this record.
443
444 =cut
445
446 sub itemtype {
447     my ( $self ) = @_;
448
449     return $self->biblioitem()->itemtype();
450 }
451
452 =head3 holds
453
454 my $holds = $biblio->holds();
455
456 return the current holds placed on this record
457
458 =cut
459
460 sub holds {
461     my ( $self, $params, $attributes ) = @_;
462     $attributes->{order_by} = 'priority' unless exists $attributes->{order_by};
463     my $hold_rs = $self->_result->reserves->search( $params, $attributes );
464     return Koha::Holds->_new_from_dbic($hold_rs);
465 }
466
467 =head3 current_holds
468
469 my $holds = $biblio->current_holds
470
471 Return the holds placed on this bibliographic record.
472 It does not include future holds.
473
474 =cut
475
476 sub current_holds {
477     my ($self) = @_;
478     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
479     return $self->holds(
480         { reservedate => { '<=' => $dtf->format_date(dt_from_string) } } );
481 }
482
483 =head3 biblioitem
484
485 my $field = $self->biblioitem()->itemtype
486
487 Returns the related Koha::Biblioitem object for this Biblio object
488
489 =cut
490
491 sub biblioitem {
492     my ($self) = @_;
493
494     $self->{_biblioitem} ||= Koha::Biblioitems->find( { biblionumber => $self->biblionumber() } );
495
496     return $self->{_biblioitem};
497 }
498
499 =head3 suggestions
500
501 my $suggestions = $self->suggestions
502
503 Returns the related Koha::Suggestions object for this Biblio object
504
505 =cut
506
507 sub suggestions {
508     my ($self) = @_;
509
510     my $suggestions_rs = $self->_result->suggestions;
511     return Koha::Suggestions->_new_from_dbic( $suggestions_rs );
512 }
513
514 =head3 get_marc_components
515
516   my $components = $self->get_marc_components();
517
518 Returns an array of MARCXML data, which are component parts of
519 this object (MARC21 773$w points to this)
520
521 =cut
522
523 sub get_marc_components {
524     my ($self, $max_results) = @_;
525
526     return [] if (C4::Context->preference('marcflavour') ne 'MARC21');
527
528     my $searchstr = $self->get_components_query;
529
530     my $components;
531     if (defined($searchstr)) {
532         my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
533         my ( $error, $results, $total_hits );
534         eval {
535             ( $error, $results, $total_hits ) = $searcher->simple_search_compat( $searchstr, 0, $max_results );
536         };
537         if( $error || $@ ) {
538             $error //= q{};
539             $error .= $@ if $@;
540             warn "Warning from simple_search_compat: '$error'";
541             $self->add_message(
542                 {
543                     type    => 'error',
544                     message => 'component_search',
545                     payload => $error,
546                 }
547             );
548         }
549         $components = $results if defined($results) && @$results;
550     }
551
552     return $components // [];
553 }
554
555 =head2 get_components_query
556
557 Returns a query which can be used to search for all component parts of MARC21 biblios
558
559 =cut
560
561 sub get_components_query {
562     my ($self) = @_;
563
564     my $builder = Koha::SearchEngine::QueryBuilder->new(
565         { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
566     my $marc = $self->metadata->record;
567
568     my $searchstr;
569     if ( C4::Context->preference('UseControlNumber') ) {
570         my $pf001 = $marc->field('001') || undef;
571
572         if ( defined($pf001) ) {
573             $searchstr = "(";
574             my $pf003 = $marc->field('003') || undef;
575
576             if ( !defined($pf003) ) {
577                 # search for 773$w='Host001'
578                 $searchstr .= "rcn:" . $pf001->data();
579             }
580             else {
581                 $searchstr .= "(";
582                 # search for (773$w='Host001' and 003='Host003') or 773$w='(Host003)Host001'
583                 $searchstr .= "(rcn:" . $pf001->data() . " AND cni:" . $pf003->data() . ")";
584                 $searchstr .= " OR rcn:\"" . $pf003->data() . " " . $pf001->data() . "\"";
585                 $searchstr .= ")";
586             }
587
588             # limit to monograph and serial component part records
589             $searchstr .= " AND (bib-level:a OR bib-level:b)";
590             $searchstr .= ")";
591         }
592     }
593     else {
594         my $cleaned_title = $marc->subfield('245', "a");
595         $cleaned_title =~ tr|/||;
596         $cleaned_title = $builder->clean_search_term($cleaned_title);
597         $searchstr = qq#Host-item:("$cleaned_title")#;
598     }
599
600     return $searchstr;
601 }
602
603 =head3 subscriptions
604
605 my $subscriptions = $self->subscriptions
606
607 Returns the related Koha::Subscriptions object for this Biblio object
608
609 =cut
610
611 sub subscriptions {
612     my ($self) = @_;
613
614     $self->{_subscriptions} ||= Koha::Subscriptions->search( { biblionumber => $self->biblionumber } );
615
616     return $self->{_subscriptions};
617 }
618
619 =head3 has_items_waiting_or_intransit
620
621 my $itemsWaitingOrInTransit = $biblio->has_items_waiting_or_intransit
622
623 Tells if this bibliographic record has items waiting or in transit.
624
625 =cut
626
627 sub has_items_waiting_or_intransit {
628     my ( $self ) = @_;
629
630     if ( Koha::Holds->search({ biblionumber => $self->id,
631                                found => ['W', 'T'] })->count ) {
632         return 1;
633     }
634
635     foreach my $item ( $self->items->as_list ) {
636         return 1 if $item->get_transfer;
637     }
638
639     return 0;
640 }
641
642 =head2 get_coins
643
644 my $coins = $biblio->get_coins;
645
646 Returns the COinS (a span) which can be included in a biblio record
647
648 =cut
649
650 sub get_coins {
651     my ( $self ) = @_;
652
653     my $record = $self->metadata->record;
654
655     my $pos7 = substr $record->leader(), 7, 1;
656     my $pos6 = substr $record->leader(), 6, 1;
657     my $mtx;
658     my $genre;
659     my ( $aulast, $aufirst ) = ( '', '' );
660     my @authors;
661     my $title;
662     my $hosttitle;
663     my $pubyear   = '';
664     my $isbn      = '';
665     my $issn      = '';
666     my $publisher = '';
667     my $pages     = '';
668     my $titletype = '';
669
670     # For the purposes of generating COinS metadata, LDR/06-07 can be
671     # considered the same for UNIMARC and MARC21
672     my $fmts6 = {
673         'a' => 'book',
674         'b' => 'manuscript',
675         'c' => 'book',
676         'd' => 'manuscript',
677         'e' => 'map',
678         'f' => 'map',
679         'g' => 'film',
680         'i' => 'audioRecording',
681         'j' => 'audioRecording',
682         'k' => 'artwork',
683         'l' => 'document',
684         'm' => 'computerProgram',
685         'o' => 'document',
686         'r' => 'document',
687     };
688     my $fmts7 = {
689         'a' => 'journalArticle',
690         's' => 'journal',
691     };
692
693     $genre = $fmts6->{$pos6} ? $fmts6->{$pos6} : 'book';
694
695     if ( $genre eq 'book' ) {
696             $genre = $fmts7->{$pos7} if $fmts7->{$pos7};
697     }
698
699     ##### We must transform mtx to a valable mtx and document type ####
700     if ( $genre eq 'book' ) {
701             $mtx = 'book';
702             $titletype = 'b';
703     } elsif ( $genre eq 'journal' ) {
704             $mtx = 'journal';
705             $titletype = 'j';
706     } elsif ( $genre eq 'journalArticle' ) {
707             $mtx   = 'journal';
708             $genre = 'article';
709             $titletype = 'a';
710     } else {
711             $mtx = 'dc';
712     }
713
714     if ( C4::Context->preference("marcflavour") eq "UNIMARC" ) {
715
716         # Setting datas
717         $aulast  = $record->subfield( '700', 'a' ) || '';
718         $aufirst = $record->subfield( '700', 'b' ) || '';
719         push @authors, "$aufirst $aulast" if ($aufirst or $aulast);
720
721         # others authors
722         if ( $record->field('200') ) {
723             for my $au ( $record->field('200')->subfield('g') ) {
724                 push @authors, $au;
725             }
726         }
727
728         $title     = $record->subfield( '200', 'a' );
729         my $subfield_210d = $record->subfield('210', 'd');
730         if ($subfield_210d and $subfield_210d =~ /(\d{4})/) {
731             $pubyear = $1;
732         }
733         $publisher = $record->subfield( '210', 'c' ) || '';
734         $isbn      = $record->subfield( '010', 'a' ) || '';
735         $issn      = $record->subfield( '011', 'a' ) || '';
736     } else {
737
738         # MARC21 need some improve
739
740         # Setting datas
741         if ( $record->field('100') ) {
742             push @authors, $record->subfield( '100', 'a' );
743         }
744
745         # others authors
746         if ( $record->field('700') ) {
747             for my $au ( $record->field('700')->subfield('a') ) {
748                 push @authors, $au;
749             }
750         }
751         $title = $record->field('245');
752         $title &&= $title->as_string('ab');
753         if ($titletype eq 'a') {
754             $pubyear   = $record->field('008') || '';
755             $pubyear   = substr($pubyear->data(), 7, 4) if $pubyear;
756             $isbn      = $record->subfield( '773', 'z' ) || '';
757             $issn      = $record->subfield( '773', 'x' ) || '';
758             $hosttitle = $record->subfield( '773', 't' ) || $record->subfield( '773', 'a') || q{};
759             my @rels = $record->subfield( '773', 'g' );
760             $pages = join(', ', @rels);
761         } else {
762             $pubyear   = $record->subfield( '260', 'c' ) || '';
763             $publisher = $record->subfield( '260', 'b' ) || '';
764             $isbn      = $record->subfield( '020', 'a' ) || '';
765             $issn      = $record->subfield( '022', 'a' ) || '';
766         }
767
768     }
769
770     my @params = (
771         [ 'ctx_ver', 'Z39.88-2004' ],
772         [ 'rft_val_fmt', "info:ofi/fmt:kev:mtx:$mtx" ],
773         [ ($mtx eq 'dc' ? 'rft.type' : 'rft.genre'), $genre ],
774         [ "rft.${titletype}title", $title ],
775     );
776
777     # rft.title is authorized only once, so by checking $titletype
778     # we ensure that rft.title is not already in the list.
779     if ($hosttitle and $titletype) {
780         push @params, [ 'rft.title', $hosttitle ];
781     }
782
783     push @params, (
784         [ 'rft.isbn', $isbn ],
785         [ 'rft.issn', $issn ],
786     );
787
788     # If it's a subscription, these informations have no meaning.
789     if ($genre ne 'journal') {
790         push @params, (
791             [ 'rft.aulast', $aulast ],
792             [ 'rft.aufirst', $aufirst ],
793             (map { [ 'rft.au', $_ ] } @authors),
794             [ 'rft.pub', $publisher ],
795             [ 'rft.date', $pubyear ],
796             [ 'rft.pages', $pages ],
797         );
798     }
799
800     my $coins_value = join( '&amp;',
801         map { $$_[1] ? $$_[0] . '=' . uri_escape_utf8( $$_[1] ) : () } @params );
802
803     return $coins_value;
804 }
805
806 =head2 get_openurl
807
808 my $url = $biblio->get_openurl;
809
810 Returns url for OpenURL resolver set in OpenURLResolverURL system preference
811
812 =cut
813
814 sub get_openurl {
815     my ( $self ) = @_;
816
817     my $OpenURLResolverURL = C4::Context->preference('OpenURLResolverURL');
818
819     if ($OpenURLResolverURL) {
820         my $uri = URI->new($OpenURLResolverURL);
821
822         if (not defined $uri->query) {
823             $OpenURLResolverURL .= '?';
824         } else {
825             $OpenURLResolverURL .= '&amp;';
826         }
827         $OpenURLResolverURL .= $self->get_coins;
828     }
829
830     return $OpenURLResolverURL;
831 }
832
833 =head3 is_serial
834
835 my $serial = $biblio->is_serial
836
837 Return boolean true if this bibbliographic record is continuing resource
838
839 =cut
840
841 sub is_serial {
842     my ( $self ) = @_;
843
844     return 1 if $self->serial;
845
846     my $record = $self->metadata->record;
847     return 1 if substr($record->leader, 7, 1) eq 's';
848
849     return 0;
850 }
851
852 =head3 custom_cover_image_url
853
854 my $image_url = $biblio->custom_cover_image_url
855
856 Return the specific url of the cover image for this bibliographic record.
857 It is built regaring the value of the system preference CustomCoverImagesURL
858
859 =cut
860
861 sub custom_cover_image_url {
862     my ( $self ) = @_;
863     my $url = C4::Context->preference('CustomCoverImagesURL');
864     if ( $url =~ m|{isbn}| ) {
865         my $isbn = $self->biblioitem->isbn;
866         return unless $isbn;
867         $url =~ s|{isbn}|$isbn|g;
868     }
869     if ( $url =~ m|{normalized_isbn}| ) {
870         my $normalized_isbn = C4::Koha::GetNormalizedISBN($self->biblioitem->isbn);
871         return unless $normalized_isbn;
872         $url =~ s|{normalized_isbn}|$normalized_isbn|g;
873     }
874     if ( $url =~ m|{issn}| ) {
875         my $issn = $self->biblioitem->issn;
876         return unless $issn;
877         $url =~ s|{issn}|$issn|g;
878     }
879
880     my $re = qr|{(?<field>\d{3})(\$(?<subfield>.))?}|;
881     if ( $url =~ $re ) {
882         my $field = $+{field};
883         my $subfield = $+{subfield};
884         my $marc_record = $self->metadata->record;
885         my $value;
886         if ( $subfield ) {
887             $value = $marc_record->subfield( $field, $subfield );
888         } else {
889             my $controlfield = $marc_record->field($field);
890             $value = $controlfield->data() if $controlfield;
891         }
892         return unless $value;
893         $url =~ s|$re|$value|;
894     }
895
896     return $url;
897 }
898
899 =head3 cover_images
900
901 Return the cover images associated with this biblio.
902
903 =cut
904
905 sub cover_images {
906     my ( $self ) = @_;
907
908     my $cover_images_rs = $self->_result->cover_images;
909     return unless $cover_images_rs;
910     return Koha::CoverImages->_new_from_dbic($cover_images_rs);
911 }
912
913 =head3 get_marc_notes
914
915     $marcnotesarray = $biblio->get_marc_notes({ marcflavour => $marcflavour });
916
917 Get all notes from the MARC record and returns them in an array.
918 The notes are stored in different fields depending on MARC flavour.
919 MARC21 5XX $u subfields receive special attention as they are URIs.
920
921 =cut
922
923 sub get_marc_notes {
924     my ( $self, $params ) = @_;
925
926     my $marcflavour = $params->{marcflavour};
927     my $opac = $params->{opac} // '0';
928     my $interface = $params->{opac} ? 'opac' : 'intranet';
929
930     my $record = $params->{record} // $self->metadata->record;
931     my $record_processor = Koha::RecordProcessor->new(
932         {
933             filters => [ 'ViewPolicy', 'ExpandCodedFields' ],
934             options => {
935                 interface     => $interface,
936                 frameworkcode => $self->frameworkcode
937             }
938         }
939     );
940     $record_processor->process($record);
941
942     my $scope = $marcflavour eq "UNIMARC"? '3..': '5..';
943     #MARC21 specs indicate some notes should be private if first indicator 0
944     my %maybe_private = (
945         541 => 1,
946         542 => 1,
947         561 => 1,
948         583 => 1,
949         590 => 1
950     );
951
952     my %hiddenlist = map { $_ => 1 }
953         split( /,/, C4::Context->preference('NotesToHide'));
954
955     my @marcnotes;
956     foreach my $field ( $record->field($scope) ) {
957         my $tag = $field->tag();
958         next if $hiddenlist{ $tag };
959         next if $opac && $maybe_private{$tag} && !$field->indicator(1);
960         if( $marcflavour ne 'UNIMARC' && $field->subfield('u') ) {
961             # Field 5XX$u always contains URI
962             # Examples: 505u, 506u, 510u, 514u, 520u, 530u, 538u, 540u, 542u, 552u, 555u, 561u, 563u, 583u
963             # We first push the other subfields, then all $u's separately
964             # Leave further actions to the template (see e.g. opac-detail)
965             my $othersub =
966                 join '', ( 'a' .. 't', 'v' .. 'z', '0' .. '9' ); # excl 'u'
967             push @marcnotes, { marcnote => $field->as_string($othersub) };
968             foreach my $sub ( $field->subfield('u') ) {
969                 $sub =~ s/^\s+|\s+$//g; # trim
970                 push @marcnotes, { marcnote => $sub };
971             }
972         } else {
973             push @marcnotes, { marcnote => $field->as_string() };
974         }
975     }
976     return \@marcnotes;
977 }
978
979 =head3 to_api
980
981     my $json = $biblio->to_api;
982
983 Overloaded method that returns a JSON representation of the Koha::Biblio object,
984 suitable for API output. The related Koha::Biblioitem object is merged as expected
985 on the API.
986
987 =cut
988
989 sub to_api {
990     my ($self, $args) = @_;
991
992     my $response = $self->SUPER::to_api( $args );
993     my $biblioitem = $self->biblioitem->to_api;
994
995     return { %$response, %$biblioitem };
996 }
997
998 =head3 to_api_mapping
999
1000 This method returns the mapping for representing a Koha::Biblio object
1001 on the API.
1002
1003 =cut
1004
1005 sub to_api_mapping {
1006     return {
1007         biblionumber     => 'biblio_id',
1008         frameworkcode    => 'framework_id',
1009         unititle         => 'uniform_title',
1010         seriestitle      => 'series_title',
1011         copyrightdate    => 'copyright_date',
1012         datecreated      => 'creation_date'
1013     };
1014 }
1015
1016 =head3 get_marc_host
1017
1018     $host = $biblio->get_marc_host;
1019     # OR:
1020     ( $host, $relatedparts ) = $biblio->get_marc_host;
1021
1022     Returns host biblio record from MARC21 773 (undef if no 773 present).
1023     It looks at the first 773 field with MARCorgCode or only a control
1024     number. Complete $w or numeric part is used to search host record.
1025     The optional parameter no_items triggers a check if $biblio has items.
1026     If there are, the sub returns undef.
1027     Called in list context, it also returns 773$g (related parts).
1028
1029 =cut
1030
1031 sub get_marc_host {
1032     my ($self, $params) = @_;
1033     my $no_items = $params->{no_items};
1034     return if C4::Context->preference('marcflavour') eq 'UNIMARC'; # TODO
1035     return if $params->{no_items} && $self->items->count > 0;
1036
1037     my $record;
1038     eval { $record = $self->metadata->record };
1039     return if !$record;
1040
1041     # We pick the first $w with your MARCOrgCode or the first $w that has no
1042     # code (between parentheses) at all.
1043     my $orgcode = C4::Context->preference('MARCOrgCode') // q{};
1044     my $hostfld;
1045     foreach my $f ( $record->field('773') ) {
1046         my $w = $f->subfield('w') or next;
1047         if( $w =~ /^\($orgcode\)\s*(\d+)/i or $w =~ /^\d+/ ) {
1048             $hostfld = $f;
1049             last;
1050         }
1051     }
1052     return if !$hostfld;
1053     my $rcn = $hostfld->subfield('w');
1054
1055     # Look for control number with/without orgcode
1056     my $engine = Koha::SearchEngine::Search->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1057     my $bibno;
1058     for my $try (1..2) {
1059         my ( $error, $results, $total_hits ) = $engine->simple_search_compat( 'Control-number='.$rcn, 0,1 );
1060         if( !$error and $total_hits == 1 ) {
1061             $bibno = $engine->extract_biblionumber( $results->[0] );
1062             last;
1063         }
1064         # Add or remove orgcode for second try
1065         if( $try == 1 && $rcn =~ /\)\s*(\d+)/ ) {
1066             $rcn = $1; # number only
1067         } elsif( $try == 1 && $rcn =~ /^\d+/ ) {
1068             $rcn = "($orgcode)$rcn";
1069         } else {
1070             last;
1071         }
1072     }
1073     if( $bibno ) {
1074         my $host = Koha::Biblios->find($bibno) or return;
1075         return wantarray ? ( $host, $hostfld->subfield('g') ) : $host;
1076     }
1077 }
1078
1079 =head2 Internal methods
1080
1081 =head3 type
1082
1083 =cut
1084
1085 sub _type {
1086     return 'Biblio';
1087 }
1088
1089 =head1 AUTHOR
1090
1091 Kyle M Hall <kyle@bywatersolutions.com>
1092
1093 =cut
1094
1095 1;