X-Git-Url: http://koha-dev.rot13.org:8081/gitweb/?a=blobdiff_plain;f=C4%2FBiblio.pm;h=7a49f47df7c53b2b71155ce2a049346e3eec7026;hb=e96f32b29c64c635cfdc4c23192b279fa6e5c9d3;hp=97fa95aeb1f40ce77c37b8cbe46615eef495a529;hpb=ce161fda9b7d47e3cfcbc73ddb877eed627d6313;p=srvgit diff --git a/C4/Biblio.pm b/C4/Biblio.pm index 97fa95aeb1..7a49f47df7 100644 --- a/C4/Biblio.pm +++ b/C4/Biblio.pm @@ -21,22 +21,19 @@ package C4::Biblio; use Modern::Perl; -use vars qw(@ISA @EXPORT); +use vars qw(@ISA @EXPORT_OK); BEGIN { require Exporter; @ISA = qw(Exporter); - @EXPORT = qw( + @EXPORT_OK = qw( AddBiblio GetBiblioData - GetMarcBiblio GetISBDView GetMarcControlnumber - GetMarcNotes GetMarcISBN GetMarcISSN GetMarcSubjects - GetMarcAuthors GetMarcSeries GetMarcUrls GetUsedMarcStructure @@ -46,6 +43,7 @@ BEGIN { GetMarcQuantity GetAuthorisedValueDesc GetMarcStructure + GetMarcSubfieldStructure IsMarcStructureInternal GetMarcFromKohaField GetMarcSubfieldStructureFromKohaField @@ -60,6 +58,7 @@ BEGIN { DelBiblio BiblioAutoLink LinkBibHeadingsToAuthorities + ApplyMarcOverlayRules TransformMarcToKoha TransformHtmlToMarc TransformHtmlToXml @@ -70,45 +69,52 @@ BEGIN { # those functions are exported but should not be used # they are useful in a few circumstances, so they are exported, # but don't use them unless you are a core developer ;-) - push @EXPORT, qw( + push @EXPORT_OK, qw( ModBiblioMarc ); } -use Carp; -use Try::Tiny; +use Carp qw( carp ); +use Try::Tiny qw( catch try ); -use Encode qw( decode is_utf8 ); +use Encode; use List::MoreUtils qw( uniq ); use MARC::Record; use MARC::File::USMARC; use MARC::File::XML; -use POSIX qw(strftime); -use Module::Load::Conditional qw(can_load); +use POSIX qw( strftime ); +use Module::Load::Conditional qw( can_load ); use C4::Koha; -use C4::Log; # logaction +use C4::Log qw( logaction ); # logaction use C4::Budgets; -use C4::ClassSource; -use C4::Charset; +use C4::ClassSource qw( GetClassSort GetClassSource ); +use C4::Charset qw( + nsb_clean + SetMarcUnicodeFlag + SetUTF8Flag +); +use C4::Languages; use C4::Linker; use C4::OAI::Sets; -use C4::Debug; +use C4::Items qw( GetMarcItem ); +use Koha::Logger; use Koha::Caches; +use Koha::ClassSources; use Koha::Authority::Types; use Koha::Acquisition::Currencies; +use Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue; use Koha::Biblio::Metadatas; use Koha::Holds; use Koha::ItemTypes; +use Koha::MarcOverlayRules; use Koha::Plugins; use Koha::SearchEngine; +use Koha::SearchEngine::Indexer; use Koha::Libraries; use Koha::Util::MARC; -use vars qw($debug $cgi_debug); - - =head1 NAME C4::Biblio - cataloging management functions @@ -179,68 +185,136 @@ The first argument is a C object containing the bib to add, while the second argument is the desired MARC framework code. -This function also accepts a third, optional argument: a hashref -to additional options. The only defined option is C, -which if present and mapped to a true value, causes C -to omit the call to save the MARC in C -This option is provided B -for the use of scripts such as C that may need -to do some manipulation of the MARC record for item parsing before -saving it and which cannot afford the performance hit of saving -the MARC record twice. Consequently, do not use that option -unless you can guarantee that C will be called. +The C<$options> argument is a hashref with additional parameters: + +=over 4 + +=item B: used when ModBiblioMarc is handled by the caller + +=item B: used when the indexing schedulling will be handled by the caller + +=back =cut sub AddBiblio { - my $record = shift; - my $frameworkcode = shift; - my $options = @_ ? shift : undef; - my $defer_marc_save = 0; + my ( $record, $frameworkcode, $options ) = @_; + + $options //= {}; + my $skip_record_index = $options->{skip_record_index} || 0; + my $defer_marc_save = $options->{defer_marc_save} || 0; + if (!$record) { carp('AddBiblio called with undefined record'); return; } - if ( defined $options and exists $options->{'defer_marc_save'} and $options->{'defer_marc_save'} ) { - $defer_marc_save = 1; - } - if (C4::Context->preference('BiblioAddsAuthorities')) { - BiblioAutoLink( $record, $frameworkcode ); - } + my $schema = Koha::Database->schema; + my ( $biblionumber, $biblioitemnumber ); + try { + $schema->txn_do(sub { + + # transform the data into koha-table style data + SetUTF8Flag($record); + my $olddata = TransformMarcToKoha({ record => $record, limit_table => 'no_items' }); + + my $biblio = Koha::Biblio->new( + { + frameworkcode => $frameworkcode, + author => $olddata->{author}, + title => $olddata->{title}, + subtitle => $olddata->{subtitle}, + medium => $olddata->{medium}, + part_number => $olddata->{part_number}, + part_name => $olddata->{part_name}, + unititle => $olddata->{unititle}, + notes => $olddata->{notes}, + serial => + ( $olddata->{serial} || $olddata->{seriestitle} ? 1 : 0 ), + seriestitle => $olddata->{seriestitle}, + copyrightdate => $olddata->{copyrightdate}, + datecreated => \'NOW()', + abstract => $olddata->{abstract}, + } + )->store; + $biblionumber = $biblio->biblionumber; + Koha::Exceptions::ObjectNotCreated->throw unless $biblio; + + my ($cn_sort) = GetClassSort( $olddata->{'biblioitems.cn_source'}, $olddata->{'cn_class'}, $olddata->{'cn_item'} ); + my $biblioitem = Koha::Biblioitem->new( + { + biblionumber => $biblionumber, + volume => $olddata->{volume}, + number => $olddata->{number}, + itemtype => $olddata->{itemtype}, + isbn => $olddata->{isbn}, + issn => $olddata->{issn}, + publicationyear => $olddata->{publicationyear}, + publishercode => $olddata->{publishercode}, + volumedate => $olddata->{volumedate}, + volumedesc => $olddata->{volumedesc}, + collectiontitle => $olddata->{collectiontitle}, + collectionissn => $olddata->{collectionissn}, + collectionvolume => $olddata->{collectionvolume}, + editionstatement => $olddata->{editionstatement}, + editionresponsibility => $olddata->{editionresponsibility}, + illus => $olddata->{illus}, + pages => $olddata->{pages}, + notes => $olddata->{bnotes}, + size => $olddata->{size}, + place => $olddata->{place}, + lccn => $olddata->{lccn}, + url => $olddata->{url}, + cn_source => $olddata->{'biblioitems.cn_source'}, + cn_class => $olddata->{cn_class}, + cn_item => $olddata->{cn_item}, + cn_suffix => $olddata->{cn_suff}, + cn_sort => $cn_sort, + totalissues => $olddata->{totalissues}, + ean => $olddata->{ean}, + agerestriction => $olddata->{agerestriction}, + } + )->store; + Koha::Exceptions::ObjectNotCreated->throw unless $biblioitem; + $biblioitemnumber = $biblioitem->biblioitemnumber; - my ( $biblionumber, $biblioitemnumber, $error ); - my $dbh = C4::Context->dbh; + _koha_marc_update_bib_ids( $record, $frameworkcode, $biblionumber, $biblioitemnumber ); - # transform the data into koha-table style data - SetUTF8Flag($record); - my $olddata = TransformMarcToKoha( $record, $frameworkcode ); - ( $biblionumber, $error ) = _koha_add_biblio( $dbh, $olddata, $frameworkcode ); - $olddata->{'biblionumber'} = $biblionumber; - ( $biblioitemnumber, $error ) = _koha_add_biblioitem( $dbh, $olddata ); + # update MARC subfield that stores biblioitems.cn_sort + _koha_marc_update_biblioitem_cn_sort( $record, $olddata, $frameworkcode ); - _koha_marc_update_bib_ids( $record, $frameworkcode, $biblionumber, $biblioitemnumber ); + if (C4::Context->preference('AutoLinkBiblios')) { + BiblioAutoLink( $record, $frameworkcode ); + } - # update MARC subfield that stores biblioitems.cn_sort - _koha_marc_update_biblioitem_cn_sort( $record, $olddata, $frameworkcode ); + # now add the record, don't index while we are in the transaction though + ModBiblioMarc( $record, $biblionumber, { skip_record_index => 1 } ) unless $defer_marc_save; - # now add the record - ModBiblioMarc( $record, $biblionumber, $frameworkcode ) unless $defer_marc_save; + # update OAI-PMH sets + if(C4::Context->preference("OAI-PMH:AutoUpdateSets")) { + C4::OAI::Sets::UpdateOAISetsBiblio($biblionumber, $record); + } - # update OAI-PMH sets - if(C4::Context->preference("OAI-PMH:AutoUpdateSets")) { - C4::OAI::Sets::UpdateOAISetsBiblio($biblionumber, $record); - } + _after_biblio_action_hooks({ action => 'create', biblio_id => $biblionumber }); - _after_biblio_action_hooks({ action => 'create', biblio_id => $biblionumber }); + logaction( "CATALOGUING", "ADD", $biblionumber, "biblio" ) if C4::Context->preference("CataloguingLog"); - logaction( "CATALOGUING", "ADD", $biblionumber, "biblio" ) if C4::Context->preference("CataloguingLog"); + }); + # We index now, after the transaction is committed + unless ( $skip_record_index ) { + my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX }); + $indexer->index_records( $biblionumber, "specialUpdate", "biblioserver" ); + } + } catch { + warn $_; + ( $biblionumber, $biblioitemnumber ) = ( undef, undef ); + }; return ( $biblionumber, $biblioitemnumber ); } =head2 ModBiblio - ModBiblio( $record,$biblionumber,$frameworkcode, $disable_autolink); + ModBiblio($record, $biblionumber, $frameworkcode, $options); Replace an existing bib record identified by C<$biblionumber> with one supplied by the MARC::Record object C<$record>. The embedded @@ -256,27 +330,50 @@ in the C and C tables, as well as which fields are used to store embedded item, biblioitem, and biblionumber data for indexing. -Unless C<$disable_autolink> is passed ModBiblio will relink record headings +The C<$options> argument is a hashref with additional parameters: + +=over 4 + +=item C + +This parameter is forwarded to L where it is used for +selecting the current rule set if MARCOverlayRules is enabled. +See L for more details. + +=item C + +Unless C is passed ModBiblio will relink record headings to authorities based on settings in the system preferences. This flag allows us to not relink records when the authority linker is saving modifications. +=item C + +Unless C is passed, ModBiblio will trigger the BatchUpdateBiblioHoldsQueue +task to rebuild the holds queue for the biblio if I is enabled. + +=back + Returns 1 on success 0 on failure =cut sub ModBiblio { - my ( $record, $biblionumber, $frameworkcode, $disable_autolink ) = @_; + my ( $record, $biblionumber, $frameworkcode, $options ) = @_; + + $options //= {}; + my $skip_record_index = $options->{skip_record_index} || 0; + if (!$record) { carp 'No record passed to ModBiblio'; return 0; } if ( C4::Context->preference("CataloguingLog") ) { - my $newrecord = GetMarcBiblio({ biblionumber => $biblionumber }); - logaction( "CATALOGUING", "MODIFY", $biblionumber, "biblio BEFORE=>" . $newrecord->as_formatted ); + my $biblio = Koha::Biblios->find($biblionumber); + logaction( "CATALOGUING", "MODIFY", $biblionumber, "biblio BEFORE=>" . $biblio->metadata->record->as_formatted ); } - if ( !$disable_autolink && C4::Context->preference('BiblioAddsAuthorities') ) { + if ( !$options->{disable_autolink} && C4::Context->preference('AutoLinkBiblios') ) { BiblioAutoLink( $record, $frameworkcode ); } @@ -297,6 +394,21 @@ sub ModBiblio { _strip_item_fields($record, $frameworkcode); + # apply overlay rules + if ( C4::Context->preference('MARCOverlayRules') + && $biblionumber + && defined $options + && exists $options->{overlay_context} ) + { + $record = ApplyMarcOverlayRules( + { + biblionumber => $biblionumber, + record => $record, + overlay_context => $options->{overlay_context}, + } + ); + } + # update biblionumber and biblioitemnumber in MARC # FIXME - this is assuming a 1 to 1 relationship between # biblios and biblioitems @@ -307,13 +419,13 @@ sub ModBiblio { _koha_marc_update_bib_ids( $record, $frameworkcode, $biblionumber, $biblioitemnumber ); # load the koha-table data object - my $oldbiblio = TransformMarcToKoha( $record, $frameworkcode ); + my $oldbiblio = TransformMarcToKoha({ record => $record }); # update MARC subfield that stores biblioitems.cn_sort _koha_marc_update_biblioitem_cn_sort( $record, $oldbiblio, $frameworkcode ); # update the MARC record (that now contains biblio and items) with the new record data - &ModBiblioMarc( $record, $biblionumber, $frameworkcode ); + ModBiblioMarc( $record, $biblionumber, { skip_record_index => $skip_record_index } ); # modify the other koha tables _koha_modify_biblio( $dbh, $oldbiblio, $frameworkcode ); @@ -326,6 +438,12 @@ sub ModBiblio { C4::OAI::Sets::UpdateOAISetsBiblio($biblionumber, $record); } + Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue( + { + biblio_ids => [ $biblionumber ] + } + ) unless $options->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue'); + return 1; } @@ -354,7 +472,7 @@ sub _strip_item_fields { =head2 DelBiblio - my $error = &DelBiblio($biblionumber); + my $error = &DelBiblio($biblionumber, $params); Exported function (core API) for deleting a biblio in koha. Deletes biblio record from Zebra and Koha tables (biblio & biblioitems) @@ -363,10 +481,19 @@ Checks to make sure that the biblio has no items attached. return: C<$error> : undef unless an error occurs +I<$params> is a hashref containing extra parameters. Valid keys are: + +=over 4 + +=item B: used when the holds queue update will be handled by the caller + +=item B: used when the indexing schedulling will be handled by the caller + +=back =cut sub DelBiblio { - my ($biblionumber) = @_; + my ($biblionumber, $params) = @_; my $biblio = Koha::Biblios->find( $biblionumber ); return unless $biblio; # Should we throw an exception instead? @@ -388,14 +515,14 @@ sub DelBiblio { # We delete any existing holds my $holds = $biblio->holds; while ( my $hold = $holds->next ) { - $hold->cancel; + # no need to update the holds queue on each step, we'll do it at the end + $hold->cancel({ skip_holds_queue => 1 }); } - # Delete in Zebra. Be careful NOT to move this line after _koha_delete_biblio - # for at least 2 reasons : - # - if something goes wrong, the biblio may be deleted from Koha but not from zebra - # and we would have no way to remove it (except manually in zebra, but I bet it would be very hard to handle the problem) - ModZebra( $biblionumber, "recordDelete", "biblioserver" ); + unless ( $params->{skip_record_index} ){ + my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX }); + $indexer->index_records( $biblionumber, "recordDelete", "biblioserver" ); + } # delete biblioitems and items from Koha tables and save in deletedbiblioitems,deleteditems $sth = $dbh->prepare("SELECT biblioitemnumber FROM biblioitems WHERE biblionumber=?"); @@ -418,6 +545,12 @@ sub DelBiblio { logaction( "CATALOGUING", "DELETE", $biblionumber, "biblio" ) if C4::Context->preference("CataloguingLog"); + Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue( + { + biblio_ids => [ $biblionumber ] + } + ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue'); + return; } @@ -435,6 +568,7 @@ Returns the number of headings changed sub BiblioAutoLink { my $record = shift; my $frameworkcode = shift; + my $verbose = shift; if (!$record) { carp('Undefined record passed to BiblioAutoLink'); return 0; @@ -452,15 +586,15 @@ sub BiblioAutoLink { my $linker = $linker_module->new( { 'options' => C4::Context->preference("LinkerOptions") } ); - my ( $headings_changed, undef ) = - LinkBibHeadingsToAuthorities( $linker, $record, $frameworkcode, C4::Context->preference("CatalogModuleRelink") || '' ); + my ( $headings_changed, $results ) = + LinkBibHeadingsToAuthorities( $linker, $record, $frameworkcode, C4::Context->preference("CatalogModuleRelink") || '', undef, $verbose ); # By default we probably don't want to relink things when cataloging - return $headings_changed; + return $headings_changed, $results; } =head2 LinkBibHeadingsToAuthorities - my $num_headings_changed, %results = LinkBibHeadingsToAuthorities($linker, $marc, $frameworkcode, [$allowrelink]); + my $num_headings_changed, %results = LinkBibHeadingsToAuthorities($linker, $marc, $frameworkcode, [$allowrelink, $tagtolink, $verbose]); Links bib headings to authority records by checking each authority-controlled field in the C @@ -482,6 +616,8 @@ sub LinkBibHeadingsToAuthorities { my $bib = shift; my $frameworkcode = shift; my $allowrelink = shift; + my $tagtolink = shift; + my $verbose = shift; my %results; if (!$bib) { carp 'LinkBibHeadingsToAuthorities called on undefined bib record'; @@ -493,6 +629,9 @@ sub LinkBibHeadingsToAuthorities { $allowrelink = 1 unless defined $allowrelink; my $num_headings_changed = 0; foreach my $field ( $bib->fields() ) { + if ( defined $tagtolink ) { + next unless $field->tag() == $tagtolink ; + } my $heading = C4::Heading->new_from_field( $field, $frameworkcode ); next unless defined $heading; @@ -502,6 +641,7 @@ sub LinkBibHeadingsToAuthorities { if ( defined $current_link && (!$allowrelink || !C4::Context->preference('LinkerRelink')) ) { $results{'linked'}->{ $heading->display_form() }++; + push(@{$results{'details'}}, { tag => $field->tag(), authid => $current_link, status => 'UNCHANGED'}) if $verbose; next; } @@ -509,17 +649,23 @@ sub LinkBibHeadingsToAuthorities { if ($authid) { $results{ $fuzzy ? 'fuzzy' : 'linked' } ->{ $heading->display_form() }++; - next if defined $current_link and $current_link == $authid; + if(defined $current_link and $current_link == $authid) { + push(@{$results{'details'}}, { tag => $field->tag(), authid => $current_link, status => 'UNCHANGED'}) if $verbose; + next; + } $field->delete_subfield( code => '9' ) if defined $current_link; $field->add_subfields( '9', $authid ); $num_headings_changed++; + push(@{$results{'details'}}, { tag => $field->tag(), authid => $authid, status => 'LOCAL_FOUND'}) if $verbose; } else { + my $authority_type = Koha::Authority::Types->find( $heading->auth_type() ); if ( defined $current_link && (!$allowrelink || C4::Context->preference('LinkerKeepStale')) ) { $results{'fuzzy'}->{ $heading->display_form() }++; + push(@{$results{'details'}}, { tag => $field->tag(), authid => $current_link, status => 'UNCHANGED'}) if $verbose; } elsif ( C4::Context->preference('AutoCreateAuthorities') ) { if ( _check_valid_auth_link( $current_link, $field ) ) { @@ -568,7 +714,7 @@ sub LinkBibHeadingsToAuthorities { $marcrecordauth->insert_fields_ordered( MARC::Field->new( '667', '', '', - 'a' => "Machine generated authority record." + 'a' => C4::Context->preference('GenerateAuthorityField667') ) ); my $cite = @@ -578,7 +724,7 @@ sub LinkBibHeadingsToAuthorities { $cite =~ s/^[\s\,]*//; $cite =~ s/[\s\,]*$//; $cite = - "Work cat.: (" + C4::Context->preference('GenerateAuthorityField670') . ": (" . ( $library ? $library->get_effective_marcorgcode : C4::Context->preference('MARCOrgCode') ) . ")" . $bib->subfield( '999', 'c' ) . ": " . $cite; @@ -595,24 +741,29 @@ sub LinkBibHeadingsToAuthorities { $num_headings_changed++; $linker->update_cache($heading, $authid); $results{'added'}->{ $heading->display_form() }++; + push(@{$results{'details'}}, { tag => $field->tag(), authid => $authid, status => 'CREATED'}) if $verbose; } } elsif ( defined $current_link ) { if ( _check_valid_auth_link( $current_link, $field ) ) { $results{'linked'}->{ $heading->display_form() }++; + push(@{$results{'details'}}, { tag => $field->tag(), authid => $authid, status => 'UNCHANGED'}) if $verbose; } else { $field->delete_subfield( code => '9' ); $num_headings_changed++; $results{'unlinked'}->{ $heading->display_form() }++; + push(@{$results{'details'}}, { tag => $field->tag(), authid => undef, status => 'NONE_FOUND', auth_type => $heading->auth_type(), tag_to_report => $authority_type->auth_tag_to_report}) if $verbose; } } else { $results{'unlinked'}->{ $heading->display_form() }++; + push(@{$results{'details'}}, { tag => $field->tag(), authid => undef, status => 'NONE_FOUND', auth_type => $heading->auth_type(), tag_to_report => $authority_type->auth_tag_to_report}) if $verbose; } } } + push(@{$results{'details'}}, { tag => '', authid => undef, status => 'UNCHANGED'}) unless %results; return $num_headings_changed, \%results; } @@ -633,9 +784,7 @@ sub _check_valid_auth_link { my ( $authid, $field ) = @_; require C4::AuthoritiesMarc; - my $authorized_heading = - C4::AuthoritiesMarc::GetAuthorizedHeading( { 'authid' => $authid } ) || ''; - return ($field->as_string('abcdefghijklmnopqrstuvwxyz') eq $authorized_heading); + return C4::AuthoritiesMarc::CompareFieldWithAuthority( { 'field' => $field, 'authid' => $authid } ); } =head2 GetBiblioData @@ -866,7 +1015,7 @@ sub GetMarcStructure { ORDER BY tagfield" ); $sth->execute($frameworkcode); - my ( $liblibrarian, $libopac, $tag, $res, $tab, $mandatory, $repeatable, $important, $ind1_defaultvalue, $ind2_defaultvalue ); + my ( $liblibrarian, $libopac, $tag, $res, $mandatory, $repeatable, $important, $ind1_defaultvalue, $ind2_defaultvalue ); while ( ( $tag, $liblibrarian, $libopac, $mandatory, $repeatable, $important, $ind1_defaultvalue, $ind2_defaultvalue ) = $sth->fetchrow ) { $res->{$tag}->{lib} = ( $forlibrarian or !$libopac ) ? $liblibrarian : $libopac; @@ -878,50 +1027,13 @@ sub GetMarcStructure { $res->{$tag}->{ind2_defaultvalue} = $ind2_defaultvalue; } - $sth = $dbh->prepare( - "SELECT tagfield,tagsubfield,liblibrarian,libopac,tab,mandatory,repeatable,authorised_value,authtypecode,value_builder,kohafield,seealso,hidden,isurl,link,defaultvalue,maxlength,important - FROM marc_subfield_structure - WHERE frameworkcode=? - ORDER BY tagfield,tagsubfield - " - ); - - $sth->execute($frameworkcode); - - my $subfield; - my $authorised_value; - my $authtypecode; - my $value_builder; - my $kohafield; - my $seealso; - my $hidden; - my $isurl; - my $link; - my $defaultvalue; - my $maxlength; - - while ( - ( $tag, $subfield, $liblibrarian, $libopac, $tab, $mandatory, $repeatable, $authorised_value, - $authtypecode, $value_builder, $kohafield, $seealso, $hidden, $isurl, $link, $defaultvalue, - $maxlength, $important - ) - = $sth->fetchrow - ) { - $res->{$tag}->{$subfield}->{lib} = ( $forlibrarian or !$libopac ) ? $liblibrarian : $libopac; - $res->{$tag}->{$subfield}->{tab} = $tab; - $res->{$tag}->{$subfield}->{mandatory} = $mandatory; - $res->{$tag}->{$subfield}->{important} = $important; - $res->{$tag}->{$subfield}->{repeatable} = $repeatable; - $res->{$tag}->{$subfield}->{authorised_value} = $authorised_value; - $res->{$tag}->{$subfield}->{authtypecode} = $authtypecode; - $res->{$tag}->{$subfield}->{value_builder} = $value_builder; - $res->{$tag}->{$subfield}->{kohafield} = $kohafield; - $res->{$tag}->{$subfield}->{seealso} = $seealso; - $res->{$tag}->{$subfield}->{hidden} = $hidden; - $res->{$tag}->{$subfield}->{isurl} = $isurl; - $res->{$tag}->{$subfield}->{'link'} = $link; - $res->{$tag}->{$subfield}->{defaultvalue} = $defaultvalue; - $res->{$tag}->{$subfield}->{maxlength} = $maxlength; + my $mss = Koha::MarcSubfieldStructures->search( { frameworkcode => $frameworkcode } )->unblessed; + for my $m (@$mss) { + $res->{ $m->{tagfield} }->{ $m->{tagsubfield} } = { + lib => ( $forlibrarian or !$m->{libopac} ) ? $m->{liblibrarian} : $m->{libopac}, + subfield => $m->{tagsubfield}, + %$m + }; } $cache->set_in_cache($cache_key, $res); @@ -949,7 +1061,7 @@ sub GetUsedMarcStructure { FROM marc_subfield_structure WHERE tab > -1 AND frameworkcode = ? - ORDER BY tagfield, tagsubfield + ORDER BY tagfield, display_order, tagsubfield }; my $sth = C4::Context->dbh->prepare($query); $sth->execute($frameworkcode); @@ -1014,7 +1126,7 @@ sub GetMarcSubfieldStructure { FROM marc_subfield_structure WHERE frameworkcode = ? AND kohafield > '' - ORDER BY frameworkcode,tagfield,tagsubfield + ORDER BY frameworkcode, tagfield, display_order, tagsubfield |, { Slice => {} }, $frameworkcode ); # Now map the output to a hash structure my $subfield_structure = {}; @@ -1079,99 +1191,6 @@ sub GetMarcSubfieldStructureFromKohaField { return wantarray ? @{$mss->{$kohafield}} : $mss->{$kohafield}->[0]; } -=head2 GetMarcBiblio - - my $record = GetMarcBiblio({ - biblionumber => $biblionumber, - embed_items => $embeditems, - opac => $opac, - borcat => $patron_category }); - -Returns MARC::Record representing a biblio record, or C if the -biblionumber doesn't exist. - -Both embed_items and opac are optional. -If embed_items is passed and is 1, items are embedded. -If opac is passed and is 1, the record is filtered as needed. - -=over 4 - -=item C<$biblionumber> - -the biblionumber - -=item C<$embeditems> - -set to true to include item information. - -=item C<$opac> - -set to true to make the result suited for OPAC view. This causes things like -OpacHiddenItems to be applied. - -=item C<$borcat> - -If the OpacHiddenItemsExceptions system preference is set, this patron category -can be used to make visible OPAC items which would be normally hidden. -It only makes sense in combination both embed_items and opac values true. - -=back - -=cut - -sub GetMarcBiblio { - my ($params) = @_; - - if (not defined $params) { - carp 'GetMarcBiblio called without parameters'; - return; - } - - my $biblionumber = $params->{biblionumber}; - my $embeditems = $params->{embed_items} || 0; - my $opac = $params->{opac} || 0; - my $borcat = $params->{borcat} // q{}; - - if (not defined $biblionumber) { - carp 'GetMarcBiblio called with undefined biblionumber'; - return; - } - - my $dbh = C4::Context->dbh; - my $sth = $dbh->prepare("SELECT biblioitemnumber FROM biblioitems WHERE biblionumber=? "); - $sth->execute($biblionumber); - my $row = $sth->fetchrow_hashref; - my $biblioitemnumber = $row->{'biblioitemnumber'}; - my $marcxml = GetXmlBiblio( $biblionumber ); - $marcxml = StripNonXmlChars( $marcxml ); - my $frameworkcode = GetFrameworkCode($biblionumber); - MARC::File::XML->default_record_format( C4::Context->preference('marcflavour') ); - my $record = MARC::Record->new(); - - if ($marcxml) { - $record = eval { - MARC::Record::new_from_xml( $marcxml, "UTF-8", - C4::Context->preference('marcflavour') ); - }; - if ($@) { warn " problem with :$biblionumber : $@ \n$marcxml"; } - return unless $record; - - C4::Biblio::_koha_marc_update_bib_ids( $record, $frameworkcode, $biblionumber, - $biblioitemnumber ); - C4::Biblio::EmbedItemsInMarcBiblio({ - marc_record => $record, - biblionumber => $biblionumber, - opac => $opac, - borcat => $borcat }) - if ($embeditems); - - return $record; - } - else { - return; - } -} - =head2 GetXmlBiblio my $marcxml = GetXmlBiblio($biblionumber); @@ -1217,7 +1236,7 @@ sub GetMarcPrice { my @listtags; my $subfield; - if ( $marcflavour eq "MARC21" || $marcflavour eq "NORMARC" ) { + if ( $marcflavour eq "MARC21" ) { @listtags = ('345', '020'); $subfield="c"; } elsif ( $marcflavour eq "UNIMARC" ) { @@ -1362,20 +1381,58 @@ descriptions rather than normal ones when they exist. sub GetAuthorisedValueDesc { my ( $tag, $subfield, $value, $framework, $tagslib, $category, $opac ) = @_; + return q{} unless defined($value); + + my $cache = Koha::Caches->get_instance(); + my $cache_key; if ( !$category ) { return $value unless defined $tagslib->{$tag}->{$subfield}->{'authorised_value'}; #---- branch if ( $tagslib->{$tag}->{$subfield}->{'authorised_value'} eq "branches" ) { - my $branch = Koha::Libraries->find($value); - return $branch? $branch->branchname: q{}; + $cache_key = "libraries:name"; + my $libraries = $cache->get_from_cache( $cache_key, { unsafe => 1 } ); + if ( !$libraries ) { + $libraries = { + map { $_->branchcode => $_->branchname } + Koha::Libraries->search( {}, + { columns => [ 'branchcode', 'branchname' ] } ) + ->as_list + }; + $cache->set_in_cache($cache_key, $libraries); + } + return $libraries->{$value}; } #---- itemtypes if ( $tagslib->{$tag}->{$subfield}->{'authorised_value'} eq "itemtypes" ) { - my $itemtype = Koha::ItemTypes->find( $value ); - return $itemtype ? $itemtype->translated_description : q||; + my $lang = C4::Languages::getlanguage; + $lang //= 'en'; + $cache_key = 'itemtype:description:' . $lang; + my $itypes = $cache->get_from_cache( $cache_key, { unsafe => 1 } ); + if ( !$itypes ) { + $itypes = + { map { $_->itemtype => $_->translated_description } + Koha::ItemTypes->search()->as_list }; + $cache->set_in_cache( $cache_key, $itypes ); + } + return $itypes->{$value}; + } + + if ( $tagslib->{$tag}->{$subfield}->{'authorised_value'} eq "cn_source" ) { + $cache_key = "cn_sources:description"; + my $cn_sources = $cache->get_from_cache( $cache_key, { unsafe => 1 } ); + if ( !$cn_sources ) { + $cn_sources = { + map { $_->cn_source => $_->description } + Koha::ClassSources->search( {}, + { columns => [ 'cn_source', 'description' ] } ) + ->as_list + }; + $cache->set_in_cache($cache_key, $cn_sources); + } + return $cn_sources->{$value}; } #---- "true" authorized value @@ -1384,10 +1441,25 @@ sub GetAuthorisedValueDesc { my $dbh = C4::Context->dbh; if ( $category ne "" ) { - my $sth = $dbh->prepare( "SELECT lib, lib_opac FROM authorised_values WHERE category = ? AND authorised_value = ?" ); - $sth->execute( $category, $value ); - my $data = $sth->fetchrow_hashref; - return ( $opac && $data->{'lib_opac'} ) ? $data->{'lib_opac'} : $data->{'lib'}; + $cache_key = "AV_descriptions:" . $category; + my $av_descriptions = $cache->get_from_cache( $cache_key, { unsafe => 1 } ); + if ( !$av_descriptions ) { + $av_descriptions = { + map { + $_->authorised_value => + { lib => $_->lib, lib_opac => $_->lib_opac } + } Koha::AuthorisedValues->search( + { category => $category }, + { + columns => [ 'authorised_value', 'lib_opac', 'lib' ] + } + )->as_list + }; + $cache->set_in_cache($cache_key, $av_descriptions); + } + return ( $opac && $av_descriptions->{$value}->{'lib_opac'} ) + ? $av_descriptions->{$value}->{'lib_opac'} + : $av_descriptions->{$value}->{'lib'}; } else { return $value; # if nothing is found return the original value } @@ -1408,9 +1480,9 @@ sub GetMarcControlnumber { return; } my $controlnumber = ""; - # Control number or Record identifier are the same field in MARC21, UNIMARC and NORMARC + # Control number or Record identifier are the same field in MARC21 and UNIMARC # Keep $marcflavour for possible later use - if ($marcflavour eq "MARC21" || $marcflavour eq "UNIMARC" || $marcflavour eq "NORMARC") { + if ($marcflavour eq "MARC21" || $marcflavour eq "UNIMARC" ) { my $controlnumberField = $record->field('001'); if ($controlnumberField) { $controlnumber = $controlnumberField->data(); @@ -1472,7 +1544,7 @@ sub GetMarcISSN { if ( $marcflavour eq "UNIMARC" ) { $scope = '011'; } - else { # assume MARC21 or NORMARC + else { # assume MARC21 $scope = '022'; } my @marcissns; @@ -1483,60 +1555,6 @@ sub GetMarcISSN { return \@marcissns; } # end GetMarcISSN -=head2 GetMarcNotes - - $marcnotesarray = GetMarcNotes( $record, $marcflavour ); - - Get all notes from the MARC record and returns them in an array. - The notes are stored in different fields depending on MARC flavour. - MARC21 5XX $u subfields receive special attention as they are URIs. - -=cut - -sub GetMarcNotes { - my ( $record, $marcflavour, $opac ) = @_; - if (!$record) { - carp 'GetMarcNotes called on undefined record'; - return; - } - - my $scope = $marcflavour eq "UNIMARC"? '3..': '5..'; - my @marcnotes; - - #MARC21 specs indicate some notes should be private if first indicator 0 - my %maybe_private = ( - 541 => 1, - 542 => 1, - 561 => 1, - 583 => 1, - 590 => 1 - ); - - my %hiddenlist = map { $_ => 1 } - split( /,/, C4::Context->preference('NotesToHide')); - foreach my $field ( $record->field($scope) ) { - my $tag = $field->tag(); - next if $hiddenlist{ $tag }; - next if $opac && $maybe_private{$tag} && !$field->indicator(1); - if( $marcflavour ne 'UNIMARC' && $field->subfield('u') ) { - # Field 5XX$u always contains URI - # Examples: 505u, 506u, 510u, 514u, 520u, 530u, 538u, 540u, 542u, 552u, 555u, 561u, 563u, 583u - # We first push the other subfields, then all $u's separately - # Leave further actions to the template (see e.g. opac-detail) - my $othersub = - join '', ( 'a' .. 't', 'v' .. 'z', '0' .. '9' ); # excl 'u' - push @marcnotes, { marcnote => $field->as_string($othersub) }; - foreach my $sub ( $field->subfield('u') ) { - $sub =~ s/^\s+|\s+$//g; # trim - push @marcnotes, { marcnote => $sub }; - } - } else { - push @marcnotes, { marcnote => $field->as_string() }; - } - } - return \@marcnotes; -} - =head2 GetMarcSubjects $marcsubjcts = GetMarcSubjects($record,$marcflavour); @@ -1557,7 +1575,7 @@ sub GetMarcSubjects { $mintag = "600"; $maxtag = "611"; $fields_filter = '6..'; - } else { # marc21/normarc + } else { # marc21 $mintag = "600"; $maxtag = "699"; $fields_filter = '6..'; @@ -1602,7 +1620,7 @@ sub GetMarcSubjects { push @link_loop, { limit => $subject_limit, 'link' => $linkvalue, - operator => (scalar @link_loop) ? ' and ' : undef + operator => (scalar @link_loop) ? ' AND ' : undef }; } my @this_link_loop = @link_loop; @@ -1626,105 +1644,6 @@ sub GetMarcSubjects { return \@marcsubjects; } #end getMARCsubjects -=head2 GetMarcAuthors - - authors = GetMarcAuthors($record,$marcflavour); - -Get all authors from the MARC record and returns them in an array. -The authors are stored in different fields depending on MARC flavour - -=cut - -sub GetMarcAuthors { - my ( $record, $marcflavour ) = @_; - if (!$record) { - carp 'GetMarcAuthors called on undefined record'; - return; - } - my ( $mintag, $maxtag, $fields_filter ); - - # tagslib useful only for UNIMARC author responsibilities - my $tagslib; - if ( $marcflavour eq "UNIMARC" ) { - # FIXME : we don't have the framework available, we take the default framework. May be buggy on some setups, will be usually correct. - $tagslib = GetMarcStructure( 1, '', { unsafe => 1 }); - $mintag = "700"; - $maxtag = "712"; - $fields_filter = '7..'; - } else { # marc21/normarc - $mintag = "700"; - $maxtag = "720"; - $fields_filter = '7..'; - } - - my @marcauthors; - my $AuthoritySeparator = C4::Context->preference('AuthoritySeparator'); - - foreach my $field ( $record->field($fields_filter) ) { - next unless $field->tag() >= $mintag && $field->tag() <= $maxtag; - my @subfields_loop; - my @link_loop; - my @subfields = $field->subfields(); - my $count_auth = 0; - - # if there is an authority link, build the link with Koha-Auth-Number: subfield9 - my $subfield9 = $field->subfield('9'); - if ($subfield9) { - my $linkvalue = $subfield9; - $linkvalue =~ s/(\(|\))//g; - @link_loop = ( { 'limit' => 'an', 'link' => $linkvalue } ); - } - - # other subfields - my $unimarc3; - for my $authors_subfield (@subfields) { - next if ( $authors_subfield->[0] eq '9' ); - - # unimarc3 contains the $3 of the author for UNIMARC. - # For french academic libraries, it's the "ppn", and it's required for idref webservice - $unimarc3 = $authors_subfield->[1] if $marcflavour eq 'UNIMARC' and $authors_subfield->[0] =~ /3/; - - # don't load unimarc subfields 3, 5 - next if ( $marcflavour eq 'UNIMARC' and ( $authors_subfield->[0] =~ /3|5/ ) ); - - my $code = $authors_subfield->[0]; - my $value = $authors_subfield->[1]; - my $linkvalue = $value; - $linkvalue =~ s/(\(|\))//g; - # UNIMARC author responsibility - if ( $marcflavour eq 'UNIMARC' and $code eq '4' ) { - $value = GetAuthorisedValueDesc( $field->tag(), $code, $value, '', $tagslib ); - $linkvalue = "($value)"; - } - # if no authority link, build a search query - unless ($subfield9) { - push @link_loop, { - limit => 'au', - 'link' => $linkvalue, - operator => (scalar @link_loop) ? ' and ' : undef - }; - } - my @this_link_loop = @link_loop; - # do not display $0 - unless ( $code eq '0') { - push @subfields_loop, { - tag => $field->tag(), - code => $code, - value => $value, - link_loop => \@this_link_loop, - separator => (scalar @subfields_loop) ? $AuthoritySeparator : '' - }; - } - } - push @marcauthors, { - MARCAUTHOR_SUBFIELDS_LOOP => \@subfields_loop, - authoritylink => $subfield9, - unimarc3 => $unimarc3 - }; - } - return \@marcauthors; -} - =head2 GetMarcUrls $marcurls = GetMarcUrls($record,$marcflavour); @@ -1806,7 +1725,7 @@ sub GetMarcSeries { $mintag = "225"; $maxtag = "225"; $fields_filter = '2..'; - } else { # marc21/normarc + } else { # marc21 $mintag = "440"; $maxtag = "490"; $fields_filter = '4..'; @@ -1839,7 +1758,7 @@ sub GetMarcSeries { push @link_loop, { 'link' => $linkvalue, - operator => (scalar @link_loop) ? ' and ' : undef + operator => (scalar @link_loop) ? ' AND ' : undef }; if ($volume_number) { @@ -1997,12 +1916,13 @@ This function returns a host field populated with data from the host record, the sub PrepHostMarcField { my ($hostbiblionumber,$hostitemnumber, $marcflavour) = @_; $marcflavour ||="MARC21"; - - my $hostrecord = GetMarcBiblio({ biblionumber => $hostbiblionumber }); + + my $biblio = Koha::Biblios->find($hostbiblionumber); + my $hostrecord = $biblio->metadata->record; my $item = Koha::Items->find($hostitemnumber); my $hostmarcfield; - if ( $marcflavour eq "MARC21" || $marcflavour eq "NORMARC" ) { + if ( $marcflavour eq "MARC21" ) { #main entry my $mainentry; @@ -2088,9 +2008,11 @@ sub TransformHtmlToXml { my ( $tags, $subfields, $values, $indicator, $ind_tag, $auth_type ) = @_; # NOTE: The parameter $ind_tag is NOT USED -- BZ 11247 + my ( $perm_loc_tag, $perm_loc_subfield ) = C4::Biblio::GetMarcFromKohaField( "items.permanent_location" ); + my $xml = MARC::File::XML::header('UTF-8'); $xml .= "\n"; - $auth_type = C4::Context->preference('marcflavour') unless $auth_type; + $auth_type = C4::Context->preference('marcflavour') unless $auth_type; # FIXME auth_type must be removed MARC::File::XML->default_record_format($auth_type); # in UNIMARC, field 100 contains the encoding @@ -2103,7 +2025,6 @@ sub TransformHtmlToXml { my $j = -1; my $close_last_tag; for ( my $i = 0 ; $i < @$tags ; $i++ ) { - if ( C4::Context->preference('marcflavour') eq 'UNIMARC' and @$tags[$i] eq "100" and @$subfields[$i] eq "a" ) { # if we have a 100 field and it's values are not correct, skip them. @@ -2121,6 +2042,13 @@ sub TransformHtmlToXml { @$values[$i] =~ s/"/"/g; @$values[$i] =~ s/'/'/g; + my $skip = @$values[$i] eq q{}; + $skip = 0 + if $perm_loc_tag + && $perm_loc_subfield + && @$tags[$i] eq $perm_loc_tag + && @$subfields[$i] eq $perm_loc_subfield; + if ( ( @$tags[$i] ne $prevtag ) ) { $close_last_tag = 0; $j++ unless ( @$tags[$i] eq "" ); @@ -2130,7 +2058,7 @@ sub TransformHtmlToXml { if ( !$first ) { $xml .= "\n"; if ( ( @$tags[$i] && @$tags[$i] > 10 ) - && ( @$values[$i] ne "" ) ) { + && ( !$skip ) ) { $xml .= "\n"; $xml .= "@$values[$i]\n"; $first = 0; @@ -2139,7 +2067,7 @@ sub TransformHtmlToXml { $first = 1; } } else { - if ( @$values[$i] ne "" ) { + if ( !$skip ) { # leader if ( @$tags[$i] eq "000" ) { @@ -2159,8 +2087,7 @@ sub TransformHtmlToXml { } } } else { # @$tags[$i] eq $prevtag - if ( @$values[$i] eq "" ) { - } else { + if ( !$skip ) { if ($first) { my $str = ( $indicator->[$j] // q{} ) . ' '; # extra space prevents substr outside of string warn my $ind1 = _default_ind_to_space( substr( $str, 0, 1 ) ); @@ -2331,7 +2258,7 @@ sub TransformHtmlToMarc { =head2 TransformMarcToKoha - $result = TransformMarcToKoha( $record, undef, $limit ) + $result = TransformMarcToKoha({ record => $record, limit_table => $limit }) Extract data from a MARC bib record into a hashref representing Koha biblio, biblioitems, and items fields. @@ -2342,9 +2269,11 @@ hash_ref. =cut sub TransformMarcToKoha { - my ( $record, $frameworkcode, $limit_table ) = @_; - # FIXME Parameter $frameworkcode is obsolete and will be removed - $limit_table //= q{}; + my ( $params ) = @_; + + my $record = $params->{record}; + my $limit_table = $params->{limit_table} // q{}; + my $kohafields = $params->{kohafields}; my $result = {}; if (!defined $record) { @@ -2355,18 +2284,41 @@ sub TransformMarcToKoha { my %tables = ( biblio => 1, biblioitems => 1, items => 1 ); if( $limit_table eq 'items' ) { %tables = ( items => 1 ); + } elsif ( $limit_table eq 'no_items' ){ + %tables = ( biblio => 1, biblioitems => 1 ); } # The next call acknowledges Default as the authoritative framework # for Koha to MARC mappings. my $mss = GetMarcSubfieldStructure( '', { unsafe => 1 } ); # Do not change framework - foreach my $kohafield ( keys %{ $mss } ) { + @{$kohafields} = keys %{ $mss } unless $kohafields; + foreach my $kohafield ( @{$kohafields} ) { my ( $table, $column ) = split /[.]/, $kohafield, 2; next unless $tables{$table}; - my $val = TransformMarcToKohaOneField( $kohafield, $record ); - next if !defined $val; + my ( $value, @values ); + foreach my $fldhash ( @{$mss->{$kohafield}} ) { + my $tag = $fldhash->{tagfield}; + my $sub = $fldhash->{tagsubfield}; + foreach my $fld ( $record->field($tag) ) { + if( $sub eq '@' || $fld->is_control_field ) { + push @values, $fld->data if $fld->data; + } else { + push @values, grep { $_ } $fld->subfield($sub); + } + } + } + if ( @values ){ + $value = join ' | ', uniq(@values); + + # Additional polishing for individual kohafields + if( $kohafield =~ /copyrightdate|publicationyear/ ) { + $value = _adjust_pubyear( $value ); + } + } + + next if !defined $value; my $key = _disambiguate( $table, $column ); - $result->{$key} = $val; + $result->{$key} = $value; } return $result; } @@ -2410,44 +2362,9 @@ sub _disambiguate { } -=head2 TransformMarcToKohaOneField - - $val = TransformMarcToKohaOneField( 'biblio.title', $marc ); - - Note: The authoritative Default framework is used implicitly. - -=cut - -sub TransformMarcToKohaOneField { - my ( $kohafield, $marc ) = @_; - - my ( @rv, $retval ); - my @mss = GetMarcSubfieldStructureFromKohaField($kohafield); - foreach my $fldhash ( @mss ) { - my $tag = $fldhash->{tagfield}; - my $sub = $fldhash->{tagsubfield}; - foreach my $fld ( $marc->field($tag) ) { - if( $sub eq '@' || $fld->is_control_field ) { - push @rv, $fld->data if $fld->data; - } else { - push @rv, grep { $_ } $fld->subfield($sub); - } - } - } - return unless @rv; - $retval = join ' | ', uniq(@rv); - - # Additional polishing for individual kohafields - if( $kohafield =~ /copyrightdate|publicationyear/ ) { - $retval = _adjust_pubyear( $retval ); - } - - return $retval; -} - =head2 _adjust_pubyear - Helper routine for TransformMarcToKohaOneField + Helper routine for TransformMarcToKoha =cut @@ -2458,16 +2375,13 @@ sub _adjust_pubyear { $retval = $1; } elsif( $retval =~ m/(\d\d\d\d)/ && $1 > 0 ) { $retval = $1; - } elsif( $retval =~ m/ - (?\d)[-]?[.Xx?]{3} - |(?\d{2})[.Xx?]{2} - |(?\d{3})[.Xx?] - |(?\d)[-]{3}\? - |(?\d\d)[-]{2}\? - |(?\d{3})[-]\? - /xms ) { # the form 198-? occurred in Dutch ISBD rules - my $digits = $+{year}; - $retval = $digits * ( 10 ** ( 4 - length($digits) )); + } elsif( $retval =~ m/(?\d{1,3})[.Xx?-]/ ) { + # See also bug 24674: enough to look at one unknown year char like .Xx-? + # At this point in code 1234? or 1234- already passed the earlier regex + # Things like 2-, 1xx, 1??? are now converted to a four positions-year. + $retval = $+{year} * ( 10 ** (4-length($+{year})) ); + } else { + $retval = undef; } return $retval; } @@ -2489,50 +2403,19 @@ sub CountItemsIssued { =head2 ModZebra - ModZebra( $biblionumber, $op, $server, $record ); + ModZebra( $record_number, $op, $server ); -$biblionumber is the biblionumber we want to index +$record_number is the authid or biblionumber we want to index -$op is specialUpdate or recordDelete, and is used to know what we want to do +$op is the operation: specialUpdate or recordDelete -$server is the server that we want to update - -$record is the updated MARC record. If it's not supplied -and is needed it will be loaded from the database. +$server is authorityserver or biblioserver =cut sub ModZebra { -###Accepts a $server variable thus we can use it for biblios authorities or other zebra dbs - my ( $biblionumber, $op, $server, $record ) = @_; - $debug && warn "ModZebra: update requested for: $biblionumber $op $server\n"; - if ( C4::Context->preference('SearchEngine') eq 'Elasticsearch' ) { - - # TODO abstract to a standard API that'll work for whatever - require Koha::SearchEngine::Elasticsearch::Indexer; - my $indexer = Koha::SearchEngine::Elasticsearch::Indexer->new( - { - index => $server eq 'biblioserver' - ? $Koha::SearchEngine::BIBLIOS_INDEX - : $Koha::SearchEngine::AUTHORITIES_INDEX - } - ); - if ( $op eq 'specialUpdate' ) { - unless ($record) { - $record = GetMarcBiblio({ - biblionumber => $biblionumber, - embed_items => 1 }); - } - $indexer->update_index_background( [$biblionumber], [$record] ); - } - elsif ( $op eq 'recordDelete' ) { - $indexer->delete_index_background( [$biblionumber] ); - } - else { - croak "ModZebra called with unknown operation: $op"; - } - } - + my ( $record_number, $op, $server ) = @_; + Koha::Logger->get->debug("ModZebra: updates requested for: $record_number $op $server"); my $dbh = C4::Context->dbh; # true ModZebra commented until indexdata fixes zebraDB crashes (it seems they occur on multiple updates @@ -2545,96 +2428,16 @@ sub ModZebra { AND operation = ? AND done = 0"; my $check_sth = $dbh->prepare_cached($check_sql); - $check_sth->execute( $server, $biblionumber, $op ); + $check_sth->execute( $server, $record_number, $op ); my ($count) = $check_sth->fetchrow_array; $check_sth->finish(); if ( $count == 0 ) { my $sth = $dbh->prepare("INSERT INTO zebraqueue (biblio_auth_number,server,operation) VALUES(?,?,?)"); - $sth->execute( $biblionumber, $server, $op ); + $sth->execute( $record_number, $server, $op ); $sth->finish; } } - -=head2 EmbedItemsInMarcBiblio - - EmbedItemsInMarcBiblio({ - marc_record => $marc, - biblionumber => $biblionumber, - item_numbers => $itemnumbers, - opac => $opac }); - -Given a MARC::Record object containing a bib record, -modify it to include the items attached to it as 9XX -per the bib's MARC framework. -if $itemnumbers is defined, only specified itemnumbers are embedded. - -If $opac is true, then opac-relevant suppressions are included. - -If opac filtering will be done, borcat should be passed to properly -override if necessary. - -=cut - -sub EmbedItemsInMarcBiblio { - my ($params) = @_; - my ($marc, $biblionumber, $itemnumbers, $opac, $borcat); - $marc = $params->{marc_record}; - if ( !$marc ) { - carp 'EmbedItemsInMarcBiblio: No MARC record passed'; - return; - } - $biblionumber = $params->{biblionumber}; - $itemnumbers = $params->{item_numbers}; - $opac = $params->{opac}; - $borcat = $params->{borcat} // q{}; - - $itemnumbers = [] unless defined $itemnumbers; - - my $frameworkcode = GetFrameworkCode($biblionumber); - _strip_item_fields($marc, $frameworkcode); - - # ... and embed the current items - my $dbh = C4::Context->dbh; - my $sth = $dbh->prepare("SELECT itemnumber FROM items WHERE biblionumber = ?"); - $sth->execute($biblionumber); - my ( $itemtag, $itemsubfield ) = GetMarcFromKohaField( "items.itemnumber" ); - - my @item_fields; # Array holding the actual MARC data for items to be included. - my @items; # Array holding items which are both in the list (sitenumbers) - # and on this biblionumber - - # Flag indicating if there is potential hiding. - my $opachiddenitems = $opac - && ( C4::Context->preference('OpacHiddenItems') !~ /^\s*$/ ); - - require C4::Items; - while ( my ($itemnumber) = $sth->fetchrow_array ) { - next if @$itemnumbers and not grep { $_ == $itemnumber } @$itemnumbers; - my $item; - if ( $opachiddenitems ) { - $item = Koha::Items->find($itemnumber); - $item = $item ? $item->unblessed : undef; - } - push @items, { itemnumber => $itemnumber, item => $item }; - } - my @items2pass = map { $_->{item} } @items; - my @hiddenitems = - $opachiddenitems - ? C4::Items::GetHiddenItemnumbers({ - items => \@items2pass, - borcat => $borcat }) - : (); - # Convert to a hash for quick searching - my %hiddenitems = map { $_ => 1 } @hiddenitems; - foreach my $itemnumber ( map { $_->{itemnumber} } @items ) { - next if $hiddenitems{$itemnumber}; - my $item_marc = C4::Items::GetMarcItem( $biblionumber, $itemnumber ); - push @item_fields, $item_marc->field($itemtag); - } - $marc->append_fields(@item_fields); -} - =head1 INTERNAL FUNCTIONS =head2 _koha_marc_update_bib_ids @@ -2665,6 +2468,13 @@ sub _koha_marc_update_bib_ids { } else { C4::Biblio::UpsertMarcSubfield($record, $biblioitem_tag, $biblioitem_subfield, $biblioitemnumber); } + + # update the control number (001) in MARC + if(C4::Context->preference('autoControlNumber') eq 'biblionumber'){ + unless($record->field('001')){ + $record->insert_fields_ordered(MARC::Field->new('001', $biblionumber)); + } + } } =head2 _koha_marc_update_biblioitem_cn_sort @@ -2702,61 +2512,6 @@ sub _koha_marc_update_biblioitem_cn_sort { } } -=head2 _koha_add_biblio - - my ($biblionumber,$error) = _koha_add_biblio($dbh,$biblioitem); - -Internal function to add a biblio ($biblio is a hash with the values) - -=cut - -sub _koha_add_biblio { - my ( $dbh, $biblio, $frameworkcode ) = @_; - - my $error; - - # set the series flag - unless (defined $biblio->{'serial'}){ - $biblio->{'serial'} = 0; - if ( $biblio->{'seriestitle'} ) { $biblio->{'serial'} = 1 } - } - - my $query = "INSERT INTO biblio - SET frameworkcode = ?, - author = ?, - title = ?, - subtitle = ?, - medium = ?, - part_number = ?, - part_name = ?, - unititle =?, - notes = ?, - serial = ?, - seriestitle = ?, - copyrightdate = ?, - datecreated=NOW(), - abstract = ? - "; - my $sth = $dbh->prepare($query); - $sth->execute( - $frameworkcode, $biblio->{'author'}, $biblio->{'title'}, $biblio->{'subtitle'}, - $biblio->{'medium'}, $biblio->{'part_number'}, $biblio->{'part_name'}, $biblio->{'unititle'}, - $biblio->{'notes'}, $biblio->{'serial'}, $biblio->{'seriestitle'}, $biblio->{'copyrightdate'}, - $biblio->{'abstract'} - ); - - my $biblionumber = $dbh->{'mysql_insertid'}; - if ( $dbh->errstr ) { - $error .= "ERROR in _koha_add_biblio $query" . $dbh->errstr; - warn $error; - } - - $sth->finish(); - - #warn "LEAVING _koha_add_biblio: ".$biblionumber."\n"; - return ( $biblionumber, $error ); -} - =head2 _koha_modify_biblio my ($biblionumber,$error) == _koha_modify_biblio($dbh,$biblio,$frameworkcode); @@ -2867,72 +2622,6 @@ sub _koha_modify_biblioitem_nonmarc { return ( $biblioitem->{'biblioitemnumber'}, $error ); } -=head2 _koha_add_biblioitem - - my ($biblioitemnumber,$error) = _koha_add_biblioitem( $dbh, $biblioitem ); - -Internal function to add a biblioitem - -=cut - -sub _koha_add_biblioitem { - my ( $dbh, $biblioitem ) = @_; - my $error; - - my ($cn_sort) = GetClassSort( $biblioitem->{'biblioitems.cn_source'}, $biblioitem->{'cn_class'}, $biblioitem->{'cn_item'} ); - my $query = "INSERT INTO biblioitems SET - biblionumber = ?, - volume = ?, - number = ?, - itemtype = ?, - isbn = ?, - issn = ?, - publicationyear = ?, - publishercode = ?, - volumedate = ?, - volumedesc = ?, - collectiontitle = ?, - collectionissn = ?, - collectionvolume= ?, - editionstatement= ?, - editionresponsibility = ?, - illus = ?, - pages = ?, - notes = ?, - size = ?, - place = ?, - lccn = ?, - url = ?, - cn_source = ?, - cn_class = ?, - cn_item = ?, - cn_suffix = ?, - cn_sort = ?, - totalissues = ?, - ean = ?, - agerestriction = ? - "; - my $sth = $dbh->prepare($query); - $sth->execute( - $biblioitem->{'biblionumber'}, $biblioitem->{'volume'}, $biblioitem->{'number'}, $biblioitem->{'itemtype'}, - $biblioitem->{'isbn'}, $biblioitem->{'issn'}, $biblioitem->{'publicationyear'}, $biblioitem->{'publishercode'}, - $biblioitem->{'volumedate'}, $biblioitem->{'volumedesc'}, $biblioitem->{'collectiontitle'}, $biblioitem->{'collectionissn'}, - $biblioitem->{'collectionvolume'}, $biblioitem->{'editionstatement'}, $biblioitem->{'editionresponsibility'}, $biblioitem->{'illus'}, - $biblioitem->{'pages'}, $biblioitem->{'bnotes'}, $biblioitem->{'size'}, $biblioitem->{'place'}, - $biblioitem->{'lccn'}, $biblioitem->{'url'}, $biblioitem->{'biblioitems.cn_source'}, - $biblioitem->{'cn_class'}, $biblioitem->{'cn_item'}, $biblioitem->{'cn_suffix'}, $cn_sort, - $biblioitem->{'totalissues'}, $biblioitem->{'ean'}, $biblioitem->{'agerestriction'} - ); - my $bibitemnum = $dbh->{'mysql_insertid'}; - - if ( $dbh->errstr ) { - $error .= "ERROR in _koha_add_biblioitem $query" . $dbh->errstr; - warn $error; - } - $sth->finish(); - return ( $bibitemnum, $error ); -} - =head2 _koha_delete_biblio $error = _koha_delete_biblio($dbh,$biblionumber); @@ -3065,7 +2754,7 @@ sub _koha_delete_biblio_metadata { =head2 ModBiblioMarc - &ModBiblioMarc($newrec,$biblionumber,$frameworkcode); + ModBiblioMarc($newrec,$biblionumber); Add MARC XML data for a biblio to koha @@ -3076,22 +2765,18 @@ Function exported, but should NOT be used, unless you really know what you're do sub ModBiblioMarc { # pass the MARC::Record to this function, and it will create the records in # the marcxml field - my ( $record, $biblionumber, $frameworkcode ) = @_; + my ( $record, $biblionumber, $params ) = @_; if ( !$record ) { carp 'ModBiblioMarc passed an undefined record'; return; } + my $skip_record_index = $params->{skip_record_index} || 0; + # Clone record as it gets modified $record = $record->clone(); my $dbh = C4::Context->dbh; my @fields = $record->fields(); - if ( !$frameworkcode ) { - $frameworkcode = ""; - } - my $sth = $dbh->prepare("UPDATE biblio SET frameworkcode=? WHERE biblionumber=?"); - $sth->execute( $frameworkcode, $biblionumber ); - $sth->finish; my $encoding = C4::Context->preference("marcflavour"); # deal with UNIMARC field 100 (encoding) : create it if needed & set encoding to unicode @@ -3147,7 +2832,10 @@ sub ModBiblioMarc { $m_rs->metadata( $record->as_xml_record($encoding) ); $m_rs->store; - ModZebra( $biblionumber, "specialUpdate", "biblioserver" ); + unless ( $skip_record_index ) { + my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX }); + $indexer->index_records( $biblionumber, "specialUpdate", "biblioserver" ); + } return $biblionumber; } @@ -3162,13 +2850,15 @@ Generate the host item entry for an analytic child entry sub prepare_host_field { my ( $hostbiblio, $marcflavour ) = @_; $marcflavour ||= C4::Context->preference('marcflavour'); - my $host = GetMarcBiblio({ biblionumber => $hostbiblio }); + + my $biblio = Koha::Biblios->find($hostbiblio); + my $host = $biblio->metadata->record; # unfortunately as_string does not 'do the right thing' # if field returns undef my %sfd; my $field; my $host_field; - if ( $marcflavour eq 'MARC21' || $marcflavour eq 'NORMARC' ) { + if ( $marcflavour eq 'MARC21' ) { if ( $field = $host->field('100') || $host->field('110') || $host->field('11') ) { my $s = $field->as_string('ab'); if ($s) { @@ -3297,17 +2987,18 @@ Update the total issue count for a particular bib record. =cut sub UpdateTotalIssues { - my ($biblionumber, $increase, $value) = @_; + my ($biblionumber, $increase, $value, $skip_holds_queue) = @_; my $totalissues; - my $record = GetMarcBiblio({ biblionumber => $biblionumber }); - unless ($record) { - carp "UpdateTotalIssues could not get biblio record"; + my $biblio = Koha::Biblios->find($biblionumber); + unless ($biblio) { + carp "UpdateTotalIssues could not get biblio"; return; } - my $biblio = Koha::Biblios->find( $biblionumber ); - unless ($biblio) { - carp "UpdateTotalIssues could not get datas of biblio"; + + my $record = $biblio->metadata->record; + unless ($record) { + carp "UpdateTotalIssues could not get biblio record"; return; } my $biblioitem = $biblio->biblioitem; @@ -3331,7 +3022,7 @@ sub UpdateTotalIssues { $record->insert_grouped_field($field); } - return ModBiblio($record, $biblionumber, $biblio->frameworkcode); + return ModBiblio($record, $biblionumber, $biblio->frameworkcode, { skip_holds_queue => $skip_holds_queue }); } =head2 RemoveAllNsb @@ -3382,8 +3073,66 @@ sub RemoveAllNsb { return $record; } -1; +=head2 ApplyMarcOverlayRules + + my $record = ApplyMarcOverlayRules($params) + +Applies marc merge rules to a record. + +C<$params> is expected to be a hashref with below keys defined. + +=over 4 + +=item C +biblionumber of old record + +=item C +Incoming record that will be merged with old record +=item C +hashref containing at least one context module and filter value on +the form {module => filter, ...}. + +=back + +Returns: + +=over 4 + +=item C<$record> + +Merged MARC record based with merge rules for C applied. If no old +record for C can be found, C is returned unchanged. +Default action when no matching context is found to return C unchanged. +If no rules are found for a certain field tag the default is to overwrite with +fields with this field tag from C. + +=back + +=cut + +sub ApplyMarcOverlayRules { + my ($params) = @_; + my $biblionumber = $params->{biblionumber}; + my $incoming_record = $params->{record}; + + if (!$biblionumber) { + carp 'ApplyMarcOverlayRules called on undefined biblionumber'; + return; + } + if (!$incoming_record) { + carp 'ApplyMarcOverlayRules called on undefined record'; + return; + } + my $biblio = Koha::Biblios->find($biblionumber); + my $old_record = $biblio->metadata->record; + + # Skip overlay rules if called with no context + if ($old_record && defined $params->{overlay_context}) { + return Koha::MarcOverlayRules->merge_records($old_record, $incoming_record, $params->{overlay_context}); + } + return $incoming_record; +} =head2 _after_biblio_action_hooks @@ -3408,6 +3157,8 @@ sub _after_biblio_action_hooks { ); } +1; + __END__ =head1 AUTHOR