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