Bug 30920: (follow-up) Cleanup warning from C4::Biblio::GetAuthorisedValueDesc
[koha-ffzg.git] / C4 / Biblio.pm
index 9eb3696..e90eb11 100644 (file)
@@ -21,24 +21,20 @@ 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
-        GetMarcHosts
         GetMarcUrls
         GetUsedMarcStructure
         GetXmlBiblio
@@ -47,6 +43,7 @@ BEGIN {
         GetMarcQuantity
         GetAuthorisedValueDesc
         GetMarcStructure
+        GetMarcSubfieldStructure
         IsMarcStructureInternal
         GetMarcFromKohaField
         GetMarcSubfieldStructureFromKohaField
@@ -61,6 +58,7 @@ BEGIN {
         DelBiblio
         BiblioAutoLink
         LinkBibHeadingsToAuthorities
+        ApplyMarcOverlayRules
         TransformMarcToKoha
         TransformHtmlToMarc
         TransformHtmlToXml
@@ -71,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
@@ -180,68 +185,130 @@ The first argument is a C<MARC::Record> 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<defer_marc_save>,
-which if present and mapped to a true value, causes C<AddBiblio>
-to omit the call to save the MARC in C<biblio_metadata.metadata>
-This option is provided B<only>
-for the use of scripts such as C<bulkmarcimport.pl> 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<ModBiblioMarc> will be called.
+The C<$options> argument is a hashref with additional parameters:
+
+=over 4
+
+=item B<defer_marc_save>: used when ModBiblioMarc is handled by the caller
+
+=item B<skip_record_index>: 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 ( $biblionumber, $biblioitemnumber, $error );
-    my $dbh = C4::Context->dbh;
+    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;
 
-    # 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 );
+            _koha_marc_update_bib_ids( $record, $frameworkcode, $biblionumber, $biblioitemnumber );
 
-    _koha_marc_update_bib_ids( $record, $frameworkcode, $biblionumber, $biblioitemnumber );
+            # update MARC subfield that stores biblioitems.cn_sort
+            _koha_marc_update_biblioitem_cn_sort( $record, $olddata, $frameworkcode );
 
-    # update MARC subfield that stores biblioitems.cn_sort
-    _koha_marc_update_biblioitem_cn_sort( $record, $olddata, $frameworkcode );
+            if (C4::Context->preference('AutoLinkBiblios')) {
+                BiblioAutoLink( $record, $frameworkcode );
+            }
 
-    # now add the record
-    ModBiblioMarc( $record, $biblionumber, $frameworkcode ) unless $defer_marc_save;
+            # now add the record
+            ModBiblioMarc( $record, $biblionumber, { skip_record_index => $skip_record_index } ) 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");
+        });
+    } 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
@@ -257,27 +324,50 @@ in the C<biblio> and C<biblioitems> 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<overlay_context>
+
+This parameter is forwarded to L</ApplyMarcOverlayRules> where it is used for
+selecting the current rule set if MARCOverlayRules is enabled.
+See L</ApplyMarcOverlayRules> for more details.
+
+=item C<disable_autolink>
+
+Unless C<disable_autolink> 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<skip_holds_queue>
+
+Unless C<skip_holds_queue> is passed, ModBiblio will trigger the BatchUpdateBiblioHoldsQueue
+task to rebuild the holds queue for the biblio if I<RealTimeHoldsQueue> 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 );
     }
 
@@ -298,6 +388,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
@@ -308,13 +413,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 );
@@ -327,6 +432,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;
 }
 
@@ -355,7 +466,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)
@@ -364,10 +475,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<skip_holds_queue>: used when the holds queue update will be handled by the caller
+
+=item B<skip_record_index>: 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?
@@ -389,14 +509,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=?");
@@ -419,6 +539,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;
 }
 
@@ -436,6 +562,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;
@@ -453,15 +580,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<MARC::Record>
@@ -483,6 +610,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';
@@ -494,6 +623,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;
 
@@ -503,30 +635,37 @@ 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;
         }
 
-        my ( $authid, $fuzzy ) = $linker->get_link($heading);
+        my ( $authid, $fuzzy, $match_count ) = $linker->get_link($heading);
         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 ) ) {
                     $results{'linked'}->{ $heading->display_form() }++;
                 }
-                else {
+                elsif ( !$match_count ) {
                     my $authority_type = Koha::Authority::Types->find( $heading->auth_type() );
                     my $marcrecordauth = MARC::Record->new();
                     if ( C4::Context->preference('marcflavour') eq 'MARC21' ) {
@@ -569,7 +708,7 @@ sub LinkBibHeadingsToAuthorities {
                         $marcrecordauth->insert_fields_ordered(
                             MARC::Field->new(
                                 '667', '', '',
-                                'a' => "Machine generated authority record."
+                                'a' => C4::Context->preference('GenerateAuthorityField667')
                             )
                         );
                         my $cite =
@@ -579,7 +718,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;
@@ -596,24 +735,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;
 }
 
@@ -634,9 +778,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
@@ -867,7 +1009,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;
@@ -879,50 +1021,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);
@@ -950,7 +1055,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);
@@ -1015,7 +1120,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 = {};
@@ -1080,99 +1185,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<undef> 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);
@@ -1218,7 +1230,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" ) {
@@ -1363,20 +1375,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 = "LibraryNames";
+            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 = $lang . 'ItemTypeDescriptions';
+            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
@@ -1385,10 +1435,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 = "AVDescriptions-" . $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
     }
