X-Git-Url: http://koha-dev.rot13.org:8081/gitweb/?a=blobdiff_plain;f=C4%2FBiblio.pm;h=7a49f47df7c53b2b71155ce2a049346e3eec7026;hb=e96f32b29c64c635cfdc4c23192b279fa6e5c9d3;hp=0a50a32c0a4909effeca676cd1a1b169817dcd58;hpb=b387502eaf23235cc5faa75c5bf1463529b20b1c;p=srvgit diff --git a/C4/Biblio.pm b/C4/Biblio.pm index 0a50a32c0a..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,46 +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 @@ -180,44 +185,38 @@ 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; - } my $schema = Koha::Database->schema; my ( $biblionumber, $biblioitemnumber ); try { $schema->txn_do(sub { - if (C4::Context->preference('BiblioAddsAuthorities')) { - BiblioAutoLink( $record, $frameworkcode ); - } - # transform the data into koha-table style data SetUTF8Flag($record); - my $olddata = TransformMarcToKoha( $record, $frameworkcode ); + my $olddata = TransformMarcToKoha({ record => $record, limit_table => 'no_items' }); my $biblio = Koha::Biblio->new( { @@ -284,8 +283,12 @@ sub AddBiblio { # update MARC subfield that stores biblioitems.cn_sort _koha_marc_update_biblioitem_cn_sort( $record, $olddata, $frameworkcode ); - # now add the record - ModBiblioMarc( $record, $biblionumber ) unless $defer_marc_save; + if (C4::Context->preference('AutoLinkBiblios')) { + BiblioAutoLink( $record, $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; # update OAI-PMH sets if(C4::Context->preference("OAI-PMH:AutoUpdateSets")) { @@ -295,7 +298,13 @@ sub AddBiblio { _after_biblio_action_hooks({ action => 'create', biblio_id => $biblionumber }); 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 ); @@ -305,7 +314,7 @@ sub AddBiblio { =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 @@ -321,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 ); } @@ -362,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 @@ -372,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 ); + ModBiblioMarc( $record, $biblionumber, { skip_record_index => $skip_record_index } ); # modify the other koha tables _koha_modify_biblio( $dbh, $oldbiblio, $frameworkcode ); @@ -391,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; } @@ -419,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) @@ -428,6 +481,15 @@ 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 { @@ -453,7 +515,8 @@ 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 }); } unless ( $params->{skip_record_index} ){ @@ -482,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; } @@ -645,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 = @@ -655,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; @@ -715,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 @@ -948,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; @@ -960,53 +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, display_order - 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; - my $display_order; - - while ( - ( $tag, $subfield, $liblibrarian, $libopac, $tab, $mandatory, $repeatable, $authorised_value, - $authtypecode, $value_builder, $kohafield, $seealso, $hidden, $isurl, $link, $defaultvalue, - $maxlength, $important, $display_order - ) - = $sth->fetchrow - ) { - $res->{$tag}->{$subfield}->{lib} = ( $forlibrarian or !$libopac ) ? $liblibrarian : $libopac; - $res->{$tag}->{$subfield}->{tab} = $tab; - $res->{$tag}->{$subfield}->{subfield} = $subfield; - $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; - $res->{$tag}->{$subfield}->{display_order} = $display_order; + 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); @@ -1164,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); @@ -1302,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" ) { @@ -1447,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 @@ -1469,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 } @@ -1493,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(); @@ -1557,7 +1544,7 @@ sub GetMarcISSN { if ( $marcflavour eq "UNIMARC" ) { $scope = '011'; } - else { # assume MARC21 or NORMARC + else { # assume MARC21 $scope = '022'; } my @marcissns; @@ -1568,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); @@ -1642,7 +1575,7 @@ sub GetMarcSubjects { $mintag = "600"; $maxtag = "611"; $fields_filter = '6..'; - } else { # marc21/normarc + } else { # marc21 $mintag = "600"; $maxtag = "699"; $fields_filter = '6..'; @@ -1687,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; @@ -1711,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); @@ -1891,7 +1725,7 @@ sub GetMarcSeries { $mintag = "225"; $maxtag = "225"; $fields_filter = '2..'; - } else { # marc21/normarc + } else { # marc21 $mintag = "440"; $maxtag = "490"; $fields_filter = '4..'; @@ -1924,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) { @@ -2082,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; @@ -2173,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 @@ -2188,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. @@ -2206,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 "" ); @@ -2215,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; @@ -2224,7 +2067,7 @@ sub TransformHtmlToXml { $first = 1; } } else { - if ( @$values[$i] ne "" ) { + if ( !$skip ) { # leader if ( @$tags[$i] eq "000" ) { @@ -2244,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 ) ); @@ -2416,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. @@ -2427,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) { @@ -2440,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; } @@ -2495,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 @@ -2543,16 +2375,11 @@ 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; } @@ -2588,7 +2415,7 @@ $server is authorityserver or biblioserver sub ModZebra { my ( $record_number, $op, $server ) = @_; - $debug && warn "ModZebra: updates requested for: $record_number $op $server\n"; + 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 @@ -2611,85 +2438,6 @@ sub ModZebra { } } -=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 @@ -2720,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 @@ -2999,7 +2754,7 @@ sub _koha_delete_biblio_metadata { =head2 ModBiblioMarc - &ModBiblioMarc($newrec,$biblionumber); + ModBiblioMarc($newrec,$biblionumber); Add MARC XML data for a biblio to koha @@ -3010,12 +2765,14 @@ 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 ) = @_; + 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; @@ -3075,8 +2832,10 @@ sub ModBiblioMarc { $m_rs->metadata( $record->as_xml_record($encoding) ); $m_rs->store; - my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX }); - $indexer->index_records( $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; } @@ -3091,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) { @@ -3226,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; @@ -3260,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 @@ -3311,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 @@ -3337,6 +3157,8 @@ sub _after_biblio_action_hooks { ); } +1; + __END__ =head1 AUTHOR