VirtualShelves.pm - expanded SQL for shelf contents, fixes bug of no itemtypes displa...
[koha.git] / C4 / Items.pm
index ca8d74f..1c6cf32 100644 (file)
@@ -19,8 +19,6 @@ package C4::Items;
 
 use strict;
 
-require Exporter;
-
 use C4::Context;
 use C4::Koha;
 use C4::Biblio;
@@ -28,34 +26,43 @@ use C4::Dates qw/format_date format_date_in_iso/;
 use MARC::Record;
 use C4::ClassSource;
 use C4::Log;
-use C4::Reserves;
+use C4::Branch;
+require C4::Reserves;
 
 use vars qw($VERSION @ISA @EXPORT);
 
-my $VERSION = 3.00;
-
-@ISA = qw( Exporter );
-
-# function exports
-@EXPORT = qw(
-    GetItem
-    AddItemFromMarc
-    AddItem
-    ModItemFromMarc
-    ModItem
-    ModDateLastSeen
-    ModItemTransfer
-
-    GetItemStatus
-    GetItemLocation
-    GetLostItems
-    GetItemsForInventory
-    GetItemsCount
-    GetItemInfosOf
-    GetItemsByBiblioitemnumber
-    GetItemsInfo
-    get_itemnumbers_of
-);
+BEGIN {
+    $VERSION = 3.01;
+
+       require Exporter;
+    @ISA = qw( Exporter );
+
+    # function exports
+    @EXPORT = qw(
+        GetItem
+        AddItemFromMarc
+        AddItem
+        AddItemBatchFromMarc
+        ModItemFromMarc
+        ModItem
+        ModDateLastSeen
+        ModItemTransfer
+        DelItem
+    
+        CheckItemPreSave
+    
+        GetItemStatus
+        GetItemLocation
+        GetLostItems
+        GetItemsForInventory
+        GetItemsCount
+        GetItemInfosOf
+        GetItemsByBiblioitemnumber
+        GetItemsInfo
+        get_itemnumbers_of
+        GetItemnumberFromBarcode
+    );
+}
 
 =head1 NAME
 
@@ -102,35 +109,41 @@ of C<C4::Items>
 
 =over 4
 
-$item = GetItem($itemnumber,$barcode);
+$item = GetItem($itemnumber,$barcode,$serial);
 
 =back
 
 Return item information, for a given itemnumber or barcode.
 The return value is a hashref mapping item column
-names to values.
+names to values.  If C<$serial> is true, include serial publication data.
 
 =cut
 
 sub GetItem {
-    my ($itemnumber,$barcode) = @_;
+    my ($itemnumber,$barcode, $serial) = @_;
     my $dbh = C4::Context->dbh;
+       my $data;
     if ($itemnumber) {
         my $sth = $dbh->prepare("
             SELECT * FROM items 
             WHERE itemnumber = ?");
         $sth->execute($itemnumber);
-        my $data = $sth->fetchrow_hashref;
-        return $data;
+        $data = $sth->fetchrow_hashref;
     } else {
         my $sth = $dbh->prepare("
             SELECT * FROM items 
             WHERE barcode = ?"
             );
-        $sth->execute($barcode);
-        my $data = $sth->fetchrow_hashref;
-        return $data;
+        $sth->execute($barcode);               
+        $data = $sth->fetchrow_hashref;
     }
+    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();
+               warn $data->{'serialseq'} , $data->{'publisheddate'};
+    }          
+    return $data;
 }    # sub GetItem
 
 =head2 AddItemFromMarc
@@ -154,7 +167,6 @@ sub AddItemFromMarc {
     # parse item hash from MARC
     my $frameworkcode = GetFrameworkCode( $biblionumber );
     my $item = &TransformMarcToKoha( $dbh, $source_item_marc, $frameworkcode );
-
     return AddItem($item, $biblionumber, $dbh, $frameworkcode);
 }
 
