3 # Copyright ByWater Solutions 2014
5 # This file is part of Koha.
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.
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.
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>.
22 use List::MoreUtils qw( any );
24 use URI::Escape qw( uri_escape_utf8 );
27 use C4::Koha qw( GetNormalizedISBN GetNormalizedUPC GetNormalizedOCLCNumber );
28 use C4::Biblio qw( DelBiblio );
29 use C4::Serials qw( CountSubscriptionFromBiblionumber );
30 use C4::Reserves qw( MergeHolds );
31 use C4::Acquisition qw( ModOrder GetOrdersByBiblionumber );
34 use Koha::DateUtils qw( dt_from_string );
37 use base qw(Koha::Object);
39 use Koha::Acquisition::Orders;
40 use Koha::ArticleRequests;
41 use Koha::Biblio::Metadatas;
42 use Koha::Biblio::Metadata::Extractor;
43 use Koha::Biblio::ItemGroups;
44 use Koha::Biblioitems;
45 use Koha::Cache::Memory::Lite;
48 use Koha::CirculationRules;
50 use Koha::ILL::Requests;
51 use Koha::Item::Transfer::Limits;
54 use Koha::Old::Checkouts;
57 use Koha::RecordProcessor;
58 use Koha::Suggestions;
59 use Koha::Subscriptions;
60 use Koha::SearchEngine;
61 use Koha::SearchEngine::Search;
62 use Koha::SearchEngine::QueryBuilder;
67 Koha::Biblio - Koha Biblio Object class
77 Overloaded I<store> method to set default values
84 $self->datecreated( dt_from_string ) unless $self->datecreated;
86 return $self->SUPER::store;
91 my $metadata = $biblio->metadata();
93 Returns a Koha::Biblio::Metadata object
100 my $metadata = $self->_result->metadata;
101 return Koha::Biblio::Metadata->_new_from_dbic($metadata);
106 my $record = $biblio->record();
108 Returns a Marc::Record object
115 return $self->metadata->record;
120 my $schema = $biblio->record_schema();
122 Returns the record schema (MARC21, USMARC or UNIMARC).
129 return $self->metadata->schema // C4::Context->preference("marcflavour");
134 my $orders = $biblio->orders();
136 Returns a Koha::Acquisition::Orders object
143 my $orders = $self->_result->orders;
144 return Koha::Acquisition::Orders->_new_from_dbic($orders);
147 =head3 uncancelled_orders
149 my $uncancelled_orders = $biblio->uncancelled_orders;
151 Returns acquisition orders related to this biblio that are not cancelled.
155 sub uncancelled_orders {
158 return $self->orders->filter_out_cancelled;
163 print $biblio->acq_status;
166 - unlinked: not any linked order found in the system
167 - acquired: some lines are complete, rest is cancelled
168 - cancelled: all lines are cancelled
169 - processing: some lines are active or new
175 my $orders = $self->orders;
176 unless ( $orders->count ) {
178 } elsif ( !$self->uncancelled_orders->count ) {
180 } elsif ( $orders->search( { orderstatus => [ 'new', 'ordered', 'partial' ] } )->count ) {
183 return 'acquired'; # some lines must be complete here
189 my $tickets = $biblio->tickets();
191 Returns all tickets linked to the biblio
197 my $rs = $self->_result->tickets;
198 return Koha::Tickets->_new_from_dbic( $rs );
203 my $ill_requests = $biblio->ill_requests();
205 Returns a Koha::ILL::Requests object
212 my $ill_requests = $self->_result->ill_requests;
213 return Koha::ILL::Requests->_new_from_dbic($ill_requests);
218 my $item_groups = $biblio->item_groups();
220 Returns a Koha::Biblio::ItemGroups object
227 my $item_groups = $self->_result->item_groups;
228 return Koha::Biblio::ItemGroups->_new_from_dbic($item_groups);
231 =head3 can_article_request
233 my $bool = $biblio->can_article_request( $borrower );
235 Returns true if article requests can be made for this record
237 $borrower must be a Koha::Patron object
241 sub can_article_request {
242 my ( $self, $borrower ) = @_;
244 my $rule = $self->article_request_type($borrower);
245 return q{} if $rule eq 'item_only' && !$self->items()->count();
246 return 1 if $rule && $rule ne 'no';
253 if ( $biblio->can_be_edited( $patron ) ) { ... }
255 Returns a boolean denoting whether the passed I<$patron> meets the required
256 conditions to manually edit the record.
261 my ( $self, $patron ) = @_;
263 Koha::Exceptions::MissingParameter->throw( error => "The patron parameter is missing or invalid" )
264 unless $patron && ref($patron) eq 'Koha::Patron';
267 ( $self->metadata->source_allows_editing && $patron->has_permission( { editcatalogue => 'edit_catalogue' } ) )
268 || $patron->has_permission( { editcatalogue => 'edit_locked_records' } ) ) ? 1 : 0;
274 $biblio->check_booking( { start_date => $datetime, end_date => $datetime, [ booking_id => $booking_id ] } );
276 Returns a boolean denoting whether the passed booking can be made without clashing.
278 Optionally, you may pass a booking id to exclude from the checks; This is helpful when you are updating an existing booking.
283 my ( $self, $params ) = @_;
285 my $start_date = dt_from_string( $params->{start_date} );
286 my $end_date = dt_from_string( $params->{end_date} );
287 my $booking_id = $params->{booking_id};
289 my $bookable_items = $self->bookable_items;
290 my $total_bookable = $bookable_items->count;
292 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
293 my $existing_bookings = $self->bookings(
297 $dtf->format_datetime($start_date),
298 $dtf->format_datetime($end_date)
303 $dtf->format_datetime($start_date),
304 $dtf->format_datetime($end_date)
308 start_date => { '<' => $dtf->format_datetime($start_date) },
309 end_date => { '>' => $dtf->format_datetime($end_date) }
316 ? $existing_bookings->search( { booking_id => { '!=' => $booking_id } } )->count
317 : $existing_bookings->count;
319 my $checkouts = $self->current_checkouts->search( { date_due => { '>=' => $dtf->format_datetime($start_date) } } );
320 $booked_count += $checkouts->count;
322 return ( ( $total_bookable - $booked_count ) > 0 ) ? 1 : 0;
325 =head3 can_be_transferred
327 $biblio->can_be_transferred({ to => $to_library, from => $from_library })
329 Checks if at least one item of a biblio can be transferred to given library.
331 This feature is controlled by two system preferences:
332 UseBranchTransferLimits to enable / disable the feature
333 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
334 for setting the limitations
336 Performance-wise, it is recommended to use this method for a biblio instead of
337 iterating each item of a biblio with Koha::Item->can_be_transferred().
339 Takes HASHref that can have the following parameters:
340 MANDATORY PARAMETERS:
343 $from : Koha::Library # if given, only items from that
344 # holdingbranch are considered
346 Returns 1 if at least one of the item of a biblio can be transferred
347 to $to_library, otherwise 0.
351 sub can_be_transferred {
352 my ($self, $params) = @_;
354 my $to = $params->{to};
355 my $from = $params->{from};
357 return 1 unless C4::Context->preference('UseBranchTransferLimits');
358 my $limittype = C4::Context->preference('BranchTransferLimitsType');
361 foreach my $item_of_bib ($self->items->as_list) {
362 next unless $item_of_bib->holdingbranch;
363 next if $from && $from->branchcode ne $item_of_bib->holdingbranch;
364 return 1 if $item_of_bib->holdingbranch eq $to->branchcode;
365 my $code = $limittype eq 'itemtype'
366 ? $item_of_bib->effective_itemtype
367 : $item_of_bib->ccode;
368 return 1 unless $code;
369 $items->{$code}->{$item_of_bib->holdingbranch} = 1;
372 # At this point we will have a HASHref containing each itemtype/ccode that
373 # this biblio has, inside which are all of the holdingbranches where those
374 # items are located at. Then, we will query Koha::Item::Transfer::Limits to
375 # find out whether a transfer limits for such $limittype from any of the
376 # listed holdingbranches to the given $to library exist. If at least one
377 # holdingbranch for that $limittype does not have a transfer limit to given
378 # $to library, then we know that the transfer is possible.
379 foreach my $code (keys %{$items}) {
380 my @holdingbranches = keys %{$items->{$code}};
381 return 1 if Koha::Item::Transfer::Limits->search({
382 toBranch => $to->branchcode,
383 fromBranch => { 'in' => \@holdingbranches },
386 group_by => [qw/fromBranch/]
387 })->count == scalar(@holdingbranches) ? 0 : 1;
394 =head3 pickup_locations
396 my $pickup_locations = $biblio->pickup_locations({ patron => $patron });
398 Returns a Koha::Libraries set of possible pickup locations for this biblio's items,
399 according to patron's home library and if item can be transferred to each pickup location.
401 Throws a I<Koha::Exceptions::MissingParameter> exception if the B<mandatory> parameter I<patron>
406 sub pickup_locations {
407 my ( $self, $params ) = @_;
409 Koha::Exceptions::MissingParameter->throw( parameter => 'patron' )
410 unless exists $params->{patron};
412 my $patron = $params->{patron};
414 my $memory_cache = Koha::Cache::Memory::Lite->get_instance();
415 my @pickup_locations;
417 foreach my $item ( $self->items->as_list ) {
418 my $cache_key = sprintf "Pickup_locations:%s:%s:%s:%s:%s",
419 $item->itype, $item->homebranch, $item->holdingbranch, $item->ccode || "", $patron->branchcode || "";
420 my $item_pickup_locations = $memory_cache->get_from_cache($cache_key);
421 unless ($item_pickup_locations) {
422 @{$item_pickup_locations} =
423 $item->pickup_locations( { patron => $patron } )->_resultset->get_column('branchcode')->all;
424 $memory_cache->set_in_cache( $cache_key, $item_pickup_locations );
426 push @pickup_locations, @{$item_pickup_locations};
427 for my $location (@{$item_pickup_locations}) {
428 push @{ $location_items->{$location} }, $item->itemnumber;
433 Koha::Libraries->search( { branchcode => { '-in' => \@pickup_locations } }, { order_by => ['branchname'] } );
435 $resultSet->{_pickup_location_items} = $location_items;
440 =head3 hidden_in_opac
442 my $bool = $biblio->hidden_in_opac({ [ rules => $rules ] })
444 Returns true if the biblio matches the hidding criteria defined in $rules.
445 Returns false otherwise. It involves the I<OpacHiddenItems> and
446 I<OpacHiddenItemsHidesRecord> system preferences.
448 Takes HASHref that can have the following parameters:
450 $rules : { <field> => [ value_1, ... ], ... }
452 Note: $rules inherits its structure from the parsed YAML from reading
453 the I<OpacHiddenItems> system preference.
458 my ( $self, $params ) = @_;
460 my $rules = $params->{rules} // {};
462 my @items = $self->items->as_list;
464 return 0 unless @items; # Do not hide if there is no item
466 # Ok, there are items, don't even try the rules unless OpacHiddenItemsHidesRecord
467 return 0 unless C4::Context->preference('OpacHiddenItemsHidesRecord');
469 return !(any { !$_->hidden_in_opac({ rules => $rules }) } @items);
472 =head3 article_request_type
474 my $type = $biblio->article_request_type( $borrower );
476 Returns the article request type based on items, or on the record
477 itself if there are no items.
479 $borrower must be a Koha::Patron object
483 sub article_request_type {
484 my ( $self, $borrower ) = @_;
486 return q{} unless $borrower;
488 my $rule = $self->article_request_type_for_items( $borrower );
489 return $rule if $rule;
491 # If the record has no items that are requestable, go by the record itemtype
492 $rule = $self->article_request_type_for_bib($borrower);
493 return $rule if $rule;
498 =head3 article_request_type_for_bib
500 my $type = $biblio->article_request_type_for_bib
502 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record
506 sub article_request_type_for_bib {
507 my ( $self, $borrower ) = @_;
509 return q{} unless $borrower;
511 my $borrowertype = $borrower->categorycode;
512 my $itemtype = $self->itemtype();
514 my $rule = Koha::CirculationRules->get_effective_rule(
516 rule_name => 'article_requests',
517 categorycode => $borrowertype,
518 itemtype => $itemtype,
522 return q{} unless $rule;
523 return $rule->rule_value || q{}
526 =head3 article_request_type_for_items
528 my $type = $biblio->article_request_type_for_items
530 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record's items
532 If there is a conflict where some items are 'bib_only' and some are 'item_only', 'bib_only' will be returned.
536 sub article_request_type_for_items {
537 my ( $self, $borrower ) = @_;
540 foreach my $item ( $self->items()->as_list() ) {
541 my $rule = $item->article_request_type($borrower);
542 return $rule if $rule eq 'bib_only'; # we don't need to go any further
546 return 'item_only' if $counts->{item_only};
547 return 'yes' if $counts->{yes};
548 return 'no' if $counts->{no};
552 =head3 article_requests
554 my $article_requests = $biblio->article_requests
556 Returns the article requests associated with this biblio
560 sub article_requests {
563 return Koha::ArticleRequests->_new_from_dbic( scalar $self->_result->article_requests );
566 =head3 current_checkouts
568 my $current_checkouts = $biblio->current_checkouts
570 Returns the current checkouts associated with this biblio
574 sub current_checkouts {
577 return Koha::Checkouts->search( { "item.biblionumber" => $self->id },
578 { join => 'item' } );
583 my $old_checkouts = $biblio->old_checkouts
585 Returns the past checkouts associated with this biblio
592 return Koha::Old::Checkouts->search( { "item.biblionumber" => $self->id },
593 { join => 'item' } );
598 my $items = $biblio->items({ [ host_items => 1 ] });
600 The optional param host_items allows you to include 'analytical' items.
602 Returns the related Koha::Items object for this biblio
607 my ($self,$params) = @_;
609 my $items_rs = $self->_result->items;
611 return Koha::Items->_new_from_dbic( $items_rs ) unless $params->{host_items};
613 my @itemnumbers = $items_rs->get_column('itemnumber')->all;
614 my $host_itemnumbers = $self->_host_itemnumbers();
615 push @itemnumbers, @{ $host_itemnumbers };
616 return Koha::Items->search({ "me.itemnumber" => { -in => \@itemnumbers } });
619 =head3 bookable_items
621 my $bookable_items = $biblio->bookable_items;
623 Returns the related Koha::Items resultset filtered to those items that can be booked.
629 return $self->items->filter_by_bookable;
634 my $host_items = $biblio->host_items();
636 Return the host items (easy analytical record)
643 return Koha::Items->new->empty
644 unless C4::Context->preference('EasyAnalyticalRecords');
646 my $host_itemnumbers = $self->_host_itemnumbers;
648 return Koha::Items->search( { itemnumber => { -in => $host_itemnumbers } } );
651 =head3 _host_itemnumbers
653 my $host_itemnumber = $biblio->_host_itemnumbers();
655 Return the itemnumbers for analytical items on this record
659 sub _host_itemnumbers {
662 my $marcflavour = C4::Context->preference("marcflavour");
663 my $analyticfield = '773';
664 if ( $marcflavour eq 'UNIMARC' ) {
665 $analyticfield = '461';
667 my $marc_record = $self->metadata->record;
669 foreach my $field ( $marc_record->field($analyticfield) ) {
670 push @itemnumbers, $field->subfield('9');
672 return \@itemnumbers;
678 my $itemtype = $biblio->itemtype();
680 Returns the itemtype for this record.
687 return $self->biblioitem()->itemtype();
692 my $holds = $biblio->holds();
694 return the current holds placed on this record
699 my ( $self, $params, $attributes ) = @_;
700 $attributes->{order_by} = 'priority' unless exists $attributes->{order_by};
701 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
702 return Koha::Holds->_new_from_dbic($hold_rs);
707 my $holds = $biblio->current_holds
709 Return the holds placed on this bibliographic record.
710 It does not include future holds.
716 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
718 { reservedate => { '<=' => $dtf->format_date(dt_from_string) } } );
723 my $field = $self->biblioitem
725 Returns the related Koha::Biblioitem object for this Biblio object
731 return Koha::Biblioitems->find( { biblionumber => $self->biblionumber } );
736 my $bookings = $item->bookings();
738 Returns the bookings attached to this biblio.
743 my ( $self, $params ) = @_;
744 my $bookings_rs = $self->_result->bookings->search($params);
745 return Koha::Bookings->_new_from_dbic($bookings_rs);
750 my $suggestions = $self->suggestions
752 Returns the related Koha::Suggestions object for this Biblio object
759 my $suggestions_rs = $self->_result->suggestions;
760 return Koha::Suggestions->_new_from_dbic( $suggestions_rs );
763 =head3 get_marc_components
765 my $components = $self->get_marc_components();
767 Returns an array of search results data, which are component parts of
768 this object (MARC21 773 points to this)
772 sub get_marc_components {
773 my ($self, $max_results) = @_;
775 return [] if (C4::Context->preference('marcflavour') ne 'MARC21');
777 my ( $searchstr, $sort ) = $self->get_components_query;
780 if (defined($searchstr)) {
781 my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
782 my ( $error, $results, $facets );
784 ( $error, $results, $facets ) = $searcher->search_compat( $searchstr, undef, [$sort], ['biblioserver'], $max_results, 0, undef, undef, 'ccl', 0 );
789 warn "Warning from search_compat: '$error'";
793 message => 'component_search',
798 $components = $results->{biblioserver}->{RECORDS} if defined($results) && $results->{biblioserver}->{hits};
801 return $components // [];
804 =head2 get_components_query
806 Returns a query which can be used to search for all component parts of MARC21 biblios
810 sub get_components_query {
813 my $builder = Koha::SearchEngine::QueryBuilder->new(
814 { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
815 my $marc = $self->metadata->record;
816 my $component_sort_field = C4::Context->preference('ComponentSortField') // "title";
817 my $component_sort_order = C4::Context->preference('ComponentSortOrder') // "asc";
818 my $sort = $component_sort_field . "_" . $component_sort_order;
821 if ( C4::Context->preference('UseControlNumber') ) {
822 my $pf001 = $marc->field('001') || undef;
824 if ( defined($pf001) ) {
826 my $pf003 = $marc->field('003') || undef;
828 if ( !defined($pf003) ) {
829 # search for 773$w='Host001'
830 $searchstr .= "rcn:\"" . $pf001->data()."\"";
834 # search for (773$w='Host001' and 003='Host003') or 773$w='(Host003)Host001'
835 $searchstr .= "(rcn:\"" . $pf001->data() . "\" AND cni:\"" . $pf003->data() . "\")";
836 $searchstr .= " OR rcn:\"" . $pf003->data() . " " . $pf001->data() . "\"";
840 # limit to monograph and serial component part records
841 $searchstr .= " AND (bib-level:a OR bib-level:b)";
846 my $cleaned_title = $marc->subfield('245', "a");
847 $cleaned_title =~ tr|/||;
848 $cleaned_title = $builder->clean_search_term($cleaned_title);
849 $searchstr = qq#Host-item:("$cleaned_title")#;
851 my ($error, $query ,$query_str) = $builder->build_query_compat( undef, [$searchstr], undef, undef, [$sort], 0 );
857 return ($query, $query_str, $sort);
860 =head3 get_marc_volumes
862 my $volumes = $self->get_marc_volumes();
864 Returns an array of MARCXML data, which are volumes parts of
865 this object (MARC21 773$w or 8xx$w point to this)
869 sub get_marc_volumes {
870 my ( $self, $max_results ) = @_;
872 return $self->{_volumes} if defined( $self->{_volumes} );
874 my $searchstr = $self->get_volumes_query;
876 if ( defined($searchstr) ) {
877 my $searcher = Koha::SearchEngine::Search->new( { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
878 my ( $errors, $results, $total_hits ) = $searcher->simple_search_compat( $searchstr, 0, $max_results );
880 ( defined($results) && scalar(@$results) ) ? $results : [];
882 $self->{_volumes} = [];
885 return $self->{_volumes};
888 =head2 get_volumes_query
890 Returns a query which can be used to search for all component parts of MARC21 biblios
894 sub get_volumes_query {
897 # MARC21 Only for now
898 return if ( C4::Context->preference('marcflavour') ne 'MARC21' );
900 my $marc = $self->metadata->record;
902 # Only build volumes query if we're in a 'Set' record
903 # or we have a monographic series.
904 # For monographic series the check on LDR 7 in (b or i or s) is omitted
905 my $leader19 = substr( $marc->leader, 19, 1 );
906 my $pf008 = $marc->field('008') || '';
907 my $mseries = ( $pf008 && substr( $pf008->data(), 21, 1 ) eq 'm' ) ? 1 : 0;
908 return unless ( $leader19 eq 'a' || $mseries );
910 my $builder = Koha::SearchEngine::QueryBuilder->new( { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
913 if ( C4::Context->preference('UseControlNumber') ) {
914 my $pf001 = $marc->field('001') || undef;
916 if ( defined($pf001) ) {
918 my $pf003 = $marc->field('003') || undef;
920 if ( !defined($pf003) ) {
922 # search for linking_field$w='Host001'
923 $searchstr .= "rcn:" . $pf001->data();
927 # search for (linking_field$w='Host001' and 003='Host003') or linking_field$w='(Host003)Host001'
928 $searchstr .= "(rcn:" . $pf001->data() . " AND cni:" . $pf003->data() . ")";
929 $searchstr .= " OR rcn:\"" . $pf003->data() . " " . $pf001->data() . "\"";
933 # exclude monograph and serial component part records
934 $searchstr .= " NOT (bib-level:a OR bib-level:b)";
938 my $cleaned_title = $marc->subfield( '245', "a" );
939 $cleaned_title =~ tr|/||;
940 $cleaned_title = $builder->clean_search_term($cleaned_title);
941 $searchstr = qq#(title-series,phr:("$cleaned_title") OR Host-item,phr:("$cleaned_title")#;
942 $searchstr .= " NOT (bib-level:a OR bib-level:b))";
950 my $subscriptions = $self->subscriptions
952 Returns the related Koha::Subscriptions object for this Biblio object
958 my $rs = $self->_result->subscriptions;
959 return Koha::Subscriptions->_new_from_dbic($rs);
964 my $serials = $self->serials
966 Returns the related Koha::Serials object for this Biblio object
972 my $rs = $self->_result->serials;
973 return Koha::Serials->_new_from_dbic($rs);
976 =head3 subscription_histories
978 my $subscription_histories = $self->subscription_histories
980 Returns the related Koha::Subscription::Histories object for this Biblio object
984 sub subscription_histories {
986 my $rs = $self->_result->subscriptionhistories;
987 return Koha::Subscription::Histories->_new_from_dbic($rs);
990 =head3 has_items_waiting_or_intransit
992 my $itemsWaitingOrInTransit = $biblio->has_items_waiting_or_intransit
994 Tells if this bibliographic record has items waiting or in transit.
998 sub has_items_waiting_or_intransit {
1001 if ( Koha::Holds->search({ biblionumber => $self->id,
1002 found => ['W', 'T'] })->count ) {
1006 foreach my $item ( $self->items->as_list ) {
1007 return 1 if $item->get_transfer;
1015 my $coins = $biblio->get_coins;
1017 Returns the COinS (a span) which can be included in a biblio record
1024 my $record = $self->metadata->record;
1026 my $pos7 = substr $record->leader(), 7, 1;
1027 my $pos6 = substr $record->leader(), 6, 1;
1030 my ( $aulast, $aufirst ) = ( '', '' );
1041 # For the purposes of generating COinS metadata, LDR/06-07 can be
1042 # considered the same for UNIMARC and MARC21
1045 'b' => 'manuscript',
1047 'd' => 'manuscript',
1051 'i' => 'audioRecording',
1052 'j' => 'audioRecording',
1055 'm' => 'computerProgram',
1060 'a' => 'journalArticle',
1064 $genre = $fmts6->{$pos6} ? $fmts6->{$pos6} : 'book';
1066 if ( $genre eq 'book' ) {
1067 $genre = $fmts7->{$pos7} if $fmts7->{$pos7};
1070 ##### We must transform mtx to a valable mtx and document type ####
1071 if ( $genre eq 'book' ) {
1074 } elsif ( $genre eq 'journal' ) {
1077 } elsif ( $genre eq 'journalArticle' ) {
1085 if ( C4::Context->preference("marcflavour") eq "UNIMARC" ) {
1088 $aulast = $record->subfield( '700', 'a' ) || '';
1089 $aufirst = $record->subfield( '700', 'b' ) || '';
1090 push @authors, "$aufirst $aulast" if ($aufirst or $aulast);
1093 if ( $record->field('200') ) {
1094 for my $au ( $record->field('200')->subfield('g') ) {
1099 $title = $record->subfield( '200', 'a' );
1100 my $subfield_210d = $record->subfield('210', 'd');
1101 if ($subfield_210d and $subfield_210d =~ /(\d{4})/) {
1104 $publisher = $record->subfield( '210', 'c' ) || '';
1105 $isbn = $record->subfield( '010', 'a' ) || '';
1106 $issn = $record->subfield( '011', 'a' ) || '';
1109 # MARC21 need some improve
1112 if ( $record->field('100') ) {
1113 push @authors, $record->subfield( '100', 'a' );
1117 if ( $record->field('700') ) {
1118 for my $au ( $record->field('700')->subfield('a') ) {
1122 $title = $record->field('245');
1123 $title &&= $title->as_string('ab');
1124 if ($titletype eq 'a') {
1125 $pubyear = $record->field('008') || '';
1126 $pubyear = substr($pubyear->data(), 7, 4) if $pubyear;
1127 $isbn = $record->subfield( '773', 'z' ) || '';
1128 $issn = $record->subfield( '773', 'x' ) || '';
1129 $hosttitle = $record->subfield( '773', 't' ) || $record->subfield( '773', 'a') || q{};
1130 my @rels = $record->subfield( '773', 'g' );
1131 $pages = join(', ', @rels);
1133 $pubyear = $record->subfield( '260', 'c' ) || '';
1134 $publisher = $record->subfield( '260', 'b' ) || '';
1135 $isbn = $record->subfield( '020', 'a' ) || '';
1136 $issn = $record->subfield( '022', 'a' ) || '';
1142 [ 'ctx_ver', 'Z39.88-2004' ],
1143 [ 'rft_val_fmt', "info:ofi/fmt:kev:mtx:$mtx" ],
1144 [ ($mtx eq 'dc' ? 'rft.type' : 'rft.genre'), $genre ],
1145 [ "rft.${titletype}title", $title ],
1148 # rft.title is authorized only once, so by checking $titletype
1149 # we ensure that rft.title is not already in the list.
1150 if ($hosttitle and $titletype) {
1151 push @params, [ 'rft.title', $hosttitle ];
1155 [ 'rft.isbn', $isbn ],
1156 [ 'rft.issn', $issn ],
1159 # If it's a subscription, these informations have no meaning.
1160 if ($genre ne 'journal') {
1162 [ 'rft.aulast', $aulast ],
1163 [ 'rft.aufirst', $aufirst ],
1164 (map { [ 'rft.au', $_ ] } @authors),
1165 [ 'rft.pub', $publisher ],
1166 [ 'rft.date', $pubyear ],
1167 [ 'rft.pages', $pages ],
1171 my $coins_value = join( '&',
1172 map { $$_[1] ? $$_[0] . '=' . uri_escape_utf8( $$_[1] ) : () } @params );
1174 return $coins_value;
1179 my $url = $biblio->get_openurl;
1181 Returns url for OpenURL resolver set in OpenURLResolverURL system preference
1188 my $OpenURLResolverURL = C4::Context->preference('OpenURLResolverURL');
1190 if ($OpenURLResolverURL) {
1191 my $uri = URI->new($OpenURLResolverURL);
1193 if (not defined $uri->query) {
1194 $OpenURLResolverURL .= '?';
1196 $OpenURLResolverURL .= '&';
1198 $OpenURLResolverURL .= $self->get_coins;
1201 return $OpenURLResolverURL;
1206 my $serial = $biblio->is_serial
1208 Return boolean true if this bibbliographic record is continuing resource
1215 return 1 if $self->serial;
1217 my $record = $self->metadata->record;
1218 return 1 if substr($record->leader, 7, 1) eq 's';
1223 =head3 custom_cover_image_url
1225 my $image_url = $biblio->custom_cover_image_url
1227 Return the specific url of the cover image for this bibliographic record.
1228 It is built regaring the value of the system preference CustomCoverImagesURL
1232 sub custom_cover_image_url {
1234 my $url = C4::Context->preference('CustomCoverImagesURL');
1235 if ( $url =~ m|{isbn}| ) {
1236 my $isbn = $self->biblioitem->isbn;
1237 return unless $isbn;
1238 $url =~ s|{isbn}|$isbn|g;
1240 if ( $url =~ m|{normalized_isbn}| ) {
1241 my $normalized_isbn = $self->normalized_isbn;
1242 return unless $normalized_isbn;
1243 $url =~ s|{normalized_isbn}|$normalized_isbn|g;
1245 if ( $url =~ m|{issn}| ) {
1246 my $issn = $self->biblioitem->issn;
1247 return unless $issn;
1248 $url =~ s|{issn}|$issn|g;
1251 my $re = qr|{(?<field>\d{3})(\$(?<subfield>.))?}|;
1252 if ( $url =~ $re ) {
1253 my $field = $+{field};
1254 my $subfield = $+{subfield};
1255 my $marc_record = $self->metadata->record;
1258 $value = $marc_record->subfield( $field, $subfield );
1260 my $controlfield = $marc_record->field($field);
1261 $value = $controlfield->data() if $controlfield;
1263 return unless $value;
1264 $url =~ s|$re|$value|;
1272 Return the cover images associated with this biblio.
1279 my $cover_images_rs = $self->_result->cover_images;
1280 return unless $cover_images_rs;
1281 return Koha::CoverImages->_new_from_dbic($cover_images_rs);
1284 =head3 get_marc_notes
1286 $marcnotesarray = $biblio->get_marc_notes({ opac => 1 });
1288 Get all notes from the MARC record and returns them in an array.
1289 The notes are stored in different fields depending on MARC flavour.
1290 MARC21 5XX $u subfields receive special attention as they are URIs.
1294 sub get_marc_notes {
1295 my ( $self, $params ) = @_;
1297 my $marcflavour = C4::Context->preference('marcflavour');
1298 my $opac = $params->{opac} // '0';
1299 my $interface = $params->{opac} ? 'opac' : 'intranet';
1301 my $record = $params->{record} // $self->metadata->record;
1302 my $record_processor = Koha::RecordProcessor->new(
1304 filters => [ 'ViewPolicy', 'ExpandCodedFields' ],
1306 interface => $interface,
1307 frameworkcode => $self->frameworkcode
1311 $record_processor->process($record);
1313 my $scope = $marcflavour eq "UNIMARC"? '3..': '5..';
1314 #MARC21 specs indicate some notes should be private if first indicator 0
1315 my %maybe_private = (
1323 my %hiddenlist = map { $_ => 1 }
1324 split( /,/, C4::Context->preference('NotesToHide'));
1327 foreach my $field ( $record->field($scope) ) {
1328 my $tag = $field->tag();
1329 next if $hiddenlist{ $tag };
1330 next if $opac && $maybe_private{$tag} && !$field->indicator(1);
1331 if( $marcflavour ne 'UNIMARC' && $field->subfield('u') ) {
1332 # Field 5XX$u always contains URI
1333 # Examples: 505u, 506u, 510u, 514u, 520u, 530u, 538u, 540u, 542u, 552u, 555u, 561u, 563u, 583u
1334 # We first push the other subfields, then all $u's separately
1335 # Leave further actions to the template (see e.g. opac-detail)
1337 join '', ( 'a' .. 't', 'v' .. 'z', '0' .. '9' ); # excl 'u'
1338 push @marcnotes, { marcnote => $field->as_string($othersub) };
1339 foreach my $sub ( $field->subfield('u') ) {
1340 $sub =~ s/^\s+|\s+$//g; # trim
1341 push @marcnotes, { marcnote => $sub, tag => $tag };
1344 push @marcnotes, { marcnote => $field->as_string(), tag => $tag };
1350 =head3 _get_marc_authors
1352 Private method to return the list of authors contained in the MARC record.
1353 See get get_marc_contributors and get_marc_authors for the public methods.
1357 sub _get_marc_authors {
1358 my ( $self, $params ) = @_;
1360 my $fields_filter = $params->{fields_filter};
1361 my $mintag = $params->{mintag};
1362 my $maxtag = $params->{maxtag};
1364 my $AuthoritySeparator = C4::Context->preference('AuthoritySeparator');
1365 my $marcflavour = C4::Context->preference('marcflavour');
1367 # tagslib useful only for UNIMARC author responsibilities
1368 my $tagslib = $marcflavour eq "UNIMARC"
1369 ? C4::Biblio::GetMarcStructure( 1, $self->frameworkcode, { unsafe => 1 } )
1373 foreach my $field ( $self->metadata->record->field($fields_filter) ) {
1376 if $mintag && $field->tag() < $mintag
1377 || $maxtag && $field->tag() > $maxtag;
1381 my @subfields = $field->subfields();
1384 # if there is an authority link, build the link with Koha-Auth-Number: subfield9
1385 my $subfield9 = $field->subfield('9');
1387 my $linkvalue = $subfield9;
1388 $linkvalue =~ s/(\(|\))//g;
1389 @link_loop = ( { 'limit' => 'an', 'link' => $linkvalue } );
1394 for my $authors_subfield (@subfields) {
1395 next if ( $authors_subfield->[0] eq '9' );
1397 # unimarc3 contains the $3 of the author for UNIMARC.
1398 # For french academic libraries, it's the "ppn", and it's required for idref webservice
1399 $unimarc3 = $authors_subfield->[1] if $marcflavour eq 'UNIMARC' and $authors_subfield->[0] =~ /3/;
1401 # don't load unimarc subfields 3, 5
1402 next if ( $marcflavour eq 'UNIMARC' and ( $authors_subfield->[0] =~ /3|5/ ) );
1404 my $code = $authors_subfield->[0];
1405 my $value = $authors_subfield->[1];
1406 my $linkvalue = $value;
1407 $linkvalue =~ s/(\(|\))//g;
1408 # UNIMARC author responsibility
1409 if ( $marcflavour eq 'UNIMARC' and $code eq '4' ) {
1410 $value = C4::Biblio::GetAuthorisedValueDesc( $field->tag(), $code, $value, '', $tagslib );
1411 $linkvalue = "($value)";
1413 # if no authority link, build a search query
1414 unless ($subfield9) {
1417 'link' => $linkvalue,
1418 operator => (scalar @link_loop) ? ' AND ' : undef
1421 my @this_link_loop = @link_loop;
1423 unless ( $code eq '0') {
1424 push @subfields_loop, {
1425 tag => $field->tag(),
1428 link_loop => \@this_link_loop,
1429 separator => (scalar @subfields_loop) ? $AuthoritySeparator : ''
1433 push @marcauthors, {
1434 MARCAUTHOR_SUBFIELDS_LOOP => \@subfields_loop,
1435 authoritylink => $subfield9,
1436 unimarc3 => $unimarc3
1439 return \@marcauthors;
1442 =head3 get_marc_contributors
1444 my $contributors = $biblio->get_marc_contributors;
1446 Get all contributors (but first author) from the MARC record and returns them in an array.
1447 They are stored in different fields depending on MARC flavour (700..720 for MARC21)
1451 sub get_marc_contributors {
1452 my ( $self, $params ) = @_;
1454 my ( $mintag, $maxtag, $fields_filter );
1455 my $marcflavour = C4::Context->preference('marcflavour');
1457 if ( $marcflavour eq "UNIMARC" ) {
1460 $fields_filter = '7..';
1461 } else { # marc21/normarc
1464 $fields_filter = '7..';
1467 return $self->_get_marc_authors(
1469 fields_filter => $fields_filter,
1476 =head3 get_marc_authors
1478 my $authors = $biblio->get_marc_authors;
1480 Get all authors from the MARC record and returns them in an array.
1481 They are stored in different fields depending on MARC flavour
1482 (main author from 100 then secondary authors from 700..720).
1486 sub get_marc_authors {
1487 my ( $self, $params ) = @_;
1489 my ( $mintag, $maxtag, $fields_filter );
1490 my $marcflavour = C4::Context->preference('marcflavour');
1492 if ( $marcflavour eq "UNIMARC" ) {
1493 $fields_filter = '200';
1494 } else { # marc21/normarc
1495 $fields_filter = '100';
1498 my @first_authors = @{$self->_get_marc_authors(
1500 fields_filter => $fields_filter,
1506 my @other_authors = @{$self->get_marc_contributors};
1508 return [@first_authors, @other_authors];
1511 =head3 normalized_isbn
1513 my $normalized_isbn = $biblio->normalized_isbn
1515 Normalizes and returns the first valid ISBN found in the record.
1516 ISBN13 are converted into ISBN10. This is required to get some book cover images.
1520 sub normalized_isbn {
1522 return C4::Koha::GetNormalizedISBN($self->biblioitem->isbn);
1525 =head3 public_read_list
1527 This method returns the list of publicly readable database fields for both API and UI output purposes
1531 sub public_read_list {
1533 'biblionumber', 'frameworkcode', 'author',
1534 'title', 'medium', 'subtitle',
1535 'part_number', 'part_name', 'unititle',
1536 'notes', 'serial', 'seriestitle',
1537 'copyrightdate', 'abstract'
1541 =head3 metadata_extractor
1543 my $extractor = $biblio->metadata_extractor
1545 Return a Koha::Biblio::Metadata::Extractor object to use to extract data from the metadata (ie. MARC record for now)
1549 sub metadata_extractor {
1552 $self->{metadata_extractor} ||= Koha::Biblio::Metadata::Extractor->new( { biblio => $self } );
1554 return $self->{metadata_extractor};
1557 =head3 normalized_upc
1559 my $normalized_upc = $biblio->normalized_upc
1561 Normalizes and returns the UPC value found in the MARC record.
1565 sub normalized_upc {
1567 return $self->metadata_extractor->get_normalized_upc;
1570 =head3 normalized_oclc
1572 my $normalized_oclc = $biblio->normalized_oclc
1574 Normalizes and returns the OCLC number found in the MARC record.
1578 sub normalized_oclc {
1580 return $self->metadata_extractor->get_normalized_oclc;
1585 my $json = $biblio->to_api;
1587 Overloaded method that returns a JSON representation of the Koha::Biblio object,
1588 suitable for API output. The related Koha::Biblioitem object is merged as expected
1594 my ($self, $args) = @_;
1596 my $json_biblio = $self->SUPER::to_api( $args );
1597 return unless $json_biblio;
1599 $args = defined $args ? {%$args} : {};
1600 delete $args->{embed};
1602 my $json_biblioitem = $self->biblioitem->to_api( $args );
1603 return unless $json_biblioitem;
1605 return { %$json_biblio, %$json_biblioitem };
1608 =head3 to_api_mapping
1610 This method returns the mapping for representing a Koha::Biblio object
1615 sub to_api_mapping {
1617 biblionumber => 'biblio_id',
1618 frameworkcode => 'framework_id',
1619 unititle => 'uniform_title',
1620 seriestitle => 'series_title',
1621 copyrightdate => 'copyright_date',
1622 datecreated => 'creation_date',
1623 deleted_on => undef,
1627 =head3 get_marc_host
1629 $host = $biblio->get_marc_host;
1631 ( $host, $relatedparts, $hostinfo ) = $biblio->get_marc_host;
1633 Returns host biblio record from MARC21 773 (undef if no 773 present).
1634 It looks at the first 773 field with MARCorgCode or only a control
1635 number. Complete $w or numeric part is used to search host record.
1636 The optional parameter no_items triggers a check if $biblio has items.
1637 If there are, the sub returns undef.
1638 Called in list context, it also returns 773$g (related parts).
1640 If there is no $w, we use $0 (host biblionumber) or $9 (host itemnumber)
1641 to search for the host record. If there is also no $0 and no $9, we search
1642 using author and title. Failing all of that, we return an undef host and
1643 form a concatenation of strings with 773$agt for host information,
1644 returned when called in list context.
1649 my ($self, $params) = @_;
1650 my $no_items = $params->{no_items};
1651 return if C4::Context->preference('marcflavour') eq 'UNIMARC'; # TODO
1652 return if $params->{no_items} && $self->items->count > 0;
1655 eval { $record = $self->metadata->record };
1658 # We pick the first $w with your MARCOrgCode or the first $w that has no
1659 # code (between parentheses) at all.
1660 my $orgcode = C4::Context->preference('MARCOrgCode') // q{};
1662 foreach my $f ( $record->field('773') ) {
1663 my $w = $f->subfield('w') or next;
1664 if( $w =~ /^\($orgcode\)\s*(\d+)/i or $w =~ /^\d+/ ) {
1670 my $engine = Koha::SearchEngine::Search->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1672 if ( !$hostfld and $record->subfield('773','t') ) {
1673 # not linked using $w
1674 my $unlinkedf = $record->field('773');
1676 if ( C4::Context->preference("EasyAnalyticalRecords") ) {
1677 if ( $unlinkedf->subfield('0') ) {
1678 # use 773$0 host biblionumber
1679 $bibno = $unlinkedf->subfield('0');
1680 } elsif ( $unlinkedf->subfield('9') ) {
1681 # use 773$9 host itemnumber
1682 my $linkeditemnumber = $unlinkedf->subfield('9');
1683 $bibno = Koha::Items->find( $linkeditemnumber )->biblionumber;
1687 my $host = Koha::Biblios->find($bibno) or return;
1688 return wantarray ? ( $host, $unlinkedf->subfield('g') ) : $host;
1690 # just return plaintext and no host record
1691 my $hostinfo = join( ", ", $unlinkedf->subfield('a'), $unlinkedf->subfield('t'), $unlinkedf->subfield('g') );
1692 return wantarray ? ( undef, $unlinkedf->subfield('g'), $hostinfo ) : undef;
1694 return if !$hostfld;
1695 my $rcn = $hostfld->subfield('w');
1697 # Look for control number with/without orgcode
1698 for my $try (1..2) {
1699 my ( $error, $results, $total_hits ) = $engine->simple_search_compat( 'Control-number='.$rcn, 0,1 );
1700 if( !$error and $total_hits == 1 ) {
1701 $bibno = $engine->extract_biblionumber( $results->[0] );
1704 # Add or remove orgcode for second try
1705 if( $try == 1 && $rcn =~ /\)\s*(\d+)/ ) {
1706 $rcn = $1; # number only
1707 } elsif( $try == 1 && $rcn =~ /^\d+/ ) {
1708 $rcn = "($orgcode)$rcn";
1714 my $host = Koha::Biblios->find($bibno) or return;
1715 return wantarray ? ( $host, $hostfld->subfield('g') ) : $host;
1719 =head3 get_marc_host_only
1721 my $host = $biblio->get_marc_host_only;
1727 sub get_marc_host_only {
1730 my ( $host ) = $self->get_marc_host;
1735 =head3 get_marc_relatedparts_only
1737 my $relatedparts = $biblio->get_marc_relatedparts_only;
1739 Return related parts only
1743 sub get_marc_relatedparts_only {
1746 my ( undef, $relatedparts ) = $self->get_marc_host;
1748 return $relatedparts;
1751 =head3 get_marc_hostinfo_only
1753 my $hostinfo = $biblio->get_marc_hostinfo_only;
1755 Return host info only
1759 sub get_marc_hostinfo_only {
1762 my ( $host, $relatedparts, $hostinfo ) = $self->get_marc_host;
1769 my $recalls = $biblio->recalls;
1771 Return recalls linked to this biblio
1777 return Koha::Recalls->_new_from_dbic( scalar $self->_result->recalls );
1780 =head3 can_be_recalled
1782 my @items_for_recall = $biblio->can_be_recalled({ patron => $patron_object });
1784 Does biblio-level checks and returns the items attached to this biblio that are available for recall
1788 sub can_be_recalled {
1789 my ( $self, $params ) = @_;
1791 return 0 if !( C4::Context->preference('UseRecalls') );
1793 my $patron = $params->{patron};
1795 my $branchcode = C4::Context->userenv->{'branch'};
1796 if ( C4::Context->preference('CircControl') eq 'PatronLibrary' and $patron ) {
1797 $branchcode = $patron->branchcode;
1800 my @all_items = Koha::Items->search({ biblionumber => $self->biblionumber })->as_list;
1802 # if there are no available items at all, no recall can be placed
1803 return 0 if ( scalar @all_items == 0 );
1808 my @all_itemnumbers;
1809 foreach my $item ( @all_items ) {
1810 push( @all_itemnumbers, $item->itemnumber );
1811 if ( $item->can_be_recalled({ patron => $patron }) ) {
1812 push( @itemtypes, $item->effective_itemtype );
1813 push( @itemnumbers, $item->itemnumber );
1814 push( @items, $item );
1818 # if there are no recallable items, no recall can be placed
1819 return 0 if ( scalar @items == 0 );
1821 # Check the circulation rule for each relevant itemtype for this biblio
1822 my ( @recalls_allowed, @recalls_per_record, @on_shelf_recalls );
1823 foreach my $itemtype ( @itemtypes ) {
1824 my $rule = Koha::CirculationRules->get_effective_rules({
1825 branchcode => $branchcode,
1826 categorycode => $patron ? $patron->categorycode : undef,
1827 itemtype => $itemtype,
1830 'recalls_per_record',
1834 push( @recalls_allowed, $rule->{recalls_allowed} ) if $rule;
1835 push( @recalls_per_record, $rule->{recalls_per_record} ) if $rule;
1836 push( @on_shelf_recalls, $rule->{on_shelf_recalls} ) if $rule;
1838 my $recalls_allowed = (sort {$b <=> $a} @recalls_allowed)[0]; # take highest
1839 my $recalls_per_record = (sort {$b <=> $a} @recalls_per_record)[0]; # take highest
1840 my %on_shelf_recalls_count = ();
1841 foreach my $count ( @on_shelf_recalls ) {
1842 $on_shelf_recalls_count{$count}++;
1844 my $on_shelf_recalls = (sort {$on_shelf_recalls_count{$b} <=> $on_shelf_recalls_count{$a}} @on_shelf_recalls)[0]; # take most common
1846 # check recalls allowed has been set and is not zero
1847 return 0 if ( !defined($recalls_allowed) || $recalls_allowed == 0 );
1850 # check borrower has not reached open recalls allowed limit
1851 return 0 if ( $patron->recalls->filter_by_current->count >= $recalls_allowed );
1853 # check borrower has not reached open recalls allowed per record limit
1854 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $recalls_per_record );
1856 # check if any of the items under this biblio are already checked out by this borrower
1857 return 0 if ( Koha::Checkouts->search({ itemnumber => [ @all_itemnumbers ], borrowernumber => $patron->borrowernumber })->count > 0 );
1860 # check item availability
1861 my $checked_out_count = 0;
1863 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1866 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1867 return 0 if ( $on_shelf_recalls eq 'all' && $checked_out_count < scalar @items );
1869 # can't recall if no items have been checked out
1870 return 0 if ( $checked_out_count == 0 );
1878 my $ratings = $biblio->ratings
1880 Return a Koha::Ratings object representing the ratings of this bibliographic record
1886 my $rs = $self->_result->ratings;
1887 return Koha::Ratings->_new_from_dbic($rs);
1890 =head3 opac_summary_html
1892 my $summary_html = $biblio->opac_summary_html
1894 Based on the syspref OPACMySummaryHTML, returns a string representing the
1895 summary of this bibliographic record.
1896 {AUTHOR}, {TITLE}, {ISBN} and {BIBLIONUMBER} will be replaced.
1900 sub opac_summary_html {
1903 my $summary_html = C4::Context->preference('OPACMySummaryHTML');
1904 return q{} unless $summary_html;
1905 my $author = $self->author || q{};
1906 my $title = $self->title || q{};
1907 $title =~ s/\/+$//; # remove trailing slash
1908 $title =~ s/\s+$//; # remove trailing space
1909 my $normalized_isbn = $self->normalized_isbn || q{};
1910 my $biblionumber = $self->biblionumber;
1912 $summary_html =~ s/{AUTHOR}/$author/g;
1913 $summary_html =~ s/{TITLE}/$title/g;
1914 $summary_html =~ s/{ISBN}/$normalized_isbn/g;
1915 $summary_html =~ s/{BIBLIONUMBER}/$biblionumber/g;
1917 return $summary_html;
1922 my $biblio = Koha::Biblios->find($biblionumber);
1923 $biblio->merge_with(\@biblio_ids);
1925 This subroutine merges a list of bibliographic records into the bibliographic record.
1926 This function DOES NOT CHANGE the bibliographic metadata of the record. But it links all
1927 items, holds, subscriptions, serials issues and article_requests to the record. After doing changes
1928 bibliographic records listed are deleted
1933 my ( $self, $biblio_ids ) = @_;
1935 my $schema = Koha::Database->new()->schema();
1936 my $ref_biblionumber = $self->biblionumber;
1937 my %results = ( 'biblio_id' => $ref_biblionumber, 'merged_ids' => [] );
1939 # Ensure the keeper isn't in the list of records to merge
1940 my @biblio_ids_to_merge = grep { $_ ne $ref_biblionumber } @$biblio_ids;
1945 foreach my $bn_merge (@biblio_ids_to_merge) {
1946 my $from_biblio = Koha::Biblios->find($bn_merge);
1947 $from_biblio->items->move_to_biblio($self);
1949 # Move article requests
1950 $from_biblio->article_requests->update(
1951 { biblionumber => $ref_biblionumber },
1952 { no_triggers => 1 }
1955 # Move subscriptions
1956 $from_biblio->subscriptions->update(
1957 { biblionumber => $ref_biblionumber },
1958 { no_triggers => 1 }
1961 # Move subscription histories
1962 $from_biblio->subscription_histories->update(
1963 { biblionumber => $ref_biblionumber },
1964 { no_triggers => 1 }
1968 $from_biblio->serials->update(
1969 { biblionumber => $ref_biblionumber },
1970 { no_triggers => 1 }
1974 $from_biblio->suggestions->update(
1975 { biblionumber => $ref_biblionumber },
1976 { no_triggers => 1 }
1979 my $orders = $from_biblio->orders->unblessed;
1980 for my $order (@$orders) {
1981 $order->{'biblionumber'} = $ref_biblionumber;
1983 # TODO Do we really need to call ModOrder here?
1986 # TODO : add error control (in ModOrder?)
1990 MergeHolds( $schema->storage->dbh, $ref_biblionumber, $bn_merge );
1992 my $error = DelBiblio($bn_merge); #DelBiblio return undef unless an error occurs
1996 push( @{ $results{merged_ids} }, $bn_merge );
2002 Koha::Exception->throw($_);
2007 =head2 Internal methods
2019 Kyle M Hall <kyle@bywatersolutions.com>