Bug 11175: (follow-up) Return empty array if no components
[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
514         $self->{_components} = $results if ( defined($results) && scalar(@$results) );
515     } else {
516         warn "Record $self->id has no 001";
517     }
518
519     return $self->{_components} || ();
520 }
521
522 =head3 subscriptions
523
524 my $subscriptions = $self->subscriptions
525
526 Returns the related Koha::Subscriptions object for this Biblio object
527
528 =cut
529
530 sub subscriptions {
531     my ($self) = @_;
532
533     $self->{_subscriptions} ||= Koha::Subscriptions->search( { biblionumber => $self->biblionumber } );
534
535     return $self->{_subscriptions};
536 }
537
538 =head3 has_items_waiting_or_intransit
539
540 my $itemsWaitingOrInTransit = $biblio->has_items_waiting_or_intransit
541
542 Tells if this bibliographic record has items waiting or in transit.
543
544 =cut
545
546 sub has_items_waiting_or_intransit {
547     my ( $self ) = @_;
548
549     if ( Koha::Holds->search({ biblionumber => $self->id,
550                                found => ['W', 'T'] })->count ) {
551         return 1;
552     }
553
554     foreach my $item ( $self->items->as_list ) {
555         return 1 if $item->get_transfer;
556     }
557
558     return 0;
559 }
560
561 =head2 get_coins
562
563 my $coins = $biblio->get_coins;
564
565 Returns the COinS (a span) which can be included in a biblio record
566
567 =cut
568
569 sub get_coins {
570     my ( $self ) = @_;
571
572     my $record = $self->metadata->record;
573
574     my $pos7 = substr $record->leader(), 7, 1;
575     my $pos6 = substr $record->leader(), 6, 1;
576     my $mtx;
577     my $genre;
578     my ( $aulast, $aufirst ) = ( '', '' );
579     my @authors;
580     my $title;
581     my $hosttitle;
582     my $pubyear   = '';
583     my $isbn      = '';
584     my $issn      = '';
585     my $publisher = '';
586     my $pages     = '';
587     my $titletype = '';
588
589     # For the purposes of generating COinS metadata, LDR/06-07 can be
590     # considered the same for UNIMARC and MARC21
591     my $fmts6 = {
592         'a' => 'book',
593         'b' => 'manuscript',
594         'c' => 'book',
595         'd' => 'manuscript',
596         'e' => 'map',
597         'f' => 'map',
598         'g' => 'film',
599         'i' => 'audioRecording',
600         'j' => 'audioRecording',
601         'k' => 'artwork',
602         'l' => 'document',
603         'm' => 'computerProgram',
604         'o' => 'document',
605         'r' => 'document',
606     };
607     my $fmts7 = {
608         'a' => 'journalArticle',
609         's' => 'journal',
610     };
611
612     $genre = $fmts6->{$pos6} ? $fmts6->{$pos6} : 'book';
613
614     if ( $genre eq 'book' ) {
615             $genre = $fmts7->{$pos7} if $fmts7->{$pos7};
616     }
617
618     ##### We must transform mtx to a valable mtx and document type ####
619     if ( $genre eq 'book' ) {
620             $mtx = 'book';
621             $titletype = 'b';
622     } elsif ( $genre eq 'journal' ) {
623             $mtx = 'journal';
624             $titletype = 'j';
625     } elsif ( $genre eq 'journalArticle' ) {
626             $mtx   = 'journal';
627             $genre = 'article';
628             $titletype = 'a';
629     } else {
630             $mtx = 'dc';
631     }
632
633     if ( C4::Context->preference("marcflavour") eq "UNIMARC" ) {
634
635         # Setting datas
636         $aulast  = $record->subfield( '700', 'a' ) || '';
637         $aufirst = $record->subfield( '700', 'b' ) || '';
638         push @authors, "$aufirst $aulast" if ($aufirst or $aulast);
639
640         # others authors
641         if ( $record->field('200') ) {
642             for my $au ( $record->field('200')->subfield('g') ) {
643                 push @authors, $au;
644             }
645         }
646
647         $title     = $record->subfield( '200', 'a' );
648         my $subfield_210d = $record->subfield('210', 'd');
649         if ($subfield_210d and $subfield_210d =~ /(\d{4})/) {
650             $pubyear = $1;
651         }
652         $publisher = $record->subfield( '210', 'c' ) || '';
653         $isbn      = $record->subfield( '010', 'a' ) || '';
654         $issn      = $record->subfield( '011', 'a' ) || '';
655     } else {
656
657         # MARC21 need some improve
658
659         # Setting datas
660         if ( $record->field('100') ) {
661             push @authors, $record->subfield( '100', 'a' );
662         }
663
664         # others authors
665         if ( $record->field('700') ) {
666             for my $au ( $record->field('700')->subfield('a') ) {
667                 push @authors, $au;
668             }
669         }
670         $title = $record->field('245');
671         $title &&= $title->as_string('ab');
672         if ($titletype eq 'a') {
673             $pubyear   = $record->field('008') || '';
674             $pubyear   = substr($pubyear->data(), 7, 4) if $pubyear;
675             $isbn      = $record->subfield( '773', 'z' ) || '';
676             $issn      = $record->subfield( '773', 'x' ) || '';
677             $hosttitle = $record->subfield( '773', 't' ) || $record->subfield( '773', 'a') || q{};
678             my @rels = $record->subfield( '773', 'g' );
679             $pages = join(', ', @rels);
680         } else {
681             $pubyear   = $record->subfield( '260', 'c' ) || '';
682             $publisher = $record->subfield( '260', 'b' ) || '';
683             $isbn      = $record->subfield( '020', 'a' ) || '';
684             $issn      = $record->subfield( '022', 'a' ) || '';
685         }
686
687     }
688
689     my @params = (
690         [ 'ctx_ver', 'Z39.88-2004' ],
691         [ 'rft_val_fmt', "info:ofi/fmt:kev:mtx:$mtx" ],
692         [ ($mtx eq 'dc' ? 'rft.type' : 'rft.genre'), $genre ],
693         [ "rft.${titletype}title", $title ],
694     );
695
696     # rft.title is authorized only once, so by checking $titletype
697     # we ensure that rft.title is not already in the list.
698     if ($hosttitle and $titletype) {
699         push @params, [ 'rft.title', $hosttitle ];
700     }
701
702     push @params, (
703         [ 'rft.isbn', $isbn ],
704         [ 'rft.issn', $issn ],
705     );
706
707     # If it's a subscription, these informations have no meaning.
708     if ($genre ne 'journal') {
709         push @params, (
710             [ 'rft.aulast', $aulast ],
711             [ 'rft.aufirst', $aufirst ],
712             (map { [ 'rft.au', $_ ] } @authors),
713             [ 'rft.pub', $publisher ],
714             [ 'rft.date', $pubyear ],
715             [ 'rft.pages', $pages ],
716         );
717     }
718
719     my $coins_value = join( '&amp;',
720         map { $$_[1] ? $$_[0] . '=' . uri_escape_utf8( $$_[1] ) : () } @params );
721
722     return $coins_value;
723 }
724
725 =head2 get_openurl
726
727 my $url = $biblio->get_openurl;
728
729 Returns url for OpenURL resolver set in OpenURLResolverURL system preference
730
731 =cut
732
733 sub get_openurl {
734     my ( $self ) = @_;
735
736     my $OpenURLResolverURL = C4::Context->preference('OpenURLResolverURL');
737
738     if ($OpenURLResolverURL) {
739         my $uri = URI->new($OpenURLResolverURL);
740
741         if (not defined $uri->query) {
742             $OpenURLResolverURL .= '?';
743         } else {
744             $OpenURLResolverURL .= '&amp;';
745         }
746         $OpenURLResolverURL .= $self->get_coins;
747     }
748
749     return $OpenURLResolverURL;
750 }
751
752 =head3 is_serial
753
754 my $serial = $biblio->is_serial
755
756 Return boolean true if this bibbliographic record is continuing resource
757
758 =cut
759
760 sub is_serial {
761     my ( $self ) = @_;
762
763     return 1 if $self->serial;
764
765     my $record = $self->metadata->record;
766     return 1 if substr($record->leader, 7, 1) eq 's';
767
768     return 0;
769 }
770
771 =head3 custom_cover_image_url
772
773 my $image_url = $biblio->custom_cover_image_url
774
775 Return the specific url of the cover image for this bibliographic record.
776 It is built regaring the value of the system preference CustomCoverImagesURL
777
778 =cut
779
780 sub custom_cover_image_url {
781     my ( $self ) = @_;
782     my $url = C4::Context->preference('CustomCoverImagesURL');
783     if ( $url =~ m|{isbn}| ) {
784         my $isbn = $self->biblioitem->isbn;
785         return unless $isbn;
786         $url =~ s|{isbn}|$isbn|g;
787     }
788     if ( $url =~ m|{normalized_isbn}| ) {
789         my $normalized_isbn = C4::Koha::GetNormalizedISBN($self->biblioitem->isbn);
790         return unless $normalized_isbn;
791         $url =~ s|{normalized_isbn}|$normalized_isbn|g;
792     }
793     if ( $url =~ m|{issn}| ) {
794         my $issn = $self->biblioitem->issn;
795         return unless $issn;
796         $url =~ s|{issn}|$issn|g;
797     }
798
799     my $re = qr|{(?<field>\d{3})(\$(?<subfield>.))?}|;
800     if ( $url =~ $re ) {
801         my $field = $+{field};
802         my $subfield = $+{subfield};
803         my $marc_record = $self->metadata->record;
804         my $value;
805         if ( $subfield ) {
806             $value = $marc_record->subfield( $field, $subfield );
807         } else {
808             my $controlfield = $marc_record->field($field);
809             $value = $controlfield->data() if $controlfield;
810         }
811         return unless $value;
812         $url =~ s|$re|$value|;
813     }
814
815     return $url;
816 }
817
818 =head3 cover_images
819
820 Return the cover images associated with this biblio.
821
822 =cut
823
824 sub cover_images {
825     my ( $self ) = @_;
826
827     my $cover_images_rs = $self->_result->cover_images;
828     return unless $cover_images_rs;
829     return Koha::CoverImages->_new_from_dbic($cover_images_rs);
830 }
831
832 =head3 get_marc_notes
833
834     $marcnotesarray = $biblio->get_marc_notes({ marcflavour => $marcflavour });
835
836 Get all notes from the MARC record and returns them in an array.
837 The notes are stored in different fields depending on MARC flavour.
838 MARC21 5XX $u subfields receive special attention as they are URIs.
839
840 =cut
841
842 sub get_marc_notes {
843     my ( $self, $params ) = @_;
844
845     my $marcflavour = $params->{marcflavour};
846     my $opac = $params->{opac};
847
848     my $scope = $marcflavour eq "UNIMARC"? '3..': '5..';
849     my @marcnotes;
850
851     #MARC21 specs indicate some notes should be private if first indicator 0
852     my %maybe_private = (
853         541 => 1,
854         542 => 1,
855         561 => 1,
856         583 => 1,
857         590 => 1
858     );
859
860     my %hiddenlist = map { $_ => 1 }
861         split( /,/, C4::Context->preference('NotesToHide'));
862     my $record = $self->metadata->record;
863     $record = transformMARCXML4XSLT( $self->biblionumber, $record, $opac );
864
865     foreach my $field ( $record->field($scope) ) {
866         my $tag = $field->tag();
867         next if $hiddenlist{ $tag };
868         next if $opac && $maybe_private{$tag} && !$field->indicator(1);
869         if( $marcflavour ne 'UNIMARC' && $field->subfield('u') ) {
870             # Field 5XX$u always contains URI
871             # Examples: 505u, 506u, 510u, 514u, 520u, 530u, 538u, 540u, 542u, 552u, 555u, 561u, 563u, 583u
872             # We first push the other subfields, then all $u's separately
873             # Leave further actions to the template (see e.g. opac-detail)
874             my $othersub =
875                 join '', ( 'a' .. 't', 'v' .. 'z', '0' .. '9' ); # excl 'u'
876             push @marcnotes, { marcnote => $field->as_string($othersub) };
877             foreach my $sub ( $field->subfield('u') ) {
878                 $sub =~ s/^\s+|\s+$//g; # trim
879                 push @marcnotes, { marcnote => $sub };
880             }
881         } else {
882             push @marcnotes, { marcnote => $field->as_string() };
883         }
884     }
885     return \@marcnotes;
886 }
887
888 =head3 to_api
889
890     my $json = $biblio->to_api;
891
892 Overloaded method that returns a JSON representation of the Koha::Biblio object,
893 suitable for API output. The related Koha::Biblioitem object is merged as expected
894 on the API.
895
896 =cut
897
898 sub to_api {
899     my ($self, $args) = @_;
900
901     my $response = $self->SUPER::to_api( $args );
902     my $biblioitem = $self->biblioitem->to_api;
903
904     return { %$response, %$biblioitem };
905 }
906
907 =head3 to_api_mapping
908
909 This method returns the mapping for representing a Koha::Biblio object
910 on the API.
911
912 =cut
913
914 sub to_api_mapping {
915     return {
916         biblionumber     => 'biblio_id',
917         frameworkcode    => 'framework_id',
918         unititle         => 'uniform_title',
919         seriestitle      => 'series_title',
920         copyrightdate    => 'copyright_date',
921         datecreated      => 'creation_date'
922     };
923 }
924
925 =head3 get_marc_host
926
927     $host = $biblio->get_marc_host;
928     # OR:
929     ( $host, $relatedparts ) = $biblio->get_marc_host;
930
931     Returns host biblio record from MARC21 773 (undef if no 773 present).
932     It looks at the first 773 field with MARCorgCode or only a control
933     number. Complete $w or numeric part is used to search host record.
934     The optional parameter no_items triggers a check if $biblio has items.
935     If there are, the sub returns undef.
936     Called in list context, it also returns 773$g (related parts).
937
938 =cut
939
940 sub get_marc_host {
941     my ($self, $params) = @_;
942     my $no_items = $params->{no_items};
943     return if C4::Context->preference('marcflavour') eq 'UNIMARC'; # TODO
944     return if $params->{no_items} && $self->items->count > 0;
945
946     my $record;
947     eval { $record = $self->metadata->record };
948     return if !$record;
949
950     # We pick the first $w with your MARCOrgCode or the first $w that has no
951     # code (between parentheses) at all.
952     my $orgcode = C4::Context->preference('MARCOrgCode') // q{};
953     my $hostfld;
954     foreach my $f ( $record->field('773') ) {
955         my $w = $f->subfield('w') or next;
956         if( $w =~ /^\($orgcode\)\s*(\d+)/i or $w =~ /^\d+/ ) {
957             $hostfld = $f;
958             last;
959         }
960     }
961     return if !$hostfld;
962     my $rcn = $hostfld->subfield('w');
963
964     # Look for control number with/without orgcode
965     my $engine = Koha::SearchEngine::Search->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
966     my $bibno;
967     for my $try (1..2) {
968         my ( $error, $results, $total_hits ) = $engine->simple_search_compat( 'Control-number='.$rcn, 0,1 );
969         if( !$error and $total_hits == 1 ) {
970             $bibno = $engine->extract_biblionumber( $results->[0] );
971             last;
972         }
973         # Add or remove orgcode for second try
974         if( $try == 1 && $rcn =~ /\)\s*(\d+)/ ) {
975             $rcn = $1; # number only
976         } elsif( $try == 1 && $rcn =~ /^\d+/ ) {
977             $rcn = "($orgcode)$rcn";
978         } else {
979             last;
980         }
981     }
982     if( $bibno ) {
983         my $host = Koha::Biblios->find($bibno) or return;
984         return wantarray ? ( $host, $hostfld->subfield('g') ) : $host;
985     }
986 }
987
988 =head2 Internal methods
989
990 =head3 type
991
992 =cut
993
994 sub _type {
995     return 'Biblio';
996 }
997
998 =head1 AUTHOR
999
1000 Kyle M Hall <kyle@bywatersolutions.com>
1001
1002 =cut
1003
1004 1;