@@ -192,7 +204,7 @@ sub AddItem {
     _set_defaults_for_add($item);
     _set_derived_columns_for_add($item);
     # FIXME - checks here
-    my ( $itemnumber, $error ) = _koha_new_item( $dbh, $item, $item->{barcode} );
+       my ( $itemnumber, $error ) = _koha_new_item( $dbh, $item, $item->{barcode} );
     $item->{'itemnumber'} = $itemnumber;
 
     # create MARC tag representing item and add to bib
@@ -205,6 +217,112 @@ sub AddItem {
     return ($item->{biblionumber}, $item->{biblioitemnumber}, $itemnumber);
 }
 
+=head2 AddItemBatchFromMarc
+
+=over 4
+
+($itemnumber_ref, $error_ref) = AddItemBatchFromMarc($record, $biblionumber, $biblioitemnumber, $frameworkcode);
+
+=back
+
+Efficiently create item records from a MARC biblio record with
+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.
+
+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.
+
+This function returns 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
+
+=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 AddItemBatchFromMarc {
+    my ($record, $biblionumber, $biblioitemnumber, $frameworkcode) = @_;
+    my $error;
+    my @itemnumbers = ();
+    my @errors = ();
+    my $dbh = C4::Context->dbh;
+
+    # 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 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;
+        }
+
+        _set_defaults_for_add($item);
+        _set_derived_columns_for_add($item);
+        my ( $itemnumber, $error ) = _koha_new_item( $dbh, $item, $item->{barcode} );
+        warn $error if $error;
+        push @itemnumbers, $itemnumber; # FIXME not checking error
+        $item->{'itemnumber'} = $itemnumber;
+
+        &logaction(C4::Context->userenv->{'number'},"CATALOGUING","ADD",$itemnumber,"item")
+        if C4::Context->preference("CataloguingLog"); 
+
+        my $new_item_marc = _marc_from_item_hash($item, $frameworkcode);
+        $item_field->replace_with($new_item_marc->field($itemtag));
+    }
+
+    # remove any MARC item fields for rejected items
+    foreach my $item_field (@bad_item_fields) {
+        $record->delete_field($item_field);
+    }
+
+    # update the MARC biblio
+    $biblionumber = ModBiblioMarc( $record, $biblionumber, $frameworkcode );
+
+    return (\@itemnumbers, \@errors);
+}
+
 =head2 ModItemFromMarc
 
 =over 4
