Bug 29002: Fix permission required to access bookings list
[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 GetNormalizedUPC GetNormalizedOCLCNumber );
27
28 use Koha::Database;
29 use Koha::DateUtils qw( dt_from_string );
30
31 use base qw(Koha::Object);
32
33 use Koha::Acquisition::Orders;
34 use Koha::ArticleRequests;
35 use Koha::Biblio::Metadatas;
36 use Koha::Biblio::Metadata::Extractor;
37 use Koha::Biblio::ItemGroups;
38 use Koha::Biblioitems;
39 use Koha::Cache::Memory::Lite;
40 use Koha::Bookings;
41 use Koha::Checkouts;
42 use Koha::CirculationRules;
43 use Koha::Exceptions;
44 use Koha::Illrequests;
45 use Koha::Item::Transfer::Limits;
46 use Koha::Items;
47 use Koha::Libraries;
48 use Koha::Old::Checkouts;
49 use Koha::Ratings;
50 use Koha::Recalls;
51 use Koha::RecordProcessor;
52 use Koha::Suggestions;
53 use Koha::Subscriptions;
54 use Koha::SearchEngine;
55 use Koha::SearchEngine::Search;
56 use Koha::SearchEngine::QueryBuilder;
57 use Koha::Tickets;
58
59 =head1 NAME
60
61 Koha::Biblio - Koha Biblio Object class
62
63 =head1 API
64
65 =head2 Class Methods
66
67 =cut
68
69 =head3 store
70
71 Overloaded I<store> method to set default values
72
73 =cut
74
75 sub store {
76     my ( $self ) = @_;
77
78     $self->datecreated( dt_from_string ) unless $self->datecreated;
79
80     return $self->SUPER::store;
81 }
82
83 =head3 metadata
84
85 my $metadata = $biblio->metadata();
86
87 Returns a Koha::Biblio::Metadata object
88
89 =cut
90
91 sub metadata {
92     my ( $self ) = @_;
93
94     my $metadata = $self->_result->metadata;
95     return Koha::Biblio::Metadata->_new_from_dbic($metadata);
96 }
97
98 =head3 record
99
100 my $record = $biblio->record();
101
102 Returns a Marc::Record object
103
104 =cut
105
106 sub record {
107     my ( $self ) = @_;
108
109     return $self->metadata->record;
110 }
111
112 =head3 record_schema
113
114 my $schema = $biblio->record_schema();
115
116 Returns the record schema (MARC21, USMARC or UNIMARC).
117
118 =cut
119
120 sub record_schema {
121     my ( $self ) = @_;
122
123     return $self->metadata->schema // C4::Context->preference("marcflavour");
124 }
125
126 =head3 orders
127
128 my $orders = $biblio->orders();
129
130 Returns a Koha::Acquisition::Orders object
131
132 =cut
133
134 sub orders {
135     my ( $self ) = @_;
136
137     my $orders = $self->_result->orders;
138     return Koha::Acquisition::Orders->_new_from_dbic($orders);
139 }
140
141 =head3 active_orders
142
143 my $active_orders = $biblio->active_orders();
144
145 Returns the active acquisition orders related to this biblio.
146 An order is considered active when it is not cancelled (i.e. when datecancellation
147 is not undef).
148
149 =cut
150
151 sub active_orders {
152     my ( $self ) = @_;
153
154     return $self->orders->search({ datecancellationprinted => undef });
155 }
156
157 =head3 tickets
158
159   my $tickets = $biblio->tickets();
160
161 Returns all tickets linked to the biblio
162
163 =cut
164
165 sub tickets {
166     my ( $self ) = @_;
167     my $rs = $self->_result->tickets;
168     return Koha::Tickets->_new_from_dbic( $rs );
169 }
170
171 =head3 ill_requests
172
173     my $ill_requests = $biblio->ill_requests();
174
175 Returns a Koha::Illrequests object
176
177 =cut
178
179 sub ill_requests {
180     my ( $self ) = @_;
181
182     my $ill_requests = $self->_result->ill_requests;
183     return Koha::Illrequests->_new_from_dbic($ill_requests);
184 }
185
186 =head3 item_groups
187
188 my $item_groups = $biblio->item_groups();
189
190 Returns a Koha::Biblio::ItemGroups object
191
192 =cut
193
194 sub item_groups {
195     my ( $self ) = @_;
196
197     my $item_groups = $self->_result->item_groups;
198     return Koha::Biblio::ItemGroups->_new_from_dbic($item_groups);
199 }
200
201 =head3 can_article_request
202
203 my $bool = $biblio->can_article_request( $borrower );
204
205 Returns true if article requests can be made for this record
206
207 $borrower must be a Koha::Patron object
208
209 =cut
210
211 sub can_article_request {
212     my ( $self, $borrower ) = @_;
213
214     my $rule = $self->article_request_type($borrower);
215     return q{} if $rule eq 'item_only' && !$self->items()->count();
216     return 1 if $rule && $rule ne 'no';
217
218     return q{};
219 }
220
221
222 =head3 check_booking
223
224   my $bookable =
225     $biblio->check_booking( { start_date => $datetime, end_date => $datetime, [ booking_id => $booking_id ] } );
226
227 Returns a boolean denoting whether the passed booking can be made without clashing.
228
229 Optionally, you may pass a booking id to exclude from the checks; This is helpful when you are updating an existing booking.
230
231 =cut
232
233 sub check_booking {
234     my ( $self, $params ) = @_;
235
236     my $start_date = dt_from_string( $params->{start_date} );
237     my $end_date   = dt_from_string( $params->{end_date} );
238     my $booking_id = $params->{booking_id};
239
240     my $bookable_items = $self->bookable_items;
241     my $total_bookable = $bookable_items->count;
242
243     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
244     my $existing_bookings = $self->bookings(
245         [
246             start_date => {
247                 '-between' => [
248                     $dtf->format_datetime($start_date),
249                     $dtf->format_datetime($end_date)
250                 ]
251             },
252             end_date => {
253                 '-between' => [
254                     $dtf->format_datetime($start_date),
255                     $dtf->format_datetime($end_date)
256                 ]
257             },
258             {
259                 start_date => { '<' => $dtf->format_datetime($start_date) },
260                 end_date   => { '>' => $dtf->format_datetime($end_date) }
261             }
262         ]
263     );
264
265     my $booked_count =
266       defined($booking_id)
267       ? $existing_bookings->search( { booking_id => { '!=' => $booking_id } } )
268       ->count
269       : $existing_bookings->count;
270
271     my $checkouts = $self->current_checkouts->search( { date_due => { '>=' => $dtf->format_datetime($start_date) } } );
272     $booked_count += $checkouts->count;
273
274     return ( ( $total_bookable - $booked_count ) > 0 ) ? 1 : 0;
275 }
276
277 =head3 assign_item_for_booking
278
279 =cut
280
281 sub assign_item_for_booking {
282     my ( $self, $params ) = @_;
283
284     my $start_date = dt_from_string( $params->{start_date} );
285     my $end_date   = dt_from_string( $params->{end_date} );
286
287     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
288     my $existing_bookings = $self->bookings(
289         [
290             start_date => {
291                 '-between' => [
292                     $dtf->format_datetime($start_date),
293                     $dtf->format_datetime($end_date)
294                 ]
295             },
296             end_date => {
297                 '-between' => [
298                     $dtf->format_datetime($start_date),
299                     $dtf->format_datetime($end_date)
300                 ]
301             },
302             {
303                 start_date => { '<' => $dtf->format_datetime($start_date) },
304                 end_date   => { '>' => $dtf->format_datetime($end_date) }
305             }
306         ]
307     );
308
309     my $checkouts = $self->current_checkouts->search( { date_due => { '>=' => $dtf->format_datetime($start_date) } } );
310
311     my $bookable_items = $self->bookable_items->search(
312         {
313             itemnumber => [
314                 '-and' => 
315                 { '-not_in' => $existing_bookings->_resultset->get_column('item_id')->as_query },
316                 { '-not_in' => $checkouts->_resultset->get_column('itemnumber')->as_query }
317             ]
318         },
319         { rows => 1 }
320     );
321     return $bookable_items->single->itemnumber;
322 }
323
324 =head3 place_booking
325
326   my $booking = $biblio->place_booking(
327     {
328         patron     => $patron,
329         start_date => $datetime,
330         end_date   => $datetime
331     }
332   );
333
334 Add a booking for this item for the dates passed.
335
336 Returns the Koha::Booking object or throws an exception if the item cannot be booked for the given dates.
337
338 =cut
339
340 sub place_booking {
341     my ( $self, $params ) = @_;
342
343     # check for mandatory params
344     my @mandatory = ( 'start_date', 'end_date', 'patron' );
345     for my $param (@mandatory) {
346         unless ( defined( $params->{$param} ) ) {
347             Koha::Exceptions::MissingParameter->throw(
348                 error => "The $param parameter is mandatory" );
349         }
350     }
351     my $patron = $params->{patron};
352
353     # New booking object
354     my $booking = Koha::Booking->new(
355         {
356             start_date     => $params->{start_date},
357             end_date       => $params->{end_date},
358             patron_id      => $patron->borrowernumber,
359             biblio_id      => $self->biblionumber
360         }
361     )->store();
362     return $booking;
363 }
364
365 =head3 can_be_transferred
366
367 $biblio->can_be_transferred({ to => $to_library, from => $from_library })
368
369 Checks if at least one item of a biblio can be transferred to given library.
370
371 This feature is controlled by two system preferences:
372 UseBranchTransferLimits to enable / disable the feature
373 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
374                          for setting the limitations
375
376 Performance-wise, it is recommended to use this method for a biblio instead of
377 iterating each item of a biblio with Koha::Item->can_be_transferred().
378
379 Takes HASHref that can have the following parameters:
380     MANDATORY PARAMETERS:
381     $to   : Koha::Library
382     OPTIONAL PARAMETERS:
383     $from : Koha::Library # if given, only items from that
384                           # holdingbranch are considered
385
386 Returns 1 if at least one of the item of a biblio can be transferred
387 to $to_library, otherwise 0.
388
389 =cut
390
391 sub can_be_transferred {
392     my ($self, $params) = @_;
393
394     my $to   = $params->{to};
395     my $from = $params->{from};
396
397     return 1 unless C4::Context->preference('UseBranchTransferLimits');
398     my $limittype = C4::Context->preference('BranchTransferLimitsType');
399
400     my $items;
401     foreach my $item_of_bib ($self->items->as_list) {
402         next unless $item_of_bib->holdingbranch;
403         next if $from && $from->branchcode ne $item_of_bib->holdingbranch;
404         return 1 if $item_of_bib->holdingbranch eq $to->branchcode;
405         my $code = $limittype eq 'itemtype'
406             ? $item_of_bib->effective_itemtype
407             : $item_of_bib->ccode;
408         return 1 unless $code;
409         $items->{$code}->{$item_of_bib->holdingbranch} = 1;
410     }
411
412     # At this point we will have a HASHref containing each itemtype/ccode that
413     # this biblio has, inside which are all of the holdingbranches where those
414     # items are located at. Then, we will query Koha::Item::Transfer::Limits to
415     # find out whether a transfer limits for such $limittype from any of the
416     # listed holdingbranches to the given $to library exist. If at least one
417     # holdingbranch for that $limittype does not have a transfer limit to given
418     # $to library, then we know that the transfer is possible.
419     foreach my $code (keys %{$items}) {
420         my @holdingbranches = keys %{$items->{$code}};
421         return 1 if Koha::Item::Transfer::Limits->search({
422             toBranch => $to->branchcode,
423             fromBranch => { 'in' => \@holdingbranches },
424             $limittype => $code
425         }, {
426             group_by => [qw/fromBranch/]
427         })->count == scalar(@holdingbranches) ? 0 : 1;
428     }
429
430     return 0;
431 }
432
433
434 =head3 pickup_locations
435
436     my $pickup_locations = $biblio->pickup_locations({ patron => $patron });
437
438 Returns a Koha::Libraries set of possible pickup locations for this biblio's items,
439 according to patron's home library and if item can be transferred to each pickup location.
440
441 Throws a I<Koha::Exceptions::MissingParameter> exception if the B<mandatory> parameter I<patron>
442 is not passed.
443
444 =cut
445
446 sub pickup_locations {
447     my ( $self, $params ) = @_;
448
449     Koha::Exceptions::MissingParameter->throw( parameter => 'patron' )
450       unless exists $params->{patron};
451
452     my $patron = $params->{patron};
453
454     my $memory_cache = Koha::Cache::Memory::Lite->get_instance();
455     my @pickup_locations;
456     foreach my $item ( $self->items->as_list ) {
457         my $cache_key = sprintf "Pickup_locations:%s:%s:%s:%s:%s",
458            $item->itype,$item->homebranch,$item->holdingbranch,$item->ccode || "",$patron->branchcode||"" ;
459         my $item_pickup_locations = $memory_cache->get_from_cache( $cache_key );
460         unless( $item_pickup_locations ){
461           @{ $item_pickup_locations } = $item->pickup_locations( { patron => $patron } )->_resultset->get_column('branchcode')->all;
462           $memory_cache->set_in_cache( $cache_key, $item_pickup_locations );
463         }
464         push @pickup_locations, @{ $item_pickup_locations }
465     }
466
467     return Koha::Libraries->search(
468         { branchcode => { '-in' => \@pickup_locations } }, { order_by => ['branchname'] } );
469 }
470
471 =head3 hidden_in_opac
472
473     my $bool = $biblio->hidden_in_opac({ [ rules => $rules ] })
474
475 Returns true if the biblio matches the hidding criteria defined in $rules.
476 Returns false otherwise. It involves the I<OpacHiddenItems> and
477 I<OpacHiddenItemsHidesRecord> system preferences.
478
479 Takes HASHref that can have the following parameters:
480     OPTIONAL PARAMETERS:
481     $rules : { <field> => [ value_1, ... ], ... }
482
483 Note: $rules inherits its structure from the parsed YAML from reading
484 the I<OpacHiddenItems> system preference.
485
486 =cut
487
488 sub hidden_in_opac {
489     my ( $self, $params ) = @_;
490
491     my $rules = $params->{rules} // {};
492
493     my @items = $self->items->as_list;
494
495     return 0 unless @items; # Do not hide if there is no item
496
497     # Ok, there are items, don't even try the rules unless OpacHiddenItemsHidesRecord
498     return 0 unless C4::Context->preference('OpacHiddenItemsHidesRecord');
499
500     return !(any { !$_->hidden_in_opac({ rules => $rules }) } @items);
501 }
502
503 =head3 article_request_type
504
505 my $type = $biblio->article_request_type( $borrower );
506
507 Returns the article request type based on items, or on the record
508 itself if there are no items.
509
510 $borrower must be a Koha::Patron object
511
512 =cut
513
514 sub article_request_type {
515     my ( $self, $borrower ) = @_;
516
517     return q{} unless $borrower;
518
519     my $rule = $self->article_request_type_for_items( $borrower );
520     return $rule if $rule;
521
522     # If the record has no items that are requestable, go by the record itemtype
523     $rule = $self->article_request_type_for_bib($borrower);
524     return $rule if $rule;
525
526     return q{};
527 }
528
529 =head3 article_request_type_for_bib
530
531 my $type = $biblio->article_request_type_for_bib
532
533 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record
534
535 =cut
536
537 sub article_request_type_for_bib {
538     my ( $self, $borrower ) = @_;
539
540     return q{} unless $borrower;
541
542     my $borrowertype = $borrower->categorycode;
543     my $itemtype     = $self->itemtype();
544
545     my $rule = Koha::CirculationRules->get_effective_rule(
546         {
547             rule_name    => 'article_requests',
548             categorycode => $borrowertype,
549             itemtype     => $itemtype,
550         }
551     );
552
553     return q{} unless $rule;
554     return $rule->rule_value || q{}
555 }
556
557 =head3 article_request_type_for_items
558
559 my $type = $biblio->article_request_type_for_items
560
561 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record's items
562
563 If there is a conflict where some items are 'bib_only' and some are 'item_only', 'bib_only' will be returned.
564
565 =cut
566
567 sub article_request_type_for_items {
568     my ( $self, $borrower ) = @_;
569
570     my $counts;
571     foreach my $item ( $self->items()->as_list() ) {
572         my $rule = $item->article_request_type($borrower);
573         return $rule if $rule eq 'bib_only';    # we don't need to go any further
574         $counts->{$rule}++;
575     }
576
577     return 'item_only' if $counts->{item_only};
578     return 'yes'       if $counts->{yes};
579     return 'no'        if $counts->{no};
580     return q{};
581 }
582
583 =head3 article_requests
584
585     my $article_requests = $biblio->article_requests
586
587 Returns the article requests associated with this biblio
588
589 =cut
590
591 sub article_requests {
592     my ( $self ) = @_;
593
594     return Koha::ArticleRequests->_new_from_dbic( scalar $self->_result->article_requests );
595 }
596
597 =head3 current_checkouts
598
599     my $current_checkouts = $biblio->current_checkouts
600
601 Returns the current checkouts associated with this biblio
602
603 =cut
604
605 sub current_checkouts {
606     my ($self) = @_;
607
608     return Koha::Checkouts->search( { "item.biblionumber" => $self->id },
609         { join => 'item' } );
610 }
611
612 =head3 old_checkouts
613
614     my $old_checkouts = $biblio->old_checkouts
615
616 Returns the past checkouts associated with this biblio
617
618 =cut
619
620 sub old_checkouts {
621     my ( $self ) = @_;
622
623     return Koha::Old::Checkouts->search( { "item.biblionumber" => $self->id },
624         { join => 'item' } );
625 }
626
627 =head3 items
628
629 my $items = $biblio->items({ [ host_items => 1 ] });
630
631 The optional param host_items allows you to include 'analytical' items.
632
633 Returns the related Koha::Items object for this biblio
634
635 =cut
636
637 sub items {
638     my ($self,$params) = @_;
639
640     my $items_rs = $self->_result->items;
641
642     return Koha::Items->_new_from_dbic( $items_rs ) unless $params->{host_items};
643
644     my @itemnumbers = $items_rs->get_column('itemnumber')->all;
645     my $host_itemnumbers = $self->_host_itemnumbers();
646     push @itemnumbers, @{ $host_itemnumbers };
647     return Koha::Items->search({ "me.itemnumber" => { -in => \@itemnumbers } });
648 }
649
650 =head3 bookable_items
651
652   my $bookable_items = $biblio->bookable_items;
653
654 Returns the related Koha::Items resultset filtered to those items that can be booked.
655
656 =cut
657
658 sub bookable_items {
659     my ($self) = @_;
660     return $self->items->filter_by_bookable;
661 }
662
663
664 =head3 host_items
665
666 my $host_items = $biblio->host_items();
667
668 Return the host items (easy analytical record)
669
670 =cut
671
672 sub host_items {
673     my ($self) = @_;
674
675     return Koha::Items->new->empty
676       unless C4::Context->preference('EasyAnalyticalRecords');
677
678     my $host_itemnumbers = $self->_host_itemnumbers;
679
680     return Koha::Items->search( { itemnumber => { -in => $host_itemnumbers } } );
681 }
682
683 =head3 _host_itemnumbers
684
685 my $host_itemnumber = $biblio->_host_itemnumbers();
686
687 Return the itemnumbers for analytical items on this record
688
689 =cut
690
691 sub _host_itemnumbers {
692     my ($self) = @_;
693
694     my $marcflavour = C4::Context->preference("marcflavour");
695     my $analyticfield = '773';
696     if ( $marcflavour eq 'UNIMARC' ) {
697         $analyticfield = '461';
698     }
699     my $marc_record = $self->metadata->record;
700     my @itemnumbers;
701     foreach my $field ( $marc_record->field($analyticfield) ) {
702         push @itemnumbers, $field->subfield('9');
703     }
704     return \@itemnumbers;
705 }
706
707
708 =head3 itemtype
709
710 my $itemtype = $biblio->itemtype();
711
712 Returns the itemtype for this record.
713
714 =cut
715
716 sub itemtype {
717     my ( $self ) = @_;
718
719     return $self->biblioitem()->itemtype();
720 }
721
722 =head3 holds
723
724 my $holds = $biblio->holds();
725
726 return the current holds placed on this record
727
728 =cut
729
730 sub holds {
731     my ( $self, $params, $attributes ) = @_;
732     $attributes->{order_by} = 'priority' unless exists $attributes->{order_by};
733     my $hold_rs = $self->_result->reserves->search( $params, $attributes );
734     return Koha::Holds->_new_from_dbic($hold_rs);
735 }
736
737 =head3 current_holds
738
739 my $holds = $biblio->current_holds
740
741 Return the holds placed on this bibliographic record.
742 It does not include future holds.
743
744 =cut
745
746 sub current_holds {
747     my ($self) = @_;
748     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
749     return $self->holds(
750         { reservedate => { '<=' => $dtf->format_date(dt_from_string) } } );
751 }
752
753 =head3 biblioitem
754
755 my $field = $self->biblioitem
756
757 Returns the related Koha::Biblioitem object for this Biblio object
758
759 =cut
760
761 sub biblioitem {
762     my ($self) = @_;
763     return Koha::Biblioitems->find( { biblionumber => $self->biblionumber } );
764 }
765
766 =head3 bookings
767
768   my $bookings = $item->bookings();
769
770 Returns the bookings attached to this biblio.
771
772 =cut
773
774 sub bookings {
775     my ( $self, $params ) = @_;
776     my $bookings_rs = $self->_result->bookings->search($params);
777     return Koha::Bookings->_new_from_dbic( $bookings_rs );
778 }
779
780 =head3 suggestions
781
782 my $suggestions = $self->suggestions
783
784 Returns the related Koha::Suggestions object for this Biblio object
785
786 =cut
787
788 sub suggestions {
789     my ($self) = @_;
790
791     my $suggestions_rs = $self->_result->suggestions;
792     return Koha::Suggestions->_new_from_dbic( $suggestions_rs );
793 }
794
795 =head3 get_marc_components
796
797   my $components = $self->get_marc_components();
798
799 Returns an array of search results data, which are component parts of
800 this object (MARC21 773 points to this)
801
802 =cut
803
804 sub get_marc_components {
805     my ($self, $max_results) = @_;
806
807     return [] if (C4::Context->preference('marcflavour') ne 'MARC21');
808
809     my ( $searchstr, $sort ) = $self->get_components_query;
810
811     my $components;
812     if (defined($searchstr)) {
813         my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
814         my ( $error, $results, $facets );
815         eval {
816             ( $error, $results, $facets ) = $searcher->search_compat( $searchstr, undef, [$sort], ['biblioserver'], $max_results, 0, undef, undef, 'ccl', 0 );
817         };
818         if( $error || $@ ) {
819             $error //= q{};
820             $error .= $@ if $@;
821             warn "Warning from search_compat: '$error'";
822             $self->add_message(
823                 {
824                     type    => 'error',
825                     message => 'component_search',
826                     payload => $error,
827                 }
828             );
829         }
830         $components = $results->{biblioserver}->{RECORDS} if defined($results) && $results->{biblioserver}->{hits};
831     }
832
833     return $components // [];
834 }
835
836 =head2 get_components_query
837
838 Returns a query which can be used to search for all component parts of MARC21 biblios
839
840 =cut
841
842 sub get_components_query {
843     my ($self) = @_;
844
845     my $builder = Koha::SearchEngine::QueryBuilder->new(
846         { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
847     my $marc = $self->metadata->record;
848     my $component_sort_field = C4::Context->preference('ComponentSortField') // "title";
849     my $component_sort_order = C4::Context->preference('ComponentSortOrder') // "asc";
850     my $sort = $component_sort_field . "_" . $component_sort_order;
851
852     my $searchstr;
853     if ( C4::Context->preference('UseControlNumber') ) {
854         my $pf001 = $marc->field('001') || undef;
855
856         if ( defined($pf001) ) {
857             $searchstr = "(";
858             my $pf003 = $marc->field('003') || undef;
859
860             if ( !defined($pf003) ) {
861                 # search for 773$w='Host001'
862                 $searchstr .= "rcn:\"" . $pf001->data()."\"";
863             }
864             else {
865                 $searchstr .= "(";
866                 # search for (773$w='Host001' and 003='Host003') or 773$w='(Host003)Host001'
867                 $searchstr .= "(rcn:\"" . $pf001->data() . "\" AND cni:\"" . $pf003->data() . "\")";
868                 $searchstr .= " OR rcn:\"" . $pf003->data() . " " . $pf001->data() . "\"";
869                 $searchstr .= ")";
870             }
871
872             # limit to monograph and serial component part records
873             $searchstr .= " AND (bib-level:a OR bib-level:b)";
874             $searchstr .= ")";
875         }
876     }
877     else {
878         my $cleaned_title = $marc->subfield('245', "a");
879         $cleaned_title =~ tr|/||;
880         $cleaned_title = $builder->clean_search_term($cleaned_title);
881         $searchstr = qq#Host-item:("$cleaned_title")#;
882     }
883     my ($error, $query ,$query_str) = $builder->build_query_compat( undef, [$searchstr], undef, undef, [$sort], 0 );
884     if( $error ){
885         warn $error;
886         return;
887     }
888
889     return ($query, $query_str, $sort);
890 }
891
892 =head3 get_marc_volumes
893
894   my $volumes = $self->get_marc_volumes();
895
896 Returns an array of MARCXML data, which are volumes parts of
897 this object (MARC21 773$w or 8xx$w point to this)
898
899 =cut
900
901 sub get_marc_volumes {
902     my ( $self, $max_results ) = @_;
903
904     return $self->{_volumes} if defined( $self->{_volumes} );
905
906     my $searchstr = $self->get_volumes_query;
907
908     if ( defined($searchstr) ) {
909         my $searcher = Koha::SearchEngine::Search->new( { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
910         my ( $errors, $results, $total_hits ) = $searcher->simple_search_compat( $searchstr, 0, $max_results );
911         $self->{_volumes} =
912             ( defined($results) && scalar(@$results) ) ? $results : [];
913     } else {
914         $self->{_volumes} = [];
915     }
916
917     return $self->{_volumes};
918 }
919
920 =head2 get_volumes_query
921
922 Returns a query which can be used to search for all component parts of MARC21 biblios
923
924 =cut
925
926 sub get_volumes_query {
927     my ($self) = @_;
928
929     # MARC21 Only for now
930     return if ( C4::Context->preference('marcflavour') ne 'MARC21' );
931
932     my $marc = $self->metadata->record;
933
934     # Only build volumes query if we're in a 'Set' record
935     # or we have a monographic series.
936     # For monographic series the check on LDR 7 in (b or i or s) is omitted
937     my $leader19 = substr( $marc->leader, 19, 1 );
938     my $pf008    = $marc->field('008') || '';
939     my $mseries  = ( $pf008 && substr( $pf008->data(), 21, 1 ) eq 'm' ) ? 1 : 0;
940     return unless ( $leader19 eq 'a' || $mseries );
941
942     my $builder = Koha::SearchEngine::QueryBuilder->new( { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
943
944     my $searchstr;
945     if ( C4::Context->preference('UseControlNumber') ) {
946         my $pf001 = $marc->field('001') || undef;
947
948         if ( defined($pf001) ) {
949             $searchstr = "(";
950             my $pf003 = $marc->field('003') || undef;
951
952             if ( !defined($pf003) ) {
953
954                 # search for linking_field$w='Host001'
955                 $searchstr .= "rcn:" . $pf001->data();
956             } else {
957                 $searchstr .= "(";
958
959                 # search for (linking_field$w='Host001' and 003='Host003') or linking_field$w='(Host003)Host001'
960                 $searchstr .= "(rcn:" . $pf001->data() . " AND cni:" . $pf003->data() . ")";
961                 $searchstr .= " OR rcn:\"" . $pf003->data() . " " . $pf001->data() . "\"";
962                 $searchstr .= ")";
963             }
964
965             # exclude monograph and serial component part records
966             $searchstr .= " NOT (bib-level:a OR bib-level:b)";
967             $searchstr .= ")";
968         }
969     } else {
970         my $cleaned_title = $marc->subfield( '245', "a" );
971         $cleaned_title =~ tr|/||;
972         $cleaned_title = $builder->clean_search_term($cleaned_title);
973         $searchstr     = qq#(title-series,phr:("$cleaned_title") OR Host-item,phr:("$cleaned_title")#;
974         $searchstr .= " NOT (bib-level:a OR bib-level:b))";
975     }
976
977     return $searchstr;
978 }
979
980 =head3 subscriptions
981
982 my $subscriptions = $self->subscriptions
983
984 Returns the related Koha::Subscriptions object for this Biblio object
985
986 =cut
987
988 sub subscriptions {
989     my ($self) = @_;
990     my $rs = $self->_result->subscriptions;
991     return Koha::Subscriptions->_new_from_dbic($rs);
992 }
993
994 =head3 has_items_waiting_or_intransit
995
996 my $itemsWaitingOrInTransit = $biblio->has_items_waiting_or_intransit
997
998 Tells if this bibliographic record has items waiting or in transit.
999
1000 =cut
1001
1002 sub has_items_waiting_or_intransit {
1003     my ( $self ) = @_;
1004
1005     if ( Koha::Holds->search({ biblionumber => $self->id,
1006                                found => ['W', 'T'] })->count ) {
1007         return 1;
1008     }
1009
1010     foreach my $item ( $self->items->as_list ) {
1011         return 1 if $item->get_transfer;
1012     }
1013
1014     return 0;
1015 }
1016
1017 =head2 get_coins
1018
1019 my $coins = $biblio->get_coins;
1020
1021 Returns the COinS (a span) which can be included in a biblio record
1022
1023 =cut
1024
1025 sub get_coins {
1026     my ( $self ) = @_;
1027
1028     my $record = $self->metadata->record;
1029
1030     my $pos7 = substr $record->leader(), 7, 1;
1031     my $pos6 = substr $record->leader(), 6, 1;
1032     my $mtx;
1033     my $genre;
1034     my ( $aulast, $aufirst ) = ( '', '' );
1035     my @authors;
1036     my $title;
1037     my $hosttitle;
1038     my $pubyear   = '';
1039     my $isbn      = '';
1040     my $issn      = '';
1041     my $publisher = '';
1042     my $pages     = '';
1043     my $titletype = '';
1044
1045     # For the purposes of generating COinS metadata, LDR/06-07 can be
1046     # considered the same for UNIMARC and MARC21
1047     my $fmts6 = {
1048         'a' => 'book',
1049         'b' => 'manuscript',
1050         'c' => 'book',
1051         'd' => 'manuscript',
1052         'e' => 'map',
1053         'f' => 'map',
1054         'g' => 'film',
1055         'i' => 'audioRecording',
1056         'j' => 'audioRecording',
1057         'k' => 'artwork',
1058         'l' => 'document',
1059         'm' => 'computerProgram',
1060         'o' => 'document',
1061         'r' => 'document',
1062     };
1063     my $fmts7 = {
1064         'a' => 'journalArticle',
1065         's' => 'journal',
1066     };
1067
1068     $genre = $fmts6->{$pos6} ? $fmts6->{$pos6} : 'book';
1069
1070     if ( $genre eq 'book' ) {
1071             $genre = $fmts7->{$pos7} if $fmts7->{$pos7};
1072     }
1073
1074     ##### We must transform mtx to a valable mtx and document type ####
1075     if ( $genre eq 'book' ) {
1076             $mtx = 'book';
1077             $titletype = 'b';
1078     } elsif ( $genre eq 'journal' ) {
1079             $mtx = 'journal';
1080             $titletype = 'j';
1081     } elsif ( $genre eq 'journalArticle' ) {
1082             $mtx   = 'journal';
1083             $genre = 'article';
1084             $titletype = 'a';
1085     } else {
1086             $mtx = 'dc';
1087     }
1088
1089     if ( C4::Context->preference("marcflavour") eq "UNIMARC" ) {
1090
1091         # Setting datas
1092         $aulast  = $record->subfield( '700', 'a' ) || '';
1093         $aufirst = $record->subfield( '700', 'b' ) || '';
1094         push @authors, "$aufirst $aulast" if ($aufirst or $aulast);
1095
1096         # others authors
1097         if ( $record->field('200') ) {
1098             for my $au ( $record->field('200')->subfield('g') ) {
1099                 push @authors, $au;
1100             }
1101         }
1102
1103         $title     = $record->subfield( '200', 'a' );
1104         my $subfield_210d = $record->subfield('210', 'd');
1105         if ($subfield_210d and $subfield_210d =~ /(\d{4})/) {
1106             $pubyear = $1;
1107         }
1108         $publisher = $record->subfield( '210', 'c' ) || '';
1109         $isbn      = $record->subfield( '010', 'a' ) || '';
1110         $issn      = $record->subfield( '011', 'a' ) || '';
1111     } else {
1112
1113         # MARC21 need some improve
1114
1115         # Setting datas
1116         if ( $record->field('100') ) {
1117             push @authors, $record->subfield( '100', 'a' );
1118         }
1119
1120         # others authors
1121         if ( $record->field('700') ) {
1122             for my $au ( $record->field('700')->subfield('a') ) {
1123                 push @authors, $au;
1124             }
1125         }
1126         $title = $record->field('245');
1127         $title &&= $title->as_string('ab');
1128         if ($titletype eq 'a') {
1129             $pubyear   = $record->field('008') || '';
1130             $pubyear   = substr($pubyear->data(), 7, 4) if $pubyear;
1131             $isbn      = $record->subfield( '773', 'z' ) || '';
1132             $issn      = $record->subfield( '773', 'x' ) || '';
1133             $hosttitle = $record->subfield( '773', 't' ) || $record->subfield( '773', 'a') || q{};
1134             my @rels = $record->subfield( '773', 'g' );
1135             $pages = join(', ', @rels);
1136         } else {
1137             $pubyear   = $record->subfield( '260', 'c' ) || '';
1138             $publisher = $record->subfield( '260', 'b' ) || '';
1139             $isbn      = $record->subfield( '020', 'a' ) || '';
1140             $issn      = $record->subfield( '022', 'a' ) || '';
1141         }
1142
1143     }
1144
1145     my @params = (
1146         [ 'ctx_ver', 'Z39.88-2004' ],
1147         [ 'rft_val_fmt', "info:ofi/fmt:kev:mtx:$mtx" ],
1148         [ ($mtx eq 'dc' ? 'rft.type' : 'rft.genre'), $genre ],
1149         [ "rft.${titletype}title", $title ],
1150     );
1151
1152     # rft.title is authorized only once, so by checking $titletype
1153     # we ensure that rft.title is not already in the list.
1154     if ($hosttitle and $titletype) {
1155         push @params, [ 'rft.title', $hosttitle ];
1156     }
1157
1158     push @params, (
1159         [ 'rft.isbn', $isbn ],
1160         [ 'rft.issn', $issn ],
1161     );
1162
1163     # If it's a subscription, these informations have no meaning.
1164     if ($genre ne 'journal') {
1165         push @params, (
1166             [ 'rft.aulast', $aulast ],
1167             [ 'rft.aufirst', $aufirst ],
1168             (map { [ 'rft.au', $_ ] } @authors),
1169             [ 'rft.pub', $publisher ],
1170             [ 'rft.date', $pubyear ],
1171             [ 'rft.pages', $pages ],
1172         );
1173     }
1174
1175     my $coins_value = join( '&amp;',
1176         map { $$_[1] ? $$_[0] . '=' . uri_escape_utf8( $$_[1] ) : () } @params );
1177
1178     return $coins_value;
1179 }
1180
1181 =head2 get_openurl
1182
1183 my $url = $biblio->get_openurl;
1184
1185 Returns url for OpenURL resolver set in OpenURLResolverURL system preference
1186
1187 =cut
1188
1189 sub get_openurl {
1190     my ( $self ) = @_;
1191
1192     my $OpenURLResolverURL = C4::Context->preference('OpenURLResolverURL');
1193
1194     if ($OpenURLResolverURL) {
1195         my $uri = URI->new($OpenURLResolverURL);
1196
1197         if (not defined $uri->query) {
1198             $OpenURLResolverURL .= '?';
1199         } else {
1200             $OpenURLResolverURL .= '&amp;';
1201         }
1202         $OpenURLResolverURL .= $self->get_coins;
1203     }
1204
1205     return $OpenURLResolverURL;
1206 }
1207
1208 =head3 is_serial
1209
1210 my $serial = $biblio->is_serial
1211
1212 Return boolean true if this bibbliographic record is continuing resource
1213
1214 =cut
1215
1216 sub is_serial {
1217     my ( $self ) = @_;
1218
1219     return 1 if $self->serial;
1220
1221     my $record = $self->metadata->record;
1222     return 1 if substr($record->leader, 7, 1) eq 's';
1223
1224     return 0;
1225 }
1226
1227 =head3 custom_cover_image_url
1228
1229 my $image_url = $biblio->custom_cover_image_url
1230
1231 Return the specific url of the cover image for this bibliographic record.
1232 It is built regaring the value of the system preference CustomCoverImagesURL
1233
1234 =cut
1235
1236 sub custom_cover_image_url {
1237     my ( $self ) = @_;
1238     my $url = C4::Context->preference('CustomCoverImagesURL');
1239     if ( $url =~ m|{isbn}| ) {
1240         my $isbn = $self->biblioitem->isbn;
1241         return unless $isbn;
1242         $url =~ s|{isbn}|$isbn|g;
1243     }
1244     if ( $url =~ m|{normalized_isbn}| ) {
1245         my $normalized_isbn = $self->normalized_isbn;
1246         return unless $normalized_isbn;
1247         $url =~ s|{normalized_isbn}|$normalized_isbn|g;
1248     }
1249     if ( $url =~ m|{issn}| ) {
1250         my $issn = $self->biblioitem->issn;
1251         return unless $issn;
1252         $url =~ s|{issn}|$issn|g;
1253     }
1254
1255     my $re = qr|{(?<field>\d{3})(\$(?<subfield>.))?}|;
1256     if ( $url =~ $re ) {
1257         my $field = $+{field};
1258         my $subfield = $+{subfield};
1259         my $marc_record = $self->metadata->record;
1260         my $value;
1261         if ( $subfield ) {
1262             $value = $marc_record->subfield( $field, $subfield );
1263         } else {
1264             my $controlfield = $marc_record->field($field);
1265             $value = $controlfield->data() if $controlfield;
1266         }
1267         return unless $value;
1268         $url =~ s|$re|$value|;
1269     }
1270
1271     return $url;
1272 }
1273
1274 =head3 cover_images
1275
1276 Return the cover images associated with this biblio.
1277
1278 =cut
1279
1280 sub cover_images {
1281     my ( $self ) = @_;
1282
1283     my $cover_images_rs = $self->_result->cover_images;
1284     return unless $cover_images_rs;
1285     return Koha::CoverImages->_new_from_dbic($cover_images_rs);
1286 }
1287
1288 =head3 get_marc_notes
1289
1290     $marcnotesarray = $biblio->get_marc_notes({ opac => 1 });
1291
1292 Get all notes from the MARC record and returns them in an array.
1293 The notes are stored in different fields depending on MARC flavour.
1294 MARC21 5XX $u subfields receive special attention as they are URIs.
1295
1296 =cut
1297
1298 sub get_marc_notes {
1299     my ( $self, $params ) = @_;
1300
1301     my $marcflavour = C4::Context->preference('marcflavour');
1302     my $opac = $params->{opac} // '0';
1303     my $interface = $params->{opac} ? 'opac' : 'intranet';
1304
1305     my $record = $params->{record} // $self->metadata->record;
1306     my $record_processor = Koha::RecordProcessor->new(
1307         {
1308             filters => [ 'ViewPolicy', 'ExpandCodedFields' ],
1309             options => {
1310                 interface     => $interface,
1311                 frameworkcode => $self->frameworkcode
1312             }
1313         }
1314     );
1315     $record_processor->process($record);
1316
1317     my $scope = $marcflavour eq "UNIMARC"? '3..': '5..';
1318     #MARC21 specs indicate some notes should be private if first indicator 0
1319     my %maybe_private = (
1320         541 => 1,
1321         542 => 1,
1322         561 => 1,
1323         583 => 1,
1324         590 => 1
1325     );
1326
1327     my %hiddenlist = map { $_ => 1 }
1328         split( /,/, C4::Context->preference('NotesToHide'));
1329
1330     my @marcnotes;
1331     foreach my $field ( $record->field($scope) ) {
1332         my $tag = $field->tag();
1333         next if $hiddenlist{ $tag };
1334         next if $opac && $maybe_private{$tag} && !$field->indicator(1);
1335         if( $marcflavour ne 'UNIMARC' && $field->subfield('u') ) {
1336             # Field 5XX$u always contains URI
1337             # Examples: 505u, 506u, 510u, 514u, 520u, 530u, 538u, 540u, 542u, 552u, 555u, 561u, 563u, 583u
1338             # We first push the other subfields, then all $u's separately
1339             # Leave further actions to the template (see e.g. opac-detail)
1340             my $othersub =
1341                 join '', ( 'a' .. 't', 'v' .. 'z', '0' .. '9' ); # excl 'u'
1342             push @marcnotes, { marcnote => $field->as_string($othersub) };
1343             foreach my $sub ( $field->subfield('u') ) {
1344                 $sub =~ s/^\s+|\s+$//g; # trim
1345                 push @marcnotes, { marcnote => $sub, tag => $tag };
1346             }
1347         } else {
1348             push @marcnotes, { marcnote => $field->as_string(), tag => $tag };
1349         }
1350     }
1351     return \@marcnotes;
1352 }
1353
1354 =head3 _get_marc_authors
1355
1356 Private method to return the list of authors contained in the MARC record.
1357 See get get_marc_contributors and get_marc_authors for the public methods.
1358
1359 =cut
1360
1361 sub _get_marc_authors {
1362     my ( $self, $params ) = @_;
1363
1364     my $fields_filter = $params->{fields_filter};
1365     my $mintag        = $params->{mintag};
1366     my $maxtag        = $params->{maxtag};
1367
1368     my $AuthoritySeparator = C4::Context->preference('AuthoritySeparator');
1369     my $marcflavour        = C4::Context->preference('marcflavour');
1370
1371     # tagslib useful only for UNIMARC author responsibilities
1372     my $tagslib = $marcflavour eq "UNIMARC"
1373       ? C4::Biblio::GetMarcStructure( 1, $self->frameworkcode, { unsafe => 1 } )
1374       : undef;
1375
1376     my @marcauthors;
1377     foreach my $field ( $self->metadata->record->field($fields_filter) ) {
1378
1379         next
1380           if $mintag && $field->tag() < $mintag
1381           || $maxtag && $field->tag() > $maxtag;
1382
1383         my @subfields_loop;
1384         my @link_loop;
1385         my @subfields  = $field->subfields();
1386         my $count_auth = 0;
1387
1388         # if there is an authority link, build the link with Koha-Auth-Number: subfield9
1389         my $subfield9 = $field->subfield('9');
1390         if ($subfield9) {
1391             my $linkvalue = $subfield9;
1392             $linkvalue =~ s/(\(|\))//g;
1393             @link_loop = ( { 'limit' => 'an', 'link' => $linkvalue } );
1394         }
1395
1396         # other subfields
1397         my $unimarc3;
1398         for my $authors_subfield (@subfields) {
1399             next if ( $authors_subfield->[0] eq '9' );
1400
1401             # unimarc3 contains the $3 of the author for UNIMARC.
1402             # For french academic libraries, it's the "ppn", and it's required for idref webservice
1403             $unimarc3 = $authors_subfield->[1] if $marcflavour eq 'UNIMARC' and $authors_subfield->[0] =~ /3/;
1404
1405             # don't load unimarc subfields 3, 5
1406             next if ( $marcflavour eq 'UNIMARC' and ( $authors_subfield->[0] =~ /3|5/ ) );
1407
1408             my $code = $authors_subfield->[0];
1409             my $value        = $authors_subfield->[1];
1410             my $linkvalue    = $value;
1411             $linkvalue =~ s/(\(|\))//g;
1412             # UNIMARC author responsibility
1413             if ( $marcflavour eq 'UNIMARC' and $code eq '4' ) {
1414                 $value = C4::Biblio::GetAuthorisedValueDesc( $field->tag(), $code, $value, '', $tagslib );
1415                 $linkvalue = "($value)";
1416             }
1417             # if no authority link, build a search query
1418             unless ($subfield9) {
1419                 push @link_loop, {
1420                     limit    => 'au',
1421                     'link'   => $linkvalue,
1422                     operator => (scalar @link_loop) ? ' AND ' : undef
1423                 };
1424             }
1425             my @this_link_loop = @link_loop;
1426             # do not display $0
1427             unless ( $code eq '0') {
1428                 push @subfields_loop, {
1429                     tag       => $field->tag(),
1430                     code      => $code,
1431                     value     => $value,
1432                     link_loop => \@this_link_loop,
1433                     separator => (scalar @subfields_loop) ? $AuthoritySeparator : ''
1434                 };
1435             }
1436         }
1437         push @marcauthors, {
1438             MARCAUTHOR_SUBFIELDS_LOOP => \@subfields_loop,
1439             authoritylink => $subfield9,
1440             unimarc3 => $unimarc3
1441         };
1442     }
1443     return \@marcauthors;
1444 }
1445
1446 =head3 get_marc_contributors
1447
1448     my $contributors = $biblio->get_marc_contributors;
1449
1450 Get all contributors (but first author) from the MARC record and returns them in an array.
1451 They are stored in different fields depending on MARC flavour (700..720 for MARC21)
1452
1453 =cut
1454
1455 sub get_marc_contributors {
1456     my ( $self, $params ) = @_;
1457
1458     my ( $mintag, $maxtag, $fields_filter );
1459     my $marcflavour = C4::Context->preference('marcflavour');
1460
1461     if ( $marcflavour eq "UNIMARC" ) {
1462         $mintag = "700";
1463         $maxtag = "712";
1464         $fields_filter = '7..';
1465     } else { # marc21/normarc
1466         $mintag = "700";
1467         $maxtag = "720";
1468         $fields_filter = '7..';
1469     }
1470
1471     return $self->_get_marc_authors(
1472         {
1473             fields_filter => $fields_filter,
1474             mintag       => $mintag,
1475             maxtag       => $maxtag
1476         }
1477     );
1478 }
1479
1480 =head3 get_marc_authors
1481
1482     my $authors = $biblio->get_marc_authors;
1483
1484 Get all authors from the MARC record and returns them in an array.
1485 They are stored in different fields depending on MARC flavour
1486 (main author from 100 then secondary authors from 700..720).
1487
1488 =cut
1489
1490 sub get_marc_authors {
1491     my ( $self, $params ) = @_;
1492
1493     my ( $mintag, $maxtag, $fields_filter );
1494     my $marcflavour = C4::Context->preference('marcflavour');
1495
1496     if ( $marcflavour eq "UNIMARC" ) {
1497         $fields_filter = '200';
1498     } else { # marc21/normarc
1499         $fields_filter = '100';
1500     }
1501
1502     my @first_authors = @{$self->_get_marc_authors(
1503         {
1504             fields_filter => $fields_filter,
1505             mintag       => $mintag,
1506             maxtag       => $maxtag
1507         }
1508     )};
1509
1510     my @other_authors = @{$self->get_marc_contributors};
1511
1512     return [@first_authors, @other_authors];
1513 }
1514
1515 =head3 normalized_isbn
1516
1517     my $normalized_isbn = $biblio->normalized_isbn
1518
1519 Normalizes and returns the first valid ISBN found in the record.
1520 ISBN13 are converted into ISBN10. This is required to get some book cover images.
1521
1522 =cut
1523
1524 sub normalized_isbn {
1525     my ( $self) = @_;
1526     return C4::Koha::GetNormalizedISBN($self->biblioitem->isbn);
1527 }
1528
1529 =head3 public_read_list
1530
1531 This method returns the list of publicly readable database fields for both API and UI output purposes
1532
1533 =cut
1534
1535 sub public_read_list {
1536     return [
1537         'biblionumber',   'frameworkcode',   'author',
1538         'title',          'medium',          'subtitle',
1539         'part_number',    'part_name',       'unititle',
1540         'notes',          'serial',          'seriestitle',
1541         'copyrightdate',  'abstract'
1542     ];
1543 }
1544
1545 =head3 metadata_extractor
1546
1547     my $extractor = $biblio->metadata_extractor
1548
1549 Return a Koha::Biblio::Metadata::Extractor object to use to extract data from the metadata (ie. MARC record for now)
1550
1551 =cut
1552
1553 sub metadata_extractor {
1554     my ($self) = @_;
1555
1556     $self->{metadata_extractor} ||= Koha::Biblio::Metadata::Extractor->new( { biblio => $self } );
1557
1558     return $self->{metadata_extractor};
1559 }
1560
1561 =head3 normalized_upc
1562
1563     my $normalized_upc = $biblio->normalized_upc
1564
1565 Normalizes and returns the UPC value found in the MARC record.
1566
1567 =cut
1568
1569 sub normalized_upc {
1570     my ($self) = @_;
1571     return $self->metadata_extractor->get_normalized_upc;
1572 }
1573
1574 =head3 normalized_oclc
1575
1576     my $normalized_oclc = $biblio->normalized_oclc
1577
1578 Normalizes and returns the OCLC number found in the MARC record.
1579
1580 =cut
1581
1582 sub normalized_oclc {
1583     my ($self) = @_;
1584     return $self->metadata_extractor->get_normalized_oclc;
1585 }
1586
1587 =head3 to_api
1588
1589     my $json = $biblio->to_api;
1590
1591 Overloaded method that returns a JSON representation of the Koha::Biblio object,
1592 suitable for API output. The related Koha::Biblioitem object is merged as expected
1593 on the API.
1594
1595 =cut
1596
1597 sub to_api {
1598     my ($self, $args) = @_;
1599
1600     my $json_biblio = $self->SUPER::to_api( $args );
1601     return unless $json_biblio;
1602
1603     $args = defined $args ? {%$args} : {};
1604     delete $args->{embed};
1605
1606     my $json_biblioitem = $self->biblioitem->to_api( $args );
1607     return unless $json_biblioitem;
1608
1609     return { %$json_biblio, %$json_biblioitem };
1610 }
1611
1612 =head3 to_api_mapping
1613
1614 This method returns the mapping for representing a Koha::Biblio object
1615 on the API.
1616
1617 =cut
1618
1619 sub to_api_mapping {
1620     return {
1621         biblionumber     => 'biblio_id',
1622         frameworkcode    => 'framework_id',
1623         unititle         => 'uniform_title',
1624         seriestitle      => 'series_title',
1625         copyrightdate    => 'copyright_date',
1626         datecreated      => 'creation_date',
1627         deleted_on       => undef,
1628     };
1629 }
1630
1631 =head3 get_marc_host
1632
1633     $host = $biblio->get_marc_host;
1634     # OR:
1635     ( $host, $relatedparts, $hostinfo ) = $biblio->get_marc_host;
1636
1637     Returns host biblio record from MARC21 773 (undef if no 773 present).
1638     It looks at the first 773 field with MARCorgCode or only a control
1639     number. Complete $w or numeric part is used to search host record.
1640     The optional parameter no_items triggers a check if $biblio has items.
1641     If there are, the sub returns undef.
1642     Called in list context, it also returns 773$g (related parts).
1643
1644     If there is no $w, we use $0 (host biblionumber) or $9 (host itemnumber)
1645     to search for the host record. If there is also no $0 and no $9, we search
1646     using author and title. Failing all of that, we return an undef host and
1647     form a concatenation of strings with 773$agt for host information,
1648     returned when called in list context.
1649
1650 =cut
1651
1652 sub get_marc_host {
1653     my ($self, $params) = @_;
1654     my $no_items = $params->{no_items};
1655     return if C4::Context->preference('marcflavour') eq 'UNIMARC'; # TODO
1656     return if $params->{no_items} && $self->items->count > 0;
1657
1658     my $record;
1659     eval { $record = $self->metadata->record };
1660     return if !$record;
1661
1662     # We pick the first $w with your MARCOrgCode or the first $w that has no
1663     # code (between parentheses) at all.
1664     my $orgcode = C4::Context->preference('MARCOrgCode') // q{};
1665     my $hostfld;
1666     foreach my $f ( $record->field('773') ) {
1667         my $w = $f->subfield('w') or next;
1668         if( $w =~ /^\($orgcode\)\s*(\d+)/i or $w =~ /^\d+/ ) {
1669             $hostfld = $f;
1670             last;
1671         }
1672     }
1673
1674     my $engine = Koha::SearchEngine::Search->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1675     my $bibno;
1676     if ( !$hostfld and $record->subfield('773','t') ) {
1677         # not linked using $w
1678         my $unlinkedf = $record->field('773');
1679         my $host;
1680         if ( C4::Context->preference("EasyAnalyticalRecords") ) {
1681             if ( $unlinkedf->subfield('0') ) {
1682                 # use 773$0 host biblionumber
1683                 $bibno = $unlinkedf->subfield('0');
1684             } elsif ( $unlinkedf->subfield('9') ) {
1685                 # use 773$9 host itemnumber
1686                 my $linkeditemnumber = $unlinkedf->subfield('9');
1687                 $bibno = Koha::Items->find( $linkeditemnumber )->biblionumber;
1688             }
1689         }
1690         if ( $bibno ) {
1691             my $host = Koha::Biblios->find($bibno) or return;
1692             return wantarray ? ( $host, $unlinkedf->subfield('g') ) : $host;
1693         }
1694         # just return plaintext and no host record
1695         my $hostinfo = join( ", ", $unlinkedf->subfield('a'), $unlinkedf->subfield('t'), $unlinkedf->subfield('g') );
1696         return wantarray ? ( undef, $unlinkedf->subfield('g'), $hostinfo ) : undef;
1697     }
1698     return if !$hostfld;
1699     my $rcn = $hostfld->subfield('w');
1700
1701     # Look for control number with/without orgcode
1702     for my $try (1..2) {
1703         my ( $error, $results, $total_hits ) = $engine->simple_search_compat( 'Control-number='.$rcn, 0,1 );
1704         if( !$error and $total_hits == 1 ) {
1705             $bibno = $engine->extract_biblionumber( $results->[0] );
1706             last;
1707         }
1708         # Add or remove orgcode for second try
1709         if( $try == 1 && $rcn =~ /\)\s*(\d+)/ ) {
1710             $rcn = $1; # number only
1711         } elsif( $try == 1 && $rcn =~ /^\d+/ ) {
1712             $rcn = "($orgcode)$rcn";
1713         } else {
1714             last;
1715         }
1716     }
1717     if( $bibno ) {
1718         my $host = Koha::Biblios->find($bibno) or return;
1719         return wantarray ? ( $host, $hostfld->subfield('g') ) : $host;
1720     }
1721 }
1722
1723 =head3 get_marc_host_only
1724
1725     my $host = $biblio->get_marc_host_only;
1726
1727 Return host only
1728
1729 =cut
1730
1731 sub get_marc_host_only {
1732     my ($self) = @_;
1733
1734     my ( $host ) = $self->get_marc_host;
1735
1736     return $host;
1737 }
1738
1739 =head3 get_marc_relatedparts_only
1740
1741     my $relatedparts = $biblio->get_marc_relatedparts_only;
1742
1743 Return related parts only
1744
1745 =cut
1746
1747 sub get_marc_relatedparts_only {
1748     my ($self) = @_;
1749
1750     my ( undef, $relatedparts ) = $self->get_marc_host;
1751
1752     return $relatedparts;
1753 }
1754
1755 =head3 get_marc_hostinfo_only
1756
1757     my $hostinfo = $biblio->get_marc_hostinfo_only;
1758
1759 Return host info only
1760
1761 =cut
1762
1763 sub get_marc_hostinfo_only {
1764     my ($self) = @_;
1765
1766     my ( $host, $relatedparts, $hostinfo ) = $self->get_marc_host;
1767
1768     return $hostinfo;
1769 }
1770
1771 =head3 recalls
1772
1773     my $recalls = $biblio->recalls;
1774
1775 Return recalls linked to this biblio
1776
1777 =cut
1778
1779 sub recalls {
1780     my ( $self ) = @_;
1781     return Koha::Recalls->_new_from_dbic( scalar $self->_result->recalls );
1782 }
1783
1784 =head3 can_be_recalled
1785
1786     my @items_for_recall = $biblio->can_be_recalled({ patron => $patron_object });
1787
1788 Does biblio-level checks and returns the items attached to this biblio that are available for recall
1789
1790 =cut
1791
1792 sub can_be_recalled {
1793     my ( $self, $params ) = @_;
1794
1795     return 0 if !( C4::Context->preference('UseRecalls') );
1796
1797     my $patron = $params->{patron};
1798
1799     my $branchcode = C4::Context->userenv->{'branch'};
1800     if ( C4::Context->preference('CircControl') eq 'PatronLibrary' and $patron ) {
1801         $branchcode = $patron->branchcode;
1802     }
1803
1804     my @all_items = Koha::Items->search({ biblionumber => $self->biblionumber })->as_list;
1805
1806     # if there are no available items at all, no recall can be placed
1807     return 0 if ( scalar @all_items == 0 );
1808
1809     my @itemtypes;
1810     my @itemnumbers;
1811     my @items;
1812     my @all_itemnumbers;
1813     foreach my $item ( @all_items ) {
1814         push( @all_itemnumbers, $item->itemnumber );
1815         if ( $item->can_be_recalled({ patron => $patron }) ) {
1816             push( @itemtypes, $item->effective_itemtype );
1817             push( @itemnumbers, $item->itemnumber );
1818             push( @items, $item );
1819         }
1820     }
1821
1822     # if there are no recallable items, no recall can be placed
1823     return 0 if ( scalar @items == 0 );
1824
1825     # Check the circulation rule for each relevant itemtype for this biblio
1826     my ( @recalls_allowed, @recalls_per_record, @on_shelf_recalls );
1827     foreach my $itemtype ( @itemtypes ) {
1828         my $rule = Koha::CirculationRules->get_effective_rules({
1829             branchcode => $branchcode,
1830             categorycode => $patron ? $patron->categorycode : undef,
1831             itemtype => $itemtype,
1832             rules => [
1833                 'recalls_allowed',
1834                 'recalls_per_record',
1835                 'on_shelf_recalls',
1836             ],
1837         });
1838         push( @recalls_allowed, $rule->{recalls_allowed} ) if $rule;
1839         push( @recalls_per_record, $rule->{recalls_per_record} ) if $rule;
1840         push( @on_shelf_recalls, $rule->{on_shelf_recalls} ) if $rule;
1841     }
1842     my $recalls_allowed = (sort {$b <=> $a} @recalls_allowed)[0]; # take highest
1843     my $recalls_per_record = (sort {$b <=> $a} @recalls_per_record)[0]; # take highest
1844     my %on_shelf_recalls_count = ();
1845     foreach my $count ( @on_shelf_recalls ) {
1846         $on_shelf_recalls_count{$count}++;
1847     }
1848     my $on_shelf_recalls = (sort {$on_shelf_recalls_count{$b} <=> $on_shelf_recalls_count{$a}} @on_shelf_recalls)[0]; # take most common
1849
1850     # check recalls allowed has been set and is not zero
1851     return 0 if ( !defined($recalls_allowed) || $recalls_allowed == 0 );
1852
1853     if ( $patron ) {
1854         # check borrower has not reached open recalls allowed limit
1855         return 0 if ( $patron->recalls->filter_by_current->count >= $recalls_allowed );
1856
1857         # check borrower has not reached open recalls allowed per record limit
1858         return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $recalls_per_record );
1859
1860         # check if any of the items under this biblio are already checked out by this borrower
1861         return 0 if ( Koha::Checkouts->search({ itemnumber => [ @all_itemnumbers ], borrowernumber => $patron->borrowernumber })->count > 0 );
1862     }
1863
1864     # check item availability
1865     my $checked_out_count = 0;
1866     foreach (@items) {
1867         if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1868     }
1869
1870     # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1871     return 0 if ( $on_shelf_recalls eq 'all' && $checked_out_count < scalar @items );
1872
1873     # can't recall if no items have been checked out
1874     return 0 if ( $checked_out_count == 0 );
1875
1876     # can recall
1877     return @items;
1878 }
1879
1880 =head3 ratings
1881
1882     my $ratings = $biblio->ratings
1883
1884 Return a Koha::Ratings object representing the ratings of this bibliographic record
1885
1886 =cut
1887
1888 sub ratings {
1889     my ( $self ) = @_;
1890     my $rs = $self->_result->ratings;
1891     return Koha::Ratings->_new_from_dbic($rs);
1892 }
1893
1894 =head3 opac_summary_html
1895
1896     my $summary_html = $biblio->opac_summary_html
1897
1898 Based on the syspref OPACMySummaryHTML, returns a string representing the
1899 summary of this bibliographic record.
1900 {AUTHOR}, {TITLE}, {ISBN} and {BIBLIONUMBER} will be replaced.
1901
1902 =cut
1903
1904 sub opac_summary_html {
1905     my ($self) = @_;
1906
1907     my $summary_html = C4::Context->preference('OPACMySummaryHTML');
1908     return q{} unless $summary_html;
1909     my $author = $self->author || q{};
1910     my $title  = $self->title  || q{};
1911     $title =~ s/\/+$//;    # remove trailing slash
1912     $title =~ s/\s+$//;    # remove trailing space
1913     my $normalized_isbn = $self->normalized_isbn || q{};
1914     my $biblionumber    = $self->biblionumber;
1915
1916     $summary_html =~ s/{AUTHOR}/$author/g;
1917     $summary_html =~ s/{TITLE}/$title/g;
1918     $summary_html =~ s/{ISBN}/$normalized_isbn/g;
1919     $summary_html =~ s/{BIBLIONUMBER}/$biblionumber/g;
1920
1921     return $summary_html;
1922 }
1923
1924 =head2 Internal methods
1925
1926 =head3 type
1927
1928 =cut
1929
1930 sub _type {
1931     return 'Biblio';
1932 }
1933
1934 =head1 AUTHOR
1935
1936 Kyle M Hall <kyle@bywatersolutions.com>
1937
1938 =cut
1939
1940 1;