X-Git-Url: http://git.rot13.org/?a=blobdiff_plain;f=C4%2FItems.pm;h=d19caf9a1df5eab6464ea769ced2adb81d32c1b5;hb=5cbb6dfeb169aca83365ead01524bc024e9cfd8b;hp=0b224b5ac8bb720ceb80f7baafed92277e83d4b0;hpb=3b43e43e494681105308a55ffe1e9cfdb513d0c1;p=koha.git diff --git a/C4/Items.pm b/C4/Items.pm index 0b224b5ac8..d19caf9a1d 100644 --- a/C4/Items.pm +++ b/C4/Items.pm @@ -19,8 +19,6 @@ package C4::Items; use strict; -require Exporter; - use C4::Context; use C4::Koha; use C4::Biblio; @@ -28,35 +26,45 @@ 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 C4::Charset; use vars qw($VERSION @ISA @EXPORT); -my $VERSION = 3.00; - -@ISA = qw( Exporter ); - -# function exports -@EXPORT = qw( - GetItem - AddItemFromMarc - AddItem - ModItemFromMarc - ModItem - ModDateLastSeen - ModItemTransfer - DelItem - - 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 + DelItemCheck + + CheckItemPreSave + + GetItemStatus + GetItemLocation + GetLostItems + GetItemsForInventory + GetItemsCount + GetItemInfosOf + GetItemsByBiblioitemnumber + GetItemsInfo + get_itemnumbers_of + GetItemnumberFromBarcode + ); +} =head1 NAME @@ -103,35 +111,47 @@ of C =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'}; } + #if we don't have an items.itype, use biblioitems.itemtype. + if( ! $data->{'itype'} ) { + my $sth = $dbh->prepare("SELECT itemtype FROM biblioitems WHERE biblionumber = ?"); + $sth->execute($data->{'biblionumber'}); + ($data->{'itype'}) = $sth->fetchrow_array; + } + return $data; } # sub GetItem =head2 AddItemFromMarc @@ -154,9 +174,13 @@ 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); + my ($itemtag,$itemsubfield)=GetMarcFromKohaField("items.itemnumber",$frameworkcode); + + my $localitemmarc=MARC::Record->new; + $localitemmarc->append_fields($source_item_marc->field($itemtag)); + my $item = &TransformMarcToKoha( $dbh, $localitemmarc, $frameworkcode ,'items'); + my $unlinked_item_subfields = _get_unlinked_item_subfields($localitemmarc, $frameworkcode); + return AddItem($item, $biblionumber, $dbh, $frameworkcode, $unlinked_item_subfields); } =head2 AddItem @@ -164,17 +188,24 @@ sub AddItemFromMarc { =over 4 my ($biblionumber, $biblioitemnumber, $itemnumber) - = AddItem($item, $biblionumber[, $dbh, $frameworkcode]); + = AddItem($item, $biblionumber[, $dbh, $frameworkcode, $unlinked_item_subfields]); =back Given a hash containing item column names as keys, create a new Koha item record. -The two optional parameters (C<$dbh> and C<$frameworkcode>) +The first two optional parameters (C<$dbh> and C<$frameworkcode>) do not need to be supplied for general use; they exist simply to allow them to be picked up from AddItemFromMarc. +The final optional parameter, C<$unlinked_item_subfields>, contains +an arrayref containing subfields present in the original MARC +representation of the item (e.g., from the item editor) that are +not mapped to C columns directly but should instead +be stored in C and included in +the biblio items tag for display and indexing. + =cut sub AddItem { @@ -183,6 +214,10 @@ sub AddItem { my $dbh = @_ ? shift : C4::Context->dbh; my $frameworkcode = @_ ? shift : GetFrameworkCode( $biblionumber ); + my $unlinked_item_subfields; + if (@_) { + $unlinked_item_subfields = shift + }; # needs old biblionumber and biblioitemnumber $item->{'biblionumber'} = $biblionumber; @@ -192,20 +227,133 @@ sub AddItem { _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 - my ( $itemnumber, $error ) = _koha_new_item( $dbh, $item, $item->{barcode} ); + 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} ); $item->{'itemnumber'} = $itemnumber; # create MARC tag representing item and add to bib - my $new_item_marc = _marc_from_item_hash($item, $frameworkcode); + my $new_item_marc = _marc_from_item_hash($item, $frameworkcode, $unlinked_item_subfields); _add_item_field_to_biblio($new_item_marc, $item->{'biblionumber'}, $frameworkcode ); - logaction(C4::Context->userenv->{'number'},"CATALOGUING","ADD",$itemnumber,"item") - if C4::Context->preference("CataloguingLog"); + logaction("CATALOGUING", "ADD", $itemnumber, "item") if C4::Context->preference("CataloguingLog"); 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 and C tables. It does +not expect that C and C +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' ); + my $unlinked_item_subfields = _get_unlinked_item_subfields($temp_item_marc, $frameworkcode); + $item->{'more_subfields_xml'} = _get_unlinked_subfields_xml($unlinked_item_subfields); + $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( $item, $item->{barcode} ); + warn $error if $error; + push @itemnumbers, $itemnumber; # FIXME not checking error + $item->{'itemnumber'} = $itemnumber; + + logaction("CATALOGUING", "ADD", $itemnumber, "item") if C4::Context->preference("CataloguingLog"); + + my $new_item_marc = _marc_from_item_hash($item, $frameworkcode, $unlinked_item_subfields); + $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 @@ -219,8 +367,49 @@ C object containing an embedded item field. This API is meant for the use of C; for other purposes, C should be used. +This function uses the hash %default_values_for_mod_from_marc, +which contains default values for item fields to +apply when modifying an item. This is needed beccause +if an item field's value is cleared, TransformMarcToKoha +does not include the column in the +hash that's passed to ModItem, which without +use of this hash makes it impossible to clear +an item field's value. See bug 2466. + +Note that only columns that can be directly +changed from the cataloging and serials +item editors are included in this hash. + =cut +my %default_values_for_mod_from_marc = ( + barcode => undef, + booksellerid => undef, + ccode => undef, + 'items.cn_source' => undef, + copynumber => undef, + damaged => 0, + dateaccessioned => undef, + enumchron => undef, + holdingbranch => undef, + homebranch => undef, + itemcallnumber => undef, + itemlost => 0, + itemnotes => undef, + itype => undef, + location => undef, + materials => undef, + notforloan => 0, + paidfor => undef, + price => undef, + replacementprice => undef, + replacementpricedate => undef, + restricted => undef, + stack => undef, + uri => undef, + wthdrawn => 0, +); + sub ModItemFromMarc { my $item_marc = shift; my $biblionumber = shift; @@ -228,16 +417,24 @@ sub ModItemFromMarc { my $dbh = C4::Context->dbh; my $frameworkcode = GetFrameworkCode( $biblionumber ); - my $item = &TransformMarcToKoha( $dbh, $item_marc, $frameworkcode ); - - return ModItem($item, $biblionumber, $itemnumber, $dbh, $frameworkcode); + my ($itemtag,$itemsubfield)=GetMarcFromKohaField("items.itemnumber",$frameworkcode); + + my $localitemmarc=MARC::Record->new; + $localitemmarc->append_fields($item_marc->field($itemtag)); + my $item = &TransformMarcToKoha( $dbh, $localitemmarc, $frameworkcode, 'items'); + foreach my $item_field (keys %default_values_for_mod_from_marc) { + $item->{$item_field} = $default_values_for_mod_from_marc{$item_field} unless exists $item->{$item_field}; + } + my $unlinked_item_subfields = _get_unlinked_item_subfields($localitemmarc, $frameworkcode); + + return ModItem($item, $biblionumber, $itemnumber, $dbh, $frameworkcode, $unlinked_item_subfields); } =head2 ModItem =over 4 -ModItem({ column => $newvalue }, $biblionumber, $itemnumber); +ModItem({ column => $newvalue }, $biblionumber, $itemnumber[, $original_item_marc]); =back @@ -248,6 +445,13 @@ The first argument is a hashref mapping from item column names to the new values. The second and third arguments are the biblionumber and itemnumber, respectively. +The fourth, optional parameter, C<$unlinked_item_subfields>, contains +an arrayref containing subfields present in the original MARC +representation of the item (e.g., from the item editor) that are +not mapped to C columns directly but should instead +be stored in C and included in +the biblio items tag for display and indexing. + If one of the changed columns is used to calculate the derived value of a column such as C, this routine will perform the necessary calculation @@ -267,8 +471,14 @@ sub ModItem { my $dbh = @_ ? shift : C4::Context->dbh; my $frameworkcode = @_ ? shift : GetFrameworkCode( $biblionumber ); + + my $unlinked_item_subfields; + if (@_) { + $unlinked_item_subfields = shift; + $item->{'more_subfields_xml'} = _get_unlinked_subfields_xml($unlinked_item_subfields); + }; - $item->{'itemnumber'} = $itemnumber; + $item->{'itemnumber'} = $itemnumber or return undef; _set_derived_columns_for_mod($item); _do_column_fixes_for_mod($item); # FIXME add checks @@ -279,15 +489,20 @@ sub ModItem { # it should be a separate function) # update items table - _koha_modify_item($dbh, $item); + _koha_modify_item($item); # update biblio MARC XML - my $whole_item = GetItem($itemnumber); - my $new_item_marc = _marc_from_item_hash($whole_item, $frameworkcode); - _replace_item_field_in_biblio($new_item_marc, $biblionumber, $itemnumber, $frameworkcode); + my $whole_item = GetItem($itemnumber) or die "FAILED GetItem($itemnumber)"; + + unless (defined $unlinked_item_subfields) { + $unlinked_item_subfields = _parse_unlinked_item_subfields_from_xml($whole_item->{'more_subfields_xml'}); + } + my $new_item_marc = _marc_from_item_hash($whole_item, $frameworkcode, $unlinked_item_subfields) + or die "FAILED _marc_from_item_hash($whole_item, $frameworkcode)"; - logaction(C4::Context->userenv->{'number'},"CATALOGUING","MODIFY",$itemnumber,$new_item_marc->as_formatted) - if C4::Context->preference("CataloguingLog"); + _replace_item_field_in_biblio($new_item_marc, $biblionumber, $itemnumber, $frameworkcode); + ($new_item_marc eq '0') and die "$new_item_marc is '0', not hashref"; # logaction line would crash anyway + logaction("CATALOGUING", "MODIFY", $itemnumber, $new_item_marc->as_formatted) if C4::Context->preference("CataloguingLog"); } =head2 ModItemTransfer @@ -377,8 +592,134 @@ sub DelItem { } } &ModBiblioMarc( $record, $biblionumber, $frameworkcode ); - &logaction(C4::Context->userenv->{'number'},"CATALOGUING","DELETE",$itemnumber,"item") - if C4::Context->preference("CataloguingLog"); + logaction("CATALOGUING", "DELETE", $itemnumber, "item") if C4::Context->preference("CataloguingLog"); +} + +=head2 DelItemCheck + +=over 4 + +DelItemCheck($dbh, $biblionumber, $itemnumber); + +=back + +Exported function (core API) for deleting an item record in Koha if there no current issue. + +=cut + +sub DelItemCheck { + my ( $dbh, $biblionumber, $itemnumber ) = @_; + my $error; + + # check that there is no issue on this item before deletion. + my $sth=$dbh->prepare("select * from issues i where i.itemnumber=?"); + $sth->execute($itemnumber); + + my $onloan=$sth->fetchrow; + + $sth->finish(); + if ($onloan){ + $error = "book_on_loan" + }else{ + # check it doesnt have a waiting reserve + $sth=$dbh->prepare("SELECT * FROM reserves WHERE found = 'W' AND itemnumber = ?"); + $sth->execute($itemnumber); + my $reserve=$sth->fetchrow; + $sth->finish(); + if ($reserve){ + $error = "book_reserved"; + }else{ + DelItem($dbh, $biblionumber, $itemnumber); + return 1; + } + } + return $error; +} + +=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 @@ -600,28 +941,35 @@ sub GetItemLocation { =over 4 -$items = GetLostItems($where,$orderby); +$items = GetLostItems( $where, $orderby ); =back -This function get the items lost into C<$items>. +This function gets a list of lost items. =over 2 =item input: + C<$where> is a hashref. it containts a field of the items table as key -and the value to match as value. -C<$orderby> is a field of the items table. +and the value to match as value. For example: + +{ barcode => 'abc123', + homebranch => 'CPL', } + +C<$orderby> is a field of the items table by which the resultset +should be orderd. =item return: -C<$items> is a reference to an array full of hasref which keys are items' table column. + +C<$items> is a reference to an array full of hashrefs with columns +from the "items" table as keys. =item usage in the perl script: -my %where; -$where{barcode} = 0001548; -my $items = GetLostItems( \%where, "homebranch" ); -$template->param(itemsloop => $items); +my $where = { barcode => '0001548' }; +my $items = GetLostItems( $where, "homebranch" ); +$template->param( itemsloop => $items ); =back @@ -636,75 +984,116 @@ sub GetLostItems { my $query = " SELECT * FROM items - WHERE itemlost IS NOT NULL - AND itemlost <> 0 + LEFT JOIN biblio ON (items.biblionumber = biblio.biblionumber) + LEFT JOIN biblioitems ON (items.biblionumber = biblioitems.biblionumber) + LEFT JOIN authorised_values ON (items.itemlost = authorised_values.authorised_value) + WHERE + authorised_values.category = 'LOST' + AND itemlost IS NOT NULL + AND itemlost <> 0 "; + my @query_parameters; foreach my $key (keys %$where) { - $query .= " AND " . $key . " LIKE '%" . $where->{$key} . "%'"; + $query .= " AND $key LIKE ?"; + push @query_parameters, "%$where->{$key}%"; + } + my @ordervalues = qw/title author homebranch itype barcode price replacementprice lib datelastseen location/; + + if ( defined $orderby && grep($orderby, @ordervalues)) { + $query .= ' ORDER BY '.$orderby; } - $query .= " ORDER BY ".$orderby if defined $orderby; my $sth = $dbh->prepare($query); - $sth->execute; - my @items; + $sth->execute( @query_parameters ); + my $items = []; while ( my $row = $sth->fetchrow_hashref ){ - push @items, $row; + push @$items, $row; } - return \@items; + return $items; } =head2 GetItemsForInventory =over 4 -$itemlist = GetItemsForInventory($minlocation,$maxlocation,$datelastseen,$offset,$size) +$itemlist = GetItemsForInventory($minlocation, $maxlocation, $location, $itemtype $datelastseen, $branch, $offset, $size); =back Retrieve a list of title/authors/barcode/callnumber, for biblio inventory. -The sub returns a list of hashes, containing itemnumber, author, title, barcode & item callnumber. -It is ordered by callnumber,title. +The sub returns a reference to a list of hashes, each containing +itemnumber, author, title, barcode, item callnumber, and date last +seen. It is ordered by callnumber then title. -The minlocation & maxlocation parameters are used to specify a range of item callnumbers +The required minlocation & maxlocation parameters are used to specify a range of item callnumbers the datelastseen can be used to specify that you want to see items not seen since a past date only. offset & size can be used to retrieve only a part of the whole listing (defaut behaviour) =cut sub GetItemsForInventory { - my ( $minlocation, $maxlocation,$location, $datelastseen, $branch, $offset, $size ) = @_; + my ( $minlocation, $maxlocation,$location, $itemtype, $ignoreissued, $datelastseen, $branch, $offset, $size ) = @_; my $dbh = C4::Context->dbh; - my $sth; + my ( @bind_params, @where_strings ); + + my $query = <<'END_SQL'; +SELECT items.itemnumber, barcode, itemcallnumber, title, author, biblio.biblionumber, datelastseen +FROM items + LEFT JOIN biblio ON items.biblionumber = biblio.biblionumber + LEFT JOIN biblioitems on items.biblionumber = biblioitems.biblionumber +END_SQL + + if ($minlocation) { + push @where_strings, 'itemcallnumber >= ?'; + push @bind_params, $minlocation; + } + + if ($maxlocation) { + push @where_strings, 'itemcallnumber <= ?'; + push @bind_params, $maxlocation; + } + if ($datelastseen) { - $datelastseen=format_date_in_iso($datelastseen); - my $query = - "SELECT itemnumber,barcode,itemcallnumber,title,author,biblio.biblionumber,datelastseen - FROM items - LEFT JOIN biblio ON items.biblionumber=biblio.biblionumber - WHERE itemcallnumber>= ? - AND itemcallnumber <=? - AND (datelastseen< ? OR datelastseen IS NULL)"; - $query.= " AND items.location=".$dbh->quote($location) if $location; - $query.= " AND items.homebranch=".$dbh->quote($branch) if $branch; - $query .= " ORDER BY itemcallnumber,title"; - $sth = $dbh->prepare($query); - $sth->execute( $minlocation, $maxlocation, $datelastseen ); + $datelastseen = format_date_in_iso($datelastseen); + push @where_strings, '(datelastseen < ? OR datelastseen IS NULL)'; + push @bind_params, $datelastseen; + } + + if ( $location ) { + push @where_strings, 'items.location = ?'; + push @bind_params, $location; + } + + if ( $branch ) { + push @where_strings, 'items.homebranch = ?'; + push @bind_params, $branch; } - else { - my $query =" - SELECT itemnumber,barcode,itemcallnumber,biblio.biblionumber,title,author,datelastseen - FROM items - LEFT JOIN biblio ON items.biblionumber=biblio.biblionumber - WHERE itemcallnumber>= ? - AND itemcallnumber <=?"; - $query.= " AND items.location=".$dbh->quote($location) if $location; - $query.= " AND items.homebranch=".$dbh->quote($branch) if $branch; - $query .= " ORDER BY itemcallnumber,title"; - $sth = $dbh->prepare($query); - $sth->execute( $minlocation, $maxlocation ); + + if ( $itemtype ) { + push @where_strings, 'biblioitems.itemtype = ?'; + push @bind_params, $itemtype; + } + if ( $ignoreissued) { + $query .= "LEFT JOIN issues ON items.itemnumber = issues.itemnumber "; + push @where_strings, 'issues.date_due IS NULL'; + } + + if ( $ignoreissued) { + $query .= "LEFT JOIN issues ON items.itemnumber = issues.itemnumber "; + push @where_strings, 'issues.date_due IS NULL'; } + + if ( @where_strings ) { + $query .= 'WHERE '; + $query .= join ' AND ', @where_strings; + } + $query .= ' ORDER BY itemcallnumber, title'; + my $sth = $dbh->prepare($query); + $sth->execute( @bind_params ); + my @results; + $size--; while ( my $row = $sth->fetchrow_hashref ) { $offset-- if ($offset); $row->{datelastseen}=format_date($row->{datelastseen}); @@ -786,7 +1175,6 @@ sub GetItemsByBiblioitemnumber { # Foreach item, get circulation information my $sth2 = $dbh->prepare( "SELECT * FROM issues,borrowers WHERE itemnumber = ? - AND returndate is NULL AND issues.borrowernumber = borrowers.borrowernumber" ); $sth2->execute( $data->{'itemnumber'} ); @@ -802,9 +1190,8 @@ sub GetItemsByBiblioitemnumber { } # else $sth2->finish; # Find the last 3 people who borrowed this item. - my $query2 = "SELECT * FROM issues, borrowers WHERE itemnumber = ? - AND issues.borrowernumber = borrowers.borrowernumber - AND returndate is not NULL + my $query2 = "SELECT * FROM old_issues, borrowers WHERE itemnumber = ? + AND old_issues.borrowernumber = borrowers.borrowernumber ORDER BY returndate desc,timestamp desc LIMIT 3"; $sth2 = $dbh->prepare($query2) || die $dbh->errstr; $sth2->execute( $data->{'itemnumber'} ) || die $sth2->errstr; @@ -875,27 +1262,43 @@ If this is set, it is set to C. sub GetItemsInfo { my ( $biblionumber, $type ) = @_; my $dbh = C4::Context->dbh; - my $query = "SELECT *,items.notforloan as itemnotforloan - FROM items - LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber - LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber"; - $query .= (C4::Context->preference('item-level_itypes')) ? - " LEFT JOIN itemtypes on items.itype = itemtypes.itemtype " - : " LEFT JOIN itemtypes on biblioitems.itemtype = itemtypes.itemtype "; - $query .= "WHERE items.biblionumber = ? ORDER BY items.dateaccessioned desc" ; + # note biblioitems.* must be avoided to prevent large marc and marcxml fields from killing performance. + my $query = " + SELECT items.*, + biblio.*, + biblioitems.volume, + biblioitems.number, + biblioitems.itemtype, + biblioitems.isbn, + biblioitems.issn, + biblioitems.publicationyear, + biblioitems.publishercode, + biblioitems.volumedate, + biblioitems.volumedesc, + biblioitems.lccn, + biblioitems.url, + items.notforloan as itemnotforloan, + itemtypes.description + FROM items + LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber + LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber + LEFT JOIN itemtypes ON itemtypes.itemtype = " + . (C4::Context->preference('item-level_itypes') ? 'items.itype' : 'biblioitems.itemtype'); + $query .= " WHERE items.biblionumber = ? ORDER BY items.dateaccessioned desc" ; my $sth = $dbh->prepare($query); $sth->execute($biblionumber); my $i = 0; my @results; - my ( $date_due, $count_reserves ); + my $serial; my $isth = $dbh->prepare( "SELECT issues.*,borrowers.cardnumber,borrowers.surname,borrowers.firstname,borrowers.branchcode as bcode FROM issues LEFT JOIN borrowers ON issues.borrowernumber=borrowers.borrowernumber - WHERE itemnumber = ? - AND returndate IS NULL" + WHERE itemnumber = ?" ); - 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 ) { + my $count_reserves; my $datedue = ''; $isth->execute( $data->{'itemnumber'} ); if ( my $idata = $isth->fetchrow_hashref ) { @@ -911,7 +1314,12 @@ sub GetItemsInfo { } } } - if ( $datedue eq '' ) { + if ( $data->{'serial'}) { + $ssth->execute($data->{'itemnumber'}) ; + ($data->{'serialseq'} , $data->{'publisheddate'}) = $ssth->fetchrow_array(); + $serial = 1; + } + if ( $datedue eq '' ) { my ( $restype, $reserves ) = C4::Reserves::CheckReserves( $data->{'itemnumber'} ); if ($restype) { @@ -919,7 +1327,7 @@ sub GetItemsInfo { } } $isth->finish; - + $ssth->finish; #get branch information..... my $bsth = $dbh->prepare( "SELECT * FROM branches WHERE branchcode = ? @@ -951,8 +1359,9 @@ sub GetItemsInfo { $sthnflstatus->execute( $authorised_valuecode, $data->{itemnotforloan} ); my ($lib) = $sthnflstatus->fetchrow; - $data->{notforloan} = $lib; + $data->{notforloanvalue} = $lib; } + $data->{itypenotforloan} = $data->{notforloan} if (C4::Context->preference('item-level_itypes')); # my stack procedures my $stackstatus = $dbh->prepare( @@ -977,10 +1386,11 @@ sub GetItemsInfo { $data->{stack} = $lib; } # Find the last 3 people who borrowed this item. - my $sth2 = $dbh->prepare("SELECT * FROM issues,borrowers + my $sth2 = $dbh->prepare("SELECT * FROM old_issues,borrowers WHERE itemnumber = ? - AND issues.borrowernumber = borrowers.borrowernumber - AND returndate IS NOT NULL LIMIT 3"); + AND old_issues.borrowernumber = borrowers.borrowernumber + ORDER BY returndate DESC + LIMIT 3"); $sth2->execute($data->{'itemnumber'}); my $ii = 0; while (my $data2 = $sth2->fetchrow_hashref()) { @@ -993,9 +1403,11 @@ sub GetItemsInfo { $results[$i] = $data; $i++; } - $sth->finish; - - return (@results); + if($serial) { + return( sort { ($b->{'publisheddate'} || $b->{'enumchron'}) cmp ($a->{'publisheddate'} || $a->{'enumchron'}) } @results ); + } else { + return (@results); + } } =head2 get_itemnumbers_of @@ -1037,6 +1449,127 @@ 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); +} + +=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, @@ -1081,9 +1614,22 @@ sub GetMarcItem { # 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 $mungeditem = { + map { + defined($itemrecord->{$_}) && $itemrecord->{$_} ne '' ? ("items.$_" => $itemrecord->{$_}) : () + } keys %{ $itemrecord } + }; my $itemmarc = TransformKohaToMarc($mungeditem); + + my $unlinked_item_subfields = _parse_unlinked_item_subfields_from_xml($mungeditem->{'items.more_subfields_xml'}); + if (defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1) { + my @fields = $itemmarc->fields(); + if ($#fields > -1) { + $fields[0]->add_subfields(@$unlinked_item_subfields); + } + } + return $itemmarc; } @@ -1322,27 +1868,15 @@ C sub _set_defaults_for_add { my $item = shift; - - # if dateaccessioned is provided, use it. Otherwise, set to NOW() - if (!(exists $item->{'dateaccessioned'}) || - ($item->{'dateaccessioned'} eq '')) { - # FIXME add check for invalid date - my $today = C4::Dates->new(); - $item->{'dateaccessioned'} = $today->output("iso"); #TODO: check time issues - } - - # 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->{dateaccessioned} ||= C4::Dates->new->output('iso'); + $item->{$_} ||= 0 for (qw( notforloan damaged itemlost wthdrawn)); } =head2 _koha_new_item =over 4 -my ($itemnumber,$error) = _koha_new_item( $dbh, $item, $barcode ); +my ($itemnumber,$error) = _koha_new_item( $item, $barcode ); =back @@ -1351,10 +1885,10 @@ Perform the actual insert into the C table. =cut sub _koha_new_item { - my ( $dbh, $item, $barcode ) = @_; + my ( $item, $barcode ) = @_; + my $dbh=C4::Context->dbh; my $error; - - my $query = + my $query = "INSERT INTO items SET biblionumber = ?, biblioitemnumber = ?, @@ -1387,10 +1921,13 @@ sub _koha_new_item { ccode = ?, itype = ?, materials = ?, - uri = ? + uri = ?, + enumchron = ?, + more_subfields_xml = ?, + copynumber = ? "; my $sth = $dbh->prepare($query); - $sth->execute( + $sth->execute( $item->{'biblionumber'}, $item->{'biblioitemnumber'}, $barcode, @@ -1421,6 +1958,9 @@ sub _koha_new_item { $item->{'itype'}, $item->{'materials'}, $item->{'uri'}, + $item->{'enumchron'}, + $item->{'more_subfields_xml'}, + $item->{'copynumber'}, ); my $itemnumber = $dbh->{'mysql_insertid'}; if ( defined $sth->errstr ) { @@ -1434,7 +1974,7 @@ sub _koha_new_item { =over 4 -my ($itemnumber,$error) =_koha_modify_item( $dbh, $item, $op ); +my ($itemnumber,$error) =_koha_modify_item( $item ); =back @@ -1444,7 +1984,8 @@ routine accepts a hashref specifying the columns to update. =cut sub _koha_modify_item { - my ( $dbh, $item ) = @_; + my ( $item ) = @_; + my $dbh=C4::Context->dbh; my $error; my $query = "UPDATE items SET "; @@ -1456,9 +1997,9 @@ sub _koha_modify_item { $query =~ s/,$//; $query .= " WHERE itemnumber=?"; push @bind, $item->{'itemnumber'}; - my $sth = $dbh->prepare($query); + my $sth = C4::Context->dbh->prepare($query); $sth->execute(@bind); - if ( $dbh->errstr ) { + if ( C4::Context->dbh->errstr ) { $error.="ERROR in _koha_modify_item $query".$dbh->errstr; warn $error; } @@ -1508,7 +2049,7 @@ sub _koha_delete_item { =over 4 -my $item_marc = _marc_from_item_hash($item, $frameworkcode); +my $item_marc = _marc_from_item_hash($item, $frameworkcode[, $unlinked_item_subfields]); =back @@ -1516,11 +2057,20 @@ Given an item hash representing a complete item record, create a C object containing an embedded tag representing that item. +The third, optional parameter C<$unlinked_item_subfields> is +an arrayref of subfields (not mapped to C fields per the +framework) to be added to the MARC representation +of the item. + =cut sub _marc_from_item_hash { my $item = shift; my $frameworkcode = shift; + my $unlinked_item_subfields; + if (@_) { + $unlinked_item_subfields = shift; + } # Tack on 'items.' prefix to column names so lookup from MARC frameworks will work # Also, don't emit a subfield if the underlying field is blank. @@ -1532,10 +2082,19 @@ sub _marc_from_item_hash { foreach my $item_field (keys %{ $mungeditem }) { my ($tag, $subfield) = GetMarcFromKohaField($item_field, $frameworkcode); next unless defined $tag and defined $subfield; # skip if not mapped to MARC field - if (my $field = $item_marc->field($tag)) { - $field->add_subfields($subfield => $mungeditem->{$item_field}); - } else { - $item_marc->add_fields( $tag, " ", " ", $subfield => $mungeditem->{$item_field}); + + + my @values = split(/\s?\|\s?/, $mungeditem->{$item_field}, -1); + foreach my $value (@values){ + if (my $field = $item_marc->field($tag)) { + $field->add_subfields($subfield => $value); + } else { + my $add_subfields = []; + if (defined $unlinked_item_subfields and ref($unlinked_item_subfields) eq 'ARRAY' and $#$unlinked_item_subfields > -1) { + $add_subfields = $unlinked_item_subfields; + } + $item_marc->add_fields( $tag, " ", " ", $subfield => $value, @$add_subfields); + } } } @@ -1562,7 +2121,6 @@ sub _add_item_field_to_biblio { my ($item_marc, $biblionumber, $frameworkcode) = @_; my $biblio_marc = GetMarcBiblio($biblionumber); - foreach my $field ($item_marc->fields()) { $biblio_marc->append_fields($field); } @@ -1588,7 +2146,7 @@ replace it with the tag from C<$item_marc>. sub _replace_item_field_in_biblio { 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); @@ -1613,4 +2171,115 @@ sub _replace_item_field_in_biblio { ModBiblioMarc($completeRecord, $biblionumber, $frameworkcode); } +=head2 _repack_item_errors + +Add an error message hash generated by C +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; +} + +=head2 _get_unlinked_item_subfields + +=over 4 + +my $unlinked_item_subfields = _get_unlinked_item_subfields($original_item_marc, $frameworkcode); + +=back + +=cut + +sub _get_unlinked_item_subfields { + my $original_item_marc = shift; + my $frameworkcode = shift; + + my $marcstructure = GetMarcStructure(1, $frameworkcode); + + # assume that this record has only one field, and that that + # field contains only the item information + my $subfields = []; + my @fields = $original_item_marc->fields(); + if ($#fields > -1) { + my $field = $fields[0]; + my $tag = $field->tag(); + foreach my $subfield ($field->subfields()) { + if (defined $subfield->[1] and + $subfield->[1] ne '' and + !$marcstructure->{$tag}->{$subfield->[0]}->{'kohafield'}) { + push @$subfields, $subfield->[0] => $subfield->[1]; + } + } + } + return $subfields; +} + +=head2 _get_unlinked_subfields_xml + +=over 4 + +my $unlinked_subfields_xml = _get_unlinked_subfields_xml($unlinked_item_subfields); + +=back + +=cut + +sub _get_unlinked_subfields_xml { + my $unlinked_item_subfields = shift; + + my $xml; + if (defined $unlinked_item_subfields and ref($unlinked_item_subfields) eq 'ARRAY' and $#$unlinked_item_subfields > -1) { + my $marc = MARC::Record->new(); + # use of tag 999 is arbitrary, and doesn't need to match the item tag + # used in the framework + $marc->append_fields(MARC::Field->new('999', ' ', ' ', @$unlinked_item_subfields)); + $marc->encoding("UTF-8"); + $xml = $marc->as_xml("USMARC"); + } + + return $xml; +} + +=head2 _parse_unlinked_item_subfields_from_xml + +=over 4 + +my $unlinked_item_subfields = _parse_unlinked_item_subfields_from_xml($whole_item->{'more_subfields_xml'}): + +=back + +=cut + +sub _parse_unlinked_item_subfields_from_xml { + my $xml = shift; + + return unless defined $xml and $xml ne ""; + my $marc = MARC::Record->new_from_xml(StripNonXmlChars($xml),'UTF-8'); + my $unlinked_subfields = []; + my @fields = $marc->fields(); + if ($#fields > -1) { + foreach my $subfield ($fields[0]->subfields()) { + push @$unlinked_subfields, $subfield->[0] => $subfield->[1]; + } + } + return $unlinked_subfields; +} + 1;