item rework: moved GetItem
[koha_gimpoz] / C4 / Biblio.pm
index fc3a986..d637cf7 100755 (executable)
@@ -27,10 +27,10 @@ use MARC::File::USMARC;
 use MARC::File::XML;
 use ZOOM;
 use C4::Koha;
+use C4::Branch;
 use C4::Dates qw/format_date/;
 use C4::Log; # logaction
 use C4::ClassSource;
-
 use vars qw($VERSION @ISA @EXPORT);
 
 # TODO: fix version
@@ -41,7 +41,7 @@ use vars qw($VERSION @ISA @EXPORT);
 # EXPORTED FUNCTIONS.
 
 # to add biblios or items
-push @EXPORT, qw( &AddBiblio &AddItem );
+push @EXPORT, qw( &AddBiblio &AddBiblioAndItems );
 
 # to get something
 push @EXPORT, qw(
@@ -52,8 +52,6 @@ push @EXPORT, qw(
   &GetBiblioItemByBiblioNumber
   &GetBiblioFromItemNumber
   
-  &GetMarcItem
-  &GetItem
   &GetItemInfosOf
   &GetItemStatus
   &GetItemLocation
@@ -86,13 +84,8 @@ push @EXPORT, qw(
 # To modify something
 push @EXPORT, qw(
   &ModBiblio
-  &ModItem
-  &ModItemTransfer
   &ModBiblioframework
   &ModZebra
-  &ModItemInMarc
-  &ModItemInMarconefield
-  &ModDateLastSeen
 );
 
 # To delete something
@@ -107,7 +100,6 @@ push @EXPORT, qw(
 # but don't use them unless you're a core developer ;-)
 push @EXPORT, qw(
   &ModBiblioMarc
-  &AddItemInMarc
 );
 
 # Others functions
@@ -227,74 +219,174 @@ sub AddBiblio {
     return ( $biblionumber, $biblioitemnumber );
 }
 
-=head2 AddItem
+=head2 AddBiblioAndItems
+
+=over 4
+
+($biblionumber,$biblioitemnumber, $itemnumber_ref, $error_ref) = AddBiblioAndItems($record, $frameworkcode);
+
+=back
+
+Efficiently add a biblio record and create item records from its
+embedded item fields.  This routine is suitable for batch jobs.
+
+The goal of this API is to have a similar effect to using AddBiblio
+and AddItems in succession, but without inefficient repeated
+parsing of the MARC XML bib record.
+
+One functional difference is that the duplicate item barcode 
+check is implemented in this API, instead of relying on
+the caller to do it, like AddItem does.
+
+This function returns the biblionumber and biblioitemnumber of the
+new bib, an arrayref of new itemsnumbers, and an arrayref of item
+errors encountered during the processing.  Each entry in the errors
+list is a hashref containing the following keys:
 
 =over 2
 
-    $biblionumber = AddItem( $record, $biblionumber)
-    Exported function (core API) for adding a new item to Koha
+=item item_sequence
+
+Sequence number of original item tag in the MARC record.
+
+=item item_barcode
+
+Item barcode, provide to assist in the construction of
+useful error messages.
+
+=item error_condition
+
+Code representing the error condition.  Can be 'duplicate_barcode',
+'invalid_homebranch', or 'invalid_holdingbranch'.
+
+=item error_information
+
+Additional information appropriate to the error condition.
 
 =back
 
 =cut
 
-sub AddItem {
-    my ( $record, $biblionumber ) = @_;
+sub AddBiblioAndItems {
+    my ( $record, $frameworkcode ) = @_;
+    my ($biblionumber,$biblioitemnumber,$error);
+    my @itemnumbers = ();
+    my @errors = ();
     my $dbh = C4::Context->dbh;
+
+    # transform the data into koha-table style data
+    # FIXME - this paragraph copied from AddBiblio
+    my $olddata = TransformMarcToKoha( $dbh, $record, $frameworkcode );
+    ($biblionumber,$error) = _koha_add_biblio( $dbh, $olddata, $frameworkcode );
+    $olddata->{'biblionumber'} = $biblionumber;
+    ($biblioitemnumber,$error) = _koha_add_biblioitem( $dbh, $olddata );
+
+    # FIXME - this paragraph copied from AddBiblio
+    _koha_marc_update_bib_ids($record, $frameworkcode, $biblionumber, $biblioitemnumber);
+
+    # now we loop through the item tags and start creating items
+    my @bad_item_fields = ();
+    my ($itemtag, $itemsubfield) = &GetMarcFromKohaField("items.itemnumber",'');
+    my $item_sequence_num = 0;
+    ITEMFIELD: foreach my $item_field ($record->field($itemtag)) {
+        $item_sequence_num++;
+        # we take the item field and stick it into a new
+        # MARC record -- this is required so far because (FIXME)
+        # TransformMarcToKoha requires a MARC::Record, not a MARC::Field
+        # and there is no TransformMarcFieldToKoha
+        my $temp_item_marc = MARC::Record->new();
+        $temp_item_marc->append_fields($item_field);
     
-    # add item in old-DB
-    my $frameworkcode = GetFrameworkCode( $biblionumber );
-    my $item = &TransformMarcToKoha( $dbh, $record, $frameworkcode );
+        # add biblionumber and biblioitemnumber
+        my $item = TransformMarcToKoha( $dbh, $temp_item_marc, $frameworkcode, 'items' );
+        $item->{'biblionumber'} = $biblionumber;
+        $item->{'biblioitemnumber'} = $biblioitemnumber;
+
+        # check for duplicate barcode
+        my %item_errors = CheckItemPreSave($item);
+        if (%item_errors) {
+            push @errors, _repack_item_errors($item_sequence_num, $item, \%item_errors);
+            push @bad_item_fields, $item_field;
+            next ITEMFIELD;
+        }
+        my $duplicate_barcode = exists($item->{'barcode'}) && GetItemnumberFromBarcode($item->{'barcode'});
+        if ($duplicate_barcode) {
+            warn "ERROR: cannot add item $item->{'barcode'} for biblio $biblionumber: duplicate barcode\n";
+        }
 
-    # needs old biblionumber and biblioitemnumber
-    $item->{'biblionumber'} = $biblionumber;
-    my $sth =
-      $dbh->prepare(
-        "SELECT biblioitemnumber,itemtype FROM biblioitems WHERE biblionumber=?"
-      );
-    $sth->execute( $item->{'biblionumber'} );
-    my $itemtype;
-    ( $item->{'biblioitemnumber'}, $itemtype ) = $sth->fetchrow;
-    $sth =
-      $dbh->prepare(
-        "SELECT notforloan FROM itemtypes WHERE itemtype=?");
-    $sth->execute( C4::Context->preference('item-level_itypes') ? $item->{'itype'} : $itemtype );
-    my $notforloan = $sth->fetchrow;
-    ##Change the notforloan field if $notforloan found
-    if ( $notforloan > 0 ) {
-        $item->{'notforloan'} = $notforloan;
-        &MARCitemchange( $record, "items.notforloan", $notforloan );
+        # Make sure item statuses are set to 0 if empty or NULL in both the item and the MARC
+        for ('notforloan', 'damaged','itemlost','wthdrawn') {
+            if (!$item->{$_} or $item->{$_} eq "") {
+                $item->{$_} = 0;
+                &MARCitemchange( $temp_item_marc, "items.$_", 0 );
+            }
+        }
+        # FIXME - dateaccessioned stuff copied from AddItem
+        if ( !$item->{'dateaccessioned'} || $item->{'dateaccessioned'} eq '' ) {
+
+            # find today's date
+            my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) =
+                localtime(time);
+            $year += 1900;
+            $mon  += 1;
+            my $date =
+            "$year-" . sprintf( "%0.2d", $mon ) . "-" . sprintf( "%0.2d", $mday );
+            $item->{'dateaccessioned'} = $date;
+            &MARCitemchange( $temp_item_marc, "items.dateaccessioned", $date );
+        }
+
+        my ( $itemnumber, $error ) = &_koha_new_items( $dbh, $item, $item->{barcode} );
+        warn $error if $error;
+        push @itemnumbers, $itemnumber; # FIXME not checking error
+
+        # FIXME - not copied from AddItem
+        # FIXME - AddItems equiv code about passing $sth to TransformKohaToMarcOneField is stupid
+        &MARCitemchange( $temp_item_marc, "items.itemnumber", $itemnumber );
+       
+        &logaction(C4::Context->userenv->{'number'},"CATALOGUING","ADD",$itemnumber,"item")
+        if C4::Context->preference("CataloguingLog"); 
+
+        $item_field->replace_with($temp_item_marc->field($itemtag));
     }
-    if ( !$item->{'dateaccessioned'} || $item->{'dateaccessioned'} eq '' ) {
-
-        # find today's date
-        my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) =
-          localtime(time);
-        $year += 1900;
-        $mon  += 1;
-        my $date =
-          "$year-" . sprintf( "%0.2d", $mon ) . "-" . sprintf( "%0.2d", $mday );
-        $item->{'dateaccessioned'} = $date;
-        &MARCitemchange( $record, "items.dateaccessioned", $date );
+
+    # remove any MARC item fields for rejected items
+    foreach my $item_field (@bad_item_fields) {
+        $record->delete_field($item_field);
     }
-    my ( $itemnumber, $error ) = &_koha_new_items( $dbh, $item, $item->{barcode} );
-    # add itemnumber to MARC::Record before adding the item.
-    $sth = $dbh->prepare(
-"SELECT tagfield,tagsubfield 
-FROM marc_subfield_structure
-WHERE frameworkcode=? 
-    AND kohafield=?"
-      );
-    &TransformKohaToMarcOneField( $sth, $record, "items.itemnumber", $itemnumber,
-        $frameworkcode );
 
-    # add the item
-    &AddItemInMarc( $record, $item->{'biblionumber'},$frameworkcode );
-   
-    &logaction(C4::Context->userenv->{'number'},"CATALOGUING","ADD",$itemnumber,"item") 
+    # now add the record
+    # FIXME - this paragraph copied from AddBiblio -- however, moved  since
+    # since we need to create the items row and plug in the itemnumbers in the
+    # MARC
+    $biblionumber = ModBiblioMarc( $record, $biblionumber, $frameworkcode );
+
+    # FIXME - when using this API, do we log both bib and item add, or just
+    #         bib
+    &logaction(C4::Context->userenv->{'number'},"CATALOGUING","ADD",$biblionumber,"biblio")
         if C4::Context->preference("CataloguingLog");
+
+    return ( $biblionumber, $biblioitemnumber, \@itemnumbers, \@errors);
     
-    return ($item->{biblionumber}, $item->{biblioitemnumber},$itemnumber);
+}
+
+sub _repack_item_errors {
+    my $item_sequence_num = shift;
+    my $item_ref = shift;
+    my $error_ref = shift;
+
+    my @repacked_errors = ();
+
+    foreach my $error_code (sort keys %{ $error_ref }) {
+        my $repacked_error = {};
+        $repacked_error->{'item_sequence'} = $item_sequence_num;
+        $repacked_error->{'item_barcode'} = exists($item_ref->{'barcode'}) ? $item_ref->{'barcode'} : '';
+        $repacked_error->{'error_code'} = $error_code;
+        $repacked_error->{'error_information'} = $error_ref->{$error_code};
+        push @repacked_errors, $repacked_error;
+    } 
+
+    return @repacked_errors;
 }
 
 =head2 ModBiblio
@@ -358,76 +450,6 @@ sub ModBiblio {
     return 1;
 }
 
-=head2 ModItem
-
-=over 2
-
-Exported function (core API) for modifying an item in Koha.
-
-=back
-
-=cut
-
-sub ModItem {
-    my ( $record, $biblionumber, $itemnumber, $delete, $new_item_hashref )
-      = @_;
-    
-    #logging
-    &logaction(C4::Context->userenv->{'number'},"CATALOGUING","MODIFY",$itemnumber,$record->as_formatted) 
-        if C4::Context->preference("CataloguingLog");
-      
-    my $dbh = C4::Context->dbh;
-    
-    # if we have a MARC record, we're coming from cataloging and so
-    # we do the whole routine: update the MARC and zebra, then update the koha
-    # tables
-    if ($record) {
-        my $frameworkcode = GetFrameworkCode( $biblionumber );
-        ModItemInMarc( $record, $biblionumber, $itemnumber, $frameworkcode );
-        my $olditem       = TransformMarcToKoha( $dbh, $record, $frameworkcode,'items');
-        $olditem->{'biblionumber'} = $biblionumber;
-        my $sth =  $dbh->prepare("select biblioitemnumber from biblioitems where biblionumber=?");
-        $sth->execute($biblionumber);
-        my ($biblioitemnumber) = $sth->fetchrow;
-        $sth->finish(); 
-        $olditem->{'biblioitemnumber'} = $biblioitemnumber;
-        _koha_modify_item( $dbh, $olditem );
-        return $biblionumber;
-    }
-
-    # otherwise, we're just looking to modify something quickly
-    # (like a status) so we just update the koha tables
-    elsif ($new_item_hashref) {
-        _koha_modify_item( $dbh, $new_item_hashref );
-    }
-}
-
-sub ModItemTransfer {
-    my ( $itemnumber, $frombranch, $tobranch ) = @_;
-    
-    my $dbh = C4::Context->dbh;
-    
-    #new entry in branchtransfers....
-    my $sth = $dbh->prepare(
-        "INSERT INTO branchtransfers (itemnumber, frombranch, datesent, tobranch)
-        VALUES (?, ?, NOW(), ?)");
-    $sth->execute($itemnumber, $frombranch, $tobranch);
-    #update holdingbranch in items .....
-     $sth= $dbh->prepare(
-          "UPDATE items SET holdingbranch = ? WHERE items.itemnumber = ?");
-    $sth->execute($tobranch,$itemnumber);
-    &ModDateLastSeen($itemnumber);
-    $sth = $dbh->prepare(
-        "SELECT biblionumber FROM items WHERE itemnumber=?"
-      );
-    $sth->execute($itemnumber);
-    while ( my ( $biblionumber ) = $sth->fetchrow ) {
-        &ModItemInMarconefield( $biblionumber, $itemnumber,
-            'items.holdingbranch', $tobranch );
-    }
-    return;
-}
-
 =head2 ModBiblioframework
 
     ModBiblioframework($biblionumber,$frameworkcode);
@@ -445,96 +467,6 @@ sub ModBiblioframework {
     return 1;
 }
 
-=head2 ModItemInMarconefield
-
-=over
-
-modify only 1 field in a MARC item (mainly used for holdingbranch, but could also be used for status modif - moving a book to "lost" on a long overdu for example)
-&ModItemInMarconefield( $biblionumber, $itemnumber, $itemfield, $newvalue )
-
-=back
-
-=cut
-
-sub ModItemInMarconefield {
-    my ( $biblionumber, $itemnumber, $itemfield, $newvalue ) = @_;
-    my $dbh = C4::Context->dbh;
-    if ( !defined $newvalue ) {
-        $newvalue = "";
-    }
-
-    my $record = GetMarcItem( $biblionumber, $itemnumber );
-    my ($tagfield, $tagsubfield) = GetMarcFromKohaField( $itemfield,'');
-    # FIXME - the condition is done this way because GetMarcFromKohaField
-    # returns (0, 0) if it can't field a MARC tag for the kohafield.  However,
-    # some fields like items.wthdrawn are mapped to subfield $0, making the
-    # customary test of "if ($tagfield && $tagsubfield)" incorrect.
-    # GetMarcFromKohaField should probably be returning (undef, undef), making
-    # the correct test "if (defined $tagfield && defined $tagsubfield)", but
-    # this would be a large change and consequently deferred for after 3.0.
-    if (not(int($tagfield) == 0 && int($tagsubfield) == 0)) { 
-        my $tag = $record->field($tagfield);
-        if ($tag) {
-#             my $tagsubs = $record->field($tagfield)->subfield($tagsubfield);
-            $tag->update( $tagsubfield => $newvalue );
-            $record->delete_field($tag);
-            $record->insert_fields_ordered($tag);
-            my $frameworkcode = GetFrameworkCode( $biblionumber );
-            &ModItemInMarc( $record, $biblionumber, $itemnumber, $frameworkcode );
-        }
-    }
-}
-
-=head2 ModItemInMarc
-
-=over
-
-&ModItemInMarc( $record, $biblionumber, $itemnumber, $frameworkcode )
-
-=back
-
-=cut
-
-sub ModItemInMarc {
-    my ( $ItemRecord, $biblionumber, $itemnumber, $frameworkcode) = @_;
-    my $dbh = C4::Context->dbh;
-    
-    # get complete MARC record & replace the item field by the new one
-    my $completeRecord = GetMarcBiblio($biblionumber);
-    my ($itemtag,$itemsubfield) = GetMarcFromKohaField("items.itemnumber",$frameworkcode);
-    my $itemField = $ItemRecord->field($itemtag);
-    my @items = $completeRecord->field($itemtag);
-    foreach (@items) {
-        if ($_->subfield($itemsubfield) eq $itemnumber) {
-#             $completeRecord->delete_field($_);
-            $_->replace_with($itemField);
-        }
-    }
-    # save the record
-    my $sth = $dbh->prepare("UPDATE biblioitems SET marc=?,marcxml=? WHERE biblionumber=?");
-    $sth->execute( $completeRecord->as_usmarc(), $completeRecord->as_xml_record(),$biblionumber );
-    $sth->finish;
-    ModZebra($biblionumber,"specialUpdate","biblioserver",$completeRecord);
-}
-
-=head2 ModDateLastSeen
-
-&ModDateLastSeen($itemnum)
-Mark item as seen. Is called when an item is issued, returned or manually marked during inventory/stocktaking
-C<$itemnum> is the item number
-
-=cut
-
-sub ModDateLastSeen {
-    my ($itemnum) = @_;
-    my $dbh       = C4::Context->dbh;
-    my $sth       =
-      $dbh->prepare(
-          "UPDATE items SET itemlost=0,datelastseen  = NOW() WHERE items.itemnumber = ?"
-      );
-    $sth->execute($itemnum);
-    return;
-}
 =head2 DelBiblio
 
 =over
@@ -638,6 +570,92 @@ sub DelItem {
         if C4::Context->preference("CataloguingLog");
 }
 
+=head2 CheckItemPreSave
+
+=over 4
+
+    my $item_ref = TransformMarcToKoha($marc, 'items');
+    # do stuff
+    my %errors = CheckItemPreSave($item_ref);
+    if (exists $errors{'duplicate_barcode'}) {
+        print "item has duplicate barcode: ", $errors{'duplicate_barcode'}, "\n";
+    } elsif (exists $errors{'invalid_homebranch'}) {
+        print "item has invalid home branch: ", $errors{'invalid_homebranch'}, "\n";
+    } elsif (exists $errors{'invalid_holdingbranch'}) {
+        print "item has invalid holding branch: ", $errors{'invalid_holdingbranch'}, "\n";
+    } else {
+        print "item is OK";
+    }
+
+=back
+
+Given a hashref containing item fields, determine if it can be
+inserted or updated in the database.  Specifically, checks for
+database integrity issues, and returns a hash containing any
+of the following keys, if applicable.
+
+=over 2
+
+=item duplicate_barcode
+
+Barcode, if it duplicates one already found in the database.
+
+=item invalid_homebranch
+
+Home branch, if not defined in branches table.
+
+=item invalid_holdingbranch
+
+Holding branch, if not defined in branches table.
+
+=back
+
+This function does NOT implement any policy-related checks,
+e.g., whether current operator is allowed to save an
+item that has a given branch code.
+
+=cut
+
+sub CheckItemPreSave {
+    my $item_ref = shift;
+
+    my %errors = ();
+
+    # check for duplicate barcode
+    if (exists $item_ref->{'barcode'} and defined $item_ref->{'barcode'}) {
+        my $existing_itemnumber = GetItemnumberFromBarcode($item_ref->{'barcode'});
+        if ($existing_itemnumber) {
+            if (!exists $item_ref->{'itemnumber'}                       # new item
+                or $item_ref->{'itemnumber'} != $existing_itemnumber) { # existing item
+                $errors{'duplicate_barcode'} = $item_ref->{'barcode'};
+            }
+        }
+    }
+
+    # check for valid home branch
+    if (exists $item_ref->{'homebranch'} and defined $item_ref->{'homebranch'}) {
+        my $branch_name = 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
+            $errors{'invalid_homebranch'} = $item_ref->{'homebranch'};
+        }
+    }
+
+    # check for valid holding branch
+    if (exists $item_ref->{'holdingbranch'} and defined $item_ref->{'holdingbranch'}) {
+        my $branch_name = 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
+            $errors{'invalid_holdingbranch'} = $item_ref->{'holdingbranch'};
+        }
+    }
+
+    return %errors;
+
+}
+
 =head2 GetBiblioData
 
 =over 4
@@ -1292,39 +1310,6 @@ sub GetBiblio {
     return ( $count, @results );
 }    # sub GetBiblio
 
-=head2 GetItem
-
-=over 4
-
-$data = &GetItem($itemnumber,$barcode);
-
-return Item information, for a given itemnumber or barcode
-
-=back
-
-=cut
-
-sub GetItem {
-    my ($itemnumber,$barcode) = @_;
-    my $dbh = C4::Context->dbh;
-    if ($itemnumber) {
-        my $sth = $dbh->prepare("
-            SELECT * FROM items 
-            WHERE itemnumber = ?");
-        $sth->execute($itemnumber);
-        my $data = $sth->fetchrow_hashref;
-        return $data;
-    } else {
-        my $sth = $dbh->prepare("
-            SELECT * FROM items 
-            WHERE barcode = ?"
-            );
-        $sth->execute($barcode);
-        my $data = $sth->fetchrow_hashref;
-        return $data;
-    }
-}    # sub GetItem
-
 =head2 get_itemnumbers_of
 
 =over 4
@@ -1734,65 +1719,6 @@ sub GetAuthorisedValueDesc {
     }
 }
 
-=head2 GetMarcItem
-
-=over 4
-
-Returns MARC::Record of the item passed in parameter.
-
-=back
-
-=cut
-
-sub GetMarcItem {
-    my ( $biblionumber, $itemnumber ) = @_;
-
-    # GetMarcItem has been revised so that it does the following:
-    #  1. Gets the item information from the items table.
-    #  2. Converts it to a MARC field for storage in the bib record.
-    #
-    # The previous behavior was:
-    #  1. Get the bib record.
-    #  2. Return the MARC tag corresponding to the item record.
-    #
-    # The difference is that one treats the items row as authoritative,
-    # while the other treats the MARC representation as authoritative
-    # under certain circumstances.
-    #
-    # FIXME - a big one
-    #
-    # As of 2007-11-27, this change hopefully does not introduce
-    # any bugs.  However, it does mean that for code that uses
-    # ModItemInMarconefield to update one subfield (corresponding to
-    # an items column) is now less efficient.
-    #
-    # The API needs to be shifted to the following:
-    #  1. User updates items record.
-    #  2. Linked bib is sent for indexing.
-    # 
-    # The missing step 1.5 is updating the item tag in the bib MARC record
-    # so that the indexes are updated.  Depending on performance considerations,
-    # this may ultimately mean of of the following:
-    #  a. MARC field for item is updated right away.
-    #  b. MARC field for item is updated only as part of indexing.
-    #  c. MARC field for item is never actually stored in bib record; instead
-    #     it is generated only when needed for indexing, item export, and
-    #     (maybe) OPAC display.
-    #
-
-    my $itemrecord = GetItem($itemnumber);
-
-    # Tack on 'items.' prefix to column names so that TransformKohaToMarc will work.
-    # Also, don't emit a subfield if the underlying field is blank.
-    my $mungeditem = { map {  $itemrecord->{$_} ne '' ? ("items.$_" => $itemrecord->{$_}) : ()  } keys %{ $itemrecord } };
-
-    my $itemmarc = TransformKohaToMarc($mungeditem);
-    return $itemmarc;
-
-}
-
-
-
 =head2 GetMarcNotes
 
 =over 4
@@ -2308,7 +2234,7 @@ sub TransformHtmlToXml {
         }
         $prevtag = @$tags[$i];
     }
-    if (C4::Context->preference('marcflavour') and !$unimarc_and_100_exist) {
+    if (C4::Context->preference('marcflavour') eq 'UNIMARC' and !$unimarc_and_100_exist) {
 #     warn "SETTING 100 for $auth_type";
         use POSIX qw(strftime);
         my $string = strftime( "%Y%m%d", localtime(time) );
@@ -2429,6 +2355,9 @@ sub TransformHtmlToMarc {
     return $record;
 }
 
+# cache inverted MARC field map
+our $inverted_field_map;
+
 =head2 TransformMarcToKoha
 
 =over 4
@@ -2437,49 +2366,70 @@ sub TransformHtmlToMarc {
 
 =back
 
-=cut
+Extract data from a MARC bib record into a hashref representing
+Koha biblio, biblioitems, and items fields. 
 
+=cut
 sub TransformMarcToKoha {
-    my ( $dbh, $record, $frameworkcode, $table ) = @_;
+    my ( $dbh, $record, $frameworkcode, $limit_table ) = @_;
 
     my $result;
 
-    # sometimes we only want to return the items data
-    if ($table eq 'items') {
-        my $sth = $dbh->prepare("SHOW COLUMNS FROM items");
-        $sth->execute();
-        while ( (my $field) = $sth->fetchrow ) {
-            my $value = get_koha_field_from_marc($table,$field,$record,$frameworkcode);
-            my $key = _disambiguate($table, $field);
-            if ($result->{$key}) {
-                $result->{$key} .= " | " . $value;
-            } else {
-                $result->{$key} = $value;
-            }
-        }
-        return $result;
+    unless (defined $inverted_field_map) {
+        $inverted_field_map = _get_inverted_marc_field_map();
+    }
+
+    my %tables = ();
+    if ($limit_table eq 'items') {
+        $tables{'items'} = 1;
     } else {
-        my @tables = ('biblio','biblioitems','items');
-        foreach my $table (@tables){
-            my $sth2 = $dbh->prepare("SHOW COLUMNS from $table");
-            $sth2->execute;
-            while (my ($field) = $sth2->fetchrow){
-                # FIXME use of _disambiguate is a temporary hack
-                # $result->{_disambiguate($table, $field)} = get_koha_field_from_marc($table,$field,$record,$frameworkcode);
-                my $value = get_koha_field_from_marc($table,$field,$record,$frameworkcode);
-                my $key = _disambiguate($table, $field);
+        $tables{'items'} = 1;
+        $tables{'biblio'} = 1;
+        $tables{'biblioitems'} = 1;
+    }
+
+    # traverse through record
+    MARCFIELD: foreach my $field ($record->fields()) {
+        my $tag = $field->tag();
+        next MARCFIELD unless exists $inverted_field_map->{$frameworkcode}->{$tag};
+        if ($field->is_control_field()) {
+            my $kohafields = $inverted_field_map->{$frameworkcode}->{$tag}->{list};
+            ENTRY: foreach my $entry (@{ $kohafields }) {
+                my ($subfield, $table, $column) = @{ $entry };
+                next ENTRY unless exists $tables{$table};
+                my $key = _disambiguate($table, $column);
                 if ($result->{$key}) {
-                    # FIXME - hack to not bring in duplicates of the same value
-                    unless (($key eq "biblionumber" or $key eq "biblioitemnumber") and ($value eq "")) {
-                        $result->{$key} .= " | " . $value;
+                    unless (($key eq "biblionumber" or $key eq "biblioitemnumber") and ($field->data() eq "")) {
+                        $result->{$key} .= " | " . $field->data();
                     }
                 } else {
-                    $result->{$key} = $value;
+                    $result->{$key} = $field->data();
+                }
+            }
+        } else {
+            # deal with subfields
+            MARCSUBFIELD: foreach my $sf ($field->subfields()) {
+                my $code = $sf->[0];
+                next MARCSUBFIELD unless exists $inverted_field_map->{$frameworkcode}->{$tag}->{sfs}->{$code};
+                my $value = $sf->[1];
+                SFENTRY: foreach my $entry (@{ $inverted_field_map->{$frameworkcode}->{$tag}->{sfs}->{$code} }) {
+                    my ($table, $column) = @{ $entry };
+                    next SFENTRY unless exists $tables{$table};
+                    my $key = _disambiguate($table, $column);
+                    if ($result->{$key}) {
+                        unless (($key eq "biblionumber" or $key eq "biblioitemnumber") and ($value eq "")) {
+                            $result->{$key} .= " | " . $value;
+                        }
+                    } else {
+                        $result->{$key} = $value;
+                    }
                 }
             }
-            $sth2->finish();
         }
-        # modify copyrightdate to keep only the 1st year found
+    }
+
+    # modify copyrightdate to keep only the 1st year found
+    if (exists $result->{'copyrightdate'}) {
         my $temp = $result->{'copyrightdate'};
         $temp =~ m/c(\d\d\d\d)/;    # search cYYYY first
         if ( $1 > 0 ) {
@@ -2489,9 +2439,11 @@ sub TransformMarcToKoha {
             $temp =~ m/(\d\d\d\d)/;
             $result->{'copyrightdate'} = $1;
         }
-    
-        # modify publicationyear to keep only the 1st year found
-        $temp = $result->{'publicationyear'};
+    }
+
+    # modify publicationyear to keep only the 1st year found
+    if (exists $result->{'publicationyear'}) {
+        my $temp = $result->{'publicationyear'};
         $temp =~ m/c(\d\d\d\d)/;    # search cYYYY first
         if ( $1 > 0 ) {
             $result->{'publicationyear'} = $1;
@@ -2500,10 +2452,28 @@ sub TransformMarcToKoha {
             $temp =~ m/(\d\d\d\d)/;
             $result->{'publicationyear'} = $1;
         }
-        return $result;
     }
+
+    return $result;
 }
 
+sub _get_inverted_marc_field_map {
+    my $relations = C4::Context->marcfromkohafield;
+
+    my $field_map = {};
+    my $relations = C4::Context->marcfromkohafield;
+
+    foreach my $frameworkcode (keys %{ $relations }) {
+        foreach my $kohafield (keys %{ $relations->{$frameworkcode} }) {
+            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 ];
+        }
+    }
+    return $field_map;
+}
 
 =head2 _disambiguate
 
@@ -3152,7 +3122,6 @@ sub GetNoZebraIndexes {
     my %indexes;
     foreach my $line (split /('|"),/,$index) {
         $line =~ /(.*)=>(.*)/;
-warn $line;
         my $index = substr($1,1); # get the index, don't forget to remove initial ' or "
         my $fields = $2;
         $index =~ s/'|"|\s//g;
@@ -3831,7 +3800,6 @@ my ($itemnumber,$error) = _koha_new_items( $dbh, $item, $barcode );
 sub _koha_new_items {
     my ( $dbh, $item, $barcode ) = @_;
     my $error;
-
     my ($items_cn_sort) = GetClassSort($item->{'items.cn_source'}, $item->{'itemcallnumber'}, "");
 
     # if dateaccessioned is provided, use it. Otherwise, set to NOW()
@@ -3915,54 +3883,6 @@ sub _koha_new_items {
     return ( $itemnumber, $error );
 }
 
-=head2 _koha_modify_item
-
-=over 4
-
-my ($itemnumber,$error) =_koha_modify_item( $dbh, $item, $op );
-
-=back
-
-=cut
-
-sub _koha_modify_item {
-    my ( $dbh, $item ) = @_;
-    my $error;
-
-    # calculate items.cn_sort
-    if($item->{'itemcallnumber'}) {
-        # This works, even when user is setting the call number blank (in which case
-        # how would we get here to calculate new (blank) of items.cn_sort?).
-        # 
-        # Why?  Because at present the only way to update itemcallnumber is via
-        # additem.pl; since it uses a MARC data-entry form, TransformMarcToKoha
-        # already has created $item->{'items.cn_sort'} and set it to undef because the 
-        # subfield for items.cn_sort in the framework is specified as ignored, meaning
-        # that it is not supplied or passed to the form.  Thus, if the user has
-        # blanked itemcallnumber, there is already a undef value for $item->{'items.cn_sort'}.
-        #
-        # This is subtle; it is also fragile.
-        $item->{'items.cn_sort'} = GetClassSort($item->{'items.cn_source'}, $item->{'itemcallnumber'}, "");
-    }
-    my $query = "UPDATE items SET ";
-    my @bind;
-    for my $key ( keys %$item ) {
-        $query.="$key=?,";
-        push @bind, $item->{$key};
-    }
-    $query =~ s/,$//;
-    $query .= " WHERE itemnumber=?";
-    push @bind, $item->{'itemnumber'};
-    my $sth = $dbh->prepare($query);
-    $sth->execute(@bind);
-    if ( $dbh->errstr ) {
-        $error.="ERROR in _koha_modify_item $query".$dbh->errstr;
-        warn $error;
-    }
-    $sth->finish();
-    return ($item->{'itemnumber'},$error);
-}
-
 =head2 _koha_delete_biblio
 
 =over 4
@@ -4159,38 +4079,6 @@ sub ModBiblioMarc {
     return $biblionumber;
 }
 
-=head2 AddItemInMarc
-
-=over 4
-
-$newbiblionumber = AddItemInMarc( $record, $biblionumber, $frameworkcode );
-
-Add an item in a MARC record and save the MARC record
-
-Function exported, but should NOT be used, unless you really know what you're doing
-
-=back
-
-=cut
-
-sub AddItemInMarc {
-
-    # pass the MARC::Record to this function, and it will create the records in the marc tables
-    my ( $record, $biblionumber, $frameworkcode ) = @_;
-    my $newrec = &GetMarcBiblio($biblionumber);
-
-    # create it
-    my @fields = $record->fields();
-    foreach my $field (@fields) {
-        $newrec->append_fields($field);
-    }
-
-    # FIXME: should we be making sure the biblionumbers are the same?
-    my $newbiblionumber =
-      &ModBiblioMarc( $newrec, $biblionumber, $frameworkcode );
-    return $newbiblionumber;
-}
-
 =head2 z3950_extended_services
 
 z3950_extended_services($serviceType,$serviceOptions,$record);