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