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