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