Bug 14224: Improve escaped characters
[koha.git] / C4 / Items.pm
index 7f9a3da..6378249 100644 (file)
@@ -34,10 +34,17 @@ use YAML qw/Load/;
 use DateTime::Format::MySQL;
 use Data::Dumper; # used as part of logging item record changes, not just for
                   # debugging; so please don't remove this
+
+use Koha::AuthorisedValues;
 use Koha::DateUtils qw/dt_from_string/;
 use Koha::Database;
 
-use Koha::Database;
+use Koha::Biblioitems;
+use Koha::Items;
+use Koha::ItemTypes;
+use Koha::SearchEngine;
+use Koha::SearchEngine::Search;
+use Koha::Libraries;
 
 use vars qw(@ISA @EXPORT);
 
@@ -61,11 +68,9 @@ BEGIN {
     
         CheckItemPreSave
     
-        GetItemStatus
         GetItemLocation
         GetLostItems
         GetItemsForInventory
-        GetItemsCount
         GetItemInfosOf
         GetItemsByBiblioitemnumber
         GetItemsInfo
@@ -77,6 +82,7 @@ BEGIN {
         GetItemnumberFromBarcode
         GetBarcodeFromItemnumber
         GetHiddenItemnumbers
+        ItemSafeToDelete
         DelItemCheck
     MoveItemFromBiblio
     GetLatestAcquisitions
@@ -85,7 +91,6 @@ BEGIN {
         ShelfToCart
 
        GetAnalyticsCount
-        GetItemHolds
 
         SearchItemsByField
         SearchItems
@@ -105,6 +110,7 @@ This module contains an API for manipulating item
 records in Koha, and is used by cataloguing, circulation,
 acquisitions, and serials management.
 
+# FIXME This POD is not up-to-date
 A Koha item record is stored in two places: the
 items table and embedded in a MARC tag in the XML
 version of the associated bib record in C<biblioitems.marcxml>.
@@ -149,38 +155,25 @@ names to values.  If C<$serial> is true, include serial publication data.
 sub GetItem {
     my ($itemnumber,$barcode, $serial) = @_;
     my $dbh = C4::Context->dbh;
-       my $data;
 
+    my $item;
     if ($itemnumber) {
-        my $sth = $dbh->prepare("
-            SELECT * FROM items 
-            WHERE itemnumber = ?");
-        $sth->execute($itemnumber);
-        $data = $sth->fetchrow_hashref;
+        $item = Koha::Items->find( $itemnumber );
     } else {
-        my $sth = $dbh->prepare("
-            SELECT * FROM items 
-            WHERE barcode = ?"
-            );
-        $sth->execute($barcode);               
-        $data = $sth->fetchrow_hashref;
-    }
-
-    return unless ( $data );
-
-    if ( $serial) {      
-    my $ssth = $dbh->prepare("SELECT serialseq,publisheddate from serialitems left join serial on serialitems.serialid=serial.serialid where serialitems.itemnumber=?");
-        $ssth->execute($data->{'itemnumber'}) ;
-        ($data->{'serialseq'} , $data->{'publisheddate'}) = $ssth->fetchrow_array();
-    }
-       #if we don't have an items.itype, use biblioitems.itemtype.
-    # FIXME this should respect the itypes systempreference
-    # if (C4::Context->preference('item-level_itypes')) {
-       if( ! $data->{'itype'} ) {
-               my $sth = $dbh->prepare("SELECT itemtype FROM biblioitems  WHERE biblionumber = ?");
-               $sth->execute($data->{'biblionumber'});
-               ($data->{'itype'}) = $sth->fetchrow_array;
-       }
+        $item = Koha::Items->find( { barcode => $barcode } );
+    }
+
+    return unless ( $item );
+
+    my $data = $item->unblessed();
+    $data->{itype} = $item->effective_itemtype(); # set the correct itype
+
+    if ($serial) {
+        my $ssth = $dbh->prepare("SELECT serialseq,publisheddate from serialitems left join serial on serialitems.serialid=serial.serialid where serialitems.itemnumber=?");
+        $ssth->execute( $data->{'itemnumber'} );
+        ( $data->{'serialseq'}, $data->{'publisheddate'} ) = $ssth->fetchrow_array();
+    }
+
     return $data;
 }    # sub GetItem
 
@@ -278,40 +271,44 @@ the biblio items tag for display and indexing.
 =cut
 
 sub AddItem {
-    my $item = shift;
+    my $item         = shift;
     my $biblionumber = shift;
 
     my $dbh           = @_ ? shift : C4::Context->dbh;
-    my $frameworkcode = @_ ? shift : GetFrameworkCode( $biblionumber );
-    my $unlinked_item_subfields;  
+    my $frameworkcode = @_ ? shift : GetFrameworkCode($biblionumber);
+    my $unlinked_item_subfields;
     if (@_) {
-        $unlinked_item_subfields = shift
-    };
+        $unlinked_item_subfields = shift;
+    }
 
     # needs old biblionumber and biblioitemnumber
     $item->{'biblionumber'} = $biblionumber;
     my $sth = $dbh->prepare("SELECT biblioitemnumber FROM biblioitems WHERE biblionumber=?");
     $sth->execute( $item->{'biblionumber'} );
-    ($item->{'biblioitemnumber'}) = $sth->fetchrow;
+    ( $item->{'biblioitemnumber'} ) = $sth->fetchrow;
 
     _set_defaults_for_add($item);
     _set_derived_columns_for_add($item);
     $item->{'more_subfields_xml'} = _get_unlinked_subfields_xml($unlinked_item_subfields);
+
     # FIXME - checks here
-    unless ( $item->{itype} ) {  # default to biblioitem.itemtype if no itype
+    unless ( $item->{itype} ) {    # default to biblioitem.itemtype if no itype
         my $itype_sth = $dbh->prepare("SELECT itemtype FROM biblioitems WHERE biblionumber = ?");
         $itype_sth->execute( $item->{'biblionumber'} );
         ( $item->{'itype'} ) = $itype_sth->fetchrow_array;
     }
 
-       my ( $itemnumber, $error ) = _koha_new_item( $item, $item->{barcode} );
+    my ( $itemnumber, $error ) = _koha_new_item( $item, $item->{barcode} );
+    return if $error;
+
     $item->{'itemnumber'} = $itemnumber;
 
     ModZebra( $item->{biblionumber}, "specialUpdate", "biblioserver" );
-   
-    logaction("CATALOGUING", "ADD", $itemnumber, "item") if C4::Context->preference("CataloguingLog");
-    
-    return ($item->{biblionumber}, $item->{biblioitemnumber}, $itemnumber);
+
+    logaction( "CATALOGUING", "ADD", $itemnumber, "item" )
+      if C4::Context->preference("CataloguingLog");
+
+    return ( $item->{biblionumber}, $item->{biblioitemnumber}, $itemnumber );
 }
 
 =head2 AddItemBatchFromMarc
@@ -324,8 +321,8 @@ embedded item fields.  This routine is suitable for batch jobs.
 
 This API assumes that the bib record has already been
 saved to the C<biblio> and C<biblioitems> tables.  It does
-not expect that C<biblioitems.marc> and C<biblioitems.marcxml>
-are populated, but it will do so via a call to ModBibiloMarc.
+not expect that C<biblio_metadata.metadata> is populated, but it
+will do so via a call to ModBibiloMarc.
 
 The goal of this API is to have a similar effect to using AddBiblio
 and AddItems in succession, but without inefficient repeated
@@ -447,13 +444,14 @@ Returns item record
 
 =cut
 
-our %default_values_for_mod_from_marc;
-
 sub _build_default_values_for_mod_marc {
     my ($frameworkcode) = @_;
-    return $default_values_for_mod_from_marc{$frameworkcode}
-      if exists $default_values_for_mod_from_marc{$frameworkcode};
-    my $marc_structure = C4::Biblio::GetMarcStructure( 1, $frameworkcode );
+
+    my $cache     = Koha::Caches->get_instance();
+    my $cache_key = "default_value_for_mod_marc-$frameworkcode";
+    my $cached    = $cache->get_from_cache($cache_key);
+    return $cached if $cached;
+
     my $default_values = {
         barcode                  => undef,
         booksellerid             => undef,
@@ -485,15 +483,18 @@ sub _build_default_values_for_mod_marc {
         uri                      => undef,
         withdrawn                => 0,
     };
+    my %default_values_for_mod_from_marc;
     while ( my ( $field, $default_value ) = each %$default_values ) {
         my $kohafield = $field;
         $kohafield =~ s|^([^\.]+)$|items.$1|;
-        $default_values_for_mod_from_marc{$frameworkcode}{$field} =
+        $default_values_for_mod_from_marc{$field} =
           $default_value
           if C4::Koha::IsKohaFieldLinked(
             { kohafield => $kohafield, frameworkcode => $frameworkcode } );
     }
-    return $default_values_for_mod_from_marc{$frameworkcode};
+
+    $cache->set_in_cache($cache_key, \%default_values_for_mod_from_marc);
+    return \%default_values_for_mod_from_marc;
 }
 
 sub ModItemFromMarc {
@@ -508,7 +509,7 @@ sub ModItemFromMarc {
     my $localitemmarc = MARC::Record->new;
     $localitemmarc->append_fields( $item_marc->field($itemtag) );
     my $item = &TransformMarcToKoha( $localitemmarc, $frameworkcode, 'items' );
-    my $default_values = _build_default_values_for_mod_marc();
+    my $default_values = _build_default_values_for_mod_marc($frameworkcode);
     foreach my $item_field ( keys %$default_values ) {
         $item->{$item_field} = $default_values->{$item_field}
           unless exists $item->{$item_field};
@@ -677,8 +678,6 @@ sub DelItem {
     # FIXME check the item has no current issues
     my $deleted = _koha_delete_item( $itemnumber );
 
-    # get the MARC record
-    my $record = GetMarcBiblio($biblionumber);
     ModZebra( $biblionumber, "specialUpdate", "biblioserver" );
 
     #search item field code
@@ -730,7 +729,6 @@ item that has a given branch code.
 
 sub CheckItemPreSave {
     my $item_ref = shift;
-    require C4::Branch;
 
     my %errors = ();
 
@@ -747,20 +745,16 @@ sub CheckItemPreSave {
 
     # check for valid home branch
     if (exists $item_ref->{'homebranch'} and defined $item_ref->{'homebranch'}) {
-        my $branch_name = C4::Branch::GetBranchName($item_ref->{'homebranch'});
-        unless (defined $branch_name) {
-            # relies on fact that branches.branchname is a non-NULL column,
-            # so GetBranchName returns undef only if branch does not exist
+        my $home_library = Koha::Libraries->find( $item_ref->{homebranch} );
+        unless (defined $home_library) {
             $errors{'invalid_homebranch'} = $item_ref->{'homebranch'};
         }
     }
 
     # check for valid holding branch
     if (exists $item_ref->{'holdingbranch'} and defined $item_ref->{'holdingbranch'}) {
-        my $branch_name = C4::Branch::GetBranchName($item_ref->{'holdingbranch'});
-        unless (defined $branch_name) {
-            # relies on fact that branches.branchname is a non-NULL column,
-            # so GetBranchName returns undef only if branch does not exist
+        my $holding_library = Koha::Libraries->find( $item_ref->{holdingbranch} );
+        unless (defined $holding_library) {
             $errors{'invalid_holdingbranch'} = $item_ref->{'holdingbranch'};
         }
     }
@@ -782,98 +776,6 @@ has copy-and-paste work.
 
 =cut
 
-=head2 GetItemStatus
-
-  $itemstatushash = GetItemStatus($fwkcode);
-
-Returns a list of valid values for the
-C<items.notforloan> field.
-
-NOTE: does B<not> return an individual item's
-status.
-
-Can be MARC dependent.
-fwkcode is optional.
-But basically could be can be loan or not
-Create a status selector with the following code
-
-=head3 in PERL SCRIPT
-
- my $itemstatushash = getitemstatus;
- my @itemstatusloop;
- foreach my $thisstatus (keys %$itemstatushash) {
-     my %row =(value => $thisstatus,
-                 statusname => $itemstatushash->{$thisstatus}->{'statusname'},
-             );
-     push @itemstatusloop, \%row;
- }
- $template->param(statusloop=>\@itemstatusloop);
-
-=head3 in TEMPLATE
-
-<select name="statusloop" id="statusloop">
-    <option value="">Default</option>
-    [% FOREACH statusloo IN statusloop %]
-        [% IF ( statusloo.selected ) %]
-            <option value="[% statusloo.value %]" selected="selected">[% statusloo.statusname %]</option>
-        [% ELSE %]
-            <option value="[% statusloo.value %]">[% statusloo.statusname %]</option>
-        [% END %]
-    [% END %]
-</select>
-
-=cut
-
-sub GetItemStatus {
-
-    # returns a reference to a hash of references to status...
-    my ($fwk) = @_;
-    my %itemstatus;
-    my $dbh = C4::Context->dbh;
-    my $sth;
-    $fwk = '' unless ($fwk);
-    my ( $tag, $subfield ) =
-      GetMarcFromKohaField( "items.notforloan", $fwk );
-    if ( $tag and $subfield ) {
-        my $sth =
-          $dbh->prepare(
-            "SELECT authorised_value
-            FROM marc_subfield_structure
-            WHERE tagfield=?
-                AND tagsubfield=?
-                AND frameworkcode=?
-            "
-          );
-        $sth->execute( $tag, $subfield, $fwk );
-        if ( my ($authorisedvaluecat) = $sth->fetchrow ) {
-            my $authvalsth =
-              $dbh->prepare(
-                "SELECT authorised_value,lib
-                FROM authorised_values 
-                WHERE category=? 
-                ORDER BY lib
-                "
-              );
-            $authvalsth->execute($authorisedvaluecat);
-            while ( my ( $authorisedvalue, $lib ) = $authvalsth->fetchrow ) {
-                $itemstatus{$authorisedvalue} = $lib;
-            }
-            return \%itemstatus;
-            exit 1;
-        }
-        else {
-
-            #No authvalue list
-            # build default
-        }
-    }
-
-    #No authvalue list
-    #build default
-    $itemstatus{"1"} = "Not For Loan";
-    return \%itemstatus;
-}
-
 =head2 GetItemLocation
 
   $itemlochash = GetItemLocation($fwk);
@@ -945,7 +847,6 @@ sub GetItemLocation {
                 $itemlocation{$authorisedvalue} = $lib;
             }
             return \%itemlocation;
-            exit 1;
         }
         else {
 
@@ -1037,7 +938,6 @@ sub GetLostItems {
   offset       => $offset,
   size         => $size,
   statushash   => $statushash,
-  interface    => $interface,
 } );
 
 Retrieve a list of title/authors/barcode/callnumber, for biblio inventory.
@@ -1068,7 +968,6 @@ sub GetItemsForInventory {
     my $offset       = $parameters->{'offset'}       // '';
     my $size         = $parameters->{'size'}         // '';
     my $statushash   = $parameters->{'statushash'}   // '';
-    my $interface    = $parameters->{'interface'}    // '';
 
     my $dbh = C4::Context->dbh;
     my ( @bind_params, @where_strings );
@@ -1148,9 +1047,19 @@ sub GetItemsForInventory {
     $sth->execute( @bind_params );
     my ($iTotalRecords) = $sth->fetchrow_array();
 
-    my $avmapping = C4::Koha::GetKohaAuthorisedValuesMapping( {
-                      interface => $interface
-                    } );
+    my @avs = Koha::AuthorisedValues->search(
+        {   'marc_subfield_structures.kohafield' => { '>' => '' },
+            'me.authorised_value'                => { '>' => '' },
+        },
+        {   join     => { category => 'marc_subfield_structures' },
+            distinct => ['marc_subfield_structures.kohafield, me.category, frameworkcode, me.authorised_value'],
+            '+select' => [ 'marc_subfield_structures.kohafield', 'marc_subfield_structures.frameworkcode', 'me.authorised_value', 'me.lib' ],
+            '+as'     => [ 'kohafield',                          'frameworkcode',                          'authorised_value',    'lib' ],
+        }
+    );
+
+    my $avmapping = { map { $_->get_column('kohafield') . ',' . $_->get_column('frameworkcode') . ',' . $_->get_column('authorised_value') => $_->get_column('lib') } @avs };
+
     foreach my $row (@$tmpresults) {
 
         # Auth values
@@ -1165,26 +1074,6 @@ sub GetItemsForInventory {
     return (\@results, $iTotalRecords);
 }
 
-=head2 GetItemsCount
-
-  $count = &GetItemsCount( $biblionumber);
-
-This function return count of item with $biblionumber
-
-=cut
-
-sub GetItemsCount {
-    my ( $biblionumber ) = @_;
-    my $dbh = C4::Context->dbh;
-    my $query = "SELECT count(*)
-          FROM  items 
-          WHERE biblionumber=?";
-    my $sth = $dbh->prepare($query);
-    $sth->execute($biblionumber);
-    my $count = $sth->fetchrow;  
-    return ($count);
-}
-
 =head2 GetItemInfosOf
 
   GetItemInfosOf(@itemnumbers);
@@ -1196,12 +1085,13 @@ sub GetItemInfosOf {
 
     my $itemnumber_values = @itemnumbers ? join( ',', @itemnumbers ) : "''";
 
+    my $dbh = C4::Context->dbh;
     my $query = "
         SELECT *
         FROM items
         WHERE itemnumber IN ($itemnumber_values)
     ";
-    return get_infos_of( $query, 'itemnumber' );
+    return $dbh->selectall_hashref($query, 'itemnumber');
 }
 
 =head2 GetItemsByBiblioitemnumber
@@ -1301,7 +1191,6 @@ If this is set, it is set to C<One Order>.
 sub GetItemsInfo {
     my ( $biblionumber ) = @_;
     my $dbh   = C4::Context->dbh;
-    # note biblioitems.* must be avoided to prevent large marc and marcxml fields from killing performance.
     require C4::Languages;
     my $language = C4::Languages::getlanguage();
     my $query = "
@@ -1332,6 +1221,7 @@ sub GetItemsInfo {
            COALESCE( localization.translation, itemtypes.description ) AS translated_description,
            itemtypes.notforloan as notforloan_per_itemtype,
            holding.branchurl,
+           holding.branchcode,
            holding.branchname,
            holding.opac_info as holding_branch_opac_info,
            home.opac_info as home_branch_opac_info
@@ -1370,22 +1260,20 @@ sub GetItemsInfo {
 
         $serial ||= $data->{'serial'};
 
+        my $descriptions;
         # get notforloan complete status if applicable
-        if ( my $code = C4::Koha::GetAuthValCode( 'items.notforloan', $data->{frameworkcode} ) ) {
-            $data->{notforloanvalue}     = C4::Koha::GetKohaAuthorisedValueLib( $code, $data->{itemnotforloan} );
-            $data->{notforloanvalueopac} = C4::Koha::GetKohaAuthorisedValueLib( $code, $data->{itemnotforloan}, 1 );
-        }
+        $descriptions = Koha::AuthorisedValues->get_description_by_koha_field({frameworkcode => $data->{frameworkcode}, kohafield => 'items.notforloan', authorised_value => $data->{itemnotforloan} });
+        $data->{notforloanvalue}     = $descriptions->{lib} // '';
+        $data->{notforloanvalueopac} = $descriptions->{opac_description} // '';
 
         # get restricted status and description if applicable
-        if ( my $code = C4::Koha::GetAuthValCode( 'items.restricted', $data->{frameworkcode} ) ) {
-            $data->{restrictedopac} = C4::Koha::GetKohaAuthorisedValueLib( $code, $data->{restricted}, 1 );
-            $data->{restricted}     = C4::Koha::GetKohaAuthorisedValueLib( $code, $data->{restricted} );
-        }
+        $descriptions = Koha::AuthorisedValues->get_description_by_koha_field({frameworkcode => $data->{frameworkcode}, kohafield => 'items.restricted', authorised_value => $data->{restricted} });
+        $data->{restricted}     = $descriptions->{lib} // '';
+        $data->{restrictedopac} = $descriptions->{opac_description} // '';
 
         # my stack procedures
-        if ( my $code = C4::Koha::GetAuthValCode( 'items.stack', $data->{frameworkcode} ) ) {
-            $data->{stack}          = C4::Koha::GetKohaAuthorisedValueLib( $code, $data->{stack} );
-        }
+        $descriptions = Koha::AuthorisedValues->get_description_by_koha_field({frameworkcode => $data->{frameworkcode}, kohafield => 'items.stack', authorised_value => $data->{stack} });
+        $data->{stack}          = $descriptions->{lib} // '';
 
         # Find the last 3 people who borrowed this item.
         my $sth2 = $dbh->prepare("SELECT * FROM old_issues,borrowers
@@ -1469,8 +1357,10 @@ sub GetItemsLocationInfo {
         $sth->execute($biblionumber);
 
         while ( my $data = $sth->fetchrow_hashref ) {
-             $data->{location_intranet} = GetKohaAuthorisedValueLib('LOC', $data->{location});
-             $data->{location_opac}= GetKohaAuthorisedValueLib('LOC', $data->{location}, 1);
+             my $av = Koha::AuthorisedValues->search({ category => 'LOC', authorised_value => $data->{location} });
+             $av = $av->count ? $av->next : undef;
+             $data->{location_intranet} = $av ? $av->lib : '';
+             $data->{location_opac}     = $av ? $av->opac_description : '';
             push @results, $data;
        }
        return @results;
@@ -1636,36 +1526,42 @@ references on array of itemnumbers.
 
 
 sub get_hostitemnumbers_of {
-       my ($biblionumber) = @_;
-       my $marcrecord = GetMarcBiblio($biblionumber);
-        my (@returnhostitemnumbers,$tag, $biblio_s, $item_s);
-       
-       my $marcflavor = C4::Context->preference('marcflavour');
-       if ($marcflavor eq 'MARC21' || $marcflavor eq 'NORMARC') {
-        $tag='773';
-        $biblio_s='0';
-        $item_s='9';
-    } elsif ($marcflavor eq 'UNIMARC') {
-        $tag='461';
-        $biblio_s='0';
-        $item_s='9';
+    my ($biblionumber) = @_;
+    my $marcrecord = GetMarcBiblio($biblionumber);
+
+    return unless $marcrecord;
+
+    my ( @returnhostitemnumbers, $tag, $biblio_s, $item_s );
+
+    my $marcflavor = C4::Context->preference('marcflavour');
+    if ( $marcflavor eq 'MARC21' || $marcflavor eq 'NORMARC' ) {
+        $tag      = '773';
+        $biblio_s = '0';
+        $item_s   = '9';
+    }
+    elsif ( $marcflavor eq 'UNIMARC' ) {
+        $tag      = '461';
+        $biblio_s = '0';
+        $item_s   = '9';
     }
 
     foreach my $hostfield ( $marcrecord->field($tag) ) {
         my $hostbiblionumber = $hostfield->subfield($biblio_s);
         my $linkeditemnumber = $hostfield->subfield($item_s);
         my @itemnumbers;
-        if (my $itemnumbers = get_itemnumbers_of($hostbiblionumber)->{$hostbiblionumber})
+        if ( my $itemnumbers =
+            get_itemnumbers_of($hostbiblionumber)->{$hostbiblionumber} )
         {
             @itemnumbers = @$itemnumbers;
         }
-        foreach my $itemnumber (@itemnumbers){
-            if ($itemnumber eq $linkeditemnumber){
-                push (@returnhostitemnumbers,$itemnumber);
+        foreach my $itemnumber (@itemnumbers) {
+            if ( $itemnumber eq $linkeditemnumber ) {
+                push( @returnhostitemnumbers, $itemnumber );
                 last;
             }
         }
     }
+
     return @returnhostitemnumbers;
 }
 
@@ -1760,104 +1656,6 @@ sub GetHiddenItemnumbers {
     return @resultitems;
 }
 
-=head3 get_item_authorised_values
-
-find the types and values for all authorised values assigned to this item.
-
-parameters: itemnumber
-
-returns: a hashref malling the authorised value to the value set for this itemnumber
-
-    $authorised_values = {
-             'CCODE'      => undef,
-             'DAMAGED'    => '0',
-             'LOC'        => '3',
-             'LOST'       => '0'
-             'NOT_LOAN'   => '0',
-             'RESTRICTED' => undef,
-             'STACK'      => undef,
-             'WITHDRAWN'  => '0',
-             'branches'   => 'CPL',
-             'cn_source'  => undef,
-             'itemtypes'  => 'SER',
-           };
-
-Notes: see C4::Biblio::get_biblio_authorised_values for a similar method at the biblio level.
-
-=cut
-
-sub get_item_authorised_values {
-    my $itemnumber = shift;
-
-    # assume that these entries in the authorised_value table are item level.
-    my $query = q(SELECT distinct authorised_value, kohafield
-                    FROM marc_subfield_structure
-                    WHERE kohafield like 'item%'
-                      AND authorised_value != '' );
-
-    my $itemlevel_authorised_values = C4::Context->dbh->selectall_hashref( $query, 'authorised_value' );
-    my $iteminfo = GetItem( $itemnumber );
-    # warn( Data::Dumper->Dump( [ $itemlevel_authorised_values ], [ 'itemlevel_authorised_values' ] ) );
-    my $return;
-    foreach my $this_authorised_value ( keys %$itemlevel_authorised_values ) {
-        my $field = $itemlevel_authorised_values->{ $this_authorised_value }->{'kohafield'};
-        $field =~ s/^items\.//;
-        if ( exists $iteminfo->{ $field } ) {
-            $return->{ $this_authorised_value } = $iteminfo->{ $field };
-        }
-    }
-    # warn( Data::Dumper->Dump( [ $return ], [ 'return' ] ) );
-    return $return;
-}
-
-=head3 get_authorised_value_images
-
-find a list of icons that are appropriate for display based on the
-authorised values for a biblio.
-
-parameters: listref of authorised values, such as comes from
-get_item_authorised_values or
-from C4::Biblio::get_biblio_authorised_values
-
-returns: listref of hashrefs for each image. Each hashref looks like this:
-
-      { imageurl => '/intranet-tmpl/prog/img/itemtypeimg/npl/WEB.gif',
-        label    => '',
-        category => '',
-        value    => '', }
-
-Notes: Currently, I put on the full path to the images on the staff
-side. This should either be configurable or not done at all. Since I
-have to deal with 'intranet' or 'opac' in
-get_biblio_authorised_values, perhaps I should be passing it in.
-
-=cut
-
-sub get_authorised_value_images {
-    my $authorised_values = shift;
-
-    my @imagelist;
-
-    my $authorised_value_list = GetAuthorisedValues();
-    # warn ( Data::Dumper->Dump( [ $authorised_value_list ], [ 'authorised_value_list' ] ) );
-    foreach my $this_authorised_value ( @$authorised_value_list ) {
-        if ( exists $authorised_values->{ $this_authorised_value->{'category'} }
-             && $authorised_values->{ $this_authorised_value->{'category'} } eq $this_authorised_value->{'authorised_value'} ) {
-            # warn ( Data::Dumper->Dump( [ $this_authorised_value ], [ 'this_authorised_value' ] ) );
-            if ( defined $this_authorised_value->{'imageurl'} ) {
-                push @imagelist, { imageurl => C4::Koha::getitemtypeimagelocation( 'intranet', $this_authorised_value->{'imageurl'} ),
-                                   label    => $this_authorised_value->{'lib'},
-                                   category => $this_authorised_value->{'category'},
-                                   value    => $this_authorised_value->{'authorised_value'}, };
-            }
-        }
-    }
-
-    # warn ( Data::Dumper->Dump( [ \@imagelist ], [ 'imagelist' ] ) );
-    return \@imagelist;
-
-}
-
 =head1 LIMITED USE FUNCTIONS
 
 The following functions, while part of the public API,
@@ -2159,6 +1957,7 @@ sub _koha_new_item {
     my $dbh=C4::Context->dbh;  
     my $error;
     $item->{permanent_location} //= $item->{location};
+    _mod_item_dates( $item );
     my $query =
            "INSERT INTO items SET
             biblionumber        = ?,
@@ -2308,68 +2107,102 @@ sub MoveItemFromBiblio {
     return;
 }
 
-=head2 DelItemCheck
+=head2 ItemSafeToDelete
 
-   DelItemCheck($dbh, $biblionumber, $itemnumber);
+   ItemSafeToDelete( $biblionumber, $itemnumber);
 
-Exported function (core API) for deleting an item record in Koha if there no current issue.
+Exported function (core API) for checking whether an item record is safe to delete.
 
-=cut
+returns 1 if the item is safe to delete,
 
-sub DelItemCheck {
-    my ( $dbh, $biblionumber, $itemnumber ) = @_;
+"book_on_loan" if the item is checked out,
 
-    $dbh ||= C4::Context->dbh;
+"not_same_branch" if the item is blocked by independent branches,
 
-    my $error;
+"book_reserved" if the there are holds aganst the item, or
 
-        my $countanalytics=GetAnalyticsCount($itemnumber);
+"linked_analytics" if the item has linked analytic records.
 
+=cut
+
+sub ItemSafeToDelete {
+    my ( $biblionumber, $itemnumber ) = @_;
+    my $status;
+    my $dbh = C4::Context->dbh;
+
+    my $error;
+
+    my $countanalytics = GetAnalyticsCount($itemnumber);
 
     # check that there is no issue on this item before deletion.
-    my $sth = $dbh->prepare(q{
+    my $sth = $dbh->prepare(
+        q{
         SELECT COUNT(*) FROM issues
         WHERE itemnumber = ?
-    });
+    }
+    );
     $sth->execute($itemnumber);
     my ($onloan) = $sth->fetchrow;
 
     my $item = GetItem($itemnumber);
 
-    if ($onloan){
-        $error = "book_on_loan" 
+    if ($onloan) {
+        $status = "book_on_loan";
     }
     elsif ( defined C4::Context->userenv
         and !C4::Context->IsSuperLibrarian()
         and C4::Context->preference("IndependentBranches")
         and ( C4::Context->userenv->{branch} ne $item->{'homebranch'} ) )
     {
-        $error = "not_same_branch";
+        $status = "not_same_branch";
     }
-       else{
+    else {
         # check it doesn't have a waiting reserve
-        $sth = $dbh->prepare(q{
+        $sth = $dbh->prepare(
+            q{
             SELECT COUNT(*) FROM reserves
             WHERE (found = 'W' OR found = 'T')
             AND itemnumber = ?
-        });
+        }
+        );
         $sth->execute($itemnumber);
         my ($reserve) = $sth->fetchrow;
-        if ($reserve){
-            $error = "book_reserved";
-        } elsif ($countanalytics > 0){
-               $error = "linked_analytics";
-       } else {
-            DelItem(
-                {
-                    biblionumber => $biblionumber,
-                    itemnumber   => $itemnumber
-                }
-            );
-            return 1;
+        if ($reserve) {
+            $status = "book_reserved";
+        }
+        elsif ( $countanalytics > 0 ) {
+            $status = "linked_analytics";
         }
+        else {
+            $status = 1;
+        }
+    }
+    return $status;
+}
+
+=head2 DelItemCheck
+
+   DelItemCheck( $biblionumber, $itemnumber);
+
+Exported function (core API) for deleting an item record in Koha if there no current issue.
+
+DelItemCheck wraps ItemSafeToDelete around DelItem.
+
+=cut
+
+sub DelItemCheck {
+    my ( $biblionumber, $itemnumber ) = @_;
+    my $status = ItemSafeToDelete( $biblionumber, $itemnumber );
+
+    if ( $status == 1 ) {
+        DelItem(
+            {
+                biblionumber => $biblionumber,
+                itemnumber   => $itemnumber
+            }
+        );
     }
-    return $error;
+    return $status;
 }
 
 =head2 _koha_modify_item
@@ -2388,6 +2221,7 @@ sub _koha_modify_item {
 
     my $query = "UPDATE items SET ";
     my @bind;
+    _mod_item_dates( $item );
     for my $key ( keys %$item ) {
         next if ( $key eq 'itemnumber' );
         $query.="$key=?,";
@@ -2405,6 +2239,34 @@ sub _koha_modify_item {
     return ($item->{'itemnumber'},$error);
 }
 
+sub _mod_item_dates { # date formatting for date fields in item hash
+    my ( $item ) = @_;
+    return if !$item || ref($item) ne 'HASH';
+
+    my @keys = grep
+        { $_ =~ /^onloan$|^date|date$|datetime$/ }
+        keys %$item;
+    # Incl. dateaccessioned,replacementpricedate,datelastborrowed,datelastseen
+    # NOTE: We do not (yet) have items fields ending with datetime
+    # Fields with _on$ have been handled already
+
+    foreach my $key ( @keys ) {
+        next if !defined $item->{$key}; # skip undefs
+        my $dt = eval { dt_from_string( $item->{$key} ) };
+            # eval: dt_from_string will die on us if we pass illegal dates
+
+        my $newstr;
+        if( defined $dt  && ref($dt) eq 'DateTime' ) {
+            if( $key =~ /datetime/ ) {
+                $newstr = DateTime::Format::MySQL->format_datetime($dt);
+            } else {
+                $newstr = DateTime::Format::MySQL->format_date($dt);
+            }
+        }
+        $item->{$key} = $newstr; # might be undef to clear garbage
+    }
+}
+
 =head2 _koha_delete_item
 
   _koha_delete_item( $itemnum );
@@ -2528,7 +2390,7 @@ sub _get_unlinked_item_subfields {
     my $original_item_marc = shift;
     my $frameworkcode = shift;
 
-    my $marcstructure = GetMarcStructure(1, $frameworkcode);
+    my $marcstructure = GetMarcStructure(1, $frameworkcode, { unsafe => 1 });
 
     # assume that this record has only one field, and that that
     # field contains only the item information
@@ -2601,36 +2463,15 @@ counts Usage of itemnumber in Analytical bibliorecords.
 
 sub GetAnalyticsCount {
     my ($itemnumber) = @_;
-    require C4::Search;
 
     ### ZOOM search here
     my $query;
     $query= "hi=".$itemnumber;
-            my ($err,$res,$result) = C4::Search::SimpleSearch($query,0,10);
+    my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
+    my ($err,$res,$result) = $searcher->simple_search_compat($query,0,10);
     return ($result);
 }
 
-=head2 GetItemHolds
-
-  $holds = &GetItemHolds($biblionumber, $itemnumber);
-
-This function return the count of holds with $biblionumber and $itemnumber
-
-=cut
-
-sub GetItemHolds {
-    my ($biblionumber, $itemnumber) = @_;
-    my $holds;
-    my $dbh            = C4::Context->dbh;
-    my $query          = "SELECT count(*)
-        FROM  reserves
-        WHERE biblionumber=? AND itemnumber=?";
-    my $sth = $dbh->prepare($query);
-    $sth->execute($biblionumber, $itemnumber);
-    $holds = $sth->fetchrow;
-    return $holds;
-}
-
 =head2 SearchItemsByField
 
     my $items = SearchItemsByField($field, $value);
@@ -2704,7 +2545,7 @@ sub _SearchItems_build_where_fragment {
                     $column = $kohafield;
                 } else {
                     # MARC field is not linked to a DB field so we need to use
-                    # ExtractValue on biblioitems.marcxml or
+                    # ExtractValue on marcxml from biblio_metadata or
                     # items.more_subfields_xml, depending on the MARC field.
                     my $xpath;
                     my $sqlfield;
@@ -2713,7 +2554,7 @@ sub _SearchItems_build_where_fragment {
                         $sqlfield = 'more_subfields_xml';
                         $xpath = '//record/datafield/subfield[@code="' . $marcsubfield . '"]';
                     } else {
-                        $sqlfield = 'marcxml';
+                        $sqlfield = 'metadata'; # From biblio_metadata
                         if ($marcfield < 10) {
                             $xpath = "//record/controlfield[\@tag=\"$marcfield\"]";
                         } else {
@@ -2822,11 +2663,16 @@ sub SearchItems {
         FROM items
           LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
           LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber
+          LEFT JOIN biblio_metadata ON biblio_metadata.biblionumber = biblio.biblionumber
+          WHERE 1
     };
     if (defined $where_str and $where_str ne '') {
-        $query .= qq{ WHERE $where_str };
+        $query .= qq{ AND $where_str };
     }
 
+    $query .= q{ AND biblio_metadata.format = 'marcxml' AND biblio_metadata.marcflavour = ? };
+    push @where_args, C4::Context->preference('marcflavour');
+
     my @columns = Koha::Database->new()->schema()->resultset('Item')->result_source->columns;
     push @columns, Koha::Database->new()->schema()->resultset('Biblio')->result_source->columns;
     push @columns, Koha::Database->new()->schema()->resultset('Biblioitem')->result_source->columns;
@@ -2912,7 +2758,11 @@ sub PrepareItemrecordDisplay {
     my $dbh = C4::Context->dbh;
     $frameworkcode = &GetFrameworkCode($bibnum) if $bibnum;
     my ( $itemtagfield, $itemtagsubfield ) = &GetMarcFromKohaField( "items.itemnumber", $frameworkcode );
-    my $tagslib = &GetMarcStructure( 1, $frameworkcode );
+
+    # Note: $tagslib obtained from GetMarcStructure() in 'unsafe' mode is
+    # a shared data structure. No plugin (including custom ones) should change
+    # its contents. See also GetMarcStructure.
+    my $tagslib = &GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } );
 
     # return nothing if we don't have found an existing framework.
     return q{} unless $tagslib;
@@ -2936,13 +2786,13 @@ sub PrepareItemrecordDisplay {
     $query .= qq{ ORDER BY lib};
     my $authorised_values_sth = $dbh->prepare( $query );
     foreach my $tag ( sort keys %{$tagslib} ) {
-        my $previous_tag = '';
         if ( $tag ne '' ) {
 
             # loop through each subfield
             my $cntsubf;
             foreach my $subfield ( sort keys %{ $tagslib->{$tag} } ) {
                 next if IsMarcStructureInternal($tagslib->{$tag}{$subfield});
+                next unless ( $tagslib->{$tag}->{$subfield}->{'tab'} );
                 next if ( $tagslib->{$tag}->{$subfield}->{'tab'} ne "10" );
                 my %subfield_data;
                 $subfield_data{tag}           = $tag;
@@ -2981,7 +2831,7 @@ sub PrepareItemrecordDisplay {
                 if (   $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.itemcallnumber'
                     && $defaultvalues
                     && $defaultvalues->{'callnumber'} ) {
-                    if( $itemrecord and $defaultvalues and not $itemrecord->field($subfield) ){
+                    if( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield) ){
                         # if the item record exists, only use default value if the item has no callnumber
                         $defaultvalue = $defaultvalues->{callnumber};
                     } elsif ( !$itemrecord and $defaultvalues ) {
@@ -2992,7 +2842,7 @@ sub PrepareItemrecordDisplay {
                 if (   ( $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.holdingbranch' || $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.homebranch' )
                     && $defaultvalues
                     && $defaultvalues->{'branchcode'} ) {
-                    if ( $itemrecord and $defaultvalues and not $itemrecord->field($subfield) ) {
+                    if ( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield) ) {
                         $defaultvalue = $defaultvalues->{branchcode};
                     }
                 }
@@ -3000,7 +2850,7 @@ sub PrepareItemrecordDisplay {
                     && $defaultvalues
                     && $defaultvalues->{'location'} ) {
 
-                    if ( $itemrecord and $defaultvalues and not $itemrecord->field($subfield) ) {
+                    if ( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield) ) {
                         # if the item record exists, only use default value if the item has no locationr
                         $defaultvalue = $defaultvalues->{location};
                     } elsif ( !$itemrecord and $defaultvalues ) {
@@ -3043,13 +2893,17 @@ sub PrepareItemrecordDisplay {
 
                         #----- itemtypes
                     } elsif ( $tagslib->{$tag}->{$subfield}->{authorised_value} eq "itemtypes" ) {
-                        my $itemtypes = GetItemTypes( style => 'array' );
+                        my $itemtypes = Koha::ItemTypes->search_with_localization;
                         push @authorised_values, ""
                           unless ( $tagslib->{$tag}->{$subfield}->{mandatory} );
-                        for my $itemtype ( @$itemtypes ) {
-                            push @authorised_values, $itemtype->{itemtype};
-                            $authorised_lib{$itemtype->{itemtype}} = $itemtype->{translated_description};
+                        while ( my $itemtype = $itemtypes->next ) {
+                            push @authorised_values, $itemtype->itemtype;
+                            $authorised_lib{$itemtype->itemtype} = $itemtype->translated_description;
+                        }
+                        if ($defaultvalues && $defaultvalues->{'itemtype'}) {
+                            $defaultvalue = $defaultvalues->{'itemtype'};
                         }
+
                         #---- class_sources
                     } elsif ( $tagslib->{$tag}->{$subfield}->{authorised_value} eq "cn_source" ) {
                         push @authorised_values, "" unless ( $tagslib->{$tag}->{$subfield}->{mandatory} );
@@ -3094,15 +2948,18 @@ sub PrepareItemrecordDisplay {
                     });
                     my $pars = { dbh => $dbh, record => undef, tagslib =>$tagslib, id => $subfield_data{id}, tabloop => undef };
                     $plugin->build( $pars );
+                    if ( $itemrecord and my $field = $itemrecord->field($tag) ) {
+                        $defaultvalue = $field->subfield($subfield);
+                    }
                     if( !$plugin->errstr ) {
                         #TODO Move html to template; see report 12176/13397
                         my $tab= $plugin->noclick? '-1': '';
                         my $class= $plugin->noclick? ' disabled': '';
                         my $title= $plugin->noclick? 'No popup': 'Tag editor';
-                        $subfield_data{marc_value} = qq[<input type="text" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="255" /><a href="#" id="buttonDot_$subfield_data{id}" tabindex="$tab" class="buttonDot $class" title="$title">...</a>\n].$plugin->javascript;
+                        $subfield_data{marc_value} = qq[<input type="text" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="255" value="$defaultvalue" /><a href="#" id="buttonDot_$subfield_data{id}" tabindex="$tab" class="buttonDot $class" title="$title">...</a>\n].$plugin->javascript;
                     } else {
                         warn $plugin->errstr;
-                        $subfield_data{marc_value} = qq(<input type="text" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="255" />); # supply default input form
+                        $subfield_data{marc_value} = qq(<input type="text" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="255" value="$defaultvalue" />); # supply default input form
                     }
                 }
                 elsif ( $tag eq '' ) {       # it's an hidden field
@@ -3138,34 +2995,6 @@ sub PrepareItemrecordDisplay {
     };
 }
 
-=head2 columns
-
-  my @columns = C4::Items::columns();
-
-Returns an array of items' table columns on success,
-and an empty array on failure.
-
-=cut
-
-sub columns {
-    my $rs = Koha::Database->new->schema->resultset('Item');
-    return $rs->result_source->columns;
-}
-
-=head2 biblioitems_columns
-
-  my @columns = C4::Items::biblioitems_columns();
-
-Returns an array of biblioitems' table columns on success,
-and an empty array on failure.
-
-=cut
-
-sub biblioitems_columns {
-    my $rs = Koha::Database->new->schema->resultset('Biblioitem');
-    return $rs->result_source->columns;
-}
-
 sub ToggleNewStatus {
     my ( $params ) = @_;
     my @rules = @{ $params->{rules} };
@@ -3173,8 +3002,8 @@ sub ToggleNewStatus {
 
     my $dbh = C4::Context->dbh;
     my @errors;
-    my @item_columns = map { "items.$_" } C4::Items::columns;
-    my @biblioitem_columns = map { "biblioitems.$_" } C4::Items::biblioitems_columns;
+    my @item_columns = map { "items.$_" } Koha::Items->columns;
+    my @biblioitem_columns = map { "biblioitems.$_" } Koha::Biblioitems->columns;
     my $report;
     for my $rule ( @rules ) {
         my $age = $rule->{age};