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