Bug 30477: Add new UNIMARC installer translation files
[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     my $ccode        = $parameters->{'ccode'}        // '';
540
541     my $dbh = C4::Context->dbh;
542     my ( @bind_params, @where_strings );
543
544     my $min_cnsort = GetClassSort($class_source,undef,$minlocation);
545     my $max_cnsort = GetClassSort($class_source,undef,$maxlocation);
546
547     my $select_columns = q{
548         SELECT DISTINCT(items.itemnumber), barcode, itemcallnumber, title, author, biblio.biblionumber, biblio.frameworkcode, datelastseen, homebranch, location, notforloan, damaged, itemlost, withdrawn, stocknumber, items.cn_sort, ccode
549
550     };
551     my $select_count = q{SELECT COUNT(DISTINCT(items.itemnumber))};
552     my $query = q{
553         FROM items
554         LEFT JOIN biblio ON items.biblionumber = biblio.biblionumber
555         LEFT JOIN biblioitems on items.biblionumber = biblioitems.biblionumber
556     };
557     if ($statushash){
558         for my $authvfield (keys %$statushash){
559             if ( scalar @{$statushash->{$authvfield}} > 0 ){
560                 my $joinedvals = join ',', @{$statushash->{$authvfield}};
561                 push @where_strings, "$authvfield in (" . $joinedvals . ")";
562             }
563         }
564     }
565
566     if ($ccode){
567         push @where_strings, 'ccode = ?';
568         push @bind_params, $ccode;
569     }
570
571     if ($minlocation) {
572         push @where_strings, 'items.cn_sort >= ?';
573         push @bind_params, $min_cnsort;
574     }
575
576     if ($maxlocation) {
577         push @where_strings, 'items.cn_sort <= ?';
578         push @bind_params, $max_cnsort;
579     }
580
581     if ($datelastseen) {
582         $datelastseen = output_pref({ str => $datelastseen, dateformat => 'iso', dateonly => 1 });
583         push @where_strings, '(datelastseen < ? OR datelastseen IS NULL)';
584         push @bind_params, $datelastseen;
585     }
586
587     if ( $location ) {
588         push @where_strings, 'items.location = ?';
589         push @bind_params, $location;
590     }
591
592     if ( $branchcode ) {
593         if($branch eq "homebranch"){
594         push @where_strings, 'items.homebranch = ?';
595         }else{
596             push @where_strings, 'items.holdingbranch = ?';
597         }
598         push @bind_params, $branchcode;
599     }
600
601     if ( $itemtype ) {
602         push @where_strings, 'biblioitems.itemtype = ?';
603         push @bind_params, $itemtype;
604     }
605     if ( $ignoreissued) {
606         $query .= "LEFT JOIN issues ON items.itemnumber = issues.itemnumber ";
607         push @where_strings, 'issues.date_due IS NULL';
608     }
609
610     if ( $ignore_waiting_holds ) {
611         $query .= "LEFT JOIN reserves ON items.itemnumber = reserves.itemnumber ";
612         push( @where_strings, q{(reserves.found != 'W' OR reserves.found IS NULL)} );
613     }
614
615     if ( @$itemtypes ) {
616         my $itemtypes_str = join ', ', @$itemtypes;
617         push @where_strings, "( biblioitems.itemtype IN (" . $itemtypes_str . ") OR items.itype IN (" . $itemtypes_str . ") )";
618     }
619
620     if ( @where_strings ) {
621         $query .= 'WHERE ';
622         $query .= join ' AND ', @where_strings;
623     }
624     my $count_query = $select_count . $query;
625     $query .= ' ORDER BY items.cn_sort, itemcallnumber, title';
626     $query .= " LIMIT $offset, $size" if ($offset and $size);
627     $query = $select_columns . $query;
628     my $sth = $dbh->prepare($query);
629     $sth->execute( @bind_params );
630
631     my @results = ();
632     my $tmpresults = $sth->fetchall_arrayref({});
633     $sth = $dbh->prepare( $count_query );
634     $sth->execute( @bind_params );
635     my ($iTotalRecords) = $sth->fetchrow_array();
636
637     my @avs = Koha::AuthorisedValues->search(
638         {   'marc_subfield_structures.kohafield' => { '>' => '' },
639             'me.authorised_value'                => { '>' => '' },
640         },
641         {   join     => { category => 'marc_subfield_structures' },
642             distinct => ['marc_subfield_structures.kohafield, me.category, frameworkcode, me.authorised_value'],
643             '+select' => [ 'marc_subfield_structures.kohafield', 'marc_subfield_structures.frameworkcode', 'me.authorised_value', 'me.lib' ],
644             '+as'     => [ 'kohafield',                          'frameworkcode',                          'authorised_value',    'lib' ],
645         }
646     )->as_list;
647
648     my $avmapping = { map { $_->get_column('kohafield') . ',' . $_->get_column('frameworkcode') . ',' . $_->get_column('authorised_value') => $_->get_column('lib') } @avs };
649
650     foreach my $row (@$tmpresults) {
651
652         # Auth values
653         foreach (keys %$row) {
654             if (
655                 defined(
656                     $avmapping->{ "items.$_," . $row->{'frameworkcode'} . "," . ( $row->{$_} // q{} ) }
657                 )
658             ) {
659                 $row->{$_} = $avmapping->{"items.$_,".$row->{'frameworkcode'}.",".$row->{$_}};
660             }
661         }
662         push @results, $row;
663     }
664
665     return (\@results, $iTotalRecords);
666 }
667
668 =head2 GetItemsInfo
669
670   @results = GetItemsInfo($biblionumber);
671
672 Returns information about items with the given biblionumber.
673
674 C<GetItemsInfo> returns a list of references-to-hash. Each element
675 contains a number of keys. Most of them are attributes from the
676 C<biblio>, C<biblioitems>, C<items>, and C<itemtypes> tables in the
677 Koha database. Other keys include:
678
679 =over 2
680
681 =item C<$data-E<gt>{branchname}>
682
683 The name (not the code) of the branch to which the book belongs.
684
685 =item C<$data-E<gt>{datelastseen}>
686
687 This is simply C<items.datelastseen>, except that while the date is
688 stored in YYYY-MM-DD format in the database, here it is converted to
689 DD/MM/YYYY format. A NULL date is returned as C<//>.
690
691 =item C<$data-E<gt>{datedue}>
692
693 =item C<$data-E<gt>{class}>
694
695 This is the concatenation of C<biblioitems.classification>, the book's
696 Dewey code, and C<biblioitems.subclass>.
697
698 =item C<$data-E<gt>{ocount}>
699
700 I think this is the number of copies of the book available.
701
702 =item C<$data-E<gt>{order}>
703
704 If this is set, it is set to C<One Order>.
705
706 =back
707
708 =cut
709
710 sub GetItemsInfo {
711     my ( $biblionumber ) = @_;
712     my $dbh   = C4::Context->dbh;
713     require C4::Languages;
714     my $language = C4::Languages::getlanguage();
715     my $query = "
716     SELECT items.*,
717            biblio.*,
718            biblioitems.volume,
719            biblioitems.number,
720            biblioitems.itemtype,
721            biblioitems.isbn,
722            biblioitems.issn,
723            biblioitems.publicationyear,
724            biblioitems.publishercode,
725            biblioitems.volumedate,
726            biblioitems.volumedesc,
727            biblioitems.lccn,
728            biblioitems.url,
729            items.notforloan as itemnotforloan,
730            issues.borrowernumber,
731            issues.date_due as datedue,
732            issues.onsite_checkout,
733            borrowers.cardnumber,
734            borrowers.surname,
735            borrowers.firstname,
736            borrowers.branchcode as bcode,
737            serial.serialseq,
738            serial.publisheddate,
739            itemtypes.description,
740            COALESCE( localization.translation, itemtypes.description ) AS translated_description,
741            itemtypes.notforloan as notforloan_per_itemtype,
742            holding.branchurl,
743            holding.branchcode,
744            holding.branchname,
745            holding.opac_info as holding_branch_opac_info,
746            home.opac_info as home_branch_opac_info,
747            IF(tmp_holdsqueue.itemnumber,1,0) AS has_pending_hold
748      FROM items
749      LEFT JOIN branches AS holding ON items.holdingbranch = holding.branchcode
750      LEFT JOIN branches AS home ON items.homebranch=home.branchcode
751      LEFT JOIN biblio      ON      biblio.biblionumber     = items.biblionumber
752      LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber
753      LEFT JOIN issues USING (itemnumber)
754      LEFT JOIN borrowers USING (borrowernumber)
755      LEFT JOIN serialitems USING (itemnumber)
756      LEFT JOIN serial USING (serialid)
757      LEFT JOIN itemtypes   ON   itemtypes.itemtype         = "
758      . (C4::Context->preference('item-level_itypes') ? 'items.itype' : 'biblioitems.itemtype');
759     $query .= q|
760     LEFT JOIN tmp_holdsqueue USING (itemnumber)
761     LEFT JOIN localization ON itemtypes.itemtype = localization.code
762         AND localization.entity = 'itemtypes'
763         AND localization.lang = ?
764     |;
765
766     $query .= " WHERE items.biblionumber = ? ORDER BY home.branchname, items.enumchron, LPAD( items.copynumber, 8, '0' ), items.dateaccessioned DESC" ;
767     my $sth = $dbh->prepare($query);
768     $sth->execute($language, $biblionumber);
769     my $i = 0;
770     my @results;
771     my $serial;
772
773     my $userenv = C4::Context->userenv;
774     my $want_not_same_branch = C4::Context->preference("IndependentBranches") && !C4::Context->IsSuperLibrarian();
775     while ( my $data = $sth->fetchrow_hashref ) {
776         if ( $data->{borrowernumber} && $want_not_same_branch) {
777             $data->{'NOTSAMEBRANCH'} = $data->{'bcode'} ne $userenv->{branch};
778         }
779
780         $serial ||= $data->{'serial'};
781
782         my $descriptions;
783         # get notforloan complete status if applicable
784         $descriptions = Koha::AuthorisedValues->get_description_by_koha_field({frameworkcode => $data->{frameworkcode}, kohafield => 'items.notforloan', authorised_value => $data->{itemnotforloan} });
785         $data->{notforloanvalue}     = $descriptions->{lib} // '';
786         $data->{notforloanvalueopac} = $descriptions->{opac_description} // '';
787
788         # get restricted status and description if applicable
789         $descriptions = Koha::AuthorisedValues->get_description_by_koha_field({frameworkcode => $data->{frameworkcode}, kohafield => 'items.restricted', authorised_value => $data->{restricted} });
790         $data->{restrictedvalue}     = $descriptions->{lib} // '';
791         $data->{restrictedvalueopac} = $descriptions->{opac_description} // '';
792
793         # my stack procedures
794         $descriptions = Koha::AuthorisedValues->get_description_by_koha_field({frameworkcode => $data->{frameworkcode}, kohafield => 'items.stack', authorised_value => $data->{stack} });
795         $data->{stack}          = $descriptions->{lib} // '';
796
797         # Find the last 3 people who borrowed this item.
798         my $sth2 = $dbh->prepare("SELECT * FROM old_issues,borrowers
799                                     WHERE itemnumber = ?
800                                     AND old_issues.borrowernumber = borrowers.borrowernumber
801                                     ORDER BY returndate DESC
802                                     LIMIT 3");
803         $sth2->execute($data->{'itemnumber'});
804         my $ii = 0;
805         while (my $data2 = $sth2->fetchrow_hashref()) {
806             $data->{"timestamp$ii"} = $data2->{'timestamp'} if $data2->{'timestamp'};
807             $data->{"card$ii"}      = $data2->{'cardnumber'} if $data2->{'cardnumber'};
808             $data->{"borrower$ii"}  = $data2->{'borrowernumber'} if $data2->{'borrowernumber'};
809             $ii++;
810         }
811
812         $results[$i] = $data;
813         $i++;
814     }
815
816     return $serial
817         ? sort { ($b->{'publisheddate'} || $b->{'enumchron'} || "") cmp ($a->{'publisheddate'} || $a->{'enumchron'} || "") } @results
818         : @results;
819 }
820
821 =head2 GetItemsLocationInfo
822
823   my @itemlocinfo = GetItemsLocationInfo($biblionumber);
824
825 Returns the branch names, shelving location and itemcallnumber for each item attached to the biblio in question
826
827 C<GetItemsInfo> returns a list of references-to-hash. Data returned:
828
829 =over 2
830
831 =item C<$data-E<gt>{homebranch}>
832
833 Branch Name of the item's homebranch
834
835 =item C<$data-E<gt>{holdingbranch}>
836
837 Branch Name of the item's holdingbranch
838
839 =item C<$data-E<gt>{location}>
840
841 Item's shelving location code
842
843 =item C<$data-E<gt>{location_intranet}>
844
845 The intranet description for the Shelving Location as set in authorised_values 'LOC'
846
847 =item C<$data-E<gt>{location_opac}>
848
849 The OPAC description for the Shelving Location as set in authorised_values 'LOC'.  Falls back to intranet description if no OPAC 
850 description is set.
851
852 =item C<$data-E<gt>{itemcallnumber}>
853
854 Item's itemcallnumber
855
856 =item C<$data-E<gt>{cn_sort}>
857
858 Item's call number normalized for sorting
859
860 =back
861
862 =cut
863
864 sub GetItemsLocationInfo {
865         my $biblionumber = shift;
866         my @results;
867
868         my $dbh = C4::Context->dbh;
869         my $query = "SELECT a.branchname as homebranch, b.branchname as holdingbranch, 
870                             location, itemcallnumber, cn_sort
871                      FROM items, branches as a, branches as b
872                      WHERE homebranch = a.branchcode AND holdingbranch = b.branchcode 
873                      AND biblionumber = ?
874                      ORDER BY cn_sort ASC";
875         my $sth = $dbh->prepare($query);
876         $sth->execute($biblionumber);
877
878         while ( my $data = $sth->fetchrow_hashref ) {
879              my $av = Koha::AuthorisedValues->search({ category => 'LOC', authorised_value => $data->{location} });
880              $av = $av->count ? $av->next : undef;
881              $data->{location_intranet} = $av ? $av->lib : '';
882              $data->{location_opac}     = $av ? $av->opac_description : '';
883              push @results, $data;
884         }
885         return @results;
886 }
887
888 =head2 GetHostItemsInfo
889
890     $hostiteminfo = GetHostItemsInfo($hostfield);
891     Returns the iteminfo for items linked to records via a host field
892
893 =cut
894
895 sub GetHostItemsInfo {
896     my ($record) = @_;
897     my @returnitemsInfo;
898
899     if( !C4::Context->preference('EasyAnalyticalRecords') ) {
900         return @returnitemsInfo;
901     }
902
903     my @fields;
904     if( C4::Context->preference('marcflavour') eq 'MARC21' ) {
905         @fields = $record->field('773');
906     } elsif( C4::Context->preference('marcflavour') eq 'UNIMARC') {
907         @fields = $record->field('461');
908     }
909
910     foreach my $hostfield ( @fields ) {
911         my $hostbiblionumber = $hostfield->subfield("0");
912         my $linkeditemnumber = $hostfield->subfield("9");
913         my @hostitemInfos = GetItemsInfo($hostbiblionumber);
914         foreach my $hostitemInfo (@hostitemInfos) {
915             if( $hostitemInfo->{itemnumber} eq $linkeditemnumber ) {
916                 push @returnitemsInfo, $hostitemInfo;
917                 last;
918             }
919         }
920     }
921     return @returnitemsInfo;
922 }
923
924 =head2 get_hostitemnumbers_of
925
926   my @itemnumbers_of = get_hostitemnumbers_of($biblionumber);
927
928 Given a biblionumber, return the list of corresponding itemnumbers that are linked to it via host fields
929
930 Return a reference on a hash where key is a biblionumber and values are
931 references on array of itemnumbers.
932
933 =cut
934
935
936 sub get_hostitemnumbers_of {
937     my ($biblionumber) = @_;
938
939     if( !C4::Context->preference('EasyAnalyticalRecords') ) {
940         return ();
941     }
942
943     my $marcrecord = C4::Biblio::GetMarcBiblio({ biblionumber => $biblionumber });
944     return unless $marcrecord;
945
946     my ( @returnhostitemnumbers, $tag, $biblio_s, $item_s );
947
948     my $marcflavor = C4::Context->preference('marcflavour');
949     if ( $marcflavor eq 'MARC21' ) {
950         $tag      = '773';
951         $biblio_s = '0';
952         $item_s   = '9';
953     }
954     elsif ( $marcflavor eq 'UNIMARC' ) {
955         $tag      = '461';
956         $biblio_s = '0';
957         $item_s   = '9';
958     }
959
960     foreach my $hostfield ( $marcrecord->field($tag) ) {
961         my $hostbiblionumber = $hostfield->subfield($biblio_s);
962         next unless $hostbiblionumber; # have tag, don't have $biblio_s subfield
963         my $linkeditemnumber = $hostfield->subfield($item_s);
964         if ( ! $linkeditemnumber ) {
965             warn "ERROR biblionumber $biblionumber has 773^0, but doesn't have 9";
966             next;
967         }
968         my $is_from_biblio = Koha::Items->search({ itemnumber => $linkeditemnumber, biblionumber => $hostbiblionumber });
969         push @returnhostitemnumbers, $linkeditemnumber
970           if $is_from_biblio;
971     }
972
973     return @returnhostitemnumbers;
974 }
975
976 =head2 GetHiddenItemnumbers
977
978     my @itemnumbers_to_hide = GetHiddenItemnumbers({ items => \@items, borcat => $category });
979
980 Given a list of items it checks which should be hidden from the OPAC given
981 the current configuration. Returns a list of itemnumbers corresponding to
982 those that should be hidden. Optionally takes a borcat parameter for certain borrower types
983 to be excluded
984
985 =cut
986
987 sub GetHiddenItemnumbers {
988     my $params = shift;
989     my $items = $params->{items};
990     if (my $exceptions = C4::Context->preference('OpacHiddenItemsExceptions') and $params->{'borcat'}){
991         foreach my $except (split(/\|/, $exceptions)){
992             if ($params->{'borcat'} eq $except){
993                 return; # we don't hide anything for this borrower category
994             }
995         }
996     }
997     my @resultitems;
998
999     my $hidingrules = C4::Context->yaml_preference('OpacHiddenItems');
1000
1001     return
1002         unless $hidingrules;
1003
1004     my $dbh = C4::Context->dbh;
1005
1006     # For each item
1007     foreach my $item (@$items) {
1008
1009         # We check each rule
1010         foreach my $field (keys %$hidingrules) {
1011             my $val;
1012             if (exists $item->{$field}) {
1013                 $val = $item->{$field};
1014             }
1015             else {
1016                 my $query = "SELECT $field from items where itemnumber = ?";
1017                 $val = $dbh->selectrow_array($query, undef, $item->{'itemnumber'});
1018             }
1019             $val = '' unless defined $val;
1020
1021             # If the results matches the values in the yaml file
1022             if (any { $val eq $_ } @{$hidingrules->{$field}}) {
1023
1024                 # We add the itemnumber to the list
1025                 push @resultitems, $item->{'itemnumber'};
1026
1027                 # If at least one rule matched for an item, no need to test the others
1028                 last;
1029             }
1030         }
1031     }
1032     return @resultitems;
1033 }
1034
1035 =head1 LIMITED USE FUNCTIONS
1036
1037 The following functions, while part of the public API,
1038 are not exported.  This is generally because they are
1039 meant to be used by only one script for a specific
1040 purpose, and should not be used in any other context
1041 without careful thought.
1042
1043 =cut
1044
1045 =head2 GetMarcItem
1046
1047   my $item_marc = GetMarcItem($biblionumber, $itemnumber);
1048
1049 Returns MARC::Record of the item passed in parameter.
1050 This function is meant for use only in C<cataloguing/additem.pl>,
1051 where it is needed to support that script's MARC-like
1052 editor.
1053
1054 =cut
1055
1056 sub GetMarcItem {
1057     my ( $biblionumber, $itemnumber ) = @_;
1058
1059     # GetMarcItem has been revised so that it does the following:
1060     #  1. Gets the item information from the items table.
1061     #  2. Converts it to a MARC field for storage in the bib record.
1062     #
1063     # The previous behavior was:
1064     #  1. Get the bib record.
1065     #  2. Return the MARC tag corresponding to the item record.
1066     #
1067     # The difference is that one treats the items row as authoritative,
1068     # while the other treats the MARC representation as authoritative
1069     # under certain circumstances.
1070
1071     my $item = Koha::Items->find($itemnumber) or return;
1072
1073     # Tack on 'items.' prefix to column names so that C4::Biblio::TransformKohaToMarc will work.
1074     # Also, don't emit a subfield if the underlying field is blank.
1075
1076     return Item2Marc($item->unblessed, $biblionumber);
1077
1078 }
1079 sub Item2Marc {
1080         my ($itemrecord,$biblionumber)=@_;
1081     my $mungeditem = { 
1082         map {  
1083             defined($itemrecord->{$_}) && $itemrecord->{$_} ne '' ? ("items.$_" => $itemrecord->{$_}) : ()  
1084         } keys %{ $itemrecord } 
1085     };
1086     my $framework = C4::Biblio::GetFrameworkCode( $biblionumber );
1087     my $itemmarc = C4::Biblio::TransformKohaToMarc( $mungeditem, { framework => $framework } );
1088     my ( $itemtag, $itemsubfield ) = C4::Biblio::GetMarcFromKohaField(
1089         "items.itemnumber", $framework,
1090     );
1091
1092     my $unlinked_item_subfields = _parse_unlinked_item_subfields_from_xml($mungeditem->{'items.more_subfields_xml'});
1093     if (defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1) {
1094                 foreach my $field ($itemmarc->field($itemtag)){
1095             $field->add_subfields(@$unlinked_item_subfields);
1096         }
1097     }
1098         return $itemmarc;
1099 }
1100
1101 =head1 PRIVATE FUNCTIONS AND VARIABLES
1102
1103 The following functions are not meant to be called
1104 directly, but are documented in order to explain
1105 the inner workings of C<C4::Items>.
1106
1107 =cut
1108
1109 =head2 _marc_from_item_hash
1110
1111   my $item_marc = _marc_from_item_hash($item, $frameworkcode[, $unlinked_item_subfields]);
1112
1113 Given an item hash representing a complete item record,
1114 create a C<MARC::Record> object containing an embedded
1115 tag representing that item.
1116
1117 The third, optional parameter C<$unlinked_item_subfields> is
1118 an arrayref of subfields (not mapped to C<items> fields per the
1119 framework) to be added to the MARC representation
1120 of the item.
1121
1122 =cut
1123
1124 sub _marc_from_item_hash {
1125     my $item = shift;
1126     my $frameworkcode = shift;
1127     my $unlinked_item_subfields;
1128     if (@_) {
1129         $unlinked_item_subfields = shift;
1130     }
1131    
1132     # Tack on 'items.' prefix to column names so lookup from MARC frameworks will work
1133     # Also, don't emit a subfield if the underlying field is blank.
1134     my $mungeditem = { map {  (defined($item->{$_}) and $item->{$_} ne '') ? 
1135                                 (/^items\./ ? ($_ => $item->{$_}) : ("items.$_" => $item->{$_})) 
1136                                 : ()  } keys %{ $item } }; 
1137
1138     my $item_marc = MARC::Record->new();
1139     foreach my $item_field ( keys %{$mungeditem} ) {
1140         my ( $tag, $subfield ) = C4::Biblio::GetMarcFromKohaField( $item_field );
1141         next unless defined $tag and defined $subfield;    # skip if not mapped to MARC field
1142         my @values = split(/\s?\|\s?/, $mungeditem->{$item_field}, -1);
1143         foreach my $value (@values){
1144             if ( my $field = $item_marc->field($tag) ) {
1145                     $field->add_subfields( $subfield => $value );
1146             } else {
1147                 my $add_subfields = [];
1148                 if (defined $unlinked_item_subfields and ref($unlinked_item_subfields) eq 'ARRAY' and $#$unlinked_item_subfields > -1) {
1149                     $add_subfields = $unlinked_item_subfields;
1150             }
1151             $item_marc->add_fields( $tag, " ", " ", $subfield => $value, @$add_subfields );
1152             }
1153         }
1154     }
1155
1156     return $item_marc;
1157 }
1158
1159 =head2 _repack_item_errors
1160
1161 Add an error message hash generated by C<CheckItemPreSave>
1162 to a list of errors.
1163
1164 =cut
1165
1166 sub _repack_item_errors {
1167     my $item_sequence_num = shift;
1168     my $item_ref = shift;
1169     my $error_ref = shift;
1170
1171     my @repacked_errors = ();
1172
1173     foreach my $error_code (sort keys %{ $error_ref }) {
1174         my $repacked_error = {};
1175         $repacked_error->{'item_sequence'} = $item_sequence_num;
1176         $repacked_error->{'item_barcode'} = exists($item_ref->{'barcode'}) ? $item_ref->{'barcode'} : '';
1177         $repacked_error->{'error_code'} = $error_code;
1178         $repacked_error->{'error_information'} = $error_ref->{$error_code};
1179         push @repacked_errors, $repacked_error;
1180     } 
1181
1182     return @repacked_errors;
1183 }
1184
1185 =head2 _get_unlinked_item_subfields
1186
1187   my $unlinked_item_subfields = _get_unlinked_item_subfields($original_item_marc, $frameworkcode);
1188
1189 =cut
1190
1191 sub _get_unlinked_item_subfields {
1192     my $original_item_marc = shift;
1193     my $frameworkcode = shift;
1194
1195     my $marcstructure = C4::Biblio::GetMarcStructure(1, $frameworkcode, { unsafe => 1 });
1196
1197     # assume that this record has only one field, and that that
1198     # field contains only the item information
1199     my $subfields = [];
1200     my @fields = $original_item_marc->fields();
1201     if ($#fields > -1) {
1202         my $field = $fields[0];
1203             my $tag = $field->tag();
1204         foreach my $subfield ($field->subfields()) {
1205             if (defined $subfield->[1] and
1206                 $subfield->[1] ne '' and
1207                 !$marcstructure->{$tag}->{$subfield->[0]}->{'kohafield'}) {
1208                 push @$subfields, $subfield->[0] => $subfield->[1];
1209             }
1210         }
1211     }
1212     return $subfields;
1213 }
1214
1215 =head2 _get_unlinked_subfields_xml
1216
1217   my $unlinked_subfields_xml = _get_unlinked_subfields_xml($unlinked_item_subfields);
1218
1219 =cut
1220
1221 sub _get_unlinked_subfields_xml {
1222     my $unlinked_item_subfields = shift;
1223
1224     my $xml;
1225     if (defined $unlinked_item_subfields and ref($unlinked_item_subfields) eq 'ARRAY' and $#$unlinked_item_subfields > -1) {
1226         my $marc = MARC::Record->new();
1227         # use of tag 999 is arbitrary, and doesn't need to match the item tag
1228         # used in the framework
1229         $marc->append_fields(MARC::Field->new('999', ' ', ' ', @$unlinked_item_subfields));
1230         $marc->encoding("UTF-8");    
1231         $xml = $marc->as_xml("USMARC");
1232     }
1233
1234     return $xml;
1235 }
1236
1237 =head2 _parse_unlinked_item_subfields_from_xml
1238
1239   my $unlinked_item_subfields = _parse_unlinked_item_subfields_from_xml($whole_item->{'more_subfields_xml'}):
1240
1241 =cut
1242
1243 sub  _parse_unlinked_item_subfields_from_xml {
1244     my $xml = shift;
1245     require C4::Charset;
1246     return unless defined $xml and $xml ne "";
1247     my $marc = MARC::Record->new_from_xml(C4::Charset::StripNonXmlChars($xml),'UTF-8');
1248     my $unlinked_subfields = [];
1249     my @fields = $marc->fields();
1250     if ($#fields > -1) {
1251         foreach my $subfield ($fields[0]->subfields()) {
1252             push @$unlinked_subfields, $subfield->[0] => $subfield->[1];
1253         }
1254     }
1255     return $unlinked_subfields;
1256 }
1257
1258 =head2 GetAnalyticsCount
1259
1260   $count= &GetAnalyticsCount($itemnumber)
1261
1262 counts Usage of itemnumber in Analytical bibliorecords. 
1263
1264 =cut
1265
1266 sub GetAnalyticsCount {
1267     my ($itemnumber) = @_;
1268
1269     ### ZOOM search here
1270     my $query;
1271     $query= "hi=".$itemnumber;
1272     my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
1273     my ($err,$res,$result) = $searcher->simple_search_compat($query,0,10);
1274     return ($result);
1275 }
1276
1277 sub _SearchItems_build_where_fragment {
1278     my ($filter) = @_;
1279
1280     my $dbh = C4::Context->dbh;
1281
1282     my $where_fragment;
1283     if (exists($filter->{conjunction})) {
1284         my (@where_strs, @where_args);
1285         foreach my $f (@{ $filter->{filters} }) {
1286             my $fragment = _SearchItems_build_where_fragment($f);
1287             if ($fragment) {
1288                 push @where_strs, $fragment->{str};
1289                 push @where_args, @{ $fragment->{args} };
1290             }
1291         }
1292         my $where_str = '';
1293         if (@where_strs) {
1294             $where_str = '(' . join (' ' . $filter->{conjunction} . ' ', @where_strs) . ')';
1295             $where_fragment = {
1296                 str => $where_str,
1297                 args => \@where_args,
1298             };
1299         }
1300     } else {
1301         my @columns = Koha::Database->new()->schema()->resultset('Item')->result_source->columns;
1302         push @columns, Koha::Database->new()->schema()->resultset('Biblio')->result_source->columns;
1303         push @columns, Koha::Database->new()->schema()->resultset('Biblioitem')->result_source->columns;
1304         push @columns, Koha::Database->new()->schema()->resultset('Issue')->result_source->columns;
1305         my @operators = qw(= != > < >= <= is like);
1306         push @operators, 'not like';
1307         my $field = $filter->{field} // q{};
1308         if ( (0 < grep { $_ eq $field } @columns) or (substr($field, 0, 5) eq 'marc:') ) {
1309             my $op = $filter->{operator};
1310             my $query = $filter->{query};
1311             my $ifnull = $filter->{ifnull};
1312
1313             if (!$op or (0 == grep { $_ eq $op } @operators)) {
1314                 $op = '='; # default operator
1315             }
1316
1317             my $column;
1318             if ($field =~ /^marc:(\d{3})(?:\$(\w))?$/) {
1319                 my $marcfield = $1;
1320                 my $marcsubfield = $2;
1321                 my ($kohafield) = $dbh->selectrow_array(q|
1322                     SELECT kohafield FROM marc_subfield_structure
1323                     WHERE tagfield=? AND tagsubfield=? AND frameworkcode=''
1324                 |, undef, $marcfield, $marcsubfield);
1325
1326                 if ($kohafield) {
1327                     $column = $kohafield;
1328                 } else {
1329                     # MARC field is not linked to a DB field so we need to use
1330                     # ExtractValue on marcxml from biblio_metadata or
1331                     # items.more_subfields_xml, depending on the MARC field.
1332                     my $xpath;
1333                     my $sqlfield;
1334                     my ($itemfield) = C4::Biblio::GetMarcFromKohaField('items.itemnumber');
1335                     if ($marcfield eq $itemfield) {
1336                         $sqlfield = 'more_subfields_xml';
1337                         $xpath = '//record/datafield/subfield[@code="' . $marcsubfield . '"]';
1338                     } else {
1339                         $sqlfield = 'metadata'; # From biblio_metadata
1340                         if ($marcfield < 10) {
1341                             $xpath = "//record/controlfield[\@tag=\"$marcfield\"]";
1342                         } else {
1343                             $xpath = "//record/datafield[\@tag=\"$marcfield\"]/subfield[\@code=\"$marcsubfield\"]";
1344                         }
1345                     }
1346                     $column = "ExtractValue($sqlfield, '$xpath')";
1347                 }
1348             } else {
1349                 $column = $field;
1350             }
1351
1352             if ( defined $ifnull ) {
1353                 $column = "COALESCE($column, ?)";
1354             }
1355
1356             if (ref $query eq 'ARRAY') {
1357                 if ($op eq '=') {
1358                     $op = 'IN';
1359                 } elsif ($op eq '!=') {
1360                     $op = 'NOT IN';
1361                 }
1362                 $where_fragment = {
1363                     str => "$column $op (" . join (',', ('?') x @$query) . ")",
1364                     args => $query,
1365                 };
1366             } elsif ( $op eq 'is' ) {
1367                 $where_fragment = {
1368                     str => "$column $op $query",
1369                     args => [],
1370                 };
1371             } else {
1372                 $where_fragment = {
1373                     str => "$column $op ?",
1374                     args => [ $query ],
1375                 };
1376             }
1377
1378             if ( defined $ifnull ) {
1379                 unshift @{ $where_fragment->{args} }, $ifnull;
1380             }
1381         }
1382     }
1383
1384     return $where_fragment;
1385 }
1386
1387 =head2 SearchItems
1388
1389     my ($items, $total) = SearchItems($filter, $params);
1390
1391 Perform a search among items
1392
1393 $filter is a reference to a hash which can be a filter, or a combination of filters.
1394
1395 A filter has the following keys:
1396
1397 =over 2
1398
1399 =item * field: the name of a SQL column in table items
1400
1401 =item * query: the value to search in this column
1402
1403 =item * operator: comparison operator. Can be one of = != > < >= <= like 'not like' is
1404
1405 =back
1406
1407 A combination of filters hash the following keys:
1408
1409 =over 2
1410
1411 =item * conjunction: 'AND' or 'OR'
1412
1413 =item * filters: array ref of filters
1414
1415 =back
1416
1417 $params is a reference to a hash that can contain the following parameters:
1418
1419 =over 2
1420
1421 =item * rows: Number of items to return. 0 returns everything (default: 0)
1422
1423 =item * page: Page to return (return items from (page-1)*rows to (page*rows)-1)
1424                (default: 1)
1425
1426 =item * sortby: A SQL column name in items table to sort on
1427
1428 =item * sortorder: 'ASC' or 'DESC'
1429
1430 =back
1431
1432 =cut
1433
1434 sub SearchItems {
1435     my ($filter, $params) = @_;
1436
1437     $filter //= {};
1438     $params //= {};
1439     return unless ref $filter eq 'HASH';
1440     return unless ref $params eq 'HASH';
1441
1442     # Default parameters
1443     $params->{rows} ||= 0;
1444     $params->{page} ||= 1;
1445     $params->{sortby} ||= 'itemnumber';
1446     $params->{sortorder} ||= 'ASC';
1447
1448     my ($where_str, @where_args);
1449     my $where_fragment = _SearchItems_build_where_fragment($filter);
1450     if ($where_fragment) {
1451         $where_str = $where_fragment->{str};
1452         @where_args = @{ $where_fragment->{args} };
1453     }
1454
1455     my $dbh = C4::Context->dbh;
1456     my $query = q{
1457         SELECT SQL_CALC_FOUND_ROWS items.*
1458         FROM items
1459           LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
1460           LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber
1461           LEFT JOIN biblio_metadata ON biblio_metadata.biblionumber = biblio.biblionumber
1462           LEFT JOIN issues ON issues.itemnumber = items.itemnumber
1463           WHERE 1
1464     };
1465     if (defined $where_str and $where_str ne '') {
1466         $query .= qq{ AND $where_str };
1467     }
1468
1469     $query .= q{ AND biblio_metadata.format = 'marcxml' AND biblio_metadata.schema = ? };
1470     push @where_args, C4::Context->preference('marcflavour');
1471
1472     my @columns = Koha::Database->new()->schema()->resultset('Item')->result_source->columns;
1473     push @columns, Koha::Database->new()->schema()->resultset('Biblio')->result_source->columns;
1474     push @columns, Koha::Database->new()->schema()->resultset('Biblioitem')->result_source->columns;
1475     push @columns, Koha::Database->new()->schema()->resultset('Issue')->result_source->columns;
1476
1477     if ( $params->{sortby} eq 'availability' ) {
1478         my $sortorder = (uc($params->{sortorder}) eq 'ASC') ? 'ASC' : 'DESC';
1479         $query .= qq{ ORDER BY onloan $sortorder };
1480     } else {
1481         my $sortby = (0 < grep {$params->{sortby} eq $_} @columns)
1482             ? $params->{sortby} : 'itemnumber';
1483         my $sortorder = (uc($params->{sortorder}) eq 'ASC') ? 'ASC' : 'DESC';
1484         $query .= qq{ ORDER BY $sortby $sortorder };
1485     }
1486
1487     my $rows = $params->{rows};
1488     my @limit_args;
1489     if ($rows > 0) {
1490         my $offset = $rows * ($params->{page}-1);
1491         $query .= qq { LIMIT ?, ? };
1492         push @limit_args, $offset, $rows;
1493     }
1494
1495     my $sth = $dbh->prepare($query);
1496     my $rv = $sth->execute(@where_args, @limit_args);
1497
1498     return unless ($rv);
1499     my ($total_rows) = $dbh->selectrow_array(q{ SELECT FOUND_ROWS() });
1500
1501     return ($sth->fetchall_arrayref({}), $total_rows);
1502 }
1503
1504
1505 =head1  OTHER FUNCTIONS
1506
1507 =head2 _find_value
1508
1509   ($indicators, $value) = _find_value($tag, $subfield, $record,$encoding);
1510
1511 Find the given $subfield in the given $tag in the given
1512 MARC::Record $record.  If the subfield is found, returns
1513 the (indicators, value) pair; otherwise, (undef, undef) is
1514 returned.
1515
1516 PROPOSITION :
1517 Such a function is used in addbiblio AND additem and serial-edit and maybe could be used in Authorities.
1518 I suggest we export it from this module.
1519
1520 =cut
1521
1522 sub _find_value {
1523     my ( $tagfield, $insubfield, $record, $encoding ) = @_;
1524     my @result;
1525     my $indicator;
1526     if ( $tagfield < 10 ) {
1527         if ( $record->field($tagfield) ) {
1528             push @result, $record->field($tagfield)->data();
1529         } else {
1530             push @result, "";
1531         }
1532     } else {
1533         foreach my $field ( $record->field($tagfield) ) {
1534             my @subfields = $field->subfields();
1535             foreach my $subfield (@subfields) {
1536                 if ( @$subfield[0] eq $insubfield ) {
1537                     push @result, @$subfield[1];
1538                     $indicator = $field->indicator(1) . $field->indicator(2);
1539                 }
1540             }
1541         }
1542     }
1543     return ( $indicator, @result );
1544 }
1545
1546
1547 =head2 PrepareItemrecordDisplay
1548
1549   PrepareItemrecordDisplay($bibnum,$itemumber,$defaultvalues,$frameworkcode);
1550
1551 Returns a hash with all the fields for Display a given item data in a template
1552
1553 $defaultvalues should either contain a hashref of values for the new item, or be undefined.
1554
1555 The $frameworkcode returns the item for the given frameworkcode, ONLY if bibnum is not provided
1556
1557 =cut
1558
1559 sub PrepareItemrecordDisplay {
1560
1561     my ( $bibnum, $itemnum, $defaultvalues, $frameworkcode ) = @_;
1562
1563     my $dbh = C4::Context->dbh;
1564     $frameworkcode = C4::Biblio::GetFrameworkCode($bibnum) if $bibnum;
1565     my ( $itemtagfield, $itemtagsubfield ) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
1566
1567     # Note: $tagslib obtained from GetMarcStructure() in 'unsafe' mode is
1568     # a shared data structure. No plugin (including custom ones) should change
1569     # its contents. See also GetMarcStructure.
1570     my $tagslib = GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } );
1571
1572     # Pick the default location from NewItemsDefaultLocation
1573     if ( C4::Context->preference('NewItemsDefaultLocation') ) {
1574         $defaultvalues //= {};
1575         $defaultvalues->{location} //= C4::Context->preference('NewItemsDefaultLocation');
1576     }
1577
1578     # return nothing if we don't have found an existing framework.
1579     return q{} unless $tagslib;
1580     my $itemrecord;
1581     if ($itemnum) {
1582         $itemrecord = C4::Items::GetMarcItem( $bibnum, $itemnum );
1583     }
1584     my @loop_data;
1585
1586     my $branch_limit = C4::Context->userenv ? C4::Context->userenv->{"branch"} : "";
1587     my $query = qq{
1588         SELECT authorised_value,lib FROM authorised_values
1589     };
1590     $query .= qq{
1591         LEFT JOIN authorised_values_branches ON ( id = av_id )
1592     } if $branch_limit;
1593     $query .= qq{
1594         WHERE category = ?
1595     };
1596     $query .= qq{ AND ( branchcode = ? OR branchcode IS NULL )} if $branch_limit;
1597     $query .= qq{ ORDER BY lib};
1598     my $authorised_values_sth = $dbh->prepare( $query );
1599     foreach my $tag ( sort keys %{$tagslib} ) {
1600         if ( $tag ne '' ) {
1601
1602             # loop through each subfield
1603             my $cntsubf;
1604             foreach my $subfield (
1605                 sort { $a->{display_order} <=> $b->{display_order} || $a->{subfield} cmp $b->{subfield} }
1606                 grep { ref($_) && %$_ } # Not a subfield (values for "important", "lib", "mandatory", etc.) or empty
1607                 values %{ $tagslib->{$tag} } )
1608             {
1609                 next unless ( $subfield->{'tab'} );
1610                 next if ( $subfield->{'tab'} ne "10" );
1611                 my %subfield_data;
1612                 $subfield_data{tag}           = $tag;
1613                 $subfield_data{subfield}      = $subfield->{subfield};
1614                 $subfield_data{countsubfield} = $cntsubf++;
1615                 $subfield_data{kohafield}     = $subfield->{kohafield};
1616                 $subfield_data{id}            = "tag_".$tag."_subfield_".$subfield->{subfield}."_".int(rand(1000000));
1617
1618                 #        $subfield_data{marc_lib}=$tagslib->{$tag}->{$subfield}->{lib};
1619                 $subfield_data{marc_lib}   = $subfield->{lib};
1620                 $subfield_data{mandatory}  = $subfield->{mandatory};
1621                 $subfield_data{repeatable} = $subfield->{repeatable};
1622                 $subfield_data{hidden}     = "display:none"
1623                   if ( ( $subfield->{hidden} > 4 )
1624                     || ( $subfield->{hidden} < -4 ) );
1625                 my ( $x, $defaultvalue );
1626                 if ($itemrecord) {
1627                     ( $x, $defaultvalue ) = _find_value( $tag, $subfield->{subfield}, $itemrecord );
1628                 }
1629                 $defaultvalue = $subfield->{defaultvalue} unless $defaultvalue;
1630                 if ( !defined $defaultvalue ) {
1631                     $defaultvalue = q||;
1632                 } else {
1633                     $defaultvalue =~ s/"/&quot;/g;
1634                     # get today date & replace <<YYYY>>, <<MM>>, <<DD>> if provided in the default value
1635                     my $today_dt = dt_from_string;
1636                     my $year     = $today_dt->strftime('%Y');
1637                     my $shortyear     = $today_dt->strftime('%y');
1638                     my $month    = $today_dt->strftime('%m');
1639                     my $day      = $today_dt->strftime('%d');
1640                     $defaultvalue =~ s/<<YYYY>>/$year/g;
1641                     $defaultvalue =~ s/<<YY>>/$shortyear/g;
1642                     $defaultvalue =~ s/<<MM>>/$month/g;
1643                     $defaultvalue =~ s/<<DD>>/$day/g;
1644
1645                     # And <<USER>> with surname (?)
1646                     my $username =
1647                       (   C4::Context->userenv
1648                         ? C4::Context->userenv->{'surname'}
1649                         : "superlibrarian" );
1650                     $defaultvalue =~ s/<<USER>>/$username/g;
1651                 }
1652
1653                 my $maxlength = $subfield->{maxlength};
1654
1655                 # search for itemcallnumber if applicable
1656                 if ( $subfield->{kohafield} eq 'items.itemcallnumber'
1657                     && C4::Context->preference('itemcallnumber') && $itemrecord) {
1658                     foreach my $itemcn_pref (split(/,/,C4::Context->preference('itemcallnumber'))){
1659                         my $CNtag      = substr( $itemcn_pref, 0, 3 );
1660                         next unless my $field = $itemrecord->field($CNtag);
1661                         my $CNsubfields = substr( $itemcn_pref, 3 );
1662                         $CNsubfields = undef if $CNsubfields eq '';
1663                         $defaultvalue = $field->as_string( $CNsubfields, ' ');
1664                         last if $defaultvalue;
1665                     }
1666                 }
1667                 if (   $subfield->{kohafield} eq 'items.itemcallnumber'
1668                     && $defaultvalues
1669                     && $defaultvalues->{'callnumber'} ) {
1670                     if( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield->{subfield}) ){
1671                         # if the item record exists, only use default value if the item has no callnumber
1672                         $defaultvalue = $defaultvalues->{callnumber};
1673                     } elsif ( !$itemrecord and $defaultvalues ) {
1674                         # if the item record *doesn't* exists, always use the default value
1675                         $defaultvalue = $defaultvalues->{callnumber};
1676                     }
1677                 }
1678                 if (   ( $subfield->{kohafield} eq 'items.holdingbranch' || $subfield->{kohafield} eq 'items.homebranch' )
1679                     && $defaultvalues
1680                     && $defaultvalues->{'branchcode'} ) {
1681                     if ( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield) ) {
1682                         $defaultvalue = $defaultvalues->{branchcode};
1683                     }
1684                 }
1685                 if (   ( $subfield->{kohafield} eq 'items.location' )
1686                     && $defaultvalues
1687                     && $defaultvalues->{'location'} ) {
1688
1689                     if ( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield->{subfield}) ) {
1690                         # if the item record exists, only use default value if the item has no locationr
1691                         $defaultvalue = $defaultvalues->{location};
1692                     } elsif ( !$itemrecord and $defaultvalues ) {
1693                         # if the item record *doesn't* exists, always use the default value
1694                         $defaultvalue = $defaultvalues->{location};
1695                     }
1696                 }
1697                 if ( $subfield->{authorised_value} ) {
1698                     my @authorised_values;
1699                     my %authorised_lib;
1700
1701                     # builds list, depending on authorised value...
1702                     #---- branch
1703                     if ( $subfield->{'authorised_value'} eq "branches" ) {
1704                         if (   ( C4::Context->preference("IndependentBranches") )
1705                             && !C4::Context->IsSuperLibrarian() ) {
1706                             my $sth = $dbh->prepare( "SELECT branchcode,branchname FROM branches WHERE branchcode = ? ORDER BY branchname" );
1707                             $sth->execute( C4::Context->userenv->{branch} );
1708                             push @authorised_values, ""
1709                               unless ( $subfield->{mandatory} );
1710                             while ( my ( $branchcode, $branchname ) = $sth->fetchrow_array ) {
1711                                 push @authorised_values, $branchcode;
1712                                 $authorised_lib{$branchcode} = $branchname;
1713                             }
1714                         } else {
1715                             my $sth = $dbh->prepare( "SELECT branchcode,branchname FROM branches ORDER BY branchname" );
1716                             $sth->execute;
1717                             push @authorised_values, ""
1718                               unless ( $subfield->{mandatory} );
1719                             while ( my ( $branchcode, $branchname ) = $sth->fetchrow_array ) {
1720                                 push @authorised_values, $branchcode;
1721                                 $authorised_lib{$branchcode} = $branchname;
1722                             }
1723                         }
1724
1725                         $defaultvalue = C4::Context->userenv ? C4::Context->userenv->{branch} : undef;
1726                         if ( $defaultvalues and $defaultvalues->{branchcode} ) {
1727                             $defaultvalue = $defaultvalues->{branchcode};
1728                         }
1729
1730                         #----- itemtypes
1731                     } elsif ( $subfield->{authorised_value} eq "itemtypes" ) {
1732                         my $itemtypes = Koha::ItemTypes->search_with_localization;
1733                         push @authorised_values, "";
1734                         while ( my $itemtype = $itemtypes->next ) {
1735                             push @authorised_values, $itemtype->itemtype;
1736                             $authorised_lib{$itemtype->itemtype} = $itemtype->translated_description;
1737                         }
1738                         if ($defaultvalues && $defaultvalues->{'itemtype'}) {
1739                             $defaultvalue = $defaultvalues->{'itemtype'};
1740                         }
1741
1742                         #---- class_sources
1743                     } elsif ( $subfield->{authorised_value} eq "cn_source" ) {
1744                         push @authorised_values, "";
1745
1746                         my $class_sources = GetClassSources();
1747                         my $default_source = $defaultvalue || C4::Context->preference("DefaultClassificationSource");
1748
1749                         foreach my $class_source (sort keys %$class_sources) {
1750                             next unless $class_sources->{$class_source}->{'used'} or
1751                                         ($class_source eq $default_source);
1752                             push @authorised_values, $class_source;
1753                             $authorised_lib{$class_source} = $class_sources->{$class_source}->{'description'};
1754                         }
1755
1756                         $defaultvalue = $default_source;
1757
1758                         #---- "true" authorised value
1759                     } else {
1760                         $authorised_values_sth->execute(
1761                             $subfield->{authorised_value},
1762                             $branch_limit ? $branch_limit : ()
1763                         );
1764                         push @authorised_values, "";
1765                         while ( my ( $value, $lib ) = $authorised_values_sth->fetchrow_array ) {
1766                             push @authorised_values, $value;
1767                             $authorised_lib{$value} = $lib;
1768                         }
1769                     }
1770                     $subfield_data{marc_value} = {
1771                         type    => 'select',
1772                         values  => \@authorised_values,
1773                         default => $defaultvalue // q{},
1774                         labels  => \%authorised_lib,
1775                     };
1776                 } elsif ( $subfield->{value_builder} ) {
1777                 # it is a plugin
1778                     require Koha::FrameworkPlugin;
1779                     my $plugin = Koha::FrameworkPlugin->new({
1780                         name => $subfield->{value_builder},
1781                         item_style => 1,
1782                     });
1783                     my $pars = { dbh => $dbh, record => undef, tagslib =>$tagslib, id => $subfield_data{id} };
1784                     $plugin->build( $pars );
1785                     if ( $itemrecord and my $field = $itemrecord->field($tag) ) {
1786                         $defaultvalue = $field->subfield($subfield->{subfield}) || q{};
1787                     }
1788                     if( !$plugin->errstr ) {
1789                         #TODO Move html to template; see report 12176/13397
1790                         my $tab= $plugin->noclick? '-1': '';
1791                         my $class= $plugin->noclick? ' disabled': '';
1792                         my $title= $plugin->noclick? 'No popup': 'Tag editor';
1793                         $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;
1794                     } else {
1795                         warn $plugin->errstr;
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" />); # supply default input form
1797                     }
1798                 }
1799                 elsif ( $tag eq '' ) {       # it's an hidden field
1800                     $subfield_data{marc_value} = qq(<input type="hidden" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength" value="$defaultvalue" />);
1801                 }
1802                 elsif ( $subfield->{'hidden'} ) {   # FIXME: shouldn't input type be "hidden" ?
1803                     $subfield_data{marc_value} = qq(<input type="text" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength" value="$defaultvalue" />);
1804                 }
1805                 elsif ( length($defaultvalue) > 100
1806                             or (C4::Context->preference("marcflavour") eq "UNIMARC" and
1807                                   300 <= $tag && $tag < 400 && $subfield->{subfield} eq 'a' )
1808                             or (C4::Context->preference("marcflavour") eq "MARC21"  and
1809                                   500 <= $tag && $tag < 600                     )
1810                           ) {
1811                     # oversize field (textarea)
1812                     $subfield_data{marc_value} = qq(<textarea id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength">$defaultvalue</textarea>\n");
1813                 } else {
1814                     $subfield_data{marc_value} = qq(<input type="text" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength" value="$defaultvalue" />);
1815                 }
1816                 push( @loop_data, \%subfield_data );
1817             }
1818         }
1819     }
1820     my $itemnumber;
1821     if ( $itemrecord && $itemrecord->field($itemtagfield) ) {
1822         $itemnumber = $itemrecord->subfield( $itemtagfield, $itemtagsubfield );
1823     }
1824     return {
1825         'itemtagfield'    => $itemtagfield,
1826         'itemtagsubfield' => $itemtagsubfield,
1827         'itemnumber'      => $itemnumber,
1828         'iteminformation' => \@loop_data
1829     };
1830 }
1831
1832 sub ToggleNewStatus {
1833     my ( $params ) = @_;
1834     my @rules = @{ $params->{rules} };
1835     my $report_only = $params->{report_only};
1836
1837     my $dbh = C4::Context->dbh;
1838     my @errors;
1839     my @item_columns = map { "items.$_" } Koha::Items->columns;
1840     my @biblioitem_columns = map { "biblioitems.$_" } Koha::Biblioitems->columns;
1841     my $report;
1842     for my $rule ( @rules ) {
1843         my $age = $rule->{age};
1844         # Default to using items.dateaccessioned if there's an old item modification rule
1845         # missing an agefield value
1846         my $agefield = $rule->{agefield} ? $rule->{agefield} : 'items.dateaccessioned';
1847         my $conditions = $rule->{conditions};
1848         my $substitutions = $rule->{substitutions};
1849         foreach ( @$substitutions ) {
1850             ( $_->{item_field} ) = ( $_->{field} =~ /items\.(.*)/ );
1851         }
1852         my @params;
1853
1854         my $query = q|
1855             SELECT items.*
1856             FROM items
1857             LEFT JOIN biblioitems ON biblioitems.biblionumber = items.biblionumber
1858             WHERE 1
1859         |;
1860         for my $condition ( @$conditions ) {
1861             if (
1862                  grep { $_ eq $condition->{field} } @item_columns
1863               or grep { $_ eq $condition->{field} } @biblioitem_columns
1864             ) {
1865                 if ( $condition->{value} =~ /\|/ ) {
1866                     my @values = split /\|/, $condition->{value};
1867                     $query .= qq| AND $condition->{field} IN (|
1868                         . join( ',', ('?') x scalar @values )
1869                         . q|)|;
1870                     push @params, @values;
1871                 } else {
1872                     $query .= qq| AND $condition->{field} = ?|;
1873                     push @params, $condition->{value};
1874                 }
1875             }
1876         }
1877         if ( defined $age ) {
1878             $query .= qq| AND TO_DAYS(NOW()) - TO_DAYS($agefield) >= ? |;
1879             push @params, $age;
1880         }
1881         my $sth = $dbh->prepare($query);
1882         $sth->execute( @params );
1883         while ( my $values = $sth->fetchrow_hashref ) {
1884             my $biblionumber = $values->{biblionumber};
1885             my $itemnumber = $values->{itemnumber};
1886             my $item = Koha::Items->find($itemnumber);
1887             for my $substitution ( @$substitutions ) {
1888                 my $field = $substitution->{item_field};
1889                 my $value = $substitution->{value};
1890                 next unless $substitution->{field};
1891                 next if ( defined $values->{ $substitution->{item_field} } and $values->{ $substitution->{item_field} } eq $substitution->{value} );
1892                 $item->$field($value);
1893                 push @{ $report->{$itemnumber} }, $substitution;
1894             }
1895             $item->store unless $report_only;
1896         }
1897     }
1898
1899     return $report;
1900 }
1901
1902 1;