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