Bug 21184: Remove GetBarcodeFromItemnumber
[koha.git] / C4 / Items.pm
blobfa9c9ace57f2815b88be20ce370d8e3d937009bf
1 package C4::Items;
3 # Copyright 2007 LibLime, Inc.
4 # Parts Copyright Biblibre 2010
6 # This file is part of Koha.
8 # Koha is free software; you can redistribute it and/or modify it
9 # under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 3 of the License, or
11 # (at your option) any later version.
13 # Koha is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with Koha; if not, see <http://www.gnu.org/licenses>.
21 use strict;
22 #use warnings; FIXME - Bug 2505
24 use Carp;
25 use C4::Context;
26 use C4::Koha;
27 use C4::Biblio;
28 use Koha::DateUtils;
29 use MARC::Record;
30 use C4::ClassSource;
31 use C4::Log;
32 use List::MoreUtils qw/any/;
33 use YAML qw/Load/;
34 use DateTime::Format::MySQL;
35 use Data::Dumper; # used as part of logging item record changes, not just for
36 # debugging; so please don't remove this
38 use Koha::AuthorisedValues;
39 use Koha::DateUtils qw/dt_from_string/;
40 use Koha::Database;
42 use Koha::Biblioitems;
43 use Koha::Items;
44 use Koha::ItemTypes;
45 use Koha::SearchEngine;
46 use Koha::SearchEngine::Search;
47 use Koha::Libraries;
49 use vars qw(@ISA @EXPORT);
51 BEGIN {
53 require Exporter;
54 @ISA = qw( Exporter );
56 # function exports
57 @EXPORT = qw(
58 GetItem
59 AddItemFromMarc
60 AddItem
61 AddItemBatchFromMarc
62 ModItemFromMarc
63 Item2Marc
64 ModItem
65 ModDateLastSeen
66 ModItemTransfer
67 DelItem
69 CheckItemPreSave
71 GetItemsForInventory
72 GetItemsInfo
73 GetItemsLocationInfo
74 GetHostItemsInfo
75 GetItemnumbersForBiblio
76 get_hostitemnumbers_of
77 GetHiddenItemnumbers
78 ItemSafeToDelete
79 DelItemCheck
80 MoveItemFromBiblio
81 GetLatestAcquisitions
83 CartToShelf
84 ShelfToCart
86 GetAnalyticsCount
88 SearchItemsByField
89 SearchItems
91 PrepareItemrecordDisplay
96 =head1 NAME
98 C4::Items - item management functions
100 =head1 DESCRIPTION
102 This module contains an API for manipulating item
103 records in Koha, and is used by cataloguing, circulation,
104 acquisitions, and serials management.
106 # FIXME This POD is not up-to-date
107 A Koha item record is stored in two places: the
108 items table and embedded in a MARC tag in the XML
109 version of the associated bib record in C<biblioitems.marcxml>.
110 This is done to allow the item information to be readily
111 indexed (e.g., by Zebra), but means that each item
112 modification transaction must keep the items table
113 and the MARC XML in sync at all times.
115 Consequently, all code that creates, modifies, or deletes
116 item records B<must> use an appropriate function from
117 C<C4::Items>. If no existing function is suitable, it is
118 better to add one to C<C4::Items> than to use add
119 one-off SQL statements to add or modify items.
121 The items table will be considered authoritative. In other
122 words, if there is ever a discrepancy between the items
123 table and the MARC XML, the items table should be considered
124 accurate.
126 =head1 HISTORICAL NOTE
128 Most of the functions in C<C4::Items> were originally in
129 the C<C4::Biblio> module.
131 =head1 CORE EXPORTED FUNCTIONS
133 The following functions are meant for use by users
134 of C<C4::Items>
136 =cut
138 =head2 GetItem
140 $item = GetItem($itemnumber,$barcode,$serial);
142 Return item information, for a given itemnumber or barcode.
143 The return value is a hashref mapping item column
144 names to values. If C<$serial> is true, include serial publication data.
146 =cut
148 sub GetItem {
149 my ($itemnumber,$barcode, $serial) = @_;
150 my $dbh = C4::Context->dbh;
152 my $item;
153 if ($itemnumber) {
154 $item = Koha::Items->find( $itemnumber );
155 } else {
156 $item = Koha::Items->find( { barcode => $barcode } );
159 return unless ( $item );
161 my $data = $item->unblessed();
162 $data->{itype} = $item->effective_itemtype(); # set the correct itype
164 if ($serial) {
165 my $ssth = $dbh->prepare("SELECT serialseq,publisheddate from serialitems left join serial on serialitems.serialid=serial.serialid where serialitems.itemnumber=?");
166 $ssth->execute( $data->{'itemnumber'} );
167 ( $data->{'serialseq'}, $data->{'publisheddate'} ) = $ssth->fetchrow_array();
170 return $data;
171 } # sub GetItem
173 =head2 CartToShelf
175 CartToShelf($itemnumber);
177 Set the current shelving location of the item record
178 to its stored permanent shelving location. This is
179 primarily used to indicate when an item whose current
180 location is a special processing ('PROC') or shelving cart
181 ('CART') location is back in the stacks.
183 =cut
185 sub CartToShelf {
186 my ( $itemnumber ) = @_;
188 unless ( $itemnumber ) {
189 croak "FAILED CartToShelf() - no itemnumber supplied";
192 my $item = GetItem($itemnumber);
193 if ( $item->{location} eq 'CART' ) {
194 $item->{location} = $item->{permanent_location};
195 ModItem($item, undef, $itemnumber);
199 =head2 ShelfToCart
201 ShelfToCart($itemnumber);
203 Set the current shelving location of the item
204 to shelving cart ('CART').
206 =cut
208 sub ShelfToCart {
209 my ( $itemnumber ) = @_;
211 unless ( $itemnumber ) {
212 croak "FAILED ShelfToCart() - no itemnumber supplied";
215 my $item = GetItem($itemnumber);
216 $item->{'location'} = 'CART';
217 ModItem($item, undef, $itemnumber);
220 =head2 AddItemFromMarc
222 my ($biblionumber, $biblioitemnumber, $itemnumber)
223 = AddItemFromMarc($source_item_marc, $biblionumber);
225 Given a MARC::Record object containing an embedded item
226 record and a biblionumber, create a new item record.
228 =cut
230 sub AddItemFromMarc {
231 my ( $source_item_marc, $biblionumber ) = @_;
232 my $dbh = C4::Context->dbh;
234 # parse item hash from MARC
235 my $frameworkcode = C4::Biblio::GetFrameworkCode( $biblionumber );
236 my ($itemtag,$itemsubfield)=C4::Biblio::GetMarcFromKohaField("items.itemnumber",$frameworkcode);
238 my $localitemmarc=MARC::Record->new;
239 $localitemmarc->append_fields($source_item_marc->field($itemtag));
240 my $item = &TransformMarcToKoha( $localitemmarc, $frameworkcode ,'items');
241 my $unlinked_item_subfields = _get_unlinked_item_subfields($localitemmarc, $frameworkcode);
242 return AddItem($item, $biblionumber, $dbh, $frameworkcode, $unlinked_item_subfields);
245 =head2 AddItem
247 my ($biblionumber, $biblioitemnumber, $itemnumber)
248 = AddItem($item, $biblionumber[, $dbh, $frameworkcode, $unlinked_item_subfields]);
250 Given a hash containing item column names as keys,
251 create a new Koha item record.
253 The first two optional parameters (C<$dbh> and C<$frameworkcode>)
254 do not need to be supplied for general use; they exist
255 simply to allow them to be picked up from AddItemFromMarc.
257 The final optional parameter, C<$unlinked_item_subfields>, contains
258 an arrayref containing subfields present in the original MARC
259 representation of the item (e.g., from the item editor) that are
260 not mapped to C<items> columns directly but should instead
261 be stored in C<items.more_subfields_xml> and included in
262 the biblio items tag for display and indexing.
264 =cut
266 sub AddItem {
267 my $item = shift;
268 my $biblionumber = shift;
270 my $dbh = @_ ? shift : C4::Context->dbh;
271 my $frameworkcode = @_ ? shift : C4::Biblio::GetFrameworkCode($biblionumber);
272 my $unlinked_item_subfields;
273 if (@_) {
274 $unlinked_item_subfields = shift;
277 # needs old biblionumber and biblioitemnumber
278 $item->{'biblionumber'} = $biblionumber;
279 my $sth = $dbh->prepare("SELECT biblioitemnumber FROM biblioitems WHERE biblionumber=?");
280 $sth->execute( $item->{'biblionumber'} );
281 ( $item->{'biblioitemnumber'} ) = $sth->fetchrow;
283 _set_defaults_for_add($item);
284 _set_derived_columns_for_add($item);
285 $item->{'more_subfields_xml'} = _get_unlinked_subfields_xml($unlinked_item_subfields);
287 # FIXME - checks here
288 unless ( $item->{itype} ) { # default to biblioitem.itemtype if no itype
289 my $itype_sth = $dbh->prepare("SELECT itemtype FROM biblioitems WHERE biblionumber = ?");
290 $itype_sth->execute( $item->{'biblionumber'} );
291 ( $item->{'itype'} ) = $itype_sth->fetchrow_array;
294 my ( $itemnumber, $error ) = _koha_new_item( $item, $item->{barcode} );
295 return if $error;
297 $item->{'itemnumber'} = $itemnumber;
299 ModZebra( $item->{biblionumber}, "specialUpdate", "biblioserver" );
301 logaction( "CATALOGUING", "ADD", $itemnumber, "item" )
302 if C4::Context->preference("CataloguingLog");
304 return ( $item->{biblionumber}, $item->{biblioitemnumber}, $itemnumber );
307 =head2 AddItemBatchFromMarc
309 ($itemnumber_ref, $error_ref) = AddItemBatchFromMarc($record,
310 $biblionumber, $biblioitemnumber, $frameworkcode);
312 Efficiently create item records from a MARC biblio record with
313 embedded item fields. This routine is suitable for batch jobs.
315 This API assumes that the bib record has already been
316 saved to the C<biblio> and C<biblioitems> tables. It does
317 not expect that C<biblio_metadata.metadata> is populated, but it
318 will do so via a call to ModBibiloMarc.
320 The goal of this API is to have a similar effect to using AddBiblio
321 and AddItems in succession, but without inefficient repeated
322 parsing of the MARC XML bib record.
324 This function returns an arrayref of new itemsnumbers and an arrayref of item
325 errors encountered during the processing. Each entry in the errors
326 list is a hashref containing the following keys:
328 =over
330 =item item_sequence
332 Sequence number of original item tag in the MARC record.
334 =item item_barcode
336 Item barcode, provide to assist in the construction of
337 useful error messages.
339 =item error_code
341 Code representing the error condition. Can be 'duplicate_barcode',
342 'invalid_homebranch', or 'invalid_holdingbranch'.
344 =item error_information
346 Additional information appropriate to the error condition.
348 =back
350 =cut
352 sub AddItemBatchFromMarc {
353 my ($record, $biblionumber, $biblioitemnumber, $frameworkcode) = @_;
354 my $error;
355 my @itemnumbers = ();
356 my @errors = ();
357 my $dbh = C4::Context->dbh;
359 # We modify the record, so lets work on a clone so we don't change the
360 # original.
361 $record = $record->clone();
362 # loop through the item tags and start creating items
363 my @bad_item_fields = ();
364 my ($itemtag, $itemsubfield) = C4::Biblio::GetMarcFromKohaField("items.itemnumber",'');
365 my $item_sequence_num = 0;
366 ITEMFIELD: foreach my $item_field ($record->field($itemtag)) {
367 $item_sequence_num++;
368 # we take the item field and stick it into a new
369 # MARC record -- this is required so far because (FIXME)
370 # TransformMarcToKoha requires a MARC::Record, not a MARC::Field
371 # and there is no TransformMarcFieldToKoha
372 my $temp_item_marc = MARC::Record->new();
373 $temp_item_marc->append_fields($item_field);
375 # add biblionumber and biblioitemnumber
376 my $item = TransformMarcToKoha( $temp_item_marc, $frameworkcode, 'items' );
377 my $unlinked_item_subfields = _get_unlinked_item_subfields($temp_item_marc, $frameworkcode);
378 $item->{'more_subfields_xml'} = _get_unlinked_subfields_xml($unlinked_item_subfields);
379 $item->{'biblionumber'} = $biblionumber;
380 $item->{'biblioitemnumber'} = $biblioitemnumber;
382 # check for duplicate barcode
383 my %item_errors = CheckItemPreSave($item);
384 if (%item_errors) {
385 push @errors, _repack_item_errors($item_sequence_num, $item, \%item_errors);
386 push @bad_item_fields, $item_field;
387 next ITEMFIELD;
390 _set_defaults_for_add($item);
391 _set_derived_columns_for_add($item);
392 my ( $itemnumber, $error ) = _koha_new_item( $item, $item->{barcode} );
393 warn $error if $error;
394 push @itemnumbers, $itemnumber; # FIXME not checking error
395 $item->{'itemnumber'} = $itemnumber;
397 logaction("CATALOGUING", "ADD", $itemnumber, "item") if C4::Context->preference("CataloguingLog");
399 my $new_item_marc = _marc_from_item_hash($item, $frameworkcode, $unlinked_item_subfields);
400 $item_field->replace_with($new_item_marc->field($itemtag));
403 # remove any MARC item fields for rejected items
404 foreach my $item_field (@bad_item_fields) {
405 $record->delete_field($item_field);
408 # update the MARC biblio
409 # $biblionumber = ModBiblioMarc( $record, $biblionumber, $frameworkcode );
411 return (\@itemnumbers, \@errors);
414 =head2 ModItemFromMarc
416 ModItemFromMarc($item_marc, $biblionumber, $itemnumber);
418 This function updates an item record based on a supplied
419 C<MARC::Record> object containing an embedded item field.
420 This API is meant for the use of C<additem.pl>; for
421 other purposes, C<ModItem> should be used.
423 This function uses the hash %default_values_for_mod_from_marc,
424 which contains default values for item fields to
425 apply when modifying an item. This is needed because
426 if an item field's value is cleared, TransformMarcToKoha
427 does not include the column in the
428 hash that's passed to ModItem, which without
429 use of this hash makes it impossible to clear
430 an item field's value. See bug 2466.
432 Note that only columns that can be directly
433 changed from the cataloging and serials
434 item editors are included in this hash.
436 Returns item record
438 =cut
440 sub _build_default_values_for_mod_marc {
441 # Has no framework parameter anymore, since Default is authoritative
442 # for Koha to MARC mappings.
444 my $cache = Koha::Caches->get_instance();
445 my $cache_key = "default_value_for_mod_marc-";
446 my $cached = $cache->get_from_cache($cache_key);
447 return $cached if $cached;
449 my $default_values = {
450 barcode => undef,
451 booksellerid => undef,
452 ccode => undef,
453 'items.cn_source' => undef,
454 coded_location_qualifier => undef,
455 copynumber => undef,
456 damaged => 0,
457 enumchron => undef,
458 holdingbranch => undef,
459 homebranch => undef,
460 itemcallnumber => undef,
461 itemlost => 0,
462 itemnotes => undef,
463 itemnotes_nonpublic => undef,
464 itype => undef,
465 location => undef,
466 permanent_location => undef,
467 materials => undef,
468 new_status => undef,
469 notforloan => 0,
470 # paidfor => undef, # commented, see bug 12817
471 price => undef,
472 replacementprice => undef,
473 replacementpricedate => undef,
474 restricted => undef,
475 stack => undef,
476 stocknumber => undef,
477 uri => undef,
478 withdrawn => 0,
480 my %default_values_for_mod_from_marc;
481 while ( my ( $field, $default_value ) = each %$default_values ) {
482 my $kohafield = $field;
483 $kohafield =~ s|^([^\.]+)$|items.$1|;
484 $default_values_for_mod_from_marc{$field} = $default_value
485 if C4::Biblio::GetMarcFromKohaField( $kohafield );
488 $cache->set_in_cache($cache_key, \%default_values_for_mod_from_marc);
489 return \%default_values_for_mod_from_marc;
492 sub ModItemFromMarc {
493 my $item_marc = shift;
494 my $biblionumber = shift;
495 my $itemnumber = shift;
497 my $frameworkcode = C4::Biblio::GetFrameworkCode($biblionumber);
498 my ( $itemtag, $itemsubfield ) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber", $frameworkcode );
500 my $localitemmarc = MARC::Record->new;
501 $localitemmarc->append_fields( $item_marc->field($itemtag) );
502 my $item = &TransformMarcToKoha( $localitemmarc, $frameworkcode, 'items' );
503 my $default_values = _build_default_values_for_mod_marc();
504 foreach my $item_field ( keys %$default_values ) {
505 $item->{$item_field} = $default_values->{$item_field}
506 unless exists $item->{$item_field};
508 my $unlinked_item_subfields = _get_unlinked_item_subfields( $localitemmarc, $frameworkcode );
510 ModItem( $item, $biblionumber, $itemnumber, { unlinked_item_subfields => $unlinked_item_subfields } );
511 return $item;
514 =head2 ModItem
516 ModItem(
517 { column => $newvalue },
518 $biblionumber,
519 $itemnumber,
521 [ unlinked_item_subfields => $unlinked_item_subfields, ]
522 [ log_action => 1, ]
526 Change one or more columns in an item record and update
527 the MARC representation of the item.
529 The first argument is a hashref mapping from item column
530 names to the new values. The second and third arguments
531 are the biblionumber and itemnumber, respectively.
532 The fourth, optional parameter (additional_params) may contain the keys
533 unlinked_item_subfields and log_action.
535 C<$unlinked_item_subfields> contains an arrayref containing
536 subfields present in the original MARC
537 representation of the item (e.g., from the item editor) that are
538 not mapped to C<items> columns directly but should instead
539 be stored in C<items.more_subfields_xml> and included in
540 the biblio items tag for display and indexing.
542 If one of the changed columns is used to calculate
543 the derived value of a column such as C<items.cn_sort>,
544 this routine will perform the necessary calculation
545 and set the value.
547 If log_action is set to false, the action will not be logged.
548 If log_action is true or undefined, the action will be logged.
550 =cut
552 sub ModItem {
553 my ( $item, $biblionumber, $itemnumber, $additional_params ) = @_;
554 my $log_action = $additional_params->{log_action} // 1;
555 my $unlinked_item_subfields = $additional_params->{unlinked_item_subfields};
557 # if $biblionumber is undefined, get it from the current item
558 unless (defined $biblionumber) {
559 $biblionumber = _get_single_item_column('biblionumber', $itemnumber);
562 if ($unlinked_item_subfields) {
563 $item->{'more_subfields_xml'} = _get_unlinked_subfields_xml($unlinked_item_subfields);
566 $item->{'itemnumber'} = $itemnumber or return;
568 my @fields = qw( itemlost withdrawn damaged );
570 # Only call GetItem if we need to set an "on" date field
571 if ( $item->{itemlost} || $item->{withdrawn} || $item->{damaged} ) {
572 my $pre_mod_item = GetItem( $item->{'itemnumber'} );
573 for my $field (@fields) {
574 if ( defined( $item->{$field} )
575 and not $pre_mod_item->{$field}
576 and $item->{$field} )
578 $item->{ $field . '_on' } =
579 DateTime::Format::MySQL->format_datetime( dt_from_string() );
584 # If the field is defined but empty, we are removing and,
585 # and thus need to clear out the 'on' field as well
586 for my $field (@fields) {
587 if ( defined( $item->{$field} ) && !$item->{$field} ) {
588 $item->{ $field . '_on' } = undef;
593 _set_derived_columns_for_mod($item);
594 _do_column_fixes_for_mod($item);
595 # FIXME add checks
596 # duplicate barcode
597 # attempt to change itemnumber
598 # attempt to change biblionumber (if we want
599 # an API to relink an item to a different bib,
600 # it should be a separate function)
602 # update items table
603 _koha_modify_item($item);
605 # request that bib be reindexed so that searching on current
606 # item status is possible
607 ModZebra( $biblionumber, "specialUpdate", "biblioserver" );
609 logaction( "CATALOGUING", "MODIFY", $itemnumber, "item " . Dumper($item) )
610 if $log_action && C4::Context->preference("CataloguingLog");
613 =head2 ModItemTransfer
615 ModItemTransfer($itenumber, $frombranch, $tobranch);
617 Marks an item as being transferred from one branch
618 to another.
620 =cut
622 sub ModItemTransfer {
623 my ( $itemnumber, $frombranch, $tobranch ) = @_;
625 my $dbh = C4::Context->dbh;
627 # Remove the 'shelving cart' location status if it is being used.
628 CartToShelf( $itemnumber ) if ( C4::Context->preference("ReturnToShelvingCart") );
630 $dbh->do("UPDATE branchtransfers SET datearrived = NOW(), comments = ? WHERE itemnumber = ? AND datearrived IS NULL", undef, "Canceled, new transfer from $frombranch to $tobranch created", $itemnumber);
632 #new entry in branchtransfers....
633 my $sth = $dbh->prepare(
634 "INSERT INTO branchtransfers (itemnumber, frombranch, datesent, tobranch)
635 VALUES (?, ?, NOW(), ?)");
636 $sth->execute($itemnumber, $frombranch, $tobranch);
638 ModItem({ holdingbranch => $tobranch }, undef, $itemnumber);
639 ModDateLastSeen($itemnumber);
640 return;
643 =head2 ModDateLastSeen
645 ModDateLastSeen( $itemnumber, $leave_item_lost );
647 Mark item as seen. Is called when an item is issued, returned or manually marked during inventory/stocktaking.
648 C<$itemnumber> is the item number
649 C<$leave_item_lost> determines if a lost item will be found or remain lost
651 =cut
653 sub ModDateLastSeen {
654 my ( $itemnumber, $leave_item_lost ) = @_;
656 my $today = output_pref({ dt => dt_from_string, dateformat => 'iso', dateonly => 1 });
658 my $params;
659 $params->{datelastseen} = $today;
660 $params->{itemlost} = 0 unless $leave_item_lost;
662 ModItem( $params, undef, $itemnumber, { log_action => 0 } );
665 =head2 DelItem
667 DelItem({ itemnumber => $itemnumber, [ biblionumber => $biblionumber ] } );
669 Exported function (core API) for deleting an item record in Koha.
671 =cut
673 sub DelItem {
674 my ( $params ) = @_;
676 my $itemnumber = $params->{itemnumber};
677 my $biblionumber = $params->{biblionumber};
679 unless ($biblionumber) {
680 my $item = Koha::Items->find( $itemnumber );
681 $biblionumber = $item ? $item->biblio->biblionumber : undef;
684 # If there is no biblionumber for the given itemnumber, there is nothing to delete
685 return 0 unless $biblionumber;
687 # FIXME check the item has no current issues
688 my $deleted = _koha_delete_item( $itemnumber );
690 ModZebra( $biblionumber, "specialUpdate", "biblioserver" );
692 #search item field code
693 logaction("CATALOGUING", "DELETE", $itemnumber, "item") if C4::Context->preference("CataloguingLog");
694 return $deleted;
697 =head2 CheckItemPreSave
699 my $item_ref = TransformMarcToKoha($marc, 'items');
700 # do stuff
701 my %errors = CheckItemPreSave($item_ref);
702 if (exists $errors{'duplicate_barcode'}) {
703 print "item has duplicate barcode: ", $errors{'duplicate_barcode'}, "\n";
704 } elsif (exists $errors{'invalid_homebranch'}) {
705 print "item has invalid home branch: ", $errors{'invalid_homebranch'}, "\n";
706 } elsif (exists $errors{'invalid_holdingbranch'}) {
707 print "item has invalid holding branch: ", $errors{'invalid_holdingbranch'}, "\n";
708 } else {
709 print "item is OK";
712 Given a hashref containing item fields, determine if it can be
713 inserted or updated in the database. Specifically, checks for
714 database integrity issues, and returns a hash containing any
715 of the following keys, if applicable.
717 =over 2
719 =item duplicate_barcode
721 Barcode, if it duplicates one already found in the database.
723 =item invalid_homebranch
725 Home branch, if not defined in branches table.
727 =item invalid_holdingbranch
729 Holding branch, if not defined in branches table.
731 =back
733 This function does NOT implement any policy-related checks,
734 e.g., whether current operator is allowed to save an
735 item that has a given branch code.
737 =cut
739 sub CheckItemPreSave {
740 my $item_ref = shift;
742 my %errors = ();
744 # check for duplicate barcode
745 if (exists $item_ref->{'barcode'} and defined $item_ref->{'barcode'}) {
746 my $existing_item= Koha::Items->find({barcode => $item_ref->{'barcode'}});
747 if ($existing_item) {
748 if (!exists $item_ref->{'itemnumber'} # new item
749 or $item_ref->{'itemnumber'} != $existing_item->itemnumber) { # existing item
750 $errors{'duplicate_barcode'} = $item_ref->{'barcode'};
755 # check for valid home branch
756 if (exists $item_ref->{'homebranch'} and defined $item_ref->{'homebranch'}) {
757 my $home_library = Koha::Libraries->find( $item_ref->{homebranch} );
758 unless (defined $home_library) {
759 $errors{'invalid_homebranch'} = $item_ref->{'homebranch'};
763 # check for valid holding branch
764 if (exists $item_ref->{'holdingbranch'} and defined $item_ref->{'holdingbranch'}) {
765 my $holding_library = Koha::Libraries->find( $item_ref->{holdingbranch} );
766 unless (defined $holding_library) {
767 $errors{'invalid_holdingbranch'} = $item_ref->{'holdingbranch'};
771 return %errors;
775 =head1 EXPORTED SPECIAL ACCESSOR FUNCTIONS
777 The following functions provide various ways of
778 getting an item record, a set of item records, or
779 lists of authorized values for certain item fields.
781 =cut
783 =head2 GetItemsForInventory
785 ($itemlist, $iTotalRecords) = GetItemsForInventory( {
786 minlocation => $minlocation,
787 maxlocation => $maxlocation,
788 location => $location,
789 itemtype => $itemtype,
790 ignoreissued => $ignoreissued,
791 datelastseen => $datelastseen,
792 branchcode => $branchcode,
793 branch => $branch,
794 offset => $offset,
795 size => $size,
796 statushash => $statushash,
797 } );
799 Retrieve a list of title/authors/barcode/callnumber, for biblio inventory.
801 The sub returns a reference to a list of hashes, each containing
802 itemnumber, author, title, barcode, item callnumber, and date last
803 seen. It is ordered by callnumber then title.
805 The required minlocation & maxlocation parameters are used to specify a range of item callnumbers
806 the datelastseen can be used to specify that you want to see items not seen since a past date only.
807 offset & size can be used to retrieve only a part of the whole listing (defaut behaviour)
808 $statushash requires a hashref that has the authorized values fieldname (intems.notforloan, etc...) as keys, and an arrayref of statuscodes we are searching for as values.
810 $iTotalRecords is the number of rows that would have been returned without the $offset, $size limit clause
812 =cut
814 sub GetItemsForInventory {
815 my ( $parameters ) = @_;
816 my $minlocation = $parameters->{'minlocation'} // '';
817 my $maxlocation = $parameters->{'maxlocation'} // '';
818 my $location = $parameters->{'location'} // '';
819 my $itemtype = $parameters->{'itemtype'} // '';
820 my $ignoreissued = $parameters->{'ignoreissued'} // '';
821 my $datelastseen = $parameters->{'datelastseen'} // '';
822 my $branchcode = $parameters->{'branchcode'} // '';
823 my $branch = $parameters->{'branch'} // '';
824 my $offset = $parameters->{'offset'} // '';
825 my $size = $parameters->{'size'} // '';
826 my $statushash = $parameters->{'statushash'} // '';
828 my $dbh = C4::Context->dbh;
829 my ( @bind_params, @where_strings );
831 my $select_columns = q{
832 SELECT items.itemnumber, barcode, itemcallnumber, title, author, biblio.biblionumber, biblio.frameworkcode, datelastseen, homebranch, location, notforloan, damaged, itemlost, withdrawn, stocknumber
834 my $select_count = q{SELECT COUNT(*)};
835 my $query = q{
836 FROM items
837 LEFT JOIN biblio ON items.biblionumber = biblio.biblionumber
838 LEFT JOIN biblioitems on items.biblionumber = biblioitems.biblionumber
840 if ($statushash){
841 for my $authvfield (keys %$statushash){
842 if ( scalar @{$statushash->{$authvfield}} > 0 ){
843 my $joinedvals = join ',', @{$statushash->{$authvfield}};
844 push @where_strings, "$authvfield in (" . $joinedvals . ")";
849 if ($minlocation) {
850 push @where_strings, 'itemcallnumber >= ?';
851 push @bind_params, $minlocation;
854 if ($maxlocation) {
855 push @where_strings, 'itemcallnumber <= ?';
856 push @bind_params, $maxlocation;
859 if ($datelastseen) {
860 $datelastseen = output_pref({ str => $datelastseen, dateformat => 'iso', dateonly => 1 });
861 push @where_strings, '(datelastseen < ? OR datelastseen IS NULL)';
862 push @bind_params, $datelastseen;
865 if ( $location ) {
866 push @where_strings, 'items.location = ?';
867 push @bind_params, $location;
870 if ( $branchcode ) {
871 if($branch eq "homebranch"){
872 push @where_strings, 'items.homebranch = ?';
873 }else{
874 push @where_strings, 'items.holdingbranch = ?';
876 push @bind_params, $branchcode;
879 if ( $itemtype ) {
880 push @where_strings, 'biblioitems.itemtype = ?';
881 push @bind_params, $itemtype;
884 if ( $ignoreissued) {
885 $query .= "LEFT JOIN issues ON items.itemnumber = issues.itemnumber ";
886 push @where_strings, 'issues.date_due IS NULL';
889 if ( @where_strings ) {
890 $query .= 'WHERE ';
891 $query .= join ' AND ', @where_strings;
893 my $count_query = $select_count . $query;
894 $query .= ' ORDER BY items.cn_sort, itemcallnumber, title';
895 $query .= " LIMIT $offset, $size" if ($offset and $size);
896 $query = $select_columns . $query;
897 my $sth = $dbh->prepare($query);
898 $sth->execute( @bind_params );
900 my @results = ();
901 my $tmpresults = $sth->fetchall_arrayref({});
902 $sth = $dbh->prepare( $count_query );
903 $sth->execute( @bind_params );
904 my ($iTotalRecords) = $sth->fetchrow_array();
906 my @avs = Koha::AuthorisedValues->search(
907 { 'marc_subfield_structures.kohafield' => { '>' => '' },
908 'me.authorised_value' => { '>' => '' },
910 { join => { category => 'marc_subfield_structures' },
911 distinct => ['marc_subfield_structures.kohafield, me.category, frameworkcode, me.authorised_value'],
912 '+select' => [ 'marc_subfield_structures.kohafield', 'marc_subfield_structures.frameworkcode', 'me.authorised_value', 'me.lib' ],
913 '+as' => [ 'kohafield', 'frameworkcode', 'authorised_value', 'lib' ],
917 my $avmapping = { map { $_->get_column('kohafield') . ',' . $_->get_column('frameworkcode') . ',' . $_->get_column('authorised_value') => $_->get_column('lib') } @avs };
919 foreach my $row (@$tmpresults) {
921 # Auth values
922 foreach (keys %$row) {
923 if (defined($avmapping->{"items.$_,".$row->{'frameworkcode'}.",".$row->{$_}})) {
924 $row->{$_} = $avmapping->{"items.$_,".$row->{'frameworkcode'}.",".$row->{$_}};
927 push @results, $row;
930 return (\@results, $iTotalRecords);
933 =head2 GetItemsInfo
935 @results = GetItemsInfo($biblionumber);
937 Returns information about items with the given biblionumber.
939 C<GetItemsInfo> returns a list of references-to-hash. Each element
940 contains a number of keys. Most of them are attributes from the
941 C<biblio>, C<biblioitems>, C<items>, and C<itemtypes> tables in the
942 Koha database. Other keys include:
944 =over 2
946 =item C<$data-E<gt>{branchname}>
948 The name (not the code) of the branch to which the book belongs.
950 =item C<$data-E<gt>{datelastseen}>
952 This is simply C<items.datelastseen>, except that while the date is
953 stored in YYYY-MM-DD format in the database, here it is converted to
954 DD/MM/YYYY format. A NULL date is returned as C<//>.
956 =item C<$data-E<gt>{datedue}>
958 =item C<$data-E<gt>{class}>
960 This is the concatenation of C<biblioitems.classification>, the book's
961 Dewey code, and C<biblioitems.subclass>.
963 =item C<$data-E<gt>{ocount}>
965 I think this is the number of copies of the book available.
967 =item C<$data-E<gt>{order}>
969 If this is set, it is set to C<One Order>.
971 =back
973 =cut
975 sub GetItemsInfo {
976 my ( $biblionumber ) = @_;
977 my $dbh = C4::Context->dbh;
978 require C4::Languages;
979 my $language = C4::Languages::getlanguage();
980 my $query = "
981 SELECT items.*,
982 biblio.*,
983 biblioitems.volume,
984 biblioitems.number,
985 biblioitems.itemtype,
986 biblioitems.isbn,
987 biblioitems.issn,
988 biblioitems.publicationyear,
989 biblioitems.publishercode,
990 biblioitems.volumedate,
991 biblioitems.volumedesc,
992 biblioitems.lccn,
993 biblioitems.url,
994 items.notforloan as itemnotforloan,
995 issues.borrowernumber,
996 issues.date_due as datedue,
997 issues.onsite_checkout,
998 borrowers.cardnumber,
999 borrowers.surname,
1000 borrowers.firstname,
1001 borrowers.branchcode as bcode,
1002 serial.serialseq,
1003 serial.publisheddate,
1004 itemtypes.description,
1005 COALESCE( localization.translation, itemtypes.description ) AS translated_description,
1006 itemtypes.notforloan as notforloan_per_itemtype,
1007 holding.branchurl,
1008 holding.branchcode,
1009 holding.branchname,
1010 holding.opac_info as holding_branch_opac_info,
1011 home.opac_info as home_branch_opac_info
1013 $query .= "
1014 FROM items
1015 LEFT JOIN branches AS holding ON items.holdingbranch = holding.branchcode
1016 LEFT JOIN branches AS home ON items.homebranch=home.branchcode
1017 LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
1018 LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber
1019 LEFT JOIN issues USING (itemnumber)
1020 LEFT JOIN borrowers USING (borrowernumber)
1021 LEFT JOIN serialitems USING (itemnumber)
1022 LEFT JOIN serial USING (serialid)
1023 LEFT JOIN itemtypes ON itemtypes.itemtype = "
1024 . (C4::Context->preference('item-level_itypes') ? 'items.itype' : 'biblioitems.itemtype');
1025 $query .= q|
1026 LEFT JOIN localization ON itemtypes.itemtype = localization.code
1027 AND localization.entity = 'itemtypes'
1028 AND localization.lang = ?
1031 $query .= " WHERE items.biblionumber = ? ORDER BY home.branchname, items.enumchron, LPAD( items.copynumber, 8, '0' ), items.dateaccessioned DESC" ;
1032 my $sth = $dbh->prepare($query);
1033 $sth->execute($language, $biblionumber);
1034 my $i = 0;
1035 my @results;
1036 my $serial;
1038 my $userenv = C4::Context->userenv;
1039 my $want_not_same_branch = C4::Context->preference("IndependentBranches") && !C4::Context->IsSuperLibrarian();
1040 while ( my $data = $sth->fetchrow_hashref ) {
1041 if ( $data->{borrowernumber} && $want_not_same_branch) {
1042 $data->{'NOTSAMEBRANCH'} = $data->{'bcode'} ne $userenv->{branch};
1045 $serial ||= $data->{'serial'};
1047 my $descriptions;
1048 # get notforloan complete status if applicable
1049 $descriptions = Koha::AuthorisedValues->get_description_by_koha_field({frameworkcode => $data->{frameworkcode}, kohafield => 'items.notforloan', authorised_value => $data->{itemnotforloan} });
1050 $data->{notforloanvalue} = $descriptions->{lib} // '';
1051 $data->{notforloanvalueopac} = $descriptions->{opac_description} // '';
1053 # get restricted status and description if applicable
1054 $descriptions = Koha::AuthorisedValues->get_description_by_koha_field({frameworkcode => $data->{frameworkcode}, kohafield => 'items.restricted', authorised_value => $data->{restricted} });
1055 $data->{restricted} = $descriptions->{lib} // '';
1056 $data->{restrictedopac} = $descriptions->{opac_description} // '';
1058 # my stack procedures
1059 $descriptions = Koha::AuthorisedValues->get_description_by_koha_field({frameworkcode => $data->{frameworkcode}, kohafield => 'items.stack', authorised_value => $data->{stack} });
1060 $data->{stack} = $descriptions->{lib} // '';
1062 # Find the last 3 people who borrowed this item.
1063 my $sth2 = $dbh->prepare("SELECT * FROM old_issues,borrowers
1064 WHERE itemnumber = ?
1065 AND old_issues.borrowernumber = borrowers.borrowernumber
1066 ORDER BY returndate DESC
1067 LIMIT 3");
1068 $sth2->execute($data->{'itemnumber'});
1069 my $ii = 0;
1070 while (my $data2 = $sth2->fetchrow_hashref()) {
1071 $data->{"timestamp$ii"} = $data2->{'timestamp'} if $data2->{'timestamp'};
1072 $data->{"card$ii"} = $data2->{'cardnumber'} if $data2->{'cardnumber'};
1073 $data->{"borrower$ii"} = $data2->{'borrowernumber'} if $data2->{'borrowernumber'};
1074 $ii++;
1077 $results[$i] = $data;
1078 $i++;
1081 return $serial
1082 ? sort { ($b->{'publisheddate'} || $b->{'enumchron'}) cmp ($a->{'publisheddate'} || $a->{'enumchron'}) } @results
1083 : @results;
1086 =head2 GetItemsLocationInfo
1088 my @itemlocinfo = GetItemsLocationInfo($biblionumber);
1090 Returns the branch names, shelving location and itemcallnumber for each item attached to the biblio in question
1092 C<GetItemsInfo> returns a list of references-to-hash. Data returned:
1094 =over 2
1096 =item C<$data-E<gt>{homebranch}>
1098 Branch Name of the item's homebranch
1100 =item C<$data-E<gt>{holdingbranch}>
1102 Branch Name of the item's holdingbranch
1104 =item C<$data-E<gt>{location}>
1106 Item's shelving location code
1108 =item C<$data-E<gt>{location_intranet}>
1110 The intranet description for the Shelving Location as set in authorised_values 'LOC'
1112 =item C<$data-E<gt>{location_opac}>
1114 The OPAC description for the Shelving Location as set in authorised_values 'LOC'. Falls back to intranet description if no OPAC
1115 description is set.
1117 =item C<$data-E<gt>{itemcallnumber}>
1119 Item's itemcallnumber
1121 =item C<$data-E<gt>{cn_sort}>
1123 Item's call number normalized for sorting
1125 =back
1127 =cut
1129 sub GetItemsLocationInfo {
1130 my $biblionumber = shift;
1131 my @results;
1133 my $dbh = C4::Context->dbh;
1134 my $query = "SELECT a.branchname as homebranch, b.branchname as holdingbranch,
1135 location, itemcallnumber, cn_sort
1136 FROM items, branches as a, branches as b
1137 WHERE homebranch = a.branchcode AND holdingbranch = b.branchcode
1138 AND biblionumber = ?
1139 ORDER BY cn_sort ASC";
1140 my $sth = $dbh->prepare($query);
1141 $sth->execute($biblionumber);
1143 while ( my $data = $sth->fetchrow_hashref ) {
1144 my $av = Koha::AuthorisedValues->search({ category => 'LOC', authorised_value => $data->{location} });
1145 $av = $av->count ? $av->next : undef;
1146 $data->{location_intranet} = $av ? $av->lib : '';
1147 $data->{location_opac} = $av ? $av->opac_description : '';
1148 push @results, $data;
1150 return @results;
1153 =head2 GetHostItemsInfo
1155 $hostiteminfo = GetHostItemsInfo($hostfield);
1156 Returns the iteminfo for items linked to records via a host field
1158 =cut
1160 sub GetHostItemsInfo {
1161 my ($record) = @_;
1162 my @returnitemsInfo;
1164 if( !C4::Context->preference('EasyAnalyticalRecords') ) {
1165 return @returnitemsInfo;
1168 my @fields;
1169 if( C4::Context->preference('marcflavour') eq 'MARC21' ||
1170 C4::Context->preference('marcflavour') eq 'NORMARC') {
1171 @fields = $record->field('773');
1172 } elsif( C4::Context->preference('marcflavour') eq 'UNIMARC') {
1173 @fields = $record->field('461');
1176 foreach my $hostfield ( @fields ) {
1177 my $hostbiblionumber = $hostfield->subfield("0");
1178 my $linkeditemnumber = $hostfield->subfield("9");
1179 my @hostitemInfos = GetItemsInfo($hostbiblionumber);
1180 foreach my $hostitemInfo (@hostitemInfos) {
1181 if( $hostitemInfo->{itemnumber} eq $linkeditemnumber ) {
1182 push @returnitemsInfo, $hostitemInfo;
1183 last;
1187 return @returnitemsInfo;
1190 =head2 GetLastAcquisitions
1192 my $lastacq = GetLastAcquisitions({'branches' => ('branch1','branch2'),
1193 'itemtypes' => ('BK','BD')}, 10);
1195 =cut
1197 sub GetLastAcquisitions {
1198 my ($data,$max) = @_;
1200 my $itemtype = C4::Context->preference('item-level_itypes') ? 'itype' : 'itemtype';
1202 my $number_of_branches = @{$data->{branches}};
1203 my $number_of_itemtypes = @{$data->{itemtypes}};
1206 my @where = ('WHERE 1 ');
1207 $number_of_branches and push @where
1208 , 'AND holdingbranch IN ('
1209 , join(',', ('?') x $number_of_branches )
1210 , ')'
1213 $number_of_itemtypes and push @where
1214 , "AND $itemtype IN ("
1215 , join(',', ('?') x $number_of_itemtypes )
1216 , ')'
1219 my $query = "SELECT biblio.biblionumber as biblionumber, title, dateaccessioned
1220 FROM items RIGHT JOIN biblio ON (items.biblionumber=biblio.biblionumber)
1221 RIGHT JOIN biblioitems ON (items.biblioitemnumber=biblioitems.biblioitemnumber)
1222 @where
1223 GROUP BY biblio.biblionumber
1224 ORDER BY dateaccessioned DESC LIMIT $max";
1226 my $dbh = C4::Context->dbh;
1227 my $sth = $dbh->prepare($query);
1229 $sth->execute((@{$data->{branches}}, @{$data->{itemtypes}}));
1231 my @results;
1232 while( my $row = $sth->fetchrow_hashref){
1233 push @results, {date => $row->{dateaccessioned}
1234 , biblionumber => $row->{biblionumber}
1235 , title => $row->{title}};
1238 return @results;
1241 =head2 GetItemnumbersForBiblio
1243 my $itemnumbers = GetItemnumbersForBiblio($biblionumber);
1245 Given a single biblionumber, return an arrayref of all the corresponding itemnumbers
1247 =cut
1249 sub GetItemnumbersForBiblio {
1250 my $biblionumber = shift;
1251 my @items;
1252 my $dbh = C4::Context->dbh;
1253 my $sth = $dbh->prepare("SELECT itemnumber FROM items WHERE biblionumber = ?");
1254 $sth->execute($biblionumber);
1255 while (my $result = $sth->fetchrow_hashref) {
1256 push @items, $result->{'itemnumber'};
1258 return \@items;
1261 =head2 get_hostitemnumbers_of
1263 my @itemnumbers_of = get_hostitemnumbers_of($biblionumber);
1265 Given a biblionumber, return the list of corresponding itemnumbers that are linked to it via host fields
1267 Return a reference on a hash where key is a biblionumber and values are
1268 references on array of itemnumbers.
1270 =cut
1273 sub get_hostitemnumbers_of {
1274 my ($biblionumber) = @_;
1275 my $marcrecord = C4::Biblio::GetMarcBiblio({ biblionumber => $biblionumber });
1277 return unless $marcrecord;
1279 my ( @returnhostitemnumbers, $tag, $biblio_s, $item_s );
1281 my $marcflavor = C4::Context->preference('marcflavour');
1282 if ( $marcflavor eq 'MARC21' || $marcflavor eq 'NORMARC' ) {
1283 $tag = '773';
1284 $biblio_s = '0';
1285 $item_s = '9';
1287 elsif ( $marcflavor eq 'UNIMARC' ) {
1288 $tag = '461';
1289 $biblio_s = '0';
1290 $item_s = '9';
1293 foreach my $hostfield ( $marcrecord->field($tag) ) {
1294 my $hostbiblionumber = $hostfield->subfield($biblio_s);
1295 next unless $hostbiblionumber; # have tag, don't have $biblio_s subfield
1296 my $linkeditemnumber = $hostfield->subfield($item_s);
1297 if ( ! $linkeditemnumber ) {
1298 warn "ERROR biblionumber $biblionumber has 773^0, but doesn't have 9";
1299 next;
1301 my $is_from_biblio = Koha::Items->search({ itemnumber => $linkeditemnumber, biblionumber => $hostbiblionumber });
1302 push @returnhostitemnumbers, $linkeditemnumber
1303 if $is_from_biblio;
1306 return @returnhostitemnumbers;
1309 =head2 GetHiddenItemnumbers
1311 my @itemnumbers_to_hide = GetHiddenItemnumbers(@items);
1313 Given a list of items it checks which should be hidden from the OPAC given
1314 the current configuration. Returns a list of itemnumbers corresponding to
1315 those that should be hidden.
1317 =cut
1319 sub GetHiddenItemnumbers {
1320 my (@items) = @_;
1321 my @resultitems;
1323 my $yaml = C4::Context->preference('OpacHiddenItems');
1324 return () if (! $yaml =~ /\S/ );
1325 $yaml = "$yaml\n\n"; # YAML is anal on ending \n. Surplus does not hurt
1326 my $hidingrules;
1327 eval {
1328 $hidingrules = YAML::Load($yaml);
1330 if ($@) {
1331 warn "Unable to parse OpacHiddenItems syspref : $@";
1332 return ();
1334 my $dbh = C4::Context->dbh;
1336 # For each item
1337 foreach my $item (@items) {
1339 # We check each rule
1340 foreach my $field (keys %$hidingrules) {
1341 my $val;
1342 if (exists $item->{$field}) {
1343 $val = $item->{$field};
1345 else {
1346 my $query = "SELECT $field from items where itemnumber = ?";
1347 $val = $dbh->selectrow_array($query, undef, $item->{'itemnumber'});
1349 $val = '' unless defined $val;
1351 # If the results matches the values in the yaml file
1352 if (any { $val eq $_ } @{$hidingrules->{$field}}) {
1354 # We add the itemnumber to the list
1355 push @resultitems, $item->{'itemnumber'};
1357 # If at least one rule matched for an item, no need to test the others
1358 last;
1362 return @resultitems;
1365 =head1 LIMITED USE FUNCTIONS
1367 The following functions, while part of the public API,
1368 are not exported. This is generally because they are
1369 meant to be used by only one script for a specific
1370 purpose, and should not be used in any other context
1371 without careful thought.
1373 =cut
1375 =head2 GetMarcItem
1377 my $item_marc = GetMarcItem($biblionumber, $itemnumber);
1379 Returns MARC::Record of the item passed in parameter.
1380 This function is meant for use only in C<cataloguing/additem.pl>,
1381 where it is needed to support that script's MARC-like
1382 editor.
1384 =cut
1386 sub GetMarcItem {
1387 my ( $biblionumber, $itemnumber ) = @_;
1389 # GetMarcItem has been revised so that it does the following:
1390 # 1. Gets the item information from the items table.
1391 # 2. Converts it to a MARC field for storage in the bib record.
1393 # The previous behavior was:
1394 # 1. Get the bib record.
1395 # 2. Return the MARC tag corresponding to the item record.
1397 # The difference is that one treats the items row as authoritative,
1398 # while the other treats the MARC representation as authoritative
1399 # under certain circumstances.
1401 my $itemrecord = GetItem($itemnumber);
1403 # Tack on 'items.' prefix to column names so that C4::Biblio::TransformKohaToMarc will work.
1404 # Also, don't emit a subfield if the underlying field is blank.
1407 return Item2Marc($itemrecord,$biblionumber);
1410 sub Item2Marc {
1411 my ($itemrecord,$biblionumber)=@_;
1412 my $mungeditem = {
1413 map {
1414 defined($itemrecord->{$_}) && $itemrecord->{$_} ne '' ? ("items.$_" => $itemrecord->{$_}) : ()
1415 } keys %{ $itemrecord }
1417 my $framework = C4::Biblio::GetFrameworkCode( $biblionumber );
1418 my $itemmarc = C4::Biblio::TransformKohaToMarc(
1419 $mungeditem, { no_split => 1},
1421 my ( $itemtag, $itemsubfield ) = C4::Biblio::GetMarcFromKohaField(
1422 "items.itemnumber", $framework,
1425 my $unlinked_item_subfields = _parse_unlinked_item_subfields_from_xml($mungeditem->{'items.more_subfields_xml'});
1426 if (defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1) {
1427 foreach my $field ($itemmarc->field($itemtag)){
1428 $field->add_subfields(@$unlinked_item_subfields);
1431 return $itemmarc;
1434 =head1 PRIVATE FUNCTIONS AND VARIABLES
1436 The following functions are not meant to be called
1437 directly, but are documented in order to explain
1438 the inner workings of C<C4::Items>.
1440 =cut
1442 =head2 %derived_columns
1444 This hash keeps track of item columns that
1445 are strictly derived from other columns in
1446 the item record and are not meant to be set
1447 independently.
1449 Each key in the hash should be the name of a
1450 column (as named by TransformMarcToKoha). Each
1451 value should be hashref whose keys are the
1452 columns on which the derived column depends. The
1453 hashref should also contain a 'BUILDER' key
1454 that is a reference to a sub that calculates
1455 the derived value.
1457 =cut
1459 my %derived_columns = (
1460 'items.cn_sort' => {
1461 'itemcallnumber' => 1,
1462 'items.cn_source' => 1,
1463 'BUILDER' => \&_calc_items_cn_sort,
1467 =head2 _set_derived_columns_for_add
1469 _set_derived_column_for_add($item);
1471 Given an item hash representing a new item to be added,
1472 calculate any derived columns. Currently the only
1473 such column is C<items.cn_sort>.
1475 =cut
1477 sub _set_derived_columns_for_add {
1478 my $item = shift;
1480 foreach my $column (keys %derived_columns) {
1481 my $builder = $derived_columns{$column}->{'BUILDER'};
1482 my $source_values = {};
1483 foreach my $source_column (keys %{ $derived_columns{$column} }) {
1484 next if $source_column eq 'BUILDER';
1485 $source_values->{$source_column} = $item->{$source_column};
1487 $builder->($item, $source_values);
1491 =head2 _set_derived_columns_for_mod
1493 _set_derived_column_for_mod($item);
1495 Given an item hash representing a new item to be modified.
1496 calculate any derived columns. Currently the only
1497 such column is C<items.cn_sort>.
1499 This routine differs from C<_set_derived_columns_for_add>
1500 in that it needs to handle partial item records. In other
1501 words, the caller of C<ModItem> may have supplied only one
1502 or two columns to be changed, so this function needs to
1503 determine whether any of the columns to be changed affect
1504 any of the derived columns. Also, if a derived column
1505 depends on more than one column, but the caller is not
1506 changing all of then, this routine retrieves the unchanged
1507 values from the database in order to ensure a correct
1508 calculation.
1510 =cut
1512 sub _set_derived_columns_for_mod {
1513 my $item = shift;
1515 foreach my $column (keys %derived_columns) {
1516 my $builder = $derived_columns{$column}->{'BUILDER'};
1517 my $source_values = {};
1518 my %missing_sources = ();
1519 my $must_recalc = 0;
1520 foreach my $source_column (keys %{ $derived_columns{$column} }) {
1521 next if $source_column eq 'BUILDER';
1522 if (exists $item->{$source_column}) {
1523 $must_recalc = 1;
1524 $source_values->{$source_column} = $item->{$source_column};
1525 } else {
1526 $missing_sources{$source_column} = 1;
1529 if ($must_recalc) {
1530 foreach my $source_column (keys %missing_sources) {
1531 $source_values->{$source_column} = _get_single_item_column($source_column, $item->{'itemnumber'});
1533 $builder->($item, $source_values);
1538 =head2 _do_column_fixes_for_mod
1540 _do_column_fixes_for_mod($item);
1542 Given an item hashref containing one or more
1543 columns to modify, fix up certain values.
1544 Specifically, set to 0 any passed value
1545 of C<notforloan>, C<damaged>, C<itemlost>, or
1546 C<withdrawn> that is either undefined or
1547 contains the empty string.
1549 =cut
1551 sub _do_column_fixes_for_mod {
1552 my $item = shift;
1554 if (exists $item->{'notforloan'} and
1555 (not defined $item->{'notforloan'} or $item->{'notforloan'} eq '')) {
1556 $item->{'notforloan'} = 0;
1558 if (exists $item->{'damaged'} and
1559 (not defined $item->{'damaged'} or $item->{'damaged'} eq '')) {
1560 $item->{'damaged'} = 0;
1562 if (exists $item->{'itemlost'} and
1563 (not defined $item->{'itemlost'} or $item->{'itemlost'} eq '')) {
1564 $item->{'itemlost'} = 0;
1566 if (exists $item->{'withdrawn'} and
1567 (not defined $item->{'withdrawn'} or $item->{'withdrawn'} eq '')) {
1568 $item->{'withdrawn'} = 0;
1570 if (exists $item->{location}
1571 and $item->{location} ne 'CART'
1572 and $item->{location} ne 'PROC'
1573 and not $item->{permanent_location}
1575 $item->{'permanent_location'} = $item->{'location'};
1577 if (exists $item->{'timestamp'}) {
1578 delete $item->{'timestamp'};
1582 =head2 _get_single_item_column
1584 _get_single_item_column($column, $itemnumber);
1586 Retrieves the value of a single column from an C<items>
1587 row specified by C<$itemnumber>.
1589 =cut
1591 sub _get_single_item_column {
1592 my $column = shift;
1593 my $itemnumber = shift;
1595 my $dbh = C4::Context->dbh;
1596 my $sth = $dbh->prepare("SELECT $column FROM items WHERE itemnumber = ?");
1597 $sth->execute($itemnumber);
1598 my ($value) = $sth->fetchrow();
1599 return $value;
1602 =head2 _calc_items_cn_sort
1604 _calc_items_cn_sort($item, $source_values);
1606 Helper routine to calculate C<items.cn_sort>.
1608 =cut
1610 sub _calc_items_cn_sort {
1611 my $item = shift;
1612 my $source_values = shift;
1614 $item->{'items.cn_sort'} = GetClassSort($source_values->{'items.cn_source'}, $source_values->{'itemcallnumber'}, "");
1617 =head2 _set_defaults_for_add
1619 _set_defaults_for_add($item_hash);
1621 Given an item hash representing an item to be added, set
1622 correct default values for columns whose default value
1623 is not handled by the DBMS. This includes the following
1624 columns:
1626 =over 2
1628 =item *
1630 C<items.dateaccessioned>
1632 =item *
1634 C<items.notforloan>
1636 =item *
1638 C<items.damaged>
1640 =item *
1642 C<items.itemlost>
1644 =item *
1646 C<items.withdrawn>
1648 =back
1650 =cut
1652 sub _set_defaults_for_add {
1653 my $item = shift;
1654 $item->{dateaccessioned} ||= output_pref({ dt => dt_from_string, dateformat => 'iso', dateonly => 1 });
1655 $item->{$_} ||= 0 for (qw( notforloan damaged itemlost withdrawn));
1658 =head2 _koha_new_item
1660 my ($itemnumber,$error) = _koha_new_item( $item, $barcode );
1662 Perform the actual insert into the C<items> table.
1664 =cut
1666 sub _koha_new_item {
1667 my ( $item, $barcode ) = @_;
1668 my $dbh=C4::Context->dbh;
1669 my $error;
1670 $item->{permanent_location} //= $item->{location};
1671 _mod_item_dates( $item );
1672 my $query =
1673 "INSERT INTO items SET
1674 biblionumber = ?,
1675 biblioitemnumber = ?,
1676 barcode = ?,
1677 dateaccessioned = ?,
1678 booksellerid = ?,
1679 homebranch = ?,
1680 price = ?,
1681 replacementprice = ?,
1682 replacementpricedate = ?,
1683 datelastborrowed = ?,
1684 datelastseen = ?,
1685 stack = ?,
1686 notforloan = ?,
1687 damaged = ?,
1688 itemlost = ?,
1689 withdrawn = ?,
1690 itemcallnumber = ?,
1691 coded_location_qualifier = ?,
1692 restricted = ?,
1693 itemnotes = ?,
1694 itemnotes_nonpublic = ?,
1695 holdingbranch = ?,
1696 paidfor = ?,
1697 location = ?,
1698 permanent_location = ?,
1699 onloan = ?,
1700 issues = ?,
1701 renewals = ?,
1702 reserves = ?,
1703 cn_source = ?,
1704 cn_sort = ?,
1705 ccode = ?,
1706 itype = ?,
1707 materials = ?,
1708 uri = ?,
1709 enumchron = ?,
1710 more_subfields_xml = ?,
1711 copynumber = ?,
1712 stocknumber = ?,
1713 new_status = ?
1715 my $sth = $dbh->prepare($query);
1716 my $today = output_pref({ dt => dt_from_string, dateformat => 'iso', dateonly => 1 });
1717 $sth->execute(
1718 $item->{'biblionumber'},
1719 $item->{'biblioitemnumber'},
1720 $barcode,
1721 $item->{'dateaccessioned'},
1722 $item->{'booksellerid'},
1723 $item->{'homebranch'},
1724 $item->{'price'},
1725 $item->{'replacementprice'},
1726 $item->{'replacementpricedate'} || $today,
1727 $item->{datelastborrowed},
1728 $item->{datelastseen} || $today,
1729 $item->{stack},
1730 $item->{'notforloan'},
1731 $item->{'damaged'},
1732 $item->{'itemlost'},
1733 $item->{'withdrawn'},
1734 $item->{'itemcallnumber'},
1735 $item->{'coded_location_qualifier'},
1736 $item->{'restricted'},
1737 $item->{'itemnotes'},
1738 $item->{'itemnotes_nonpublic'},
1739 $item->{'holdingbranch'},
1740 $item->{'paidfor'},
1741 $item->{'location'},
1742 $item->{'permanent_location'},
1743 $item->{'onloan'},
1744 $item->{'issues'},
1745 $item->{'renewals'},
1746 $item->{'reserves'},
1747 $item->{'items.cn_source'},
1748 $item->{'items.cn_sort'},
1749 $item->{'ccode'},
1750 $item->{'itype'},
1751 $item->{'materials'},
1752 $item->{'uri'},
1753 $item->{'enumchron'},
1754 $item->{'more_subfields_xml'},
1755 $item->{'copynumber'},
1756 $item->{'stocknumber'},
1757 $item->{'new_status'},
1760 my $itemnumber;
1761 if ( defined $sth->errstr ) {
1762 $error.="ERROR in _koha_new_item $query".$sth->errstr;
1764 else {
1765 $itemnumber = $dbh->{'mysql_insertid'};
1768 return ( $itemnumber, $error );
1771 =head2 MoveItemFromBiblio
1773 MoveItemFromBiblio($itenumber, $frombiblio, $tobiblio);
1775 Moves an item from a biblio to another
1777 Returns undef if the move failed or the biblionumber of the destination record otherwise
1779 =cut
1781 sub MoveItemFromBiblio {
1782 my ($itemnumber, $frombiblio, $tobiblio) = @_;
1783 my $dbh = C4::Context->dbh;
1784 my ( $tobiblioitem ) = $dbh->selectrow_array(q|
1785 SELECT biblioitemnumber
1786 FROM biblioitems
1787 WHERE biblionumber = ?
1788 |, undef, $tobiblio );
1789 my $return = $dbh->do(q|
1790 UPDATE items
1791 SET biblioitemnumber = ?,
1792 biblionumber = ?
1793 WHERE itemnumber = ?
1794 AND biblionumber = ?
1795 |, undef, $tobiblioitem, $tobiblio, $itemnumber, $frombiblio );
1796 if ($return == 1) {
1797 ModZebra( $tobiblio, "specialUpdate", "biblioserver" );
1798 ModZebra( $frombiblio, "specialUpdate", "biblioserver" );
1799 # Checking if the item we want to move is in an order
1800 require C4::Acquisition;
1801 my $order = C4::Acquisition::GetOrderFromItemnumber($itemnumber);
1802 if ($order) {
1803 # Replacing the biblionumber within the order if necessary
1804 $order->{'biblionumber'} = $tobiblio;
1805 C4::Acquisition::ModOrder($order);
1808 # Update reserves, hold_fill_targets, tmp_holdsqueue and linktracker tables
1809 for my $table_name ( qw( reserves hold_fill_targets tmp_holdsqueue linktracker ) ) {
1810 $dbh->do( qq|
1811 UPDATE $table_name
1812 SET biblionumber = ?
1813 WHERE itemnumber = ?
1814 |, undef, $tobiblio, $itemnumber );
1816 return $tobiblio;
1818 return;
1821 =head2 ItemSafeToDelete
1823 ItemSafeToDelete( $biblionumber, $itemnumber);
1825 Exported function (core API) for checking whether an item record is safe to delete.
1827 returns 1 if the item is safe to delete,
1829 "book_on_loan" if the item is checked out,
1831 "not_same_branch" if the item is blocked by independent branches,
1833 "book_reserved" if the there are holds aganst the item, or
1835 "linked_analytics" if the item has linked analytic records.
1837 =cut
1839 sub ItemSafeToDelete {
1840 my ( $biblionumber, $itemnumber ) = @_;
1841 my $status;
1842 my $dbh = C4::Context->dbh;
1844 my $error;
1846 my $countanalytics = GetAnalyticsCount($itemnumber);
1848 # check that there is no issue on this item before deletion.
1849 my $sth = $dbh->prepare(
1851 SELECT COUNT(*) FROM issues
1852 WHERE itemnumber = ?
1855 $sth->execute($itemnumber);
1856 my ($onloan) = $sth->fetchrow;
1858 my $item = GetItem($itemnumber);
1860 if ($onloan) {
1861 $status = "book_on_loan";
1863 elsif ( defined C4::Context->userenv
1864 and !C4::Context->IsSuperLibrarian()
1865 and C4::Context->preference("IndependentBranches")
1866 and ( C4::Context->userenv->{branch} ne $item->{'homebranch'} ) )
1868 $status = "not_same_branch";
1870 else {
1871 # check it doesn't have a waiting reserve
1872 $sth = $dbh->prepare(
1874 SELECT COUNT(*) FROM reserves
1875 WHERE (found = 'W' OR found = 'T')
1876 AND itemnumber = ?
1879 $sth->execute($itemnumber);
1880 my ($reserve) = $sth->fetchrow;
1881 if ($reserve) {
1882 $status = "book_reserved";
1884 elsif ( $countanalytics > 0 ) {
1885 $status = "linked_analytics";
1887 else {
1888 $status = 1;
1891 return $status;
1894 =head2 DelItemCheck
1896 DelItemCheck( $biblionumber, $itemnumber);
1898 Exported function (core API) for deleting an item record in Koha if there no current issue.
1900 DelItemCheck wraps ItemSafeToDelete around DelItem.
1902 =cut
1904 sub DelItemCheck {
1905 my ( $biblionumber, $itemnumber ) = @_;
1906 my $status = ItemSafeToDelete( $biblionumber, $itemnumber );
1908 if ( $status == 1 ) {
1909 DelItem(
1911 biblionumber => $biblionumber,
1912 itemnumber => $itemnumber
1916 return $status;
1919 =head2 _koha_modify_item
1921 my ($itemnumber,$error) =_koha_modify_item( $item );
1923 Perform the actual update of the C<items> row. Note that this
1924 routine accepts a hashref specifying the columns to update.
1926 =cut
1928 sub _koha_modify_item {
1929 my ( $item ) = @_;
1930 my $dbh=C4::Context->dbh;
1931 my $error;
1933 my $query = "UPDATE items SET ";
1934 my @bind;
1935 _mod_item_dates( $item );
1936 for my $key ( keys %$item ) {
1937 next if ( $key eq 'itemnumber' );
1938 $query.="$key=?,";
1939 push @bind, $item->{$key};
1941 $query =~ s/,$//;
1942 $query .= " WHERE itemnumber=?";
1943 push @bind, $item->{'itemnumber'};
1944 my $sth = $dbh->prepare($query);
1945 $sth->execute(@bind);
1946 if ( $sth->err ) {
1947 $error.="ERROR in _koha_modify_item $query: ".$sth->errstr;
1948 warn $error;
1950 return ($item->{'itemnumber'},$error);
1953 sub _mod_item_dates { # date formatting for date fields in item hash
1954 my ( $item ) = @_;
1955 return if !$item || ref($item) ne 'HASH';
1957 my @keys = grep
1958 { $_ =~ /^onloan$|^date|date$|datetime$/ }
1959 keys %$item;
1960 # Incl. dateaccessioned,replacementpricedate,datelastborrowed,datelastseen
1961 # NOTE: We do not (yet) have items fields ending with datetime
1962 # Fields with _on$ have been handled already
1964 foreach my $key ( @keys ) {
1965 next if !defined $item->{$key}; # skip undefs
1966 my $dt = eval { dt_from_string( $item->{$key} ) };
1967 # eval: dt_from_string will die on us if we pass illegal dates
1969 my $newstr;
1970 if( defined $dt && ref($dt) eq 'DateTime' ) {
1971 if( $key =~ /datetime/ ) {
1972 $newstr = DateTime::Format::MySQL->format_datetime($dt);
1973 } else {
1974 $newstr = DateTime::Format::MySQL->format_date($dt);
1977 $item->{$key} = $newstr; # might be undef to clear garbage
1981 =head2 _koha_delete_item
1983 _koha_delete_item( $itemnum );
1985 Internal function to delete an item record from the koha tables
1987 =cut
1989 sub _koha_delete_item {
1990 my ( $itemnum ) = @_;
1992 my $dbh = C4::Context->dbh;
1993 # save the deleted item to deleteditems table
1994 my $sth = $dbh->prepare("SELECT * FROM items WHERE itemnumber=?");
1995 $sth->execute($itemnum);
1996 my $data = $sth->fetchrow_hashref();
1998 # There is no item to delete
1999 return 0 unless $data;
2001 my $query = "INSERT INTO deleteditems SET ";
2002 my @bind = ();
2003 foreach my $key ( keys %$data ) {
2004 next if ( $key eq 'timestamp' ); # timestamp will be set by db
2005 $query .= "$key = ?,";
2006 push( @bind, $data->{$key} );
2008 $query =~ s/\,$//;
2009 $sth = $dbh->prepare($query);
2010 $sth->execute(@bind);
2012 # delete from items table
2013 $sth = $dbh->prepare("DELETE FROM items WHERE itemnumber=?");
2014 my $deleted = $sth->execute($itemnum);
2015 return ( $deleted == 1 ) ? 1 : 0;
2018 =head2 _marc_from_item_hash
2020 my $item_marc = _marc_from_item_hash($item, $frameworkcode[, $unlinked_item_subfields]);
2022 Given an item hash representing a complete item record,
2023 create a C<MARC::Record> object containing an embedded
2024 tag representing that item.
2026 The third, optional parameter C<$unlinked_item_subfields> is
2027 an arrayref of subfields (not mapped to C<items> fields per the
2028 framework) to be added to the MARC representation
2029 of the item.
2031 =cut
2033 sub _marc_from_item_hash {
2034 my $item = shift;
2035 my $frameworkcode = shift;
2036 my $unlinked_item_subfields;
2037 if (@_) {
2038 $unlinked_item_subfields = shift;
2041 # Tack on 'items.' prefix to column names so lookup from MARC frameworks will work
2042 # Also, don't emit a subfield if the underlying field is blank.
2043 my $mungeditem = { map { (defined($item->{$_}) and $item->{$_} ne '') ?
2044 (/^items\./ ? ($_ => $item->{$_}) : ("items.$_" => $item->{$_}))
2045 : () } keys %{ $item } };
2047 my $item_marc = MARC::Record->new();
2048 foreach my $item_field ( keys %{$mungeditem} ) {
2049 my ( $tag, $subfield ) = C4::Biblio::GetMarcFromKohaField( $item_field, $frameworkcode );
2050 next unless defined $tag and defined $subfield; # skip if not mapped to MARC field
2051 my @values = split(/\s?\|\s?/, $mungeditem->{$item_field}, -1);
2052 foreach my $value (@values){
2053 if ( my $field = $item_marc->field($tag) ) {
2054 $field->add_subfields( $subfield => $value );
2055 } else {
2056 my $add_subfields = [];
2057 if (defined $unlinked_item_subfields and ref($unlinked_item_subfields) eq 'ARRAY' and $#$unlinked_item_subfields > -1) {
2058 $add_subfields = $unlinked_item_subfields;
2060 $item_marc->add_fields( $tag, " ", " ", $subfield => $value, @$add_subfields );
2065 return $item_marc;
2068 =head2 _repack_item_errors
2070 Add an error message hash generated by C<CheckItemPreSave>
2071 to a list of errors.
2073 =cut
2075 sub _repack_item_errors {
2076 my $item_sequence_num = shift;
2077 my $item_ref = shift;
2078 my $error_ref = shift;
2080 my @repacked_errors = ();
2082 foreach my $error_code (sort keys %{ $error_ref }) {
2083 my $repacked_error = {};
2084 $repacked_error->{'item_sequence'} = $item_sequence_num;
2085 $repacked_error->{'item_barcode'} = exists($item_ref->{'barcode'}) ? $item_ref->{'barcode'} : '';
2086 $repacked_error->{'error_code'} = $error_code;
2087 $repacked_error->{'error_information'} = $error_ref->{$error_code};
2088 push @repacked_errors, $repacked_error;
2091 return @repacked_errors;
2094 =head2 _get_unlinked_item_subfields
2096 my $unlinked_item_subfields = _get_unlinked_item_subfields($original_item_marc, $frameworkcode);
2098 =cut
2100 sub _get_unlinked_item_subfields {
2101 my $original_item_marc = shift;
2102 my $frameworkcode = shift;
2104 my $marcstructure = GetMarcStructure(1, $frameworkcode, { unsafe => 1 });
2106 # assume that this record has only one field, and that that
2107 # field contains only the item information
2108 my $subfields = [];
2109 my @fields = $original_item_marc->fields();
2110 if ($#fields > -1) {
2111 my $field = $fields[0];
2112 my $tag = $field->tag();
2113 foreach my $subfield ($field->subfields()) {
2114 if (defined $subfield->[1] and
2115 $subfield->[1] ne '' and
2116 !$marcstructure->{$tag}->{$subfield->[0]}->{'kohafield'}) {
2117 push @$subfields, $subfield->[0] => $subfield->[1];
2121 return $subfields;
2124 =head2 _get_unlinked_subfields_xml
2126 my $unlinked_subfields_xml = _get_unlinked_subfields_xml($unlinked_item_subfields);
2128 =cut
2130 sub _get_unlinked_subfields_xml {
2131 my $unlinked_item_subfields = shift;
2133 my $xml;
2134 if (defined $unlinked_item_subfields and ref($unlinked_item_subfields) eq 'ARRAY' and $#$unlinked_item_subfields > -1) {
2135 my $marc = MARC::Record->new();
2136 # use of tag 999 is arbitrary, and doesn't need to match the item tag
2137 # used in the framework
2138 $marc->append_fields(MARC::Field->new('999', ' ', ' ', @$unlinked_item_subfields));
2139 $marc->encoding("UTF-8");
2140 $xml = $marc->as_xml("USMARC");
2143 return $xml;
2146 =head2 _parse_unlinked_item_subfields_from_xml
2148 my $unlinked_item_subfields = _parse_unlinked_item_subfields_from_xml($whole_item->{'more_subfields_xml'}):
2150 =cut
2152 sub _parse_unlinked_item_subfields_from_xml {
2153 my $xml = shift;
2154 require C4::Charset;
2155 return unless defined $xml and $xml ne "";
2156 my $marc = MARC::Record->new_from_xml(C4::Charset::StripNonXmlChars($xml),'UTF-8');
2157 my $unlinked_subfields = [];
2158 my @fields = $marc->fields();
2159 if ($#fields > -1) {
2160 foreach my $subfield ($fields[0]->subfields()) {
2161 push @$unlinked_subfields, $subfield->[0] => $subfield->[1];
2164 return $unlinked_subfields;
2167 =head2 GetAnalyticsCount
2169 $count= &GetAnalyticsCount($itemnumber)
2171 counts Usage of itemnumber in Analytical bibliorecords.
2173 =cut
2175 sub GetAnalyticsCount {
2176 my ($itemnumber) = @_;
2178 ### ZOOM search here
2179 my $query;
2180 $query= "hi=".$itemnumber;
2181 my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
2182 my ($err,$res,$result) = $searcher->simple_search_compat($query,0,10);
2183 return ($result);
2186 =head2 SearchItemsByField
2188 my $items = SearchItemsByField($field, $value);
2190 SearchItemsByField will search for items on a specific given field.
2191 For instance you can search all items with a specific stocknumber like this:
2193 my $items = SearchItemsByField('stocknumber', $stocknumber);
2195 =cut
2197 sub SearchItemsByField {
2198 my ($field, $value) = @_;
2200 my $filters = {
2201 field => $field,
2202 query => $value,
2205 my ($results) = SearchItems($filters);
2206 return $results;
2209 sub _SearchItems_build_where_fragment {
2210 my ($filter) = @_;
2212 my $dbh = C4::Context->dbh;
2214 my $where_fragment;
2215 if (exists($filter->{conjunction})) {
2216 my (@where_strs, @where_args);
2217 foreach my $f (@{ $filter->{filters} }) {
2218 my $fragment = _SearchItems_build_where_fragment($f);
2219 if ($fragment) {
2220 push @where_strs, $fragment->{str};
2221 push @where_args, @{ $fragment->{args} };
2224 my $where_str = '';
2225 if (@where_strs) {
2226 $where_str = '(' . join (' ' . $filter->{conjunction} . ' ', @where_strs) . ')';
2227 $where_fragment = {
2228 str => $where_str,
2229 args => \@where_args,
2232 } else {
2233 my @columns = Koha::Database->new()->schema()->resultset('Item')->result_source->columns;
2234 push @columns, Koha::Database->new()->schema()->resultset('Biblio')->result_source->columns;
2235 push @columns, Koha::Database->new()->schema()->resultset('Biblioitem')->result_source->columns;
2236 my @operators = qw(= != > < >= <= like);
2237 my $field = $filter->{field};
2238 if ( (0 < grep /^$field$/, @columns) or (substr($field, 0, 5) eq 'marc:') ) {
2239 my $op = $filter->{operator};
2240 my $query = $filter->{query};
2242 if (!$op or (0 == grep /^$op$/, @operators)) {
2243 $op = '='; # default operator
2246 my $column;
2247 if ($field =~ /^marc:(\d{3})(?:\$(\w))?$/) {
2248 my $marcfield = $1;
2249 my $marcsubfield = $2;
2250 my ($kohafield) = $dbh->selectrow_array(q|
2251 SELECT kohafield FROM marc_subfield_structure
2252 WHERE tagfield=? AND tagsubfield=? AND frameworkcode=''
2253 |, undef, $marcfield, $marcsubfield);
2255 if ($kohafield) {
2256 $column = $kohafield;
2257 } else {
2258 # MARC field is not linked to a DB field so we need to use
2259 # ExtractValue on marcxml from biblio_metadata or
2260 # items.more_subfields_xml, depending on the MARC field.
2261 my $xpath;
2262 my $sqlfield;
2263 my ($itemfield) = C4::Biblio::GetMarcFromKohaField('items.itemnumber');
2264 if ($marcfield eq $itemfield) {
2265 $sqlfield = 'more_subfields_xml';
2266 $xpath = '//record/datafield/subfield[@code="' . $marcsubfield . '"]';
2267 } else {
2268 $sqlfield = 'metadata'; # From biblio_metadata
2269 if ($marcfield < 10) {
2270 $xpath = "//record/controlfield[\@tag=\"$marcfield\"]";
2271 } else {
2272 $xpath = "//record/datafield[\@tag=\"$marcfield\"]/subfield[\@code=\"$marcsubfield\"]";
2275 $column = "ExtractValue($sqlfield, '$xpath')";
2277 } else {
2278 $column = $field;
2281 if (ref $query eq 'ARRAY') {
2282 if ($op eq '=') {
2283 $op = 'IN';
2284 } elsif ($op eq '!=') {
2285 $op = 'NOT IN';
2287 $where_fragment = {
2288 str => "$column $op (" . join (',', ('?') x @$query) . ")",
2289 args => $query,
2291 } else {
2292 $where_fragment = {
2293 str => "$column $op ?",
2294 args => [ $query ],
2300 return $where_fragment;
2303 =head2 SearchItems
2305 my ($items, $total) = SearchItems($filter, $params);
2307 Perform a search among items
2309 $filter is a reference to a hash which can be a filter, or a combination of filters.
2311 A filter has the following keys:
2313 =over 2
2315 =item * field: the name of a SQL column in table items
2317 =item * query: the value to search in this column
2319 =item * operator: comparison operator. Can be one of = != > < >= <= like
2321 =back
2323 A combination of filters hash the following keys:
2325 =over 2
2327 =item * conjunction: 'AND' or 'OR'
2329 =item * filters: array ref of filters
2331 =back
2333 $params is a reference to a hash that can contain the following parameters:
2335 =over 2
2337 =item * rows: Number of items to return. 0 returns everything (default: 0)
2339 =item * page: Page to return (return items from (page-1)*rows to (page*rows)-1)
2340 (default: 1)
2342 =item * sortby: A SQL column name in items table to sort on
2344 =item * sortorder: 'ASC' or 'DESC'
2346 =back
2348 =cut
2350 sub SearchItems {
2351 my ($filter, $params) = @_;
2353 $filter //= {};
2354 $params //= {};
2355 return unless ref $filter eq 'HASH';
2356 return unless ref $params eq 'HASH';
2358 # Default parameters
2359 $params->{rows} ||= 0;
2360 $params->{page} ||= 1;
2361 $params->{sortby} ||= 'itemnumber';
2362 $params->{sortorder} ||= 'ASC';
2364 my ($where_str, @where_args);
2365 my $where_fragment = _SearchItems_build_where_fragment($filter);
2366 if ($where_fragment) {
2367 $where_str = $where_fragment->{str};
2368 @where_args = @{ $where_fragment->{args} };
2371 my $dbh = C4::Context->dbh;
2372 my $query = q{
2373 SELECT SQL_CALC_FOUND_ROWS items.*
2374 FROM items
2375 LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
2376 LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber
2377 LEFT JOIN biblio_metadata ON biblio_metadata.biblionumber = biblio.biblionumber
2378 WHERE 1
2380 if (defined $where_str and $where_str ne '') {
2381 $query .= qq{ AND $where_str };
2384 $query .= q{ AND biblio_metadata.format = 'marcxml' AND biblio_metadata.marcflavour = ? };
2385 push @where_args, C4::Context->preference('marcflavour');
2387 my @columns = Koha::Database->new()->schema()->resultset('Item')->result_source->columns;
2388 push @columns, Koha::Database->new()->schema()->resultset('Biblio')->result_source->columns;
2389 push @columns, Koha::Database->new()->schema()->resultset('Biblioitem')->result_source->columns;
2390 my $sortby = (0 < grep {$params->{sortby} eq $_} @columns)
2391 ? $params->{sortby} : 'itemnumber';
2392 my $sortorder = (uc($params->{sortorder}) eq 'ASC') ? 'ASC' : 'DESC';
2393 $query .= qq{ ORDER BY $sortby $sortorder };
2395 my $rows = $params->{rows};
2396 my @limit_args;
2397 if ($rows > 0) {
2398 my $offset = $rows * ($params->{page}-1);
2399 $query .= qq { LIMIT ?, ? };
2400 push @limit_args, $offset, $rows;
2403 my $sth = $dbh->prepare($query);
2404 my $rv = $sth->execute(@where_args, @limit_args);
2406 return unless ($rv);
2407 my ($total_rows) = $dbh->selectrow_array(q{ SELECT FOUND_ROWS() });
2409 return ($sth->fetchall_arrayref({}), $total_rows);
2413 =head1 OTHER FUNCTIONS
2415 =head2 _find_value
2417 ($indicators, $value) = _find_value($tag, $subfield, $record,$encoding);
2419 Find the given $subfield in the given $tag in the given
2420 MARC::Record $record. If the subfield is found, returns
2421 the (indicators, value) pair; otherwise, (undef, undef) is
2422 returned.
2424 PROPOSITION :
2425 Such a function is used in addbiblio AND additem and serial-edit and maybe could be used in Authorities.
2426 I suggest we export it from this module.
2428 =cut
2430 sub _find_value {
2431 my ( $tagfield, $insubfield, $record, $encoding ) = @_;
2432 my @result;
2433 my $indicator;
2434 if ( $tagfield < 10 ) {
2435 if ( $record->field($tagfield) ) {
2436 push @result, $record->field($tagfield)->data();
2437 } else {
2438 push @result, "";
2440 } else {
2441 foreach my $field ( $record->field($tagfield) ) {
2442 my @subfields = $field->subfields();
2443 foreach my $subfield (@subfields) {
2444 if ( @$subfield[0] eq $insubfield ) {
2445 push @result, @$subfield[1];
2446 $indicator = $field->indicator(1) . $field->indicator(2);
2451 return ( $indicator, @result );
2455 =head2 PrepareItemrecordDisplay
2457 PrepareItemrecordDisplay($itemrecord,$bibnum,$itemumber,$frameworkcode);
2459 Returns a hash with all the fields for Display a given item data in a template
2461 The $frameworkcode returns the item for the given frameworkcode, ONLY if bibnum is not provided
2463 =cut
2465 sub PrepareItemrecordDisplay {
2467 my ( $bibnum, $itemnum, $defaultvalues, $frameworkcode ) = @_;
2469 my $dbh = C4::Context->dbh;
2470 $frameworkcode = C4::Biblio::GetFrameworkCode($bibnum) if $bibnum;
2471 my ( $itemtagfield, $itemtagsubfield ) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber", $frameworkcode );
2473 # Note: $tagslib obtained from GetMarcStructure() in 'unsafe' mode is
2474 # a shared data structure. No plugin (including custom ones) should change
2475 # its contents. See also GetMarcStructure.
2476 my $tagslib = &GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } );
2478 # return nothing if we don't have found an existing framework.
2479 return q{} unless $tagslib;
2480 my $itemrecord;
2481 if ($itemnum) {
2482 $itemrecord = C4::Items::GetMarcItem( $bibnum, $itemnum );
2484 my @loop_data;
2486 my $branch_limit = C4::Context->userenv ? C4::Context->userenv->{"branch"} : "";
2487 my $query = qq{
2488 SELECT authorised_value,lib FROM authorised_values
2490 $query .= qq{
2491 LEFT JOIN authorised_values_branches ON ( id = av_id )
2492 } if $branch_limit;
2493 $query .= qq{
2494 WHERE category = ?
2496 $query .= qq{ AND ( branchcode = ? OR branchcode IS NULL )} if $branch_limit;
2497 $query .= qq{ ORDER BY lib};
2498 my $authorised_values_sth = $dbh->prepare( $query );
2499 foreach my $tag ( sort keys %{$tagslib} ) {
2500 if ( $tag ne '' ) {
2502 # loop through each subfield
2503 my $cntsubf;
2504 foreach my $subfield ( sort keys %{ $tagslib->{$tag} } ) {
2505 next if IsMarcStructureInternal($tagslib->{$tag}{$subfield});
2506 next unless ( $tagslib->{$tag}->{$subfield}->{'tab'} );
2507 next if ( $tagslib->{$tag}->{$subfield}->{'tab'} ne "10" );
2508 my %subfield_data;
2509 $subfield_data{tag} = $tag;
2510 $subfield_data{subfield} = $subfield;
2511 $subfield_data{countsubfield} = $cntsubf++;
2512 $subfield_data{kohafield} = $tagslib->{$tag}->{$subfield}->{'kohafield'};
2513 $subfield_data{id} = "tag_".$tag."_subfield_".$subfield."_".int(rand(1000000));
2515 # $subfield_data{marc_lib}=$tagslib->{$tag}->{$subfield}->{lib};
2516 $subfield_data{marc_lib} = $tagslib->{$tag}->{$subfield}->{lib};
2517 $subfield_data{mandatory} = $tagslib->{$tag}->{$subfield}->{mandatory};
2518 $subfield_data{repeatable} = $tagslib->{$tag}->{$subfield}->{repeatable};
2519 $subfield_data{hidden} = "display:none"
2520 if ( ( $tagslib->{$tag}->{$subfield}->{hidden} > 4 )
2521 || ( $tagslib->{$tag}->{$subfield}->{hidden} < -4 ) );
2522 my ( $x, $defaultvalue );
2523 if ($itemrecord) {
2524 ( $x, $defaultvalue ) = _find_value( $tag, $subfield, $itemrecord );
2526 $defaultvalue = $tagslib->{$tag}->{$subfield}->{defaultvalue} unless $defaultvalue;
2527 if ( !defined $defaultvalue ) {
2528 $defaultvalue = q||;
2529 } else {
2530 $defaultvalue =~ s/"/&quot;/g;
2533 # search for itemcallnumber if applicable
2534 if ( $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.itemcallnumber'
2535 && C4::Context->preference('itemcallnumber') ) {
2536 my $CNtag = substr( C4::Context->preference('itemcallnumber'), 0, 3 );
2537 my $CNsubfield = substr( C4::Context->preference('itemcallnumber'), 3, 1 );
2538 if ( $itemrecord and my $field = $itemrecord->field($CNtag) ) {
2539 $defaultvalue = $field->subfield($CNsubfield);
2542 if ( $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.itemcallnumber'
2543 && $defaultvalues
2544 && $defaultvalues->{'callnumber'} ) {
2545 if( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield) ){
2546 # if the item record exists, only use default value if the item has no callnumber
2547 $defaultvalue = $defaultvalues->{callnumber};
2548 } elsif ( !$itemrecord and $defaultvalues ) {
2549 # if the item record *doesn't* exists, always use the default value
2550 $defaultvalue = $defaultvalues->{callnumber};
2553 if ( ( $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.holdingbranch' || $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.homebranch' )
2554 && $defaultvalues
2555 && $defaultvalues->{'branchcode'} ) {
2556 if ( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield) ) {
2557 $defaultvalue = $defaultvalues->{branchcode};
2560 if ( ( $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.location' )
2561 && $defaultvalues
2562 && $defaultvalues->{'location'} ) {
2564 if ( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield) ) {
2565 # if the item record exists, only use default value if the item has no locationr
2566 $defaultvalue = $defaultvalues->{location};
2567 } elsif ( !$itemrecord and $defaultvalues ) {
2568 # if the item record *doesn't* exists, always use the default value
2569 $defaultvalue = $defaultvalues->{location};
2572 if ( $tagslib->{$tag}->{$subfield}->{authorised_value} ) {
2573 my @authorised_values;
2574 my %authorised_lib;
2576 # builds list, depending on authorised value...
2577 #---- branch
2578 if ( $tagslib->{$tag}->{$subfield}->{'authorised_value'} eq "branches" ) {
2579 if ( ( C4::Context->preference("IndependentBranches") )
2580 && !C4::Context->IsSuperLibrarian() ) {
2581 my $sth = $dbh->prepare( "SELECT branchcode,branchname FROM branches WHERE branchcode = ? ORDER BY branchname" );
2582 $sth->execute( C4::Context->userenv->{branch} );
2583 push @authorised_values, ""
2584 unless ( $tagslib->{$tag}->{$subfield}->{mandatory} );
2585 while ( my ( $branchcode, $branchname ) = $sth->fetchrow_array ) {
2586 push @authorised_values, $branchcode;
2587 $authorised_lib{$branchcode} = $branchname;
2589 } else {
2590 my $sth = $dbh->prepare( "SELECT branchcode,branchname FROM branches ORDER BY branchname" );
2591 $sth->execute;
2592 push @authorised_values, ""
2593 unless ( $tagslib->{$tag}->{$subfield}->{mandatory} );
2594 while ( my ( $branchcode, $branchname ) = $sth->fetchrow_array ) {
2595 push @authorised_values, $branchcode;
2596 $authorised_lib{$branchcode} = $branchname;
2600 $defaultvalue = C4::Context->userenv ? C4::Context->userenv->{branch} : undef;
2601 if ( $defaultvalues and $defaultvalues->{branchcode} ) {
2602 $defaultvalue = $defaultvalues->{branchcode};
2605 #----- itemtypes
2606 } elsif ( $tagslib->{$tag}->{$subfield}->{authorised_value} eq "itemtypes" ) {
2607 my $itemtypes = Koha::ItemTypes->search_with_localization;
2608 push @authorised_values, ""
2609 unless ( $tagslib->{$tag}->{$subfield}->{mandatory} );
2610 while ( my $itemtype = $itemtypes->next ) {
2611 push @authorised_values, $itemtype->itemtype;
2612 $authorised_lib{$itemtype->itemtype} = $itemtype->translated_description;
2614 if ($defaultvalues && $defaultvalues->{'itemtype'}) {
2615 $defaultvalue = $defaultvalues->{'itemtype'};
2618 #---- class_sources
2619 } elsif ( $tagslib->{$tag}->{$subfield}->{authorised_value} eq "cn_source" ) {
2620 push @authorised_values, "" unless ( $tagslib->{$tag}->{$subfield}->{mandatory} );
2622 my $class_sources = GetClassSources();
2623 my $default_source = C4::Context->preference("DefaultClassificationSource");
2625 foreach my $class_source (sort keys %$class_sources) {
2626 next unless $class_sources->{$class_source}->{'used'} or
2627 ($class_source eq $default_source);
2628 push @authorised_values, $class_source;
2629 $authorised_lib{$class_source} = $class_sources->{$class_source}->{'description'};
2632 $defaultvalue = $default_source;
2634 #---- "true" authorised value
2635 } else {
2636 $authorised_values_sth->execute(
2637 $tagslib->{$tag}->{$subfield}->{authorised_value},
2638 $branch_limit ? $branch_limit : ()
2640 push @authorised_values, ""
2641 unless ( $tagslib->{$tag}->{$subfield}->{mandatory} );
2642 while ( my ( $value, $lib ) = $authorised_values_sth->fetchrow_array ) {
2643 push @authorised_values, $value;
2644 $authorised_lib{$value} = $lib;
2647 $subfield_data{marc_value} = {
2648 type => 'select',
2649 values => \@authorised_values,
2650 default => "$defaultvalue",
2651 labels => \%authorised_lib,
2653 } elsif ( $tagslib->{$tag}->{$subfield}->{value_builder} ) {
2654 # it is a plugin
2655 require Koha::FrameworkPlugin;
2656 my $plugin = Koha::FrameworkPlugin->new({
2657 name => $tagslib->{$tag}->{$subfield}->{value_builder},
2658 item_style => 1,
2660 my $pars = { dbh => $dbh, record => undef, tagslib =>$tagslib, id => $subfield_data{id}, tabloop => undef };
2661 $plugin->build( $pars );
2662 if ( $itemrecord and my $field = $itemrecord->field($tag) ) {
2663 $defaultvalue = $field->subfield($subfield);
2665 if( !$plugin->errstr ) {
2666 #TODO Move html to template; see report 12176/13397
2667 my $tab= $plugin->noclick? '-1': '';
2668 my $class= $plugin->noclick? ' disabled': '';
2669 my $title= $plugin->noclick? 'No popup': 'Tag editor';
2670 $subfield_data{marc_value} = qq[<input type="text" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="255" value="$defaultvalue" /><a href="#" id="buttonDot_$subfield_data{id}" tabindex="$tab" class="buttonDot $class" title="$title">...</a>\n].$plugin->javascript;
2671 } else {
2672 warn $plugin->errstr;
2673 $subfield_data{marc_value} = qq(<input type="text" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="255" value="$defaultvalue" />); # supply default input form
2676 elsif ( $tag eq '' ) { # it's an hidden field
2677 $subfield_data{marc_value} = qq(<input type="hidden" tabindex="1" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="255" value="$defaultvalue" />);
2679 elsif ( $tagslib->{$tag}->{$subfield}->{'hidden'} ) { # FIXME: shouldn't input type be "hidden" ?
2680 $subfield_data{marc_value} = qq(<input type="text" tabindex="1" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="255" value="$defaultvalue" />);
2682 elsif ( length($defaultvalue) > 100
2683 or (C4::Context->preference("marcflavour") eq "UNIMARC" and
2684 300 <= $tag && $tag < 400 && $subfield eq 'a' )
2685 or (C4::Context->preference("marcflavour") eq "MARC21" and
2686 500 <= $tag && $tag < 600 )
2688 # oversize field (textarea)
2689 $subfield_data{marc_value} = qq(<textarea tabindex="1" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="255">$defaultvalue</textarea>\n");
2690 } else {
2691 $subfield_data{marc_value} = "<input type=\"text\" name=\"field_value\" value=\"$defaultvalue\" size=\"50\" maxlength=\"255\" />";
2693 push( @loop_data, \%subfield_data );
2697 my $itemnumber;
2698 if ( $itemrecord && $itemrecord->field($itemtagfield) ) {
2699 $itemnumber = $itemrecord->subfield( $itemtagfield, $itemtagsubfield );
2701 return {
2702 'itemtagfield' => $itemtagfield,
2703 'itemtagsubfield' => $itemtagsubfield,
2704 'itemnumber' => $itemnumber,
2705 'iteminformation' => \@loop_data
2709 sub ToggleNewStatus {
2710 my ( $params ) = @_;
2711 my @rules = @{ $params->{rules} };
2712 my $report_only = $params->{report_only};
2714 my $dbh = C4::Context->dbh;
2715 my @errors;
2716 my @item_columns = map { "items.$_" } Koha::Items->columns;
2717 my @biblioitem_columns = map { "biblioitems.$_" } Koha::Biblioitems->columns;
2718 my $report;
2719 for my $rule ( @rules ) {
2720 my $age = $rule->{age};
2721 my $conditions = $rule->{conditions};
2722 my $substitutions = $rule->{substitutions};
2723 my @params;
2725 my $query = q|
2726 SELECT items.biblionumber, items.itemnumber
2727 FROM items
2728 LEFT JOIN biblioitems ON biblioitems.biblionumber = items.biblionumber
2729 WHERE 1
2731 for my $condition ( @$conditions ) {
2732 if (
2733 grep {/^$condition->{field}$/} @item_columns
2734 or grep {/^$condition->{field}$/} @biblioitem_columns
2736 if ( $condition->{value} =~ /\|/ ) {
2737 my @values = split /\|/, $condition->{value};
2738 $query .= qq| AND $condition->{field} IN (|
2739 . join( ',', ('?') x scalar @values )
2740 . q|)|;
2741 push @params, @values;
2742 } else {
2743 $query .= qq| AND $condition->{field} = ?|;
2744 push @params, $condition->{value};
2748 if ( defined $age ) {
2749 $query .= q| AND TO_DAYS(NOW()) - TO_DAYS(dateaccessioned) >= ? |;
2750 push @params, $age;
2752 my $sth = $dbh->prepare($query);
2753 $sth->execute( @params );
2754 while ( my $values = $sth->fetchrow_hashref ) {
2755 my $biblionumber = $values->{biblionumber};
2756 my $itemnumber = $values->{itemnumber};
2757 my $item = C4::Items::GetItem( $itemnumber );
2758 for my $substitution ( @$substitutions ) {
2759 next unless $substitution->{field};
2760 C4::Items::ModItem( {$substitution->{field} => $substitution->{value}}, $biblionumber, $itemnumber )
2761 unless $report_only;
2762 push @{ $report->{$itemnumber} }, $substitution;
2767 return $report;