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