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