@@ -267,7 +385,7 @@ sub ModItem {
     my $dbh           = @_ ? shift : C4::Context->dbh;
     my $frameworkcode = @_ ? shift : GetFrameworkCode( $biblionumber );
 
-    $item->{'itemnumber'} = $itemnumber;
+    $item->{'itemnumber'} = $itemnumber or return undef;
     _set_derived_columns_for_mod($item);
     _do_column_fixes_for_mod($item);
     # FIXME add checks
@@ -281,10 +399,11 @@ sub ModItem {
     _koha_modify_item($dbh, $item);
 
     # update biblio MARC XML
-    my $whole_item = GetItem($itemnumber);
-    my $new_item_marc = _marc_from_item_hash($whole_item, $frameworkcode);
+    my $whole_item = GetItem($itemnumber) or die "FAILED GetItem($itemnumber)";
+    my $new_item_marc = _marc_from_item_hash($whole_item, $frameworkcode) or die "FAILED _marc_from_item_hash($whole_item, $frameworkcode)";
     _replace_item_field_in_biblio($new_item_marc, $biblionumber, $itemnumber, $frameworkcode);
-    
+       (C4::Context->userenv eq '0') and die "userenv is '0', not hashref";         # logaction line would crash anyway
+       ($new_item_marc       eq '0') and die "$new_item_marc is '0', not hashref";  # logaction line would crash anyway
     logaction(C4::Context->userenv->{'number'},"CATALOGUING","MODIFY",$itemnumber,$new_item_marc->as_formatted)
         if C4::Context->preference("CataloguingLog");
 }
@@ -338,6 +457,134 @@ sub ModDateLastSeen {
     ModItem({ itemlost => 0, datelastseen => $today->output("iso") }, undef, $itemnumber);
 }
 
+=head2 DelItem
+
+=over 4
+
+DelItem($biblionumber, $itemnumber);
+
+=back
+
+Exported function (core API) for deleting an item record in Koha.
+
+=cut
+
+sub DelItem {
+    my ( $dbh, $biblionumber, $itemnumber ) = @_;
+    
+    # FIXME check the item has no current issues
+    
+    _koha_delete_item( $dbh, $itemnumber );
+
+    # get the MARC record
+    my $record = GetMarcBiblio($biblionumber);
+    my $frameworkcode = GetFrameworkCode($biblionumber);
+
+    # backup the record
+    my $copy2deleted = $dbh->prepare("UPDATE deleteditems SET marc=? WHERE itemnumber=?");
+    $copy2deleted->execute( $record->as_usmarc(), $itemnumber );
+
+    #search item field code
+    my ( $itemtag, $itemsubfield ) = GetMarcFromKohaField("items.itemnumber",$frameworkcode);
+    my @fields = $record->field($itemtag);
+
+    # delete the item specified
+    foreach my $field (@fields) {
+        if ( $field->subfield($itemsubfield) eq $itemnumber ) {
+            $record->delete_field($field);
+        }
+    }
+    &ModBiblioMarc( $record, $biblionumber, $frameworkcode );
+    &logaction(C4::Context->userenv->{'number'},"CATALOGUING","DELETE",$itemnumber,"item") 
+        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;
+
+}
+
 =head1 EXPORTED SPECIAL ACCESSOR FUNCTIONS
 
 The following functions provide various ways of 
@@ -844,7 +1091,7 @@ sub GetItemsInfo {
     $sth->execute($biblionumber);
     my $i = 0;
     my @results;
-    my ( $date_due, $count_reserves );
+    my ( $date_due, $count_reserves, $serial );
 
     my $isth    = $dbh->prepare(
         "SELECT issues.*,borrowers.cardnumber,borrowers.surname,borrowers.firstname,borrowers.branchcode as bcode
@@ -852,7 +1099,9 @@ sub GetItemsInfo {
         WHERE  itemnumber = ?
             AND returndate IS NULL"
        );
-    while ( my $data = $sth->fetchrow_hashref ) {
+       my $ssth = $dbh->prepare("SELECT serialseq,publisheddate from serialitems left join serial on serialitems.serialid=serial.serialid where serialitems.itemnumber=? "); 
+       while ( my $data = $sth->fetchrow_hashref ) {
+          warn $data->{itemnumber};
         my $datedue = '';
         $isth->execute( $data->{'itemnumber'} );
         if ( my $idata = $isth->fetchrow_hashref ) {
@@ -868,7 +1117,13 @@ sub GetItemsInfo {
         }
         }
         }
-        if ( $datedue eq '' ) {
+               if ( $data->{'serial'}) {       
+                       $ssth->execute($data->{'itemnumber'}) ;
+                       ($data->{'serialseq'} , $data->{'publisheddate'}) = $ssth->fetchrow_array();
+                       warn $data->{'serialseq'} , $data->{'publisheddate'};
+                       $serial = 1;
+        }
+               if ( $datedue eq '' ) {
             my ( $restype, $reserves ) =
               C4::Reserves::CheckReserves( $data->{'itemnumber'} );
             if ($restype) {
@@ -876,7 +1131,7 @@ sub GetItemsInfo {
             }
         }
         $isth->finish;
-
+        $ssth->finish;
         #get branch information.....
         my $bsth = $dbh->prepare(
             "SELECT * FROM branches WHERE branchcode = ?
@@ -951,8 +1206,11 @@ sub GetItemsInfo {
         $i++;
     }
     $sth->finish;
-
-    return (@results);
+       if($serial) {
+               return( sort { $b->{'publisheddate'} cmp $a->{'publisheddate'} } @results );
+       } else {
+       return (@results);
+       }
 }
 
 =head2 get_itemnumbers_of
@@ -994,6 +1252,27 @@ sub get_itemnumbers_of {
     return \%itemnumbers_of;
 }
 
+=head2 GetItemnumberFromBarcode
+
+=over 4
+
+$result = GetItemnumberFromBarcode($barcode);
+
+=back
+
+=cut
+
+sub GetItemnumberFromBarcode {
+    my ($barcode) = @_;
+    my $dbh = C4::Context->dbh;
+
+    my $rq =
+      $dbh->prepare("SELECT itemnumber FROM items WHERE items.barcode=?");
+    $rq->execute($barcode);
+    my ($result) = $rq->fetchrow;
+    return ($result);
+}
+
 =head1 LIMITED USE FUNCTIONS
 
 The following functions, while part of the public API,
@@ -1289,10 +1568,10 @@ sub _set_defaults_for_add {
     }
 
     # various item status fields cannot be null
-    $item->{'notforloan'} = 0 unless exists $item->{'notforloan'} and defined $item->{'notforloan'};
-    $item->{'damaged'}    = 0 unless exists $item->{'damaged'}    and defined $item->{'damaged'};
-    $item->{'itemlost'}   = 0 unless exists $item->{'itemlost'}   and defined $item->{'itemlost'};
-    $item->{'wthdrawn'}   = 0 unless exists $item->{'wthdrawn'}   and defined $item->{'wthdrawn'};
+    $item->{'notforloan'} = 0 unless exists $item->{'notforloan'} and defined $item->{'notforloan'} and $item->{'notforloan'} ne '';
+    $item->{'damaged'}    = 0 unless exists $item->{'damaged'}    and defined $item->{'damaged'}    and $item->{'damaged'} ne '';
+    $item->{'itemlost'}   = 0 unless exists $item->{'itemlost'}   and defined $item->{'itemlost'}   and $item->{'itemlost'} ne '';
+    $item->{'wthdrawn'}   = 0 unless exists $item->{'wthdrawn'}   and defined $item->{'wthdrawn'}   and $item->{'wthdrawn'} ne '';
 }
 
 =head2 _koha_new_item
@@ -1310,8 +1589,7 @@ Perform the actual insert into the C<items> table.
 sub _koha_new_item {
     my ( $dbh, $item, $barcode ) = @_;
     my $error;
-
-    my $query = 
+    my $query =
            "INSERT INTO items SET
             biblionumber        = ?,
             biblioitemnumber    = ?,
@@ -1344,10 +1622,10 @@ sub _koha_new_item {
             ccode               = ?,
             itype               = ?,
             materials           = ?,
-            uri                 = ?
+                       uri                 = ?
           ";
     my $sth = $dbh->prepare($query);
-    $sth->execute(
+   $sth->execute(
             $item->{'biblionumber'},
             $item->{'biblioitemnumber'},
             $barcode,
@@ -1423,6 +1701,44 @@ sub _koha_modify_item {
     return ($item->{'itemnumber'},$error);
 }
 
+=head2 _koha_delete_item
+
+=over 4
+
+_koha_delete_item( $dbh, $itemnum );
+
+=back
+
+Internal function to delete an item record from the koha tables
+
+=cut
+
+sub _koha_delete_item {
+    my ( $dbh, $itemnum ) = @_;
+
+    # save the deleted item to deleteditems table
+    my $sth = $dbh->prepare("SELECT * FROM items WHERE itemnumber=?");
+    $sth->execute($itemnum);
+    my $data = $sth->fetchrow_hashref();
+    $sth->finish();
+    my $query = "INSERT INTO deleteditems SET ";
+    my @bind  = ();
+    foreach my $key ( keys %$data ) {
+        $query .= "$key = ?,";
+        push( @bind, $data->{$key} );
+    }
+    $query =~ s/\,$//;
+    $sth = $dbh->prepare($query);
+    $sth->execute(@bind);
+    $sth->finish();
+
+    # delete from items table
+    $sth = $dbh->prepare("DELETE FROM items WHERE itemnumber=?");
+    $sth->execute($itemnum);
+    $sth->finish();
+    return undef;
+}
+
 =head2 _marc_from_item_hash
 
 =over 4
@@ -1532,4 +1848,30 @@ sub _replace_item_field_in_biblio {
     ModBiblioMarc($completeRecord, $biblionumber, $frameworkcode);
 }
 
+=head2 _repack_item_errors
+
+Add an error message hash generated by C<CheckItemPreSave>
+to a list of errors.
+
+=cut
+
+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;
+}
+
 1;