@@ -1409,9 +1474,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();
@@ -1473,7 +1538,7 @@ sub GetMarcISSN {
     if ( $marcflavour eq "UNIMARC" ) {
         $scope = '011';
     }
-    else {    # assume MARC21 or NORMARC
+    else {    # assume MARC21
         $scope = '022';
     }
     my @marcissns;
@@ -1484,60 +1549,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 %blacklist = map { $_ => 1 }
-        split( /,/, C4::Context->preference('NotesBlacklist'));
-    foreach my $field ( $record->field($scope) ) {
-        my $tag = $field->tag();
-        next if $blacklist{ $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);
@@ -1558,7 +1569,7 @@ sub GetMarcSubjects {
         $mintag = "600";
         $maxtag = "611";
         $fields_filter = '6..';
-    } else { # marc21/normarc
+    } else { # marc21
         $mintag = "600";
         $maxtag = "699";
         $fields_filter = '6..';
@@ -1603,7 +1614,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;
@@ -1627,105 +1638,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);
@@ -1807,7 +1719,7 @@ sub GetMarcSeries {
         $mintag = "225";
         $maxtag = "225";
         $fields_filter = '2..';
-    } else {    # marc21/normarc
+    } else {    # marc21
         $mintag = "440";
         $maxtag = "490";
         $fields_filter = '4..';
@@ -1840,7 +1752,7 @@ sub GetMarcSeries {
 
             push @link_loop, {
                 'link' => $linkvalue,
-                operator => (scalar @link_loop) ? ' and ' : undef
+                operator => (scalar @link_loop) ? ' AND ' : undef
             };
 
             if ($volume_number) {
@@ -1861,53 +1773,6 @@ sub GetMarcSeries {
     return \@marcseries;
 }    #end getMARCseriess
 
-=head2 GetMarcHosts
-
-  $marchostsarray = GetMarcHosts($record,$marcflavour);
-
-Get all host records (773s MARC21, 461 UNIMARC) from the MARC record and returns them in an array.
-
-=cut
-
-sub GetMarcHosts {
-    my ( $record, $marcflavour ) = @_;
-    if (!$record) {
-        carp 'GetMarcHosts called on undefined record';
-        return;
-    }
-
-    my ( $tag,$title_subf,$bibnumber_subf,$itemnumber_subf);
-    $marcflavour ||="MARC21";
-    if ( $marcflavour eq "MARC21" || $marcflavour eq "NORMARC" ) {
-        $tag = "773";
-        $title_subf = "t";
-        $bibnumber_subf ="0";
-        $itemnumber_subf='9';
-    }
-    elsif ($marcflavour eq "UNIMARC") {
-        $tag = "461";
-        $title_subf = "t";
-        $bibnumber_subf ="0";
-        $itemnumber_subf='9';
-    };
-
-    my @marchosts;
-
-    foreach my $field ( $record->field($tag)) {
-
-        my @fields_loop;
-
-        my $hostbiblionumber = $field->subfield("$bibnumber_subf");
-        my $hosttitle = $field->subfield($title_subf);
-        my $hostitemnumber=$field->subfield($itemnumber_subf);
-        push @fields_loop, { hostbiblionumber => $hostbiblionumber, hosttitle => $hosttitle, hostitemnumber => $hostitemnumber};
-        push @marchosts, { MARCHOSTS_FIELDS_LOOP => \@fields_loop };
-
-        }
-    my $marchostsarray = \@marchosts;
-    return $marchostsarray;
-}
-
 =head2 UpsertMarcSubfield
 
     my $record = C4::Biblio::UpsertMarcSubfield($MARC::Record, $fieldTag, $subfieldCode, $subfieldContent);
@@ -2045,12 +1910,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;
@@ -2136,9 +2002,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 .= "<record>\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
@@ -2146,13 +2014,11 @@ sub TransformHtmlToXml {
     # MARC::Record->new_from_xml will fail (and Koha will die)
     my $unimarc_and_100_exist = 0;
     $unimarc_and_100_exist = 1 if $auth_type eq 'ITEM';    # if we rebuild an item, no need of a 100 field
-    my $prevvalue;
     my $prevtag = -1;
     my $first   = 1;
     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.
@@ -2170,6 +2036,13 @@ sub TransformHtmlToXml {
         @$values[$i] =~ s/"/&quot;/g;
         @$values[$i] =~ s/'/&apos;/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 "" );
@@ -2179,7 +2052,7 @@ sub TransformHtmlToXml {
             if ( !$first ) {
                 $xml .= "</datafield>\n";
                 if (   ( @$tags[$i] && @$tags[$i] > 10 )
-                    && ( @$values[$i] ne "" ) ) {
+                    && ( !$skip ) ) {
                     $xml .= "<datafield tag=\"@$tags[$i]\" ind1=\"$ind1\" ind2=\"$ind2\">\n";
                     $xml .= "<subfield code=\"@$subfields[$i]\">@$values[$i]</subfield>\n";
                     $first = 0;
@@ -2188,7 +2061,7 @@ sub TransformHtmlToXml {
                     $first = 1;
                 }
             } else {
-                if ( @$values[$i] ne "" ) {
+                if ( !$skip ) {
 
                     # leader
                     if ( @$tags[$i] eq "000" ) {
@@ -2208,8 +2081,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 ) );
@@ -2380,7 +2252,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.
@@ -2391,9 +2263,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) {
@@ -2404,18 +2278,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;
 }
@@ -2459,44 +2356,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
 
@@ -2507,16 +2369,13 @@ sub _adjust_pubyear {
         $retval = $1;
     } elsif( $retval =~ m/(\d\d\d\d)/ && $1 > 0 ) {
         $retval = $1;
-    } elsif( $retval =~ m/
-             (?<year>\d)[-]?[.Xx?]{3}
-            |(?<year>\d{2})[.Xx?]{2}
-            |(?<year>\d{3})[.Xx?]
-            |(?<year>\d)[-]{3}\?
-            |(?<year>\d\d)[-]{2}\?
-            |(?<year>\d{3})[-]\?
-    /xms ) { # the form 198-? occurred in Dutch ISBD rules
-        my $digits = $+{year};
-        $retval = $digits * ( 10 ** ( 4 - length($digits) ));
+    } elsif( $retval =~ m/(?<year>\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;
 }
@@ -2538,51 +2397,19 @@ sub CountItemsIssued {
 
 =head2 ModZebra
 
-  ModZebra( $biblionumber, $op, $server, $record );
-
-$biblionumber is the biblionumber we want to index
+    ModZebra( $record_number, $op, $server );
 
-$op is specialUpdate or recordDelete, and is used to know what we want to do
+$record_number is the authid or biblionumber we want to index
 
-$server is the server that we want to update
+$op is the operation: specialUpdate or recordDelete
 
-$record is the update MARC record if it's available. If it's not supplied
-and is needed, it'll 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 });
-            }
-            my $records = [$record];
-            $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
@@ -2595,96 +2422,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
@@ -2715,6 +2462,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
@@ -2752,61 +2506,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);
@@ -2917,72 +2616,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);
@@ -3115,7 +2748,7 @@ sub _koha_delete_biblio_metadata {
 
 =head2 ModBiblioMarc
 
-  &ModBiblioMarc($newrec,$biblionumber,$frameworkcode);
+  ModBiblioMarc($newrec,$biblionumber);
 
 Add MARC XML data for a biblio to koha
 
@@ -3126,22 +2759,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
@@ -3197,7 +2826,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;
 }
@@ -3212,13 +2844,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) {
@@ -3347,17 +2981,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;
@@ -3381,7 +3016,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
@@ -3432,8 +3067,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>
+biblionumber of old record
+
+=item C<record>
+Incoming record that will be merged with old record
+
+=item C<overlay_context>
+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<context> applied. If no old
+record for C<biblionumber> can be found, C<record> is returned unchanged.
+Default action when no matching context is found to return C<record> unchanged.
+If no rules are found for a certain field tag the default is to overwrite with
+fields with this field tag from C<record>.
+
+=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
 
@@ -3447,28 +3140,19 @@ sub _after_biblio_action_hooks {
     my $biblio_id = $args->{biblio_id};
     my $action    = $args->{action};
 
-    if ( C4::Context->config("enable_plugins") ) {
-
-        my @plugins = Koha::Plugins->new->GetPlugins({
-            method => 'after_biblio_action',
-        });
-
-        if (@plugins) {
-
-            my $biblio = Koha::Biblios->find( $biblio_id );
-
-            foreach my $plugin ( @plugins ) {
-                try {
-                    $plugin->after_biblio_action({ action => $action, biblio => $biblio, biblio_id => $biblio_id });
-                }
-                catch {
-                    warn "$_";
-                };
-            }
+    my $biblio = Koha::Biblios->find( $biblio_id );
+    Koha::Plugins->call(
+        'after_biblio_action',
+        {
+            action    => $action,
+            biblio    => $biblio,
+            biblio_id => $biblio_id,
         }
-    }
+    );
 }
 
+1;
+
 __END__
 
 =head1 AUTHOR