Bug 23463: Fix safe_return return value in deletion tools
[srvgit] / C4 / Items.pm
1 package C4::Items;
2
3 # Copyright 2007 LibLime, Inc.
4 # Parts Copyright Biblibre 2010
5 #
6 # This file is part of Koha.
7 #
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.
12 #
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.
17 #
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>.
20
21 use Modern::Perl;
22
23 use vars qw(@ISA @EXPORT);
24 BEGIN {
25     require Exporter;
26     @ISA = qw(Exporter);
27
28     @EXPORT = qw(
29         AddItemFromMarc
30         AddItemBatchFromMarc
31         ModItemFromMarc
32         Item2Marc
33         ModDateLastSeen
34         ModItemTransfer
35         CheckItemPreSave
36         GetItemsForInventory
37         GetItemsInfo
38         GetItemsLocationInfo
39         GetHostItemsInfo
40         get_hostitemnumbers_of
41         GetHiddenItemnumbers
42         MoveItemFromBiblio
43         CartToShelf
44         GetAnalyticsCount
45         SearchItems
46         PrepareItemrecordDisplay
47     );
48 }
49
50 use Carp;
51 use Try::Tiny;
52 use C4::Context;
53 use C4::Koha;
54 use C4::Biblio;
55 use Koha::DateUtils;
56 use MARC::Record;
57 use C4::ClassSource;
58 use C4::Log;
59 use List::MoreUtils qw(any);
60 use YAML qw(Load);
61 use DateTime::Format::MySQL;
62 use Data::Dumper; # used as part of logging item record changes, not just for
63                   # debugging; so please don't remove this
64
65 use Koha::AuthorisedValues;
66 use Koha::DateUtils qw(dt_from_string);
67 use Koha::Database;
68
69 use Koha::Biblioitems;
70 use Koha::Items;
71 use Koha::ItemTypes;
72 use Koha::SearchEngine;
73 use Koha::SearchEngine::Search;
74 use Koha::Libraries;
75
76 =head1 NAME
77
78 C4::Items - item management functions
79
80 =head1 DESCRIPTION
81
82 This module contains an API for manipulating item 
83 records in Koha, and is used by cataloguing, circulation,
84 acquisitions, and serials management.
85
86 # FIXME This POD is not up-to-date
87 A Koha item record is stored in two places: the
88 items table and embedded in a MARC tag in the XML
89 version of the associated bib record in C<biblioitems.marcxml>.
90 This is done to allow the item information to be readily
91 indexed (e.g., by Zebra), but means that each item
92 modification transaction must keep the items table
93 and the MARC XML in sync at all times.
94
95 The items table will be considered authoritative.  In other
96 words, if there is ever a discrepancy between the items
97 table and the MARC XML, the items table should be considered
98 accurate.
99
100 =head1 HISTORICAL NOTE
101
102 Most of the functions in C<C4::Items> were originally in
103 the C<C4::Biblio> module.
104
105 =head1 CORE EXPORTED FUNCTIONS
106
107 The following functions are meant for use by users
108 of C<C4::Items>
109
110 =cut
111
112 =head2 CartToShelf
113
114   CartToShelf($itemnumber);
115
116 Set the current shelving location of the item record
117 to its stored permanent shelving location.  This is
118 primarily used to indicate when an item whose current
119 location is a special processing ('PROC') or shelving cart
120 ('CART') location is back in the stacks.
121
122 =cut
123
124 sub CartToShelf {
125     my ( $itemnumber ) = @_;
126
127     unless ( $itemnumber ) {
128         croak "FAILED CartToShelf() - no itemnumber supplied";
129     }
130
131     my $item = Koha::Items->find($itemnumber);
132     if ( $item->location eq 'CART' ) {
133         $item->location($item->permanent_location)->store;
134     }
135 }
136
137 =head2 AddItemFromMarc
138
139   my ($biblionumber, $biblioitemnumber, $itemnumber) 
140       = AddItemFromMarc($source_item_marc, $biblionumber);
141
142 Given a MARC::Record object containing an embedded item
143 record and a biblionumber, create a new item record.
144
145 =cut
146
147 sub AddItemFromMarc {
148     my ( $source_item_marc, $biblionumber ) = @_;
149     my $dbh = C4::Context->dbh;
150
151     # parse item hash from MARC
152     my $frameworkcode = C4::Biblio::GetFrameworkCode($biblionumber);
153     my ( $itemtag, $itemsubfield ) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
154
155     my $localitemmarc = MARC::Record->new;
156     $localitemmarc->append_fields( $source_item_marc->field($itemtag) );
157
158     my $item_values = C4::Biblio::TransformMarcToKoha( $localitemmarc, $frameworkcode, 'items' );
159     my $unlinked_item_subfields = _get_unlinked_item_subfields( $localitemmarc, $frameworkcode );
160     $item_values->{more_subfields_xml} = _get_unlinked_subfields_xml($unlinked_item_subfields);
161     $item_values->{biblionumber} = $biblionumber;
162     $item_values->{cn_source} = delete $item_values->{'items.cn_source'}; # Because of C4::Biblio::_disambiguate
163     my $item = Koha::Item->new( $item_values )->store;
164     return ( $item->biblionumber, $item->biblioitemnumber, $item->itemnumber );
165 }
166
167 =head2 AddItemBatchFromMarc
168
169   ($itemnumber_ref, $error_ref) = AddItemBatchFromMarc($record, 
170              $biblionumber, $biblioitemnumber, $frameworkcode);
171
172 Efficiently create item records from a MARC biblio record with
173 embedded item fields.  This routine is suitable for batch jobs.
174
175 This API assumes that the bib record has already been
176 saved to the C<biblio> and C<biblioitems> tables.  It does
177 not expect that C<biblio_metadata.metadata> is populated, but it
178 will do so via a call to ModBibiloMarc.
179
180 The goal of this API is to have a similar effect to using AddBiblio
181 and AddItems in succession, but without inefficient repeated
182 parsing of the MARC XML bib record.
183
184 This function returns an arrayref of new itemsnumbers and an arrayref of item
185 errors encountered during the processing.  Each entry in the errors
186 list is a hashref containing the following keys:
187
188 =over
189
190 =item item_sequence
191
192 Sequence number of original item tag in the MARC record.
193
194 =item item_barcode
195
196 Item barcode, provide to assist in the construction of
197 useful error messages.
198
199 =item error_code
200
201 Code representing the error condition.  Can be 'duplicate_barcode',
202 'invalid_homebranch', or 'invalid_holdingbranch'.
203
204 =item error_information
205
206 Additional information appropriate to the error condition.
207
208 =back
209
210 =cut
211
212 sub AddItemBatchFromMarc {
213     my ($record, $biblionumber, $biblioitemnumber, $frameworkcode) = @_;
214     my $error;
215     my @itemnumbers = ();
216     my @errors = ();
217     my $dbh = C4::Context->dbh;
218
219     # We modify the record, so lets work on a clone so we don't change the
220     # original.
221     $record = $record->clone();
222     # loop through the item tags and start creating items
223     my @bad_item_fields = ();
224     my ($itemtag, $itemsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
225     my $item_sequence_num = 0;
226     ITEMFIELD: foreach my $item_field ($record->field($itemtag)) {
227         $item_sequence_num++;
228         # we take the item field and stick it into a new
229         # MARC record -- this is required so far because (FIXME)
230         # TransformMarcToKoha requires a MARC::Record, not a MARC::Field
231         # and there is no TransformMarcFieldToKoha
232         my $temp_item_marc = MARC::Record->new();
233         $temp_item_marc->append_fields($item_field);
234     
235         # add biblionumber and biblioitemnumber
236         my $item = TransformMarcToKoha( $temp_item_marc, $frameworkcode, 'items' );
237         my $unlinked_item_subfields = _get_unlinked_item_subfields($temp_item_marc, $frameworkcode);
238         $item->{'more_subfields_xml'} = _get_unlinked_subfields_xml($unlinked_item_subfields);
239         $item->{'biblionumber'} = $biblionumber;
240         $item->{'biblioitemnumber'} = $biblioitemnumber;
241         $item->{cn_source} = delete $item->{'items.cn_source'}; # Because of C4::Biblio::_disambiguate
242
243         # check for duplicate barcode
244         my %item_errors = CheckItemPreSave($item);
245         if (%item_errors) {
246             push @errors, _repack_item_errors($item_sequence_num, $item, \%item_errors);
247             push @bad_item_fields, $item_field;
248             next ITEMFIELD;
249         }
250
251         my $item_object = Koha::Item->new($item)->store;
252         push @itemnumbers, $item_object->itemnumber; # FIXME not checking error
253
254         logaction("CATALOGUING", "ADD", $item->itemnumber, "item") if C4::Context->preference("CataloguingLog");
255
256         my $new_item_marc = _marc_from_item_hash($item->unblessed, $frameworkcode, $unlinked_item_subfields);
257         $item_field->replace_with($new_item_marc->field($itemtag));
258     }
259
260     # remove any MARC item fields for rejected items
261     foreach my $item_field (@bad_item_fields) {
262         $record->delete_field($item_field);
263     }
264
265     # update the MARC biblio
266  #   $biblionumber = ModBiblioMarc( $record, $biblionumber, $frameworkcode );
267
268     return (\@itemnumbers, \@errors);
269 }
270
271 sub ModItemFromMarc {
272     my $item_marc = shift;
273     my $biblionumber = shift;
274     my $itemnumber = shift;
275
276     my $frameworkcode = C4::Biblio::GetFrameworkCode($biblionumber);
277     my ( $itemtag, $itemsubfield ) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
278
279     my $localitemmarc = MARC::Record->new;
280     $localitemmarc->append_fields( $item_marc->field($itemtag) );
281     my $item_object = Koha::Items->find($itemnumber);
282     my $item = TransformMarcToKoha( $localitemmarc, $frameworkcode, 'items' );
283     $item->{cn_source} = delete $item->{'items.cn_source'}; # Because of C4::Biblio::_disambiguate
284     $item_object->set($item);
285     my $unlinked_item_subfields = _get_unlinked_item_subfields( $localitemmarc, $frameworkcode );
286     $item_object->more_subfields_xml(_get_unlinked_subfields_xml($unlinked_item_subfields))->store;
287     $item_object->store;
288
289     return $item_object->unblessed;
290 }
291
292 =head2 ModItemTransfer
293
294   ModItemTransfer($itemnumber, $frombranch, $tobranch, $trigger);
295
296 Marks an item as being transferred from one branch to another and records the trigger.
297
298 =cut
299
300 sub ModItemTransfer {
301     my ( $itemnumber, $frombranch, $tobranch, $trigger ) = @_;
302
303     my $dbh = C4::Context->dbh;
304     my $item = Koha::Items->find( $itemnumber );
305
306     # Remove the 'shelving cart' location status if it is being used.
307     CartToShelf( $itemnumber ) if $item->location && $item->location eq 'CART' && ( !$item->permanent_location || $item->permanent_location ne 'CART' );
308
309     $dbh->do("UPDATE branchtransfers SET datearrived = NOW(), comments = ? WHERE itemnumber = ? AND datearrived IS NULL", undef, "Canceled, new transfer from $frombranch to $tobranch created", $itemnumber);
310
311     #new entry in branchtransfers....
312     my $sth = $dbh->prepare(
313         "INSERT INTO branchtransfers (itemnumber, frombranch, datesent, tobranch, reason)
314         VALUES (?, ?, NOW(), ?, ?)");
315     $sth->execute($itemnumber, $frombranch, $tobranch, $trigger);
316
317     # FIXME we are fetching the item twice in the 2 next statements!
318     Koha::Items->find($itemnumber)->holdingbranch($frombranch)->store({ log_action => 0 });
319     ModDateLastSeen($itemnumber);
320     return;
321 }
322
323 =head2 ModDateLastSeen
324
325 ModDateLastSeen( $itemnumber, $leave_item_lost );
326
327 Mark item as seen. Is called when an item is issued, returned or manually marked during inventory/stocktaking.
328 C<$itemnumber> is the item number
329 C<$leave_item_lost> determines if a lost item will be found or remain lost
330
331 =cut
332
333 sub ModDateLastSeen {
334     my ( $itemnumber, $leave_item_lost ) = @_;
335
336     my $today = output_pref({ dt => dt_from_string, dateformat => 'iso', dateonly => 1 });
337
338     my $item = Koha::Items->find($itemnumber);
339     $item->datelastseen($today);
340     $item->itemlost(0) unless $leave_item_lost;
341     $item->store({ log_action => 0 });
342 }
343
344 =head2 CheckItemPreSave
345
346     my $item_ref = TransformMarcToKoha($marc, 'items');
347     # do stuff
348     my %errors = CheckItemPreSave($item_ref);
349     if (exists $errors{'duplicate_barcode'}) {
350         print "item has duplicate barcode: ", $errors{'duplicate_barcode'}, "\n";
351     } elsif (exists $errors{'invalid_homebranch'}) {
352         print "item has invalid home branch: ", $errors{'invalid_homebranch'}, "\n";
353     } elsif (exists $errors{'invalid_holdingbranch'}) {
354         print "item has invalid holding branch: ", $errors{'invalid_holdingbranch'}, "\n";
355     } else {
356         print "item is OK";
357     }
358
359 Given a hashref containing item fields, determine if it can be
360 inserted or updated in the database.  Specifically, checks for
361 database integrity issues, and returns a hash containing any
362 of the following keys, if applicable.
363
364 =over 2
365
366 =item duplicate_barcode
367
368 Barcode, if it duplicates one already found in the database.
369
370 =item invalid_homebranch
371
372 Home branch, if not defined in branches table.
373
374 =item invalid_holdingbranch
375
376 Holding branch, if not defined in branches table.
377
378 =back
379
380 This function does NOT implement any policy-related checks,
381 e.g., whether current operator is allowed to save an
382 item that has a given branch code.
383
384 =cut
385
386 sub CheckItemPreSave {
387     my $item_ref = shift;
388
389     my %errors = ();
390
391     # check for duplicate barcode
392     if (exists $item_ref->{'barcode'} and defined $item_ref->{'barcode'}) {
393         my $existing_item= Koha::Items->find({barcode => $item_ref->{'barcode'}});
394         if ($existing_item) {
395             if (!exists $item_ref->{'itemnumber'}                       # new item
396                 or $item_ref->{'itemnumber'} != $existing_item->itemnumber) { # existing item
397                 $errors{'duplicate_barcode'} = $item_ref->{'barcode'};
398             }
399         }
400     }
401
402     # check for valid home branch
403     if (exists $item_ref->{'homebranch'} and defined $item_ref->{'homebranch'}) {
404         my $home_library = Koha::Libraries->find( $item_ref->{homebranch} );
405         unless (defined $home_library) {
406             $errors{'invalid_homebranch'} = $item_ref->{'homebranch'};
407         }
408     }
409
410     # check for valid holding branch
411     if (exists $item_ref->{'holdingbranch'} and defined $item_ref->{'holdingbranch'}) {
412         my $holding_library = Koha::Libraries->find( $item_ref->{holdingbranch} );
413         unless (defined $holding_library) {
414             $errors{'invalid_holdingbranch'} = $item_ref->{'holdingbranch'};
415         }
416     }
417
418     return %errors;
419
420 }
421
422 =head1 EXPORTED SPECIAL ACCESSOR FUNCTIONS
423
424 The following functions provide various ways of 
425 getting an item record, a set of item records, or
426 lists of authorized values for certain item fields.
427
428 =cut
429
430 =head2 GetItemsForInventory
431
432 ($itemlist, $iTotalRecords) = GetItemsForInventory( {
433   minlocation  => $minlocation,
434   maxlocation  => $maxlocation,
435   location     => $location,
436   itemtype     => $itemtype,
437   ignoreissued => $ignoreissued,
438   datelastseen => $datelastseen,
439   branchcode   => $branchcode,
440   branch       => $branch,
441   offset       => $offset,
442   size         => $size,
443   statushash   => $statushash,
444 } );
445
446 Retrieve a list of title/authors/barcode/callnumber, for biblio inventory.
447
448 The sub returns a reference to a list of hashes, each containing
449 itemnumber, author, title, barcode, item callnumber, and date last
450 seen. It is ordered by callnumber then title.
451
452 The required minlocation & maxlocation parameters are used to specify a range of item callnumbers
453 the datelastseen can be used to specify that you want to see items not seen since a past date only.
454 offset & size can be used to retrieve only a part of the whole listing (defaut behaviour)
455 $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.
456
457 $iTotalRecords is the number of rows that would have been returned without the $offset, $size limit clause
458
459 =cut
460
461 sub GetItemsForInventory {
462     my ( $parameters ) = @_;
463     my $minlocation  = $parameters->{'minlocation'}  // '';
464     my $maxlocation  = $parameters->{'maxlocation'}  // '';
465     my $class_source = $parameters->{'class_source'}  // C4::Context->preference('DefaultClassificationSource');
466     my $location     = $parameters->{'location'}     // '';
467     my $itemtype     = $parameters->{'itemtype'}     // '';
468     my $ignoreissued = $parameters->{'ignoreissued'} // '';
469     my $datelastseen = $parameters->{'datelastseen'} // '';
470     my $branchcode   = $parameters->{'branchcode'}   // '';
471     my $branch       = $parameters->{'branch'}       // '';
472     my $offset       = $parameters->{'offset'}       // '';
473     my $size         = $parameters->{'size'}         // '';
474     my $statushash   = $parameters->{'statushash'}   // '';
475     my $ignore_waiting_holds = $parameters->{'ignore_waiting_holds'} // '';
476
477     my $dbh = C4::Context->dbh;
478     my ( @bind_params, @where_strings );
479
480     my $min_cnsort = GetClassSort($class_source,undef,$minlocation);
481     my $max_cnsort = GetClassSort($class_source,undef,$maxlocation);
482
483     my $select_columns = q{
484         SELECT DISTINCT(items.itemnumber), barcode, itemcallnumber, title, author, biblio.biblionumber, biblio.frameworkcode, datelastseen, homebranch, location, notforloan, damaged, itemlost, withdrawn, stocknumber, items.cn_sort
485     };
486     my $select_count = q{SELECT COUNT(DISTINCT(items.itemnumber))};
487     my $query = q{
488         FROM items
489         LEFT JOIN biblio ON items.biblionumber = biblio.biblionumber
490         LEFT JOIN biblioitems on items.biblionumber = biblioitems.biblionumber
491     };
492     if ($statushash){
493         for my $authvfield (keys %$statushash){
494             if ( scalar @{$statushash->{$authvfield}} > 0 ){
495                 my $joinedvals = join ',', @{$statushash->{$authvfield}};
496                 push @where_strings, "$authvfield in (" . $joinedvals . ")";
497             }
498         }
499     }
500
501     if ($minlocation) {
502         push @where_strings, 'items.cn_sort >= ?';
503         push @bind_params, $min_cnsort;
504     }
505
506     if ($maxlocation) {
507         push @where_strings, 'items.cn_sort <= ?';
508         push @bind_params, $max_cnsort;
509     }
510
511     if ($datelastseen) {
512         $datelastseen = output_pref({ str => $datelastseen, dateformat => 'iso', dateonly => 1 });
513         push @where_strings, '(datelastseen < ? OR datelastseen IS NULL)';
514         push @bind_params, $datelastseen;
515     }
516
517     if ( $location ) {
518         push @where_strings, 'items.location = ?';
519         push @bind_params, $location;
520     }
521
522     if ( $branchcode ) {
523         if($branch eq "homebranch"){
524         push @where_strings, 'items.homebranch = ?';
525         }else{
526             push @where_strings, 'items.holdingbranch = ?';
527         }
528         push @bind_params, $branchcode;
529     }
530
531     if ( $itemtype ) {
532         push @where_strings, 'biblioitems.itemtype = ?';
533         push @bind_params, $itemtype;
534     }
535
536     if ( $ignoreissued) {
537         $query .= "LEFT JOIN issues ON items.itemnumber = issues.itemnumber ";
538         push @where_strings, 'issues.date_due IS NULL';
539     }
540
541     if ( $ignore_waiting_holds ) {
542         $query .= "LEFT JOIN reserves ON items.itemnumber = reserves.itemnumber ";
543         push( @where_strings, q{(reserves.found != 'W' OR reserves.found IS NULL)} );
544     }
545
546     if ( @where_strings ) {
547         $query .= 'WHERE ';
548         $query .= join ' AND ', @where_strings;
549     }
550     my $count_query = $select_count . $query;
551     $query .= ' ORDER BY items.cn_sort, itemcallnumber, title';
552     $query .= " LIMIT $offset, $size" if ($offset and $size);
553     $query = $select_columns . $query;
554     my $sth = $dbh->prepare($query);
555     $sth->execute( @bind_params );
556
557     my @results = ();
558     my $tmpresults = $sth->fetchall_arrayref({});
559     $sth = $dbh->prepare( $count_query );
560     $sth->execute( @bind_params );
561     my ($iTotalRecords) = $sth->fetchrow_array();
562
563     my @avs = Koha::AuthorisedValues->search(
564         {   'marc_subfield_structures.kohafield' => { '>' => '' },
565             'me.authorised_value'                => { '>' => '' },
566         },
567         {   join     => { category => 'marc_subfield_structures' },
568             distinct => ['marc_subfield_structures.kohafield, me.category, frameworkcode, me.authorised_value'],
569             '+select' => [ 'marc_subfield_structures.kohafield', 'marc_subfield_structures.frameworkcode', 'me.authorised_value', 'me.lib' ],
570             '+as'     => [ 'kohafield',                          'frameworkcode',                          'authorised_value',    'lib' ],
571         }
572     );
573
574     my $avmapping = { map { $_->get_column('kohafield') . ',' . $_->get_column('frameworkcode') . ',' . $_->get_column('authorised_value') => $_->get_column('lib') } @avs };
575
576     foreach my $row (@$tmpresults) {
577
578         # Auth values
579         foreach (keys %$row) {
580             if (
581                 defined(
582                     $avmapping->{ "items.$_," . $row->{'frameworkcode'} . "," . ( $row->{$_} // q{} ) }
583                 )
584             ) {
585                 $row->{$_} = $avmapping->{"items.$_,".$row->{'frameworkcode'}.",".$row->{$_}};
586             }
587         }
588         push @results, $row;
589     }
590
591     return (\@results, $iTotalRecords);
592 }
593
594 =head2 GetItemsInfo
595
596   @results = GetItemsInfo($biblionumber);
597
598 Returns information about items with the given biblionumber.
599
600 C<GetItemsInfo> returns a list of references-to-hash. Each element
601 contains a number of keys. Most of them are attributes from the
602 C<biblio>, C<biblioitems>, C<items>, and C<itemtypes> tables in the
603 Koha database. Other keys include:
604
605 =over 2
606
607 =item C<$data-E<gt>{branchname}>
608
609 The name (not the code) of the branch to which the book belongs.
610
611 =item C<$data-E<gt>{datelastseen}>
612
613 This is simply C<items.datelastseen>, except that while the date is
614 stored in YYYY-MM-DD format in the database, here it is converted to
615 DD/MM/YYYY format. A NULL date is returned as C<//>.
616
617 =item C<$data-E<gt>{datedue}>
618
619 =item C<$data-E<gt>{class}>
620
621 This is the concatenation of C<biblioitems.classification>, the book's
622 Dewey code, and C<biblioitems.subclass>.
623
624 =item C<$data-E<gt>{ocount}>
625
626 I think this is the number of copies of the book available.
627
628 =item C<$data-E<gt>{order}>
629
630 If this is set, it is set to C<One Order>.
631
632 =back
633
634 =cut
635
636 sub GetItemsInfo {
637     my ( $biblionumber ) = @_;
638     my $dbh   = C4::Context->dbh;
639     require C4::Languages;
640     my $language = C4::Languages::getlanguage();
641     my $query = "
642     SELECT items.*,
643            biblio.*,
644            biblioitems.volume,
645            biblioitems.number,
646            biblioitems.itemtype,
647            biblioitems.isbn,
648            biblioitems.issn,
649            biblioitems.publicationyear,
650            biblioitems.publishercode,
651            biblioitems.volumedate,
652            biblioitems.volumedesc,
653            biblioitems.lccn,
654            biblioitems.url,
655            items.notforloan as itemnotforloan,
656            issues.borrowernumber,
657            issues.date_due as datedue,
658            issues.onsite_checkout,
659            borrowers.cardnumber,
660            borrowers.surname,
661            borrowers.firstname,
662            borrowers.branchcode as bcode,
663            serial.serialseq,
664            serial.publisheddate,
665            itemtypes.description,
666            COALESCE( localization.translation, itemtypes.description ) AS translated_description,
667            itemtypes.notforloan as notforloan_per_itemtype,
668            holding.branchurl,
669            holding.branchcode,
670            holding.branchname,
671            holding.opac_info as holding_branch_opac_info,
672            home.opac_info as home_branch_opac_info,
673            IF(tmp_holdsqueue.itemnumber,1,0) AS has_pending_hold
674      FROM items
675      LEFT JOIN branches AS holding ON items.holdingbranch = holding.branchcode
676      LEFT JOIN branches AS home ON items.homebranch=home.branchcode
677      LEFT JOIN biblio      ON      biblio.biblionumber     = items.biblionumber
678      LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber
679      LEFT JOIN issues USING (itemnumber)
680      LEFT JOIN borrowers USING (borrowernumber)
681      LEFT JOIN serialitems USING (itemnumber)
682      LEFT JOIN serial USING (serialid)
683      LEFT JOIN itemtypes   ON   itemtypes.itemtype         = "
684      . (C4::Context->preference('item-level_itypes') ? 'items.itype' : 'biblioitems.itemtype');
685     $query .= q|
686     LEFT JOIN tmp_holdsqueue USING (itemnumber)
687     LEFT JOIN localization ON itemtypes.itemtype = localization.code
688         AND localization.entity = 'itemtypes'
689         AND localization.lang = ?
690     |;
691
692     $query .= " WHERE items.biblionumber = ? ORDER BY home.branchname, items.enumchron, LPAD( items.copynumber, 8, '0' ), items.dateaccessioned DESC" ;
693     my $sth = $dbh->prepare($query);
694     $sth->execute($language, $biblionumber);
695     my $i = 0;
696     my @results;
697     my $serial;
698
699     my $userenv = C4::Context->userenv;
700     my $want_not_same_branch = C4::Context->preference("IndependentBranches") && !C4::Context->IsSuperLibrarian();
701     while ( my $data = $sth->fetchrow_hashref ) {
702         if ( $data->{borrowernumber} && $want_not_same_branch) {
703             $data->{'NOTSAMEBRANCH'} = $data->{'bcode'} ne $userenv->{branch};
704         }
705
706         $serial ||= $data->{'serial'};
707
708         my $descriptions;
709         # get notforloan complete status if applicable
710         $descriptions = Koha::AuthorisedValues->get_description_by_koha_field({frameworkcode => $data->{frameworkcode}, kohafield => 'items.notforloan', authorised_value => $data->{itemnotforloan} });
711         $data->{notforloanvalue}     = $descriptions->{lib} // '';
712         $data->{notforloanvalueopac} = $descriptions->{opac_description} // '';
713
714         # get restricted status and description if applicable
715         $descriptions = Koha::AuthorisedValues->get_description_by_koha_field({frameworkcode => $data->{frameworkcode}, kohafield => 'items.restricted', authorised_value => $data->{restricted} });
716         $data->{restrictedvalue}     = $descriptions->{lib} // '';
717         $data->{restrictedvalueopac} = $descriptions->{opac_description} // '';
718
719         # my stack procedures
720         $descriptions = Koha::AuthorisedValues->get_description_by_koha_field({frameworkcode => $data->{frameworkcode}, kohafield => 'items.stack', authorised_value => $data->{stack} });
721         $data->{stack}          = $descriptions->{lib} // '';
722
723         # Find the last 3 people who borrowed this item.
724         my $sth2 = $dbh->prepare("SELECT * FROM old_issues,borrowers
725                                     WHERE itemnumber = ?
726                                     AND old_issues.borrowernumber = borrowers.borrowernumber
727                                     ORDER BY returndate DESC
728                                     LIMIT 3");
729         $sth2->execute($data->{'itemnumber'});
730         my $ii = 0;
731         while (my $data2 = $sth2->fetchrow_hashref()) {
732             $data->{"timestamp$ii"} = $data2->{'timestamp'} if $data2->{'timestamp'};
733             $data->{"card$ii"}      = $data2->{'cardnumber'} if $data2->{'cardnumber'};
734             $data->{"borrower$ii"}  = $data2->{'borrowernumber'} if $data2->{'borrowernumber'};
735             $ii++;
736         }
737
738         $results[$i] = $data;
739         $i++;
740     }
741
742     return $serial
743         ? sort { ($b->{'publisheddate'} || $b->{'enumchron'}) cmp ($a->{'publisheddate'} || $a->{'enumchron'}) } @results
744         : @results;
745 }
746
747 =head2 GetItemsLocationInfo
748
749   my @itemlocinfo = GetItemsLocationInfo($biblionumber);
750
751 Returns the branch names, shelving location and itemcallnumber for each item attached to the biblio in question
752
753 C<GetItemsInfo> returns a list of references-to-hash. Data returned:
754
755 =over 2
756
757 =item C<$data-E<gt>{homebranch}>
758
759 Branch Name of the item's homebranch
760
761 =item C<$data-E<gt>{holdingbranch}>
762
763 Branch Name of the item's holdingbranch
764
765 =item C<$data-E<gt>{location}>
766
767 Item's shelving location code
768
769 =item C<$data-E<gt>{location_intranet}>
770
771 The intranet description for the Shelving Location as set in authorised_values 'LOC'
772
773 =item C<$data-E<gt>{location_opac}>
774
775 The OPAC description for the Shelving Location as set in authorised_values 'LOC'.  Falls back to intranet description if no OPAC 
776 description is set.
777
778 =item C<$data-E<gt>{itemcallnumber}>
779
780 Item's itemcallnumber
781
782 =item C<$data-E<gt>{cn_sort}>
783
784 Item's call number normalized for sorting
785
786 =back
787   
788 =cut
789
790 sub GetItemsLocationInfo {
791         my $biblionumber = shift;
792         my @results;
793
794         my $dbh = C4::Context->dbh;
795         my $query = "SELECT a.branchname as homebranch, b.branchname as holdingbranch, 
796                             location, itemcallnumber, cn_sort
797                      FROM items, branches as a, branches as b
798                      WHERE homebranch = a.branchcode AND holdingbranch = b.branchcode 
799                      AND biblionumber = ?
800                      ORDER BY cn_sort ASC";
801         my $sth = $dbh->prepare($query);
802         $sth->execute($biblionumber);
803
804         while ( my $data = $sth->fetchrow_hashref ) {
805              my $av = Koha::AuthorisedValues->search({ category => 'LOC', authorised_value => $data->{location} });
806              $av = $av->count ? $av->next : undef;
807              $data->{location_intranet} = $av ? $av->lib : '';
808              $data->{location_opac}     = $av ? $av->opac_description : '';
809              push @results, $data;
810         }
811         return @results;
812 }
813
814 =head2 GetHostItemsInfo
815
816     $hostiteminfo = GetHostItemsInfo($hostfield);
817     Returns the iteminfo for items linked to records via a host field
818
819 =cut
820
821 sub GetHostItemsInfo {
822     my ($record) = @_;
823     my @returnitemsInfo;
824
825     if( !C4::Context->preference('EasyAnalyticalRecords') ) {
826         return @returnitemsInfo;
827     }
828
829     my @fields;
830     if( C4::Context->preference('marcflavour') eq 'MARC21' ||
831       C4::Context->preference('marcflavour') eq 'NORMARC') {
832         @fields = $record->field('773');
833     } elsif( C4::Context->preference('marcflavour') eq 'UNIMARC') {
834         @fields = $record->field('461');
835     }
836
837     foreach my $hostfield ( @fields ) {
838         my $hostbiblionumber = $hostfield->subfield("0");
839         my $linkeditemnumber = $hostfield->subfield("9");
840         my @hostitemInfos = GetItemsInfo($hostbiblionumber);
841         foreach my $hostitemInfo (@hostitemInfos) {
842             if( $hostitemInfo->{itemnumber} eq $linkeditemnumber ) {
843                 push @returnitemsInfo, $hostitemInfo;
844                 last;
845             }
846         }
847     }
848     return @returnitemsInfo;
849 }
850
851 =head2 get_hostitemnumbers_of
852
853   my @itemnumbers_of = get_hostitemnumbers_of($biblionumber);
854
855 Given a biblionumber, return the list of corresponding itemnumbers that are linked to it via host fields
856
857 Return a reference on a hash where key is a biblionumber and values are
858 references on array of itemnumbers.
859
860 =cut
861
862
863 sub get_hostitemnumbers_of {
864     my ($biblionumber) = @_;
865
866     if( !C4::Context->preference('EasyAnalyticalRecords') ) {
867         return ();
868     }
869
870     my $marcrecord = C4::Biblio::GetMarcBiblio({ biblionumber => $biblionumber });
871     return unless $marcrecord;
872
873     my ( @returnhostitemnumbers, $tag, $biblio_s, $item_s );
874
875     my $marcflavor = C4::Context->preference('marcflavour');
876     if ( $marcflavor eq 'MARC21' || $marcflavor eq 'NORMARC' ) {
877         $tag      = '773';
878         $biblio_s = '0';
879         $item_s   = '9';
880     }
881     elsif ( $marcflavor eq 'UNIMARC' ) {
882         $tag      = '461';
883         $biblio_s = '0';
884         $item_s   = '9';
885     }
886
887     foreach my $hostfield ( $marcrecord->field($tag) ) {
888         my $hostbiblionumber = $hostfield->subfield($biblio_s);
889         next unless $hostbiblionumber; # have tag, don't have $biblio_s subfield
890         my $linkeditemnumber = $hostfield->subfield($item_s);
891         if ( ! $linkeditemnumber ) {
892             warn "ERROR biblionumber $biblionumber has 773^0, but doesn't have 9";
893             next;
894         }
895         my $is_from_biblio = Koha::Items->search({ itemnumber => $linkeditemnumber, biblionumber => $hostbiblionumber });
896         push @returnhostitemnumbers, $linkeditemnumber
897           if $is_from_biblio;
898     }
899
900     return @returnhostitemnumbers;
901 }
902
903 =head2 GetHiddenItemnumbers
904
905     my @itemnumbers_to_hide = GetHiddenItemnumbers({ items => \@items, borcat => $category });
906
907 Given a list of items it checks which should be hidden from the OPAC given
908 the current configuration. Returns a list of itemnumbers corresponding to
909 those that should be hidden. Optionally takes a borcat parameter for certain borrower types
910 to be excluded
911
912 =cut
913
914 sub GetHiddenItemnumbers {
915     my $params = shift;
916     my $items = $params->{items};
917     if (my $exceptions = C4::Context->preference('OpacHiddenItemsExceptions') and $params->{'borcat'}){
918         foreach my $except (split(/\|/, $exceptions)){
919             if ($params->{'borcat'} eq $except){
920                 return; # we don't hide anything for this borrower category
921             }
922         }
923     }
924     my @resultitems;
925
926     my $yaml = C4::Context->preference('OpacHiddenItems');
927     return () if (! $yaml =~ /\S/ );
928     $yaml = "$yaml\n\n"; # YAML is anal on ending \n. Surplus does not hurt
929     my $hidingrules;
930     eval {
931         $hidingrules = YAML::Load($yaml);
932     };
933     if ($@) {
934         warn "Unable to parse OpacHiddenItems syspref : $@";
935         return ();
936     }
937     my $dbh = C4::Context->dbh;
938
939     # For each item
940     foreach my $item (@$items) {
941
942         # We check each rule
943         foreach my $field (keys %$hidingrules) {
944             my $val;
945             if (exists $item->{$field}) {
946                 $val = $item->{$field};
947             }
948             else {
949                 my $query = "SELECT $field from items where itemnumber = ?";
950                 $val = $dbh->selectrow_array($query, undef, $item->{'itemnumber'});
951             }
952             $val = '' unless defined $val;
953
954             # If the results matches the values in the yaml file
955             if (any { $val eq $_ } @{$hidingrules->{$field}}) {
956
957                 # We add the itemnumber to the list
958                 push @resultitems, $item->{'itemnumber'};
959
960                 # If at least one rule matched for an item, no need to test the others
961                 last;
962             }
963         }
964     }
965     return @resultitems;
966 }
967
968 =head1 LIMITED USE FUNCTIONS
969
970 The following functions, while part of the public API,
971 are not exported.  This is generally because they are
972 meant to be used by only one script for a specific
973 purpose, and should not be used in any other context
974 without careful thought.
975
976 =cut
977
978 =head2 GetMarcItem
979
980   my $item_marc = GetMarcItem($biblionumber, $itemnumber);
981
982 Returns MARC::Record of the item passed in parameter.
983 This function is meant for use only in C<cataloguing/additem.pl>,
984 where it is needed to support that script's MARC-like
985 editor.
986
987 =cut
988
989 sub GetMarcItem {
990     my ( $biblionumber, $itemnumber ) = @_;
991
992     # GetMarcItem has been revised so that it does the following:
993     #  1. Gets the item information from the items table.
994     #  2. Converts it to a MARC field for storage in the bib record.
995     #
996     # The previous behavior was:
997     #  1. Get the bib record.
998     #  2. Return the MARC tag corresponding to the item record.
999     #
1000     # The difference is that one treats the items row as authoritative,
1001     # while the other treats the MARC representation as authoritative
1002     # under certain circumstances.
1003
1004     my $item = Koha::Items->find($itemnumber) or return;
1005
1006     # Tack on 'items.' prefix to column names so that C4::Biblio::TransformKohaToMarc will work.
1007     # Also, don't emit a subfield if the underlying field is blank.
1008
1009     return Item2Marc($item->unblessed, $biblionumber);
1010
1011 }
1012 sub Item2Marc {
1013         my ($itemrecord,$biblionumber)=@_;
1014     my $mungeditem = { 
1015         map {  
1016             defined($itemrecord->{$_}) && $itemrecord->{$_} ne '' ? ("items.$_" => $itemrecord->{$_}) : ()  
1017         } keys %{ $itemrecord } 
1018     };
1019     my $framework = C4::Biblio::GetFrameworkCode( $biblionumber );
1020     my $itemmarc = C4::Biblio::TransformKohaToMarc( $mungeditem ); # Bug 21774: no_split parameter removed to allow cloned subfields
1021     my ( $itemtag, $itemsubfield ) = C4::Biblio::GetMarcFromKohaField(
1022         "items.itemnumber", $framework,
1023     );
1024
1025     my $unlinked_item_subfields = _parse_unlinked_item_subfields_from_xml($mungeditem->{'items.more_subfields_xml'});
1026     if (defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1) {
1027                 foreach my $field ($itemmarc->field($itemtag)){
1028             $field->add_subfields(@$unlinked_item_subfields);
1029         }
1030     }
1031         return $itemmarc;
1032 }
1033
1034 =head1 PRIVATE FUNCTIONS AND VARIABLES
1035
1036 The following functions are not meant to be called
1037 directly, but are documented in order to explain
1038 the inner workings of C<C4::Items>.
1039
1040 =cut
1041
1042 =head2 MoveItemFromBiblio
1043
1044   MoveItemFromBiblio($itenumber, $frombiblio, $tobiblio);
1045
1046 Moves an item from a biblio to another
1047
1048 Returns undef if the move failed or the biblionumber of the destination record otherwise
1049
1050 =cut
1051
1052 sub MoveItemFromBiblio {
1053     my ($itemnumber, $frombiblio, $tobiblio) = @_;
1054     my $dbh = C4::Context->dbh;
1055     my ( $tobiblioitem ) = $dbh->selectrow_array(q|
1056         SELECT biblioitemnumber
1057         FROM biblioitems
1058         WHERE biblionumber = ?
1059     |, undef, $tobiblio );
1060     my $return = $dbh->do(q|
1061         UPDATE items
1062         SET biblioitemnumber = ?,
1063             biblionumber = ?
1064         WHERE itemnumber = ?
1065             AND biblionumber = ?
1066     |, undef, $tobiblioitem, $tobiblio, $itemnumber, $frombiblio );
1067     if ($return == 1) {
1068         ModZebra( $tobiblio, "specialUpdate", "biblioserver" );
1069         ModZebra( $frombiblio, "specialUpdate", "biblioserver" );
1070             # Checking if the item we want to move is in an order 
1071         require C4::Acquisition;
1072         my $order = C4::Acquisition::GetOrderFromItemnumber($itemnumber);
1073             if ($order) {
1074                     # Replacing the biblionumber within the order if necessary
1075                     $order->{'biblionumber'} = $tobiblio;
1076                 C4::Acquisition::ModOrder($order);
1077             }
1078
1079         # Update reserves, hold_fill_targets, tmp_holdsqueue and linktracker tables
1080         for my $table_name ( qw( reserves hold_fill_targets tmp_holdsqueue linktracker ) ) {
1081             $dbh->do( qq|
1082                 UPDATE $table_name
1083                 SET biblionumber = ?
1084                 WHERE itemnumber = ?
1085             |, undef, $tobiblio, $itemnumber );
1086         }
1087         return $tobiblio;
1088         }
1089     return;
1090 }
1091
1092 =head2 _marc_from_item_hash
1093
1094   my $item_marc = _marc_from_item_hash($item, $frameworkcode[, $unlinked_item_subfields]);
1095
1096 Given an item hash representing a complete item record,
1097 create a C<MARC::Record> object containing an embedded
1098 tag representing that item.
1099
1100 The third, optional parameter C<$unlinked_item_subfields> is
1101 an arrayref of subfields (not mapped to C<items> fields per the
1102 framework) to be added to the MARC representation
1103 of the item.
1104
1105 =cut
1106
1107 sub _marc_from_item_hash {
1108     my $item = shift;
1109     my $frameworkcode = shift;
1110     my $unlinked_item_subfields;
1111     if (@_) {
1112         $unlinked_item_subfields = shift;
1113     }
1114    
1115     # Tack on 'items.' prefix to column names so lookup from MARC frameworks will work
1116     # Also, don't emit a subfield if the underlying field is blank.
1117     my $mungeditem = { map {  (defined($item->{$_}) and $item->{$_} ne '') ? 
1118                                 (/^items\./ ? ($_ => $item->{$_}) : ("items.$_" => $item->{$_})) 
1119                                 : ()  } keys %{ $item } }; 
1120
1121     my $item_marc = MARC::Record->new();
1122     foreach my $item_field ( keys %{$mungeditem} ) {
1123         my ( $tag, $subfield ) = C4::Biblio::GetMarcFromKohaField( $item_field );
1124         next unless defined $tag and defined $subfield;    # skip if not mapped to MARC field
1125         my @values = split(/\s?\|\s?/, $mungeditem->{$item_field}, -1);
1126         foreach my $value (@values){
1127             if ( my $field = $item_marc->field($tag) ) {
1128                     $field->add_subfields( $subfield => $value );
1129             } else {
1130                 my $add_subfields = [];
1131                 if (defined $unlinked_item_subfields and ref($unlinked_item_subfields) eq 'ARRAY' and $#$unlinked_item_subfields > -1) {
1132                     $add_subfields = $unlinked_item_subfields;
1133             }
1134             $item_marc->add_fields( $tag, " ", " ", $subfield => $value, @$add_subfields );
1135             }
1136         }
1137     }
1138
1139     return $item_marc;
1140 }
1141
1142 =head2 _repack_item_errors
1143
1144 Add an error message hash generated by C<CheckItemPreSave>
1145 to a list of errors.
1146
1147 =cut
1148
1149 sub _repack_item_errors {
1150     my $item_sequence_num = shift;
1151     my $item_ref = shift;
1152     my $error_ref = shift;
1153
1154     my @repacked_errors = ();
1155
1156     foreach my $error_code (sort keys %{ $error_ref }) {
1157         my $repacked_error = {};
1158         $repacked_error->{'item_sequence'} = $item_sequence_num;
1159         $repacked_error->{'item_barcode'} = exists($item_ref->{'barcode'}) ? $item_ref->{'barcode'} : '';
1160         $repacked_error->{'error_code'} = $error_code;
1161         $repacked_error->{'error_information'} = $error_ref->{$error_code};
1162         push @repacked_errors, $repacked_error;
1163     } 
1164
1165     return @repacked_errors;
1166 }
1167
1168 =head2 _get_unlinked_item_subfields
1169
1170   my $unlinked_item_subfields = _get_unlinked_item_subfields($original_item_marc, $frameworkcode);
1171
1172 =cut
1173
1174 sub _get_unlinked_item_subfields {
1175     my $original_item_marc = shift;
1176     my $frameworkcode = shift;
1177
1178     my $marcstructure = C4::Biblio::GetMarcStructure(1, $frameworkcode, { unsafe => 1 });
1179
1180     # assume that this record has only one field, and that that
1181     # field contains only the item information
1182     my $subfields = [];
1183     my @fields = $original_item_marc->fields();
1184     if ($#fields > -1) {
1185         my $field = $fields[0];
1186             my $tag = $field->tag();
1187         foreach my $subfield ($field->subfields()) {
1188             if (defined $subfield->[1] and
1189                 $subfield->[1] ne '' and
1190                 !$marcstructure->{$tag}->{$subfield->[0]}->{'kohafield'}) {
1191                 push @$subfields, $subfield->[0] => $subfield->[1];
1192             }
1193         }
1194     }
1195     return $subfields;
1196 }
1197
1198 =head2 _get_unlinked_subfields_xml
1199
1200   my $unlinked_subfields_xml = _get_unlinked_subfields_xml($unlinked_item_subfields);
1201
1202 =cut
1203
1204 sub _get_unlinked_subfields_xml {
1205     my $unlinked_item_subfields = shift;
1206
1207     my $xml;
1208     if (defined $unlinked_item_subfields and ref($unlinked_item_subfields) eq 'ARRAY' and $#$unlinked_item_subfields > -1) {
1209         my $marc = MARC::Record->new();
1210         # use of tag 999 is arbitrary, and doesn't need to match the item tag
1211         # used in the framework
1212         $marc->append_fields(MARC::Field->new('999', ' ', ' ', @$unlinked_item_subfields));
1213         $marc->encoding("UTF-8");    
1214         $xml = $marc->as_xml("USMARC");
1215     }
1216
1217     return $xml;
1218 }
1219
1220 =head2 _parse_unlinked_item_subfields_from_xml
1221
1222   my $unlinked_item_subfields = _parse_unlinked_item_subfields_from_xml($whole_item->{'more_subfields_xml'}):
1223
1224 =cut
1225
1226 sub  _parse_unlinked_item_subfields_from_xml {
1227     my $xml = shift;
1228     require C4::Charset;
1229     return unless defined $xml and $xml ne "";
1230     my $marc = MARC::Record->new_from_xml(C4::Charset::StripNonXmlChars($xml),'UTF-8');
1231     my $unlinked_subfields = [];
1232     my @fields = $marc->fields();
1233     if ($#fields > -1) {
1234         foreach my $subfield ($fields[0]->subfields()) {
1235             push @$unlinked_subfields, $subfield->[0] => $subfield->[1];
1236         }
1237     }
1238     return $unlinked_subfields;
1239 }
1240
1241 =head2 GetAnalyticsCount
1242
1243   $count= &GetAnalyticsCount($itemnumber)
1244
1245 counts Usage of itemnumber in Analytical bibliorecords. 
1246
1247 =cut
1248
1249 sub GetAnalyticsCount {
1250     my ($itemnumber) = @_;
1251
1252     ### ZOOM search here
1253     my $query;
1254     $query= "hi=".$itemnumber;
1255     my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
1256     my ($err,$res,$result) = $searcher->simple_search_compat($query,0,10);
1257     return ($result);
1258 }
1259
1260 sub _SearchItems_build_where_fragment {
1261     my ($filter) = @_;
1262
1263     my $dbh = C4::Context->dbh;
1264
1265     my $where_fragment;
1266     if (exists($filter->{conjunction})) {
1267         my (@where_strs, @where_args);
1268         foreach my $f (@{ $filter->{filters} }) {
1269             my $fragment = _SearchItems_build_where_fragment($f);
1270             if ($fragment) {
1271                 push @where_strs, $fragment->{str};
1272                 push @where_args, @{ $fragment->{args} };
1273             }
1274         }
1275         my $where_str = '';
1276         if (@where_strs) {
1277             $where_str = '(' . join (' ' . $filter->{conjunction} . ' ', @where_strs) . ')';
1278             $where_fragment = {
1279                 str => $where_str,
1280                 args => \@where_args,
1281             };
1282         }
1283     } else {
1284         my @columns = Koha::Database->new()->schema()->resultset('Item')->result_source->columns;
1285         push @columns, Koha::Database->new()->schema()->resultset('Biblio')->result_source->columns;
1286         push @columns, Koha::Database->new()->schema()->resultset('Biblioitem')->result_source->columns;
1287         my @operators = qw(= != > < >= <= like);
1288         my $field = $filter->{field} // q{};
1289         if ( (0 < grep { $_ eq $field } @columns) or (substr($field, 0, 5) eq 'marc:') ) {
1290             my $op = $filter->{operator};
1291             my $query = $filter->{query};
1292
1293             if (!$op or (0 == grep { $_ eq $op } @operators)) {
1294                 $op = '='; # default operator
1295             }
1296
1297             my $column;
1298             if ($field =~ /^marc:(\d{3})(?:\$(\w))?$/) {
1299                 my $marcfield = $1;
1300                 my $marcsubfield = $2;
1301                 my ($kohafield) = $dbh->selectrow_array(q|
1302                     SELECT kohafield FROM marc_subfield_structure
1303                     WHERE tagfield=? AND tagsubfield=? AND frameworkcode=''
1304                 |, undef, $marcfield, $marcsubfield);
1305
1306                 if ($kohafield) {
1307                     $column = $kohafield;
1308                 } else {
1309                     # MARC field is not linked to a DB field so we need to use
1310                     # ExtractValue on marcxml from biblio_metadata or
1311                     # items.more_subfields_xml, depending on the MARC field.
1312                     my $xpath;
1313                     my $sqlfield;
1314                     my ($itemfield) = C4::Biblio::GetMarcFromKohaField('items.itemnumber');
1315                     if ($marcfield eq $itemfield) {
1316                         $sqlfield = 'more_subfields_xml';
1317                         $xpath = '//record/datafield/subfield[@code="' . $marcsubfield . '"]';
1318                     } else {
1319                         $sqlfield = 'metadata'; # From biblio_metadata
1320                         if ($marcfield < 10) {
1321                             $xpath = "//record/controlfield[\@tag=\"$marcfield\"]";
1322                         } else {
1323                             $xpath = "//record/datafield[\@tag=\"$marcfield\"]/subfield[\@code=\"$marcsubfield\"]";
1324                         }
1325                     }
1326                     $column = "ExtractValue($sqlfield, '$xpath')";
1327                 }
1328             } elsif ($field eq 'issues') {
1329                 # Consider NULL as 0 for issues count
1330                 $column = 'COALESCE(issues,0)';
1331             } else {
1332                 $column = $field;
1333             }
1334
1335             if (ref $query eq 'ARRAY') {
1336                 if ($op eq '=') {
1337                     $op = 'IN';
1338                 } elsif ($op eq '!=') {
1339                     $op = 'NOT IN';
1340                 }
1341                 $where_fragment = {
1342                     str => "$column $op (" . join (',', ('?') x @$query) . ")",
1343                     args => $query,
1344                 };
1345             } else {
1346                 $where_fragment = {
1347                     str => "$column $op ?",
1348                     args => [ $query ],
1349                 };
1350             }
1351         }
1352     }
1353
1354     return $where_fragment;
1355 }
1356
1357 =head2 SearchItems
1358
1359     my ($items, $total) = SearchItems($filter, $params);
1360
1361 Perform a search among items
1362
1363 $filter is a reference to a hash which can be a filter, or a combination of filters.
1364
1365 A filter has the following keys:
1366
1367 =over 2
1368
1369 =item * field: the name of a SQL column in table items
1370
1371 =item * query: the value to search in this column
1372
1373 =item * operator: comparison operator. Can be one of = != > < >= <= like
1374
1375 =back
1376
1377 A combination of filters hash the following keys:
1378
1379 =over 2
1380
1381 =item * conjunction: 'AND' or 'OR'
1382
1383 =item * filters: array ref of filters
1384
1385 =back
1386
1387 $params is a reference to a hash that can contain the following parameters:
1388
1389 =over 2
1390
1391 =item * rows: Number of items to return. 0 returns everything (default: 0)
1392
1393 =item * page: Page to return (return items from (page-1)*rows to (page*rows)-1)
1394                (default: 1)
1395
1396 =item * sortby: A SQL column name in items table to sort on
1397
1398 =item * sortorder: 'ASC' or 'DESC'
1399
1400 =back
1401
1402 =cut
1403
1404 sub SearchItems {
1405     my ($filter, $params) = @_;
1406
1407     $filter //= {};
1408     $params //= {};
1409     return unless ref $filter eq 'HASH';
1410     return unless ref $params eq 'HASH';
1411
1412     # Default parameters
1413     $params->{rows} ||= 0;
1414     $params->{page} ||= 1;
1415     $params->{sortby} ||= 'itemnumber';
1416     $params->{sortorder} ||= 'ASC';
1417
1418     my ($where_str, @where_args);
1419     my $where_fragment = _SearchItems_build_where_fragment($filter);
1420     if ($where_fragment) {
1421         $where_str = $where_fragment->{str};
1422         @where_args = @{ $where_fragment->{args} };
1423     }
1424
1425     my $dbh = C4::Context->dbh;
1426     my $query = q{
1427         SELECT SQL_CALC_FOUND_ROWS items.*
1428         FROM items
1429           LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
1430           LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber
1431           LEFT JOIN biblio_metadata ON biblio_metadata.biblionumber = biblio.biblionumber
1432           WHERE 1
1433     };
1434     if (defined $where_str and $where_str ne '') {
1435         $query .= qq{ AND $where_str };
1436     }
1437
1438     $query .= q{ AND biblio_metadata.format = 'marcxml' AND biblio_metadata.schema = ? };
1439     push @where_args, C4::Context->preference('marcflavour');
1440
1441     my @columns = Koha::Database->new()->schema()->resultset('Item')->result_source->columns;
1442     push @columns, Koha::Database->new()->schema()->resultset('Biblio')->result_source->columns;
1443     push @columns, Koha::Database->new()->schema()->resultset('Biblioitem')->result_source->columns;
1444     my $sortby = (0 < grep {$params->{sortby} eq $_} @columns)
1445         ? $params->{sortby} : 'itemnumber';
1446     my $sortorder = (uc($params->{sortorder}) eq 'ASC') ? 'ASC' : 'DESC';
1447     $query .= qq{ ORDER BY $sortby $sortorder };
1448
1449     my $rows = $params->{rows};
1450     my @limit_args;
1451     if ($rows > 0) {
1452         my $offset = $rows * ($params->{page}-1);
1453         $query .= qq { LIMIT ?, ? };
1454         push @limit_args, $offset, $rows;
1455     }
1456
1457     my $sth = $dbh->prepare($query);
1458     my $rv = $sth->execute(@where_args, @limit_args);
1459
1460     return unless ($rv);
1461     my ($total_rows) = $dbh->selectrow_array(q{ SELECT FOUND_ROWS() });
1462
1463     return ($sth->fetchall_arrayref({}), $total_rows);
1464 }
1465
1466
1467 =head1  OTHER FUNCTIONS
1468
1469 =head2 _find_value
1470
1471   ($indicators, $value) = _find_value($tag, $subfield, $record,$encoding);
1472
1473 Find the given $subfield in the given $tag in the given
1474 MARC::Record $record.  If the subfield is found, returns
1475 the (indicators, value) pair; otherwise, (undef, undef) is
1476 returned.
1477
1478 PROPOSITION :
1479 Such a function is used in addbiblio AND additem and serial-edit and maybe could be used in Authorities.
1480 I suggest we export it from this module.
1481
1482 =cut
1483
1484 sub _find_value {
1485     my ( $tagfield, $insubfield, $record, $encoding ) = @_;
1486     my @result;
1487     my $indicator;
1488     if ( $tagfield < 10 ) {
1489         if ( $record->field($tagfield) ) {
1490             push @result, $record->field($tagfield)->data();
1491         } else {
1492             push @result, "";
1493         }
1494     } else {
1495         foreach my $field ( $record->field($tagfield) ) {
1496             my @subfields = $field->subfields();
1497             foreach my $subfield (@subfields) {
1498                 if ( @$subfield[0] eq $insubfield ) {
1499                     push @result, @$subfield[1];
1500                     $indicator = $field->indicator(1) . $field->indicator(2);
1501                 }
1502             }
1503         }
1504     }
1505     return ( $indicator, @result );
1506 }
1507
1508
1509 =head2 PrepareItemrecordDisplay
1510
1511   PrepareItemrecordDisplay($itemrecord,$bibnum,$itemumber,$frameworkcode);
1512
1513 Returns a hash with all the fields for Display a given item data in a template
1514
1515 The $frameworkcode returns the item for the given frameworkcode, ONLY if bibnum is not provided
1516
1517 =cut
1518
1519 sub PrepareItemrecordDisplay {
1520
1521     my ( $bibnum, $itemnum, $defaultvalues, $frameworkcode ) = @_;
1522
1523     my $dbh = C4::Context->dbh;
1524     $frameworkcode = C4::Biblio::GetFrameworkCode($bibnum) if $bibnum;
1525     my ( $itemtagfield, $itemtagsubfield ) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
1526
1527     # Note: $tagslib obtained from GetMarcStructure() in 'unsafe' mode is
1528     # a shared data structure. No plugin (including custom ones) should change
1529     # its contents. See also GetMarcStructure.
1530     my $tagslib = GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } );
1531
1532     # return nothing if we don't have found an existing framework.
1533     return q{} unless $tagslib;
1534     my $itemrecord;
1535     if ($itemnum) {
1536         $itemrecord = C4::Items::GetMarcItem( $bibnum, $itemnum );
1537     }
1538     my @loop_data;
1539
1540     my $branch_limit = C4::Context->userenv ? C4::Context->userenv->{"branch"} : "";
1541     my $query = qq{
1542         SELECT authorised_value,lib FROM authorised_values
1543     };
1544     $query .= qq{
1545         LEFT JOIN authorised_values_branches ON ( id = av_id )
1546     } if $branch_limit;
1547     $query .= qq{
1548         WHERE category = ?
1549     };
1550     $query .= qq{ AND ( branchcode = ? OR branchcode IS NULL )} if $branch_limit;
1551     $query .= qq{ ORDER BY lib};
1552     my $authorised_values_sth = $dbh->prepare( $query );
1553     foreach my $tag ( sort keys %{$tagslib} ) {
1554         if ( $tag ne '' ) {
1555
1556             # loop through each subfield
1557             my $cntsubf;
1558             foreach my $subfield ( sort keys %{ $tagslib->{$tag} } ) {
1559                 next if IsMarcStructureInternal($tagslib->{$tag}{$subfield});
1560                 next unless ( $tagslib->{$tag}->{$subfield}->{'tab'} );
1561                 next if ( $tagslib->{$tag}->{$subfield}->{'tab'} ne "10" );
1562                 my %subfield_data;
1563                 $subfield_data{tag}           = $tag;
1564                 $subfield_data{subfield}      = $subfield;
1565                 $subfield_data{countsubfield} = $cntsubf++;
1566                 $subfield_data{kohafield}     = $tagslib->{$tag}->{$subfield}->{'kohafield'};
1567                 $subfield_data{id}            = "tag_".$tag."_subfield_".$subfield."_".int(rand(1000000));
1568
1569                 #        $subfield_data{marc_lib}=$tagslib->{$tag}->{$subfield}->{lib};
1570                 $subfield_data{marc_lib}   = $tagslib->{$tag}->{$subfield}->{lib};
1571                 $subfield_data{mandatory}  = $tagslib->{$tag}->{$subfield}->{mandatory};
1572                 $subfield_data{repeatable} = $tagslib->{$tag}->{$subfield}->{repeatable};
1573                 $subfield_data{hidden}     = "display:none"
1574                   if ( ( $tagslib->{$tag}->{$subfield}->{hidden} > 4 )
1575                     || ( $tagslib->{$tag}->{$subfield}->{hidden} < -4 ) );
1576                 my ( $x, $defaultvalue );
1577                 if ($itemrecord) {
1578                     ( $x, $defaultvalue ) = _find_value( $tag, $subfield, $itemrecord );
1579                 }
1580                 $defaultvalue = $tagslib->{$tag}->{$subfield}->{defaultvalue} unless $defaultvalue;
1581                 if ( !defined $defaultvalue ) {
1582                     $defaultvalue = q||;
1583                 } else {
1584                     $defaultvalue =~ s/"/&quot;/g;
1585                 }
1586
1587                 my $maxlength = $tagslib->{$tag}->{$subfield}->{maxlength};
1588
1589                 # search for itemcallnumber if applicable
1590                 if ( $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.itemcallnumber'
1591                     && C4::Context->preference('itemcallnumber') && $itemrecord) {
1592                     foreach my $itemcn_pref (split(/,/,C4::Context->preference('itemcallnumber'))){
1593                         my $CNtag      = substr( $itemcn_pref, 0, 3 );
1594                         next unless my $field = $itemrecord->field($CNtag);
1595                         my $CNsubfields = substr( $itemcn_pref, 3 );
1596                         $defaultvalue = $field->as_string( $CNsubfields, ' ');
1597                         last if $defaultvalue;
1598                     }
1599                 }
1600                 if (   $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.itemcallnumber'
1601                     && $defaultvalues
1602                     && $defaultvalues->{'callnumber'} ) {
1603                     if( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield) ){
1604                         # if the item record exists, only use default value if the item has no callnumber
1605                         $defaultvalue = $defaultvalues->{callnumber};
1606                     } elsif ( !$itemrecord and $defaultvalues ) {
1607                         # if the item record *doesn't* exists, always use the default value
1608                         $defaultvalue = $defaultvalues->{callnumber};
1609                     }
1610                 }
1611                 if (   ( $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.holdingbranch' || $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.homebranch' )
1612                     && $defaultvalues
1613                     && $defaultvalues->{'branchcode'} ) {
1614                     if ( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield) ) {
1615                         $defaultvalue = $defaultvalues->{branchcode};
1616                     }
1617                 }
1618                 if (   ( $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.location' )
1619                     && $defaultvalues
1620                     && $defaultvalues->{'location'} ) {
1621
1622                     if ( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield) ) {
1623                         # if the item record exists, only use default value if the item has no locationr
1624                         $defaultvalue = $defaultvalues->{location};
1625                     } elsif ( !$itemrecord and $defaultvalues ) {
1626                         # if the item record *doesn't* exists, always use the default value
1627                         $defaultvalue = $defaultvalues->{location};
1628                     }
1629                 }
1630                 if ( $tagslib->{$tag}->{$subfield}->{authorised_value} ) {
1631                     my @authorised_values;
1632                     my %authorised_lib;
1633
1634                     # builds list, depending on authorised value...
1635                     #---- branch
1636                     if ( $tagslib->{$tag}->{$subfield}->{'authorised_value'} eq "branches" ) {
1637                         if (   ( C4::Context->preference("IndependentBranches") )
1638                             && !C4::Context->IsSuperLibrarian() ) {
1639                             my $sth = $dbh->prepare( "SELECT branchcode,branchname FROM branches WHERE branchcode = ? ORDER BY branchname" );
1640                             $sth->execute( C4::Context->userenv->{branch} );
1641                             push @authorised_values, ""
1642                               unless ( $tagslib->{$tag}->{$subfield}->{mandatory} );
1643                             while ( my ( $branchcode, $branchname ) = $sth->fetchrow_array ) {
1644                                 push @authorised_values, $branchcode;
1645                                 $authorised_lib{$branchcode} = $branchname;
1646                             }
1647                         } else {
1648                             my $sth = $dbh->prepare( "SELECT branchcode,branchname FROM branches ORDER BY branchname" );
1649                             $sth->execute;
1650                             push @authorised_values, ""
1651                               unless ( $tagslib->{$tag}->{$subfield}->{mandatory} );
1652                             while ( my ( $branchcode, $branchname ) = $sth->fetchrow_array ) {
1653                                 push @authorised_values, $branchcode;
1654                                 $authorised_lib{$branchcode} = $branchname;
1655                             }
1656                         }
1657
1658                         $defaultvalue = C4::Context->userenv ? C4::Context->userenv->{branch} : undef;
1659                         if ( $defaultvalues and $defaultvalues->{branchcode} ) {
1660                             $defaultvalue = $defaultvalues->{branchcode};
1661                         }
1662
1663                         #----- itemtypes
1664                     } elsif ( $tagslib->{$tag}->{$subfield}->{authorised_value} eq "itemtypes" ) {
1665                         my $itemtypes = Koha::ItemTypes->search_with_localization;
1666                         push @authorised_values, ""
1667                           unless ( $tagslib->{$tag}->{$subfield}->{mandatory} );
1668                         while ( my $itemtype = $itemtypes->next ) {
1669                             push @authorised_values, $itemtype->itemtype;
1670                             $authorised_lib{$itemtype->itemtype} = $itemtype->translated_description;
1671                         }
1672                         if ($defaultvalues && $defaultvalues->{'itemtype'}) {
1673                             $defaultvalue = $defaultvalues->{'itemtype'};
1674                         }
1675
1676                         #---- class_sources
1677                     } elsif ( $tagslib->{$tag}->{$subfield}->{authorised_value} eq "cn_source" ) {
1678                         push @authorised_values, "" unless ( $tagslib->{$tag}->{$subfield}->{mandatory} );
1679
1680                         my $class_sources = GetClassSources();
1681                         my $default_source = $defaultvalue || C4::Context->preference("DefaultClassificationSource");
1682
1683                         foreach my $class_source (sort keys %$class_sources) {
1684                             next unless $class_sources->{$class_source}->{'used'} or
1685                                         ($class_source eq $default_source);
1686                             push @authorised_values, $class_source;
1687                             $authorised_lib{$class_source} = $class_sources->{$class_source}->{'description'};
1688                         }
1689
1690                         $defaultvalue = $default_source;
1691
1692                         #---- "true" authorised value
1693                     } else {
1694                         $authorised_values_sth->execute(
1695                             $tagslib->{$tag}->{$subfield}->{authorised_value},
1696                             $branch_limit ? $branch_limit : ()
1697                         );
1698                         push @authorised_values, ""
1699                           unless ( $tagslib->{$tag}->{$subfield}->{mandatory} );
1700                         while ( my ( $value, $lib ) = $authorised_values_sth->fetchrow_array ) {
1701                             push @authorised_values, $value;
1702                             $authorised_lib{$value} = $lib;
1703                         }
1704                     }
1705                     $subfield_data{marc_value} = {
1706                         type    => 'select',
1707                         values  => \@authorised_values,
1708                         default => $defaultvalue // q{},
1709                         labels  => \%authorised_lib,
1710                     };
1711                 } elsif ( $tagslib->{$tag}->{$subfield}->{value_builder} ) {
1712                 # it is a plugin
1713                     require Koha::FrameworkPlugin;
1714                     my $plugin = Koha::FrameworkPlugin->new({
1715                         name => $tagslib->{$tag}->{$subfield}->{value_builder},
1716                         item_style => 1,
1717                     });
1718                     my $pars = { dbh => $dbh, record => undef, tagslib =>$tagslib, id => $subfield_data{id}, tabloop => undef };
1719                     $plugin->build( $pars );
1720                     if ( $itemrecord and my $field = $itemrecord->field($tag) ) {
1721                         $defaultvalue = $field->subfield($subfield) || q{};
1722                     }
1723                     if( !$plugin->errstr ) {
1724                         #TODO Move html to template; see report 12176/13397
1725                         my $tab= $plugin->noclick? '-1': '';
1726                         my $class= $plugin->noclick? ' disabled': '';
1727                         my $title= $plugin->noclick? 'No popup': 'Tag editor';
1728                         $subfield_data{marc_value} = qq[<input type="text" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength" value="$defaultvalue" /><a href="#" id="buttonDot_$subfield_data{id}" class="buttonDot $class" title="$title">...</a>\n].$plugin->javascript;
1729                     } else {
1730                         warn $plugin->errstr;
1731                         $subfield_data{marc_value} = qq(<input type="text" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength" value="$defaultvalue" />); # supply default input form
1732                     }
1733                 }
1734                 elsif ( $tag eq '' ) {       # it's an hidden field
1735                     $subfield_data{marc_value} = qq(<input type="hidden" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength" value="$defaultvalue" />);
1736                 }
1737                 elsif ( $tagslib->{$tag}->{$subfield}->{'hidden'} ) {   # FIXME: shouldn't input type be "hidden" ?
1738                     $subfield_data{marc_value} = qq(<input type="text" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength" value="$defaultvalue" />);
1739                 }
1740                 elsif ( length($defaultvalue) > 100
1741                             or (C4::Context->preference("marcflavour") eq "UNIMARC" and
1742                                   300 <= $tag && $tag < 400 && $subfield eq 'a' )
1743                             or (C4::Context->preference("marcflavour") eq "MARC21"  and
1744                                   500 <= $tag && $tag < 600                     )
1745                           ) {
1746                     # oversize field (textarea)
1747                     $subfield_data{marc_value} = qq(<textarea id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength">$defaultvalue</textarea>\n");
1748                 } else {
1749                     $subfield_data{marc_value} = qq(<input type="text" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength" value="$defaultvalue" />);
1750                 }
1751                 push( @loop_data, \%subfield_data );
1752             }
1753         }
1754     }
1755     my $itemnumber;
1756     if ( $itemrecord && $itemrecord->field($itemtagfield) ) {
1757         $itemnumber = $itemrecord->subfield( $itemtagfield, $itemtagsubfield );
1758     }
1759     return {
1760         'itemtagfield'    => $itemtagfield,
1761         'itemtagsubfield' => $itemtagsubfield,
1762         'itemnumber'      => $itemnumber,
1763         'iteminformation' => \@loop_data
1764     };
1765 }
1766
1767 sub ToggleNewStatus {
1768     my ( $params ) = @_;
1769     my @rules = @{ $params->{rules} };
1770     my $report_only = $params->{report_only};
1771
1772     my $dbh = C4::Context->dbh;
1773     my @errors;
1774     my @item_columns = map { "items.$_" } Koha::Items->columns;
1775     my @biblioitem_columns = map { "biblioitems.$_" } Koha::Biblioitems->columns;
1776     my $report;
1777     for my $rule ( @rules ) {
1778         my $age = $rule->{age};
1779         my $conditions = $rule->{conditions};
1780         my $substitutions = $rule->{substitutions};
1781         foreach ( @$substitutions ) {
1782             ( $_->{item_field} ) = ( $_->{field} =~ /items\.(.*)/ );
1783         }
1784         my @params;
1785
1786         my $query = q|
1787             SELECT items.*
1788             FROM items
1789             LEFT JOIN biblioitems ON biblioitems.biblionumber = items.biblionumber
1790             WHERE 1
1791         |;
1792         for my $condition ( @$conditions ) {
1793             if (
1794                  grep { $_ eq $condition->{field} } @item_columns
1795               or grep { $_ eq $condition->{field} } @biblioitem_columns
1796             ) {
1797                 if ( $condition->{value} =~ /\|/ ) {
1798                     my @values = split /\|/, $condition->{value};
1799                     $query .= qq| AND $condition->{field} IN (|
1800                         . join( ',', ('?') x scalar @values )
1801                         . q|)|;
1802                     push @params, @values;
1803                 } else {
1804                     $query .= qq| AND $condition->{field} = ?|;
1805                     push @params, $condition->{value};
1806                 }
1807             }
1808         }
1809         if ( defined $age ) {
1810             $query .= q| AND TO_DAYS(NOW()) - TO_DAYS(dateaccessioned) >= ? |;
1811             push @params, $age;
1812         }
1813         my $sth = $dbh->prepare($query);
1814         $sth->execute( @params );
1815         while ( my $values = $sth->fetchrow_hashref ) {
1816             my $biblionumber = $values->{biblionumber};
1817             my $itemnumber = $values->{itemnumber};
1818             my $item = Koha::Items->find($itemnumber);
1819             for my $substitution ( @$substitutions ) {
1820                 my $field = $substitution->{item_field};
1821                 my $value = $substitution->{value};
1822                 next unless $substitution->{field};
1823                 next if ( defined $values->{ $substitution->{item_field} } and $values->{ $substitution->{item_field} } eq $substitution->{value} );
1824                 $item->$field($value);
1825                 push @{ $report->{$itemnumber} }, $substitution;
1826             }
1827             $item->store unless $report_only;
1828         }
1829     }
1830
1831     return $report;
1832 }
1833
1834 1;