Bug 14695 [QA Followup] - Disable "Holds to place (count)" unless "Hold next availabl...
[koha.git] / C4 / Biblio.pm
index 7e0aa1f..3a34d79 100644 (file)
@@ -31,20 +31,23 @@ use POSIX qw(strftime);
 use Module::Load::Conditional qw(can_load);
 
 use C4::Koha;
-use C4::Dates qw/format_date/;
 use C4::Log;    # logaction
 use C4::Budgets;
 use C4::ClassSource;
 use C4::Charset;
 use C4::Linker;
 use C4::OAI::Sets;
+use C4::Debug;
 
 use Koha::Cache;
+use Koha::Authority::Types;
+use Koha::Acquisition::Currencies;
+use Koha::SearchEngine;
 
-use vars qw($VERSION @ISA @EXPORT);
+use vars qw(@ISA @EXPORT);
+use vars qw($debug $cgi_debug);
 
 BEGIN {
-    $VERSION = 3.07.00.049;
 
     require Exporter;
     @ISA = qw( Exporter );
@@ -57,13 +60,14 @@ BEGIN {
 
     # to get something
     push @EXPORT, qw(
-      &GetBiblio
-      &GetBiblioData
-      &GetBiblioItemData
-      &GetBiblioItemInfosOf
-      &GetBiblioItemByBiblioNumber
-      &GetBiblioFromItemNumber
-      &GetBiblionumberFromItemnumber
+      GetBiblio
+      GetBiblioData
+      GetMarcBiblio
+      GetBiblioItemData
+      GetBiblioItemInfosOf
+      GetBiblioItemByBiblioNumber
+      GetBiblioFromItemNumber
+      GetBiblionumberFromItemnumber
 
       &GetRecordValue
       &GetFieldMapping
@@ -77,7 +81,6 @@ BEGIN {
       &GetMarcISBN
       &GetMarcISSN
       &GetMarcSubjects
-      &GetMarcBiblio
       &GetMarcAuthors
       &GetMarcSeries
       &GetMarcHosts
@@ -91,6 +94,7 @@ BEGIN {
 
       &GetAuthorisedValueDesc
       &GetMarcStructure
+      &IsMarcStructureInternal
       &GetMarcFromKohaField
       &GetMarcSubfieldStructureFromKohaField
       &GetFrameworkCode
@@ -256,7 +260,7 @@ sub AddBiblio {
 
     # transform the data into koha-table style data
     SetUTF8Flag($record);
-    my $olddata = TransformMarcToKoha( $dbh, $record, $frameworkcode );
+    my $olddata = TransformMarcToKoha( $record, $frameworkcode );
     ( $biblionumber, $error ) = _koha_add_biblio( $dbh, $olddata, $frameworkcode );
     $olddata->{'biblionumber'} = $biblionumber;
     ( $biblioitemnumber, $error ) = _koha_add_biblioitem( $dbh, $olddata );
@@ -339,7 +343,7 @@ sub ModBiblio {
     _koha_marc_update_bib_ids( $record, $frameworkcode, $biblionumber, $biblioitemnumber );
 
     # load the koha-table data object
-    my $oldbiblio = TransformMarcToKoha( $dbh, $record, $frameworkcode );
+    my $oldbiblio = TransformMarcToKoha( $record, $frameworkcode );
 
     # update MARC subfield that stores biblioitems.cn_sort
     _koha_marc_update_biblioitem_cn_sort( $record, $oldbiblio, $frameworkcode );
@@ -573,8 +577,7 @@ sub LinkBibHeadingsToAuthorities {
                     $results{'linked'}->{ $heading->display_form() }++;
                 }
                 else {
-                    my $authtypedata =
-                      C4::AuthoritiesMarc::GetAuthType( $heading->auth_type() );
+                    my $authority_type = Koha::Authority::Types->find( $heading->auth_type() );
                     my $marcrecordauth = MARC::Record->new();
                     if ( C4::Context->preference('marcflavour') eq 'MARC21' ) {
                         $marcrecordauth->leader('     nz  a22     o  4500');
@@ -583,7 +586,7 @@ sub LinkBibHeadingsToAuthorities {
                     $field->delete_subfield( code => '9' )
                       if defined $current_link;
                     my $authfield =
-                      MARC::Field->new( $authtypedata->{auth_tag_to_report},
+                      MARC::Field->new( $authority_type->auth_tag_to_report,
                         '', '', "a" => "" . $field->subfield('a') );
                     map {
                         $authfield->add_subfields( $_->[0] => $_->[1] )
@@ -924,12 +927,14 @@ Return the ISBD view which can be included in opac and intranet
 sub GetISBDView {
     my ( $biblionumber, $template ) = @_;
     my $record   = GetMarcBiblio($biblionumber, 1);
+    $template ||= '';
+    my $sysprefname = $template eq 'opac' ? 'opacisbd' : 'isbd';
     return unless defined $record;
     my $itemtype = &GetFrameworkCode($biblionumber);
     my ( $holdingbrtagf, $holdingbrtagsubf ) = &GetMarcFromKohaField( "items.holdingbranch", $itemtype );
     my $tagslib = &GetMarcStructure( 1, $itemtype );
 
-    my $ISBD = C4::Context->preference('isbd');
+    my $ISBD = C4::Context->preference($sysprefname);
     my $bloc = $ISBD;
     my $res;
     my $blocres;
@@ -1081,6 +1086,27 @@ sub GetBiblioItemInfosOf {
 
 =head1 FUNCTIONS FOR HANDLING MARC MANAGEMENT
 
+=head2 IsMarcStructureInternal
+
+    my $tagslib = C4::Biblio::GetMarcStructure();
+    for my $tag ( sort keys %$tagslib ) {
+        next unless $tag;
+        for my $subfield ( sort keys %{ $tagslib->{$tag} } ) {
+            next if IsMarcStructureInternal($tagslib->{$tag}{$subfield});
+        }
+        # Process subfield
+    }
+
+GetMarcStructure creates keys (lib, tab, mandatory, repeatable) for a display purpose.
+These different values should not be processed as valid subfields.
+
+=cut
+
+sub IsMarcStructureInternal {
+    my ( $subfield ) = @_;
+    return ref $subfield ? 0 : 1;
+}
+
 =head2 GetMarcStructure
 
   $res = GetMarcStructure($forlibrarian,$frameworkcode);
@@ -1093,7 +1119,6 @@ $frameworkcode : the framework code to read
 
 sub GetMarcStructure {
     my ( $forlibrarian, $frameworkcode ) = @_;
-    my $dbh = C4::Context->dbh;
     $frameworkcode = "" unless $frameworkcode;
 
     $forlibrarian = $forlibrarian ? 1 : 0;
@@ -1102,6 +1127,7 @@ sub GetMarcStructure {
     my $cached = $cache->get_from_cache($cache_key);
     return $cached if $cached;
 
+    my $dbh = C4::Context->dbh;
     my $sth = $dbh->prepare(
         "SELECT tagfield,liblibrarian,libopac,mandatory,repeatable 
         FROM marc_tag_structure 
@@ -1183,18 +1209,44 @@ C<$frameworkcode> is the framework code.
 
 sub GetUsedMarcStructure {
     my $frameworkcode = shift || '';
-    my $query = qq/
+    my $query = q{
         SELECT *
         FROM   marc_subfield_structure
         WHERE   tab > -1 
             AND frameworkcode = ?
         ORDER BY tagfield, tagsubfield
-    /;
+    };
     my $sth = C4::Context->dbh->prepare($query);
     $sth->execute($frameworkcode);
     return $sth->fetchall_arrayref( {} );
 }
 
+=head2 GetMarcSubfieldStructure
+
+=cut
+
+sub GetMarcSubfieldStructure {
+    my ( $frameworkcode ) = @_;
+
+    $frameworkcode //= '';
+
+    my $cache     = Koha::Cache->get_instance();
+    my $cache_key = "MarcSubfieldStructure-$frameworkcode";
+    my $cached    = $cache->get_from_cache($cache_key);
+    return $cached if $cached;
+
+    my $dbh = C4::Context->dbh;
+    my $subfield_structure = $dbh->selectall_hashref( q|
+        SELECT *
+        FROM marc_subfield_structure
+        WHERE frameworkcode = ?
+        AND kohafield > ''
+    |, 'kohafield', {}, $frameworkcode );
+
+    $cache->set_in_cache( $cache_key, $subfield_structure );
+    return $subfield_structure;
+}
+
 =head2 GetMarcFromKohaField
 
   ($MARCfield,$MARCsubfield)=GetMarcFromKohaField($kohafield,$frameworkcode);
@@ -1205,14 +1257,10 @@ for the given frameworkcode or default framework if $frameworkcode is missing
 =cut
 
 sub GetMarcFromKohaField {
-    my $kohafield = shift;
-    my $frameworkcode = shift || '';
+    my ( $kohafield, $frameworkcode ) = @_;
     return (0, undef) unless $kohafield;
-    my $relations = C4::Context->marcfromkohafield;
-    if ( my $mf = $relations->{$frameworkcode}->{$kohafield} ) {
-        return @$mf;
-    }
-    return (0, undef);
+    my $mss = GetMarcSubfieldStructure( $frameworkcode );
+    return ( $mss->{$kohafield}{tagfield}, $mss->{$kohafield}{tagsubfield} );
 }
 
 =head2 GetMarcSubfieldStructureFromKohaField
@@ -1227,40 +1275,46 @@ $frameworkcode is optional. If not given, then the default framework is used.
 =cut
 
 sub GetMarcSubfieldStructureFromKohaField {
-    my ($kohafield, $frameworkcode) = @_;
+    my ( $kohafield, $frameworkcode ) = @_;
 
-    return undef unless $kohafield;
-    $frameworkcode //= '';
+    return unless $kohafield;
 
-    my $dbh = C4::Context->dbh;
-    my $query = qq{
-        SELECT *
-        FROM marc_subfield_structure
-        WHERE kohafield = ?
-          AND frameworkcode = ?
-    };
-    my $sth = $dbh->prepare($query);
-    $sth->execute($kohafield, $frameworkcode);
-    my $result = $sth->fetchrow_hashref;
-    $sth->finish;
-
-    return $result;
+    my $mss = GetMarcSubfieldStructure( $frameworkcode );
+    return exists $mss->{$kohafield}
+        ? $mss->{$kohafield}
+        : undef;
 }
 
 =head2 GetMarcBiblio
 
-  my $record = GetMarcBiblio($biblionumber, [$embeditems]);
+  my $record = GetMarcBiblio($biblionumber, [$embeditems], [$opac]);
+
+Returns MARC::Record representing a biblio record, or C<undef> if the
+biblionumber doesn't exist.
+
+=over 4
+
+=item C<$biblionumber>
+
+the biblionumber
+
+=item C<$embeditems>
 
-Returns MARC::Record representing bib identified by
-C<$biblionumber>.  If no bib exists, returns undef.
-C<$embeditems>.  If set to true, items data are included.
-The MARC record contains biblio data, and items data if $embeditems is set to true.
+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.
+
+=back
 
 =cut
 
 sub GetMarcBiblio {
     my $biblionumber = shift;
     my $embeditems   = shift || 0;
+    my $opac         = shift || 0;
 
     if (not defined $biblionumber) {
         carp 'GetMarcBiblio called with undefined biblionumber';
@@ -1268,23 +1322,31 @@ sub GetMarcBiblio {
     }
 
     my $dbh          = C4::Context->dbh;
-    my $sth          = $dbh->prepare("SELECT marcxml FROM biblioitems WHERE biblionumber=? ");
+    my $sth          = $dbh->prepare("SELECT biblioitemnumber, marcxml FROM biblioitems WHERE biblionumber=? ");
     $sth->execute($biblionumber);
     my $row     = $sth->fetchrow_hashref;
+    my $biblioitemnumber = $row->{'biblioitemnumber'};
     my $marcxml = StripNonXmlChars( $row->{'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, "utf8", C4::Context->preference('marcflavour') ) };
+        $record = eval {
+            MARC::Record::new_from_xml( $marcxml, "utf8",
+                C4::Context->preference('marcflavour') );
+        };
         if ($@) { warn " problem with :$biblionumber : $@ \n$marcxml"; }
         return unless $record;
 
-        C4::Biblio::_koha_marc_update_bib_ids($record, '', $biblionumber, $biblionumber);
-       C4::Biblio::EmbedItemsInMarcBiblio($record, $biblionumber) if ($embeditems);
+        C4::Biblio::_koha_marc_update_bib_ids( $record, $frameworkcode, $biblionumber,
+            $biblioitemnumber );
+        C4::Biblio::EmbedItemsInMarcBiblio( $record, $biblionumber, undef, $opac )
+          if ($embeditems);
 
         return $record;
-    } else {
+    }
+    else {
         return;
     }
 }
@@ -1508,10 +1570,10 @@ sub MungeMarcPrice {
     my ( $price ) = @_;
     return unless ( $price =~ m/\d/ ); ## No digits means no price.
     # Look for the currency symbol and the normalized code of the active currency, if it's there,
-    my $active_currency = C4::Budgets->GetCurrency();
-    my $symbol = $active_currency->{'symbol'};
-    my $isocode = $active_currency->{'isocode'};
-    $isocode = $active_currency->{'currency'} unless defined $isocode;
+    my $active_currency = Koha::Acquisition::Currencies->get_active;
+    my $symbol = $active_currency->symbol;
+    my $isocode = $active_currency->isocode;
+    $isocode = $active_currency->currency unless defined $isocode;
     my $localprice;
     if ( $symbol ) {
         my @matches =($price=~ /
@@ -1622,7 +1684,6 @@ descriptions rather than normal ones when they exist.
 
 sub GetAuthorisedValueDesc {
     my ( $tag, $subfield, $value, $framework, $tagslib, $category, $opac ) = @_;
-    my $dbh = C4::Context->dbh;
 
     if ( !$category ) {
 
@@ -1635,13 +1696,14 @@ sub GetAuthorisedValueDesc {
 
         #---- itemtypes
         if ( $tagslib->{$tag}->{$subfield}->{'authorised_value'} eq "itemtypes" ) {
-            return getitemtypeinfo($value)->{description};
+            return getitemtypeinfo($value)->{translated_description};
         }
 
         #---- "true" authorized value
         $category = $tagslib->{$tag}->{$subfield}->{'authorised_value'};
     }
 
+    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 );
@@ -1744,10 +1806,11 @@ sub GetMarcISSN {
 
 =head2 GetMarcNotes
 
-  $marcnotesarray = GetMarcNotes( $record, $marcflavour );
+    $marcnotesarray = GetMarcNotes( $record, $marcflavour );
 
-Get all notes from the MARC record and returns them in an array.
-The note are stored in different fields depending on MARC flavour
+    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 field 555 gets special attention for the $u subfields.
 
 =cut
 
@@ -1757,38 +1820,28 @@ sub GetMarcNotes {
         carp 'GetMarcNotes called on undefined record';
         return;
     }
-    my $scope;
-    if ( $marcflavour eq "UNIMARC" ) {
-        $scope = '3..';
-    } else {    # assume marc21 if not unimarc
-        $scope = '5..';
-    }
+
+    my $scope = $marcflavour eq "UNIMARC"? '3..': '5..';
     my @marcnotes;
-    my $note = "";
-    my $tag  = "";
-    my $marcnote;
-    my %blacklist = map { $_ => 1 } split(/,/,C4::Context->preference('NotesBlacklist'));
+    my %blacklist = map { $_ => 1 }
+        split( /,/, C4::Context->preference('NotesBlacklist'));
     foreach my $field ( $record->field($scope) ) {
         my $tag = $field->tag();
-        if (!$blacklist{$tag}) {
-            my $value = $field->as_string();
-            if ( $note ne "" ) {
-                $marcnote = { marcnote => $note, };
-                push @marcnotes, $marcnote;
-                $note = $value;
-            }
-            if ( $note ne $value ) {
-                $note = $note . " " . $value;
+        next if $blacklist{ $tag };
+        if( $marcflavour ne 'UNIMARC' && $tag =~ /555/ ) {
+            # Field 555$u contains URLs
+            # We first push the regular subfields and all $u's separately
+            # Leave further actions to the template
+            push @marcnotes, { marcnote => $field->as_string('abcd') };
+            foreach my $sub ( $field->subfield('u') ) {
+                push @marcnotes, { marcnote => $sub };
             }
+        } else {
+            push @marcnotes, { marcnote => $field->as_string() };
         }
     }
-
-    if ($note) {
-        $marcnote = { marcnote => $note };
-        push @marcnotes, $marcnote;    #load last tag into array
-    }
     return \@marcnotes;
-}    # end GetMarcNotes
+}
 
 =head2 GetMarcSubjects
 
@@ -2190,17 +2243,18 @@ sub TransformKohaToMarc {
     my $hash = shift;
     my $record = MARC::Record->new();
     SetMarcUnicodeFlag( $record, C4::Context->preference("marcflavour") );
-    my $db_to_marc = C4::Context->marcfromkohafield;
+    # FIXME Do not we want to get the marc subfield structure for the biblio framework?
+    my $mss = GetMarcSubfieldStructure();
     my $tag_hr = {};
-    while ( my ($name, $value) = each %$hash ) {
-        next unless my $dtm = $db_to_marc->{''}->{$name};
-        next unless ( scalar( @$dtm ) );
-        my ($tag, $letter) = @$dtm;
-        $tag .= '';
+    while ( my ($kohafield, $value) = each %$hash ) {
+        next unless exists $mss->{$kohafield};
+        next unless $mss->{$kohafield};
+        my $tagfield    = $mss->{$kohafield}{tagfield} . '';
+        my $tagsubfield = $mss->{$kohafield}{tagsubfield};
         foreach my $value ( split(/\s?\|\s?/, $value, -1) ) {
             next if $value eq '';
-            $tag_hr->{$tag} //= [];
-            push @{$tag_hr->{$tag}}, [($letter, $value)];
+            $tag_hr->{$tagfield} //= [];
+            push @{$tag_hr->{$tagfield}}, [($tagsubfield, $value)];
         }
     }
     foreach my $tag (sort keys %$tag_hr) {
@@ -2315,6 +2369,8 @@ $auth_type contains :
 
 sub TransformHtmlToXml {
     my ( $tags, $subfields, $values, $indicator, $ind_tag, $auth_type ) = @_;
+    # NOTE: The parameter $ind_tag is NOT USED -- BZ 11247
+
     my $xml = MARC::File::XML::header('UTF-8');
     $xml .= "<record>\n";
     $auth_type = C4::Context->preference('marcflavour') unless $auth_type;
@@ -2471,9 +2527,9 @@ sub _default_ind_to_space {
 =cut
 
 sub TransformHtmlToMarc {
-    my $cgi    = shift;
+    my ($cgi, $isbiblio) = @_;
 
-    my @params = $cgi->param();
+    my @params = $cgi->multi_param();
 
     # explicitly turn on the UTF-8 flag for all
     # 'tag_' parameters to avoid incorrect character
@@ -2490,20 +2546,21 @@ sub TransformHtmlToMarc {
 
     # creating a new record
     my $record = MARC::Record->new();
-    my $i      = 0;
     my @fields;
+    my ($biblionumbertagfield, $biblionumbertagsubfield) = (-1, -1);
+    ($biblionumbertagfield, $biblionumbertagsubfield) =
+        &GetMarcFromKohaField( "biblio.biblionumber", '' ) if $isbiblio;
 #FIXME This code assumes that the CGI params will be in the same order as the fields in the template; this is no absolute guarantee!
-    while ( $params[$i] ) {    # browse all CGI params
+    for (my $i = 0; $params[$i]; $i++ ) {    # browse all CGI params
         my $param    = $params[$i];
         my $newfield = 0;
 
         # if we are on biblionumber, store it in the MARC::Record (it may not be in the edited fields)
         if ( $param eq 'biblionumber' ) {
-            my ( $biblionumbertagfield, $biblionumbertagsubfield ) = &GetMarcFromKohaField( "biblio.biblionumber", '' );
             if ( $biblionumbertagfield < 10 ) {
-                $newfield = MARC::Field->new( $biblionumbertagfield, $cgi->param($param), );
+                $newfield = MARC::Field->new( $biblionumbertagfield, scalar $cgi->param($param), );
             } else {
-                $newfield = MARC::Field->new( $biblionumbertagfield, '', '', "$biblionumbertagsubfield" => $cgi->param($param), );
+                $newfield = MARC::Field->new( $biblionumbertagfield, '', '', "$biblionumbertagsubfield" => scalar $cgi->param($param), );
             }
             push @fields, $newfield if ($newfield);
         } elsif ( $param =~ /^tag_(\d*)_indicator1_/ ) {    # new field start when having 'input name="..._indicator1_..."
@@ -2516,18 +2573,20 @@ sub TransformHtmlToMarc {
 
             if ( $tag < 10 ) {                              # no code for theses fields
                                                             # in MARC editor, 000 contains the leader.
+                next if $tag == $biblionumbertagfield;
+                my $fval= $cgi->param($params[$j+1]);
                 if ( $tag eq '000' ) {
                     # Force a fake leader even if not provided to avoid crashing
                     # during decoding MARC record containing UTF-8 characters
                     $record->leader(
-                        length( $cgi->param($params[$j+1]) ) == 24
-                        ? $cgi->param( $params[ $j + 1 ] )
+                        length( $fval ) == 24
+                        ? $fval
                         : '     nam a22        4500'
                        )
                     ;
                     # between 001 and 009 (included)
-                } elsif ( $cgi->param( $params[ $j + 1 ] ) ne '' ) {
-                    $newfield = MARC::Field->new( $tag, $cgi->param( $params[ $j + 1 ] ), );
+                } elsif ( $fval ne '' ) {
+                    $newfield = MARC::Field->new( $tag, $fval, );
                 }
 
                 # > 009, deal with subfields
@@ -2535,16 +2594,20 @@ sub TransformHtmlToMarc {
                 # browse subfields for this tag (reason for _code_ match)
                 while(defined $params[$j] && $params[$j] =~ /_code_/) {
                     last unless defined $params[$j+1];
+                    $j += 2 and next
+                        if $tag == $biblionumbertagfield and
+                           $cgi->param($params[$j]) eq $biblionumbertagsubfield;
                     #if next param ne subfield, then it was probably empty
                     #try next param by incrementing j
                     if($params[$j+1]!~/_subfield_/) {$j++; next; }
+                    my $fkey= $cgi->param($params[$j]);
                     my $fval= $cgi->param($params[$j+1]);
                     #check if subfield value not empty and field exists
                     if($fval ne '' && $newfield) {
-                        $newfield->add_subfields( $cgi->param($params[$j]) => $fval);
+                        $newfield->add_subfields( $fkey => $fval);
                     }
                     elsif($fval ne '') {
-                        $newfield = MARC::Field->new( $tag, $ind1, $ind2, $cgi->param($params[$j]) => $fval );
+                        $newfield = MARC::Field->new( $tag, $ind1, $ind2, $fkey => $fval );
                     }
                     $j += 2;
                 } #end-of-while
@@ -2552,19 +2615,15 @@ sub TransformHtmlToMarc {
             }
             push @fields, $newfield if ($newfield);
         }
-        $i++;
     }
 
     $record->append_fields(@fields);
     return $record;
 }
 
-# cache inverted MARC field map
-our $inverted_field_map;
-
 =head2 TransformMarcToKoha
 
-  $result = TransformMarcToKoha( $dbh, $record, $frameworkcode )
+  $result = TransformMarcToKoha( $record, $frameworkcode )
 
 Extract data from a MARC bib record into a hashref representing
 Koha biblio, biblioitems, and items fields. 
@@ -2575,7 +2634,7 @@ hash_ref
 =cut
 
 sub TransformMarcToKoha {
-    my ( $dbh, $record, $frameworkcode, $limit_table ) = @_;
+    my ( $record, $frameworkcode, $limit_table ) = @_;
 
     my $result = {};
     if (!defined $record) {
@@ -2585,9 +2644,7 @@ sub TransformMarcToKoha {
     $limit_table = $limit_table || 0;
     $frameworkcode = '' unless defined $frameworkcode;
 
-    unless ( defined $inverted_field_map ) {
-        $inverted_field_map = _get_inverted_marc_field_map();
-    }
+    my $inverted_field_map = _get_inverted_marc_field_map($frameworkcode);
 
     my %tables = ();
     if ( defined $limit_table && $limit_table eq 'items' ) {
@@ -2601,9 +2658,9 @@ sub TransformMarcToKoha {
     # traverse through record
   MARCFIELD: foreach my $field ( $record->fields() ) {
         my $tag = $field->tag();
-        next MARCFIELD unless exists $inverted_field_map->{$frameworkcode}->{$tag};
+        next MARCFIELD unless exists $inverted_field_map->{$tag};
         if ( $field->is_control_field() ) {
-            my $kohafields = $inverted_field_map->{$frameworkcode}->{$tag}->{list};
+            my $kohafields = $inverted_field_map->{$tag}->{list};
           ENTRY: foreach my $entry ( @{$kohafields} ) {
                 my ( $subfield, $table, $column ) = @{$entry};
                 next ENTRY unless exists $tables{$table};
@@ -2621,9 +2678,9 @@ sub TransformMarcToKoha {
             # deal with subfields
           MARCSUBFIELD: foreach my $sf ( $field->subfields() ) {
                 my $code = $sf->[0];
-                next MARCSUBFIELD unless exists $inverted_field_map->{$frameworkcode}->{$tag}->{sfs}->{$code};
+                next MARCSUBFIELD unless exists $inverted_field_map->{$tag}->{sfs}->{$code};
                 my $value = $sf->[1];
-              SFENTRY: foreach my $entry ( @{ $inverted_field_map->{$frameworkcode}->{$tag}->{sfs}->{$code} } ) {
+              SFENTRY: foreach my $entry ( @{ $inverted_field_map->{$tag}->{sfs}->{$code} } ) {
                     my ( $table, $column ) = @{$entry};
                     next SFENTRY unless exists $tables{$table};
                     my $key = _disambiguate( $table, $column );
@@ -2666,18 +2723,17 @@ sub TransformMarcToKoha {
 }
 
 sub _get_inverted_marc_field_map {
+    my ( $frameworkcode ) = @_;
     my $field_map = {};
-    my $relations = C4::Context->marcfromkohafield;
+    my $mss = GetMarcSubfieldStructure( $frameworkcode );
 
-    foreach my $frameworkcode ( keys %{$relations} ) {
-        foreach my $kohafield ( keys %{ $relations->{$frameworkcode} } ) {
-            next unless @{ $relations->{$frameworkcode}->{$kohafield} };    # not all columns are mapped to MARC tag & subfield
-            my $tag      = $relations->{$frameworkcode}->{$kohafield}->[0];
-            my $subfield = $relations->{$frameworkcode}->{$kohafield}->[1];
-            my ( $table, $column ) = split /[.]/, $kohafield, 2;
-            push @{ $field_map->{$frameworkcode}->{$tag}->{list} }, [ $subfield, $table, $column ];
-            push @{ $field_map->{$frameworkcode}->{$tag}->{sfs}->{$subfield} }, [ $table, $column ];
-        }
+    foreach my $kohafield ( keys %{ $mss } ) {
+        next unless exists $mss->{$kohafield};    # not all columns are mapped to MARC tag & subfield
+        my $tag      = $mss->{$kohafield}{tagfield};
+        my $subfield = $mss->{$kohafield}{tagsubfield};
+        my ( $table, $column ) = split /[.]/, $kohafield, 2;
+        push @{ $field_map->{$tag}->{list} }, [ $subfield, $table, $column ];
+        push @{ $field_map->{$tag}->{sfs}->{$subfield} }, [ $table, $column ];
     }
     return $field_map;
 }
@@ -2844,31 +2900,60 @@ sub TransformMarcToKohaOneField {
 
 =head2 ModZebra
 
-  ModZebra( $biblionumber, $op, $server );
+  ModZebra( $biblionumber, $op, $server, $record );
 
 $biblionumber is the biblionumber we want to index
 
-$op is specialUpdate or delete, and is used to know what we want to do
+$op is specialUpdate or recordDelete, and is used to know what we want to do
 
 $server is the server that we want to update
 
+$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.
+
 =cut
 
 sub ModZebra {
 ###Accepts a $server variable thus we can use it for biblios authorities or other zebra dbs
-    my ( $biblionumber, $op, $server ) = @_;
+    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::ElasticSearch::Indexer;
+        my $indexer = Koha::ElasticSearch::Indexer->new(
+            {
+                index => $server eq 'biblioserver'
+                ? $Koha::SearchEngine::BIBLIOS_INDEX
+                : $Koha::SearchEngine::AUTHORITIES_INDEX
+            }
+        );
+        if ( $op eq 'specialUpdate' ) {
+            unless ($record) {
+                $record = GetMarcBiblio($biblionumber, 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 $dbh = C4::Context->dbh;
 
     # true ModZebra commented until indexdata fixes zebraDB crashes (it seems they occur on multiple updates
     # at the same time
     # replaced by a zebraqueue table, that is filled with ModZebra to run.
     # the table is emptied by rebuild_zebra.pl script (using the -z switch)
-
     my $check_sql = "SELECT COUNT(*) FROM zebraqueue
-                     WHERE server = ?
-                     AND   biblio_auth_number = ?
-                     AND   operation = ?
-                     AND   done = 0";
+    WHERE server = ?
+        AND   biblio_auth_number = ?
+        AND   operation = ?
+        AND   done = 0";
     my $check_sth = $dbh->prepare_cached($check_sql);
     $check_sth->execute( $server, $biblionumber, $op );
     my ($count) = $check_sth->fetchrow_array;
@@ -2883,17 +2968,19 @@ sub ModZebra {
 
 =head2 EmbedItemsInMarcBiblio
 
-    EmbedItemsInMarcBiblio($marc, $biblionumber, $itemnumbers);
+    EmbedItemsInMarcBiblio($marc, $biblionumber, $itemnumbers, $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 $itemnumbers is defined, only specified itemnumbers are embedded.
+
+If $opac is true, then opac-relevant suppressions are included.
 
 =cut
 
 sub EmbedItemsInMarcBiblio {
-    my ($marc, $biblionumber, $itemnumbers) = @_;
+    my ($marc, $biblionumber, $itemnumbers, $opac) = @_;
     if ( !$marc ) {
         carp 'EmbedItemsInMarcBiblio: No MARC record passed';
         return;
@@ -2910,10 +2997,24 @@ sub EmbedItemsInMarcBiblio {
     $sth->execute($biblionumber);
     my @item_fields;
     my ( $itemtag, $itemsubfield ) = GetMarcFromKohaField( "items.itemnumber", $frameworkcode );
-    while (my ($itemnumber) = $sth->fetchrow_array) {
+    my @items;
+    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;
-        require C4::Items;
-        my $item_marc = C4::Items::GetMarcItem($biblionumber, $itemnumber);
+        my $i = $opachiddenitems ? C4::Items::GetItem($itemnumber) : undef;
+        push @items, { itemnumber => $itemnumber, item => $i };
+    }
+    my @hiddenitems =
+      $opachiddenitems
+      ? C4::Items::GetHiddenItemnumbers( map { $_->{item} } @items )
+      : ();
+    # 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);
@@ -3408,72 +3509,10 @@ sub ModBiblioMarc {
     $sth = $dbh->prepare("UPDATE biblioitems SET marc=?,marcxml=? WHERE biblionumber=?");
     $sth->execute( $record->as_usmarc(), $record->as_xml_record($encoding), $biblionumber );
     $sth->finish;
-    ModZebra( $biblionumber, "specialUpdate", "biblioserver" );
+    ModZebra( $biblionumber, "specialUpdate", "biblioserver", $record );
     return $biblionumber;
 }
 
-=head2 get_biblio_authorised_values
-
-find the types and values for all authorised values assigned to this biblio.
-
-parameters:
-    biblionumber
-    MARC::Record of the bib
-
-returns: a hashref mapping the authorised value to the value set for this biblionumber
-
-  $authorised_values = {
-                       'Scent'     => 'flowery',
-                       'Audience'  => 'Young Adult',
-                       'itemtypes' => 'SER',
-                        };
-
-Notes: forlibrarian should probably be passed in, and called something different.
-
-=cut
-
-sub get_biblio_authorised_values {
-    my $biblionumber = shift;
-    my $record       = shift;
-
-    my $forlibrarian  = 1;                                 # are we in staff or opac?
-    my $frameworkcode = GetFrameworkCode($biblionumber);
-
-    my $authorised_values;
-
-    my $tagslib = GetMarcStructure( $forlibrarian, $frameworkcode )
-      or return $authorised_values;
-
-    # assume that these entries in the authorised_value table are bibliolevel.
-    # ones that start with 'item%' are item level.
-    my $query = q(SELECT distinct authorised_value, kohafield
-                    FROM marc_subfield_structure
-                    WHERE authorised_value !=''
-                      AND (kohafield like 'biblio%'
-                       OR  kohafield like '') );
-    my $bibliolevel_authorised_values = C4::Context->dbh->selectall_hashref( $query, 'authorised_value' );
-
-    foreach my $tag ( keys(%$tagslib) ) {
-        foreach my $subfield ( keys( %{ $tagslib->{$tag} } ) ) {
-
-            # warn "checking $subfield. type is: " . ref $tagslib->{ $tag }{ $subfield };
-            if ( 'HASH' eq ref $tagslib->{$tag}{$subfield} ) {
-                if ( defined $tagslib->{$tag}{$subfield}{'authorised_value'} && exists $bibliolevel_authorised_values->{ $tagslib->{$tag}{$subfield}{'authorised_value'} } ) {
-                    if ( defined $record->field($tag) ) {
-                        my $this_subfield_value = $record->field($tag)->subfield($subfield);
-                        if ( defined $this_subfield_value ) {
-                            $authorised_values->{ $tagslib->{$tag}{$subfield}{'authorised_value'} } = $this_subfield_value;
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    # warn ( Data::Dumper->Dump( [ $authorised_values ], [ 'authorised_values' ] ) );
-    return $authorised_values;
-}
-
 =head2 CountBiblioInOrders
 
     $count = &CountBiblioInOrders( $biblionumber);
@@ -3626,7 +3665,7 @@ sub prepare_host_field {
         if ( $field = $host->field('205') ) {
             my $s = $field->as_string();
             if ($s) {
-                $sfd{a} = $s;
+                $sfd{e} = $s;
             }
         }
         #URL