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