Bug 23463: Remove _set_defaults_for_add
[koha.git] / C4 / Items.pm
1 package C4::Items;
2
3 # Copyright 2007 LibLime, Inc.
4 # Parts Copyright Biblibre 2010
5 #
6 # This file is part of Koha.
7 #
8 # Koha is free software; you can redistribute it and/or modify it
9 # under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # Koha is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with Koha; if not, see <http://www.gnu.org/licenses>.
20
21 use Modern::Perl;
22
23 use vars qw(@ISA @EXPORT);
24 BEGIN {
25     require Exporter;
26     @ISA = qw(Exporter);
27
28     @EXPORT = qw(
29         AddItemFromMarc
30         AddItemBatchFromMarc
31         ModItemFromMarc
32         Item2Marc
33         ModItem
34         ModDateLastSeen
35         ModItemTransfer
36         DelItem
37         CheckItemPreSave
38         GetItemsForInventory
39         GetItemsInfo
40         GetItemsLocationInfo
41         GetHostItemsInfo
42         get_hostitemnumbers_of
43         GetHiddenItemnumbers
44         ItemSafeToDelete
45         DelItemCheck
46         MoveItemFromBiblio
47         CartToShelf
48         GetAnalyticsCount
49         SearchItems
50         PrepareItemrecordDisplay
51     );
52 }
53
54 use Carp;
55 use Try::Tiny;
56 use C4::Context;
57 use C4::Koha;
58 use C4::Biblio;
59 use Koha::DateUtils;
60 use MARC::Record;
61 use C4::ClassSource;
62 use C4::Log;
63 use List::MoreUtils qw(any);
64 use YAML qw(Load);
65 use DateTime::Format::MySQL;
66 use Data::Dumper; # used as part of logging item record changes, not just for
67                   # debugging; so please don't remove this
68
69 use Koha::AuthorisedValues;
70 use Koha::DateUtils qw(dt_from_string);
71 use Koha::Database;
72
73 use Koha::Biblioitems;
74 use Koha::Items;
75 use Koha::ItemTypes;
76 use Koha::SearchEngine;
77 use Koha::SearchEngine::Search;
78 use Koha::Libraries;
79
80 =head1 NAME
81
82 C4::Items - item management functions
83
84 =head1 DESCRIPTION
85
86 This module contains an API for manipulating item 
87 records in Koha, and is used by cataloguing, circulation,
88 acquisitions, and serials management.
89
90 # FIXME This POD is not up-to-date
91 A Koha item record is stored in two places: the
92 items table and embedded in a MARC tag in the XML
93 version of the associated bib record in C<biblioitems.marcxml>.
94 This is done to allow the item information to be readily
95 indexed (e.g., by Zebra), but means that each item
96 modification transaction must keep the items table
97 and the MARC XML in sync at all times.
98
99 The items table will be considered authoritative.  In other
100 words, if there is ever a discrepancy between the items
101 table and the MARC XML, the items table should be considered
102 accurate.
103
104 =head1 HISTORICAL NOTE
105
106 Most of the functions in C<C4::Items> were originally in
107 the C<C4::Biblio> module.
108
109 =head1 CORE EXPORTED FUNCTIONS
110
111 The following functions are meant for use by users
112 of C<C4::Items>
113
114 =cut
115
116 =head2 CartToShelf
117
118   CartToShelf($itemnumber);
119
120 Set the current shelving location of the item record
121 to its stored permanent shelving location.  This is
122 primarily used to indicate when an item whose current
123 location is a special processing ('PROC') or shelving cart
124 ('CART') location is back in the stacks.
125
126 =cut
127
128 sub CartToShelf {
129     my ( $itemnumber ) = @_;
130
131     unless ( $itemnumber ) {
132         croak "FAILED CartToShelf() - no itemnumber supplied";
133     }
134
135     my $item = Koha::Items->find($itemnumber);
136     if ( $item->location eq 'CART' ) {
137         ModItem({ location => $item->permanent_location}, undef, $itemnumber);
138     }
139 }
140
141 =head2 AddItemFromMarc
142
143   my ($biblionumber, $biblioitemnumber, $itemnumber) 
144       = AddItemFromMarc($source_item_marc, $biblionumber);
145
146 Given a MARC::Record object containing an embedded item
147 record and a biblionumber, create a new item record.
148
149 =cut
150
151 sub AddItemFromMarc {
152     my ( $source_item_marc, $biblionumber ) = @_;
153     my $dbh = C4::Context->dbh;
154
155     # parse item hash from MARC
156     my $frameworkcode = C4::Biblio::GetFrameworkCode($biblionumber);
157     my ( $itemtag, $itemsubfield ) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
158
159     my $localitemmarc = MARC::Record->new;
160     $localitemmarc->append_fields( $source_item_marc->field($itemtag) );
161
162 #RMME
163     my $item = C4::Biblio::TransformMarcToKoha( $localitemmarc, $frameworkcode, 'items' );
164     my $unlinked_item_subfields = _get_unlinked_item_subfields( $localitemmarc, $frameworkcode );
165     return AddItem( $item, $biblionumber, $dbh, $frameworkcode, $unlinked_item_subfields );
166
167     my $item_values = C4::Biblio::TransformMarcToKoha( $localitemmarc, $frameworkcode, 'items' );
168     $item_values->{biblionumber} = $biblionumber;
169     # FIXME RM my $unlinked_item_subfields = _get_unlinked_item_subfields( $localitemmarc, $frameworkcode );
170     my $item = Koha::Item->new( $item_values ); # FIXME Handle $unlinked_item_subfields
171     return ( $item->biblionumber, $item->biblioitemnumber, $item->itemnumber );
172 }
173
174 =head2 AddItem
175
176   my ($biblionumber, $biblioitemnumber, $itemnumber) 
177       = AddItem($item, $biblionumber[, $dbh, $frameworkcode, $unlinked_item_subfields]);
178
179 Given a hash containing item column names as keys,
180 create a new Koha item record.
181
182 The first two optional parameters (C<$dbh> and C<$frameworkcode>)
183 do not need to be supplied for general use; they exist
184 simply to allow them to be picked up from AddItemFromMarc.
185
186 The final optional parameter, C<$unlinked_item_subfields>, contains
187 an arrayref containing subfields present in the original MARC
188 representation of the item (e.g., from the item editor) that are
189 not mapped to C<items> columns directly but should instead
190 be stored in C<items.more_subfields_xml> and included in 
191 the biblio items tag for display and indexing.
192
193 =cut
194
195 sub AddItem {
196     my $item         = shift;
197     my $biblionumber = shift;
198
199     my $dbh           = @_ ? shift : C4::Context->dbh;
200     my $unlinked_item_subfields;
201     if (@_) {
202         $unlinked_item_subfields = shift;
203     }
204
205     _set_derived_columns_for_add($item);
206     $item->{'more_subfields_xml'} = _get_unlinked_subfields_xml($unlinked_item_subfields);
207
208     my ( $itemnumber, $error ) = _koha_new_item( $item, $item->{barcode} );
209     return if $error;
210
211     $item->{'itemnumber'} = $itemnumber;
212
213     C4::Biblio::ModZebra( $item->{biblionumber}, "specialUpdate", "biblioserver" );
214
215     logaction( "CATALOGUING", "ADD", $itemnumber, "item" )
216       if C4::Context->preference("CataloguingLog");
217
218     _after_item_action_hooks({ action => 'create', item_id => $itemnumber });
219
220     return ( $item->{biblionumber}, $item->{biblioitemnumber}, $itemnumber );
221 }
222
223 =head2 AddItemBatchFromMarc
224
225   ($itemnumber_ref, $error_ref) = AddItemBatchFromMarc($record, 
226              $biblionumber, $biblioitemnumber, $frameworkcode);
227
228 Efficiently create item records from a MARC biblio record with
229 embedded item fields.  This routine is suitable for batch jobs.
230
231 This API assumes that the bib record has already been
232 saved to the C<biblio> and C<biblioitems> tables.  It does
233 not expect that C<biblio_metadata.metadata> is populated, but it
234 will do so via a call to ModBibiloMarc.
235
236 The goal of this API is to have a similar effect to using AddBiblio
237 and AddItems in succession, but without inefficient repeated
238 parsing of the MARC XML bib record.
239
240 This function returns an arrayref of new itemsnumbers and an arrayref of item
241 errors encountered during the processing.  Each entry in the errors
242 list is a hashref containing the following keys:
243
244 =over
245
246 =item item_sequence
247
248 Sequence number of original item tag in the MARC record.
249
250 =item item_barcode
251
252 Item barcode, provide to assist in the construction of
253 useful error messages.
254
255 =item error_code
256
257 Code representing the error condition.  Can be 'duplicate_barcode',
258 'invalid_homebranch', or 'invalid_holdingbranch'.
259
260 =item error_information
261
262 Additional information appropriate to the error condition.
263
264 =back
265
266 =cut
267
268 sub AddItemBatchFromMarc {
269     my ($record, $biblionumber, $biblioitemnumber, $frameworkcode) = @_;
270     my $error;
271     my @itemnumbers = ();
272     my @errors = ();
273     my $dbh = C4::Context->dbh;
274
275     # We modify the record, so lets work on a clone so we don't change the
276     # original.
277     $record = $record->clone();
278     # loop through the item tags and start creating items
279     my @bad_item_fields = ();
280     my ($itemtag, $itemsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
281     my $item_sequence_num = 0;
282     ITEMFIELD: foreach my $item_field ($record->field($itemtag)) {
283         $item_sequence_num++;
284         # we take the item field and stick it into a new
285         # MARC record -- this is required so far because (FIXME)
286         # TransformMarcToKoha requires a MARC::Record, not a MARC::Field
287         # and there is no TransformMarcFieldToKoha
288         my $temp_item_marc = MARC::Record->new();
289         $temp_item_marc->append_fields($item_field);
290     
291         # add biblionumber and biblioitemnumber
292         my $item = TransformMarcToKoha( $temp_item_marc, $frameworkcode, 'items' );
293         my $unlinked_item_subfields = _get_unlinked_item_subfields($temp_item_marc, $frameworkcode);
294         $item->{'more_subfields_xml'} = _get_unlinked_subfields_xml($unlinked_item_subfields);
295         $item->{'biblionumber'} = $biblionumber;
296         $item->{'biblioitemnumber'} = $biblioitemnumber;
297
298         # check for duplicate barcode
299         my %item_errors = CheckItemPreSave($item);
300         if (%item_errors) {
301             push @errors, _repack_item_errors($item_sequence_num, $item, \%item_errors);
302             push @bad_item_fields, $item_field;
303             next ITEMFIELD;
304         }
305
306         _set_derived_columns_for_add($item);
307         my ( $itemnumber, $error ) = _koha_new_item( $item, $item->{barcode} );
308         warn $error if $error;
309         push @itemnumbers, $itemnumber; # FIXME not checking error
310         $item->{'itemnumber'} = $itemnumber;
311
312         logaction("CATALOGUING", "ADD", $itemnumber, "item") if C4::Context->preference("CataloguingLog"); 
313
314         my $new_item_marc = _marc_from_item_hash($item, $frameworkcode, $unlinked_item_subfields);
315         $item_field->replace_with($new_item_marc->field($itemtag));
316     }
317
318     # remove any MARC item fields for rejected items
319     foreach my $item_field (@bad_item_fields) {
320         $record->delete_field($item_field);
321     }
322
323     # update the MARC biblio
324  #   $biblionumber = ModBiblioMarc( $record, $biblionumber, $frameworkcode );
325
326     return (\@itemnumbers, \@errors);
327 }
328
329 =head2 ModItemFromMarc
330
331   ModItemFromMarc($item_marc, $biblionumber, $itemnumber);
332
333 This function updates an item record based on a supplied
334 C<MARC::Record> object containing an embedded item field.
335 This API is meant for the use of C<additem.pl>; for 
336 other purposes, C<ModItem> should be used.
337
338 This function uses the hash %default_values_for_mod_from_marc,
339 which contains default values for item fields to
340 apply when modifying an item.  This is needed because
341 if an item field's value is cleared, TransformMarcToKoha
342 does not include the column in the
343 hash that's passed to ModItem, which without
344 use of this hash makes it impossible to clear
345 an item field's value.  See bug 2466.
346
347 Note that only columns that can be directly
348 changed from the cataloging and serials
349 item editors are included in this hash.
350
351 Returns item record
352
353 =cut
354
355 sub _build_default_values_for_mod_marc {
356     # Has no framework parameter anymore, since Default is authoritative
357     # for Koha to MARC mappings.
358
359     my $cache     = Koha::Caches->get_instance();
360     my $cache_key = "default_value_for_mod_marc-";
361     my $cached    = $cache->get_from_cache($cache_key);
362     return $cached if $cached;
363
364     my $default_values = {
365         barcode                  => undef,
366         booksellerid             => undef,
367         ccode                    => undef,
368         'items.cn_source'        => undef,
369         coded_location_qualifier => undef,
370         copynumber               => undef,
371         damaged                  => 0,
372         enumchron                => undef,
373         holdingbranch            => undef,
374         homebranch               => undef,
375         itemcallnumber           => undef,
376         itemlost                 => 0,
377         itemnotes                => undef,
378         itemnotes_nonpublic      => undef,
379         itype                    => undef,
380         location                 => undef,
381         permanent_location       => undef,
382         materials                => undef,
383         new_status               => undef,
384         notforloan               => 0,
385         price                    => undef,
386         replacementprice         => undef,
387         replacementpricedate     => undef,
388         restricted               => undef,
389         stack                    => undef,
390         stocknumber              => undef,
391         uri                      => undef,
392         withdrawn                => 0,
393     };
394     my %default_values_for_mod_from_marc;
395     while ( my ( $field, $default_value ) = each %$default_values ) {
396         my $kohafield = $field;
397         $kohafield =~ s|^([^\.]+)$|items.$1|;
398         $default_values_for_mod_from_marc{$field} = $default_value
399             if C4::Biblio::GetMarcFromKohaField( $kohafield );
400     }
401
402     $cache->set_in_cache($cache_key, \%default_values_for_mod_from_marc);
403     return \%default_values_for_mod_from_marc;
404 }
405
406 sub ModItemFromMarc {
407     my $item_marc = shift;
408     my $biblionumber = shift;
409     my $itemnumber = shift;
410
411     my $frameworkcode = C4::Biblio::GetFrameworkCode($biblionumber);
412     my ( $itemtag, $itemsubfield ) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
413
414     my $localitemmarc = MARC::Record->new;
415     $localitemmarc->append_fields( $item_marc->field($itemtag) );
416     my $item = TransformMarcToKoha( $localitemmarc, $frameworkcode, 'items' );
417     my $default_values = _build_default_values_for_mod_marc();
418     foreach my $item_field ( keys %$default_values ) {
419         $item->{$item_field} = $default_values->{$item_field}
420           unless exists $item->{$item_field};
421     }
422     my $unlinked_item_subfields = _get_unlinked_item_subfields( $localitemmarc, $frameworkcode );
423
424     ModItem( $item, $biblionumber, $itemnumber, { unlinked_item_subfields => $unlinked_item_subfields } );
425     return $item;
426 }
427
428 =head2 ModItem
429
430 ModItem(
431     { column => $newvalue },
432     $biblionumber,
433     $itemnumber,
434     {
435         [ unlinked_item_subfields => $unlinked_item_subfields, ]
436         [ log_action => 1, ]
437     }
438 );
439
440 Change one or more columns in an item record.
441
442 The first argument is a hashref mapping from item column
443 names to the new values.  The second and third arguments
444 are the biblionumber and itemnumber, respectively.
445 The fourth, optional parameter (additional_params) may contain the keys
446 unlinked_item_subfields and log_action.
447
448 C<$unlinked_item_subfields> contains an arrayref containing
449 subfields present in the original MARC
450 representation of the item (e.g., from the item editor) that are
451 not mapped to C<items> columns directly but should instead
452 be stored in C<items.more_subfields_xml> and included in 
453 the biblio items tag for display and indexing.
454
455 If one of the changed columns is used to calculate
456 the derived value of a column such as C<items.cn_sort>, 
457 this routine will perform the necessary calculation
458 and set the value.
459
460 If log_action is set to false, the action will not be logged.
461 If log_action is true or undefined, the action will be logged.
462
463 =cut
464
465 sub ModItem {
466     my ( $item, $biblionumber, $itemnumber, $additional_params ) = @_;
467     my $log_action = $additional_params->{log_action} // 1;
468     my $unlinked_item_subfields = $additional_params->{unlinked_item_subfields};
469
470     return unless %$item;
471     $item->{'itemnumber'} = $itemnumber or return;
472
473     # if $biblionumber is undefined, get it from the current item
474     unless (defined $biblionumber) {
475         $biblionumber = _get_single_item_column('biblionumber', $itemnumber);
476     }
477
478     if ($unlinked_item_subfields) {
479         $item->{'more_subfields_xml'} = _get_unlinked_subfields_xml($unlinked_item_subfields);
480     };
481
482     my @fields = qw( itemlost withdrawn damaged );
483
484     # Only retrieve the item if we need to set an "on" date field
485     if ( $item->{itemlost} || $item->{withdrawn} || $item->{damaged} ) {
486         my $pre_mod_item = Koha::Items->find( $item->{'itemnumber'} );
487         for my $field (@fields) {
488             if (    defined( $item->{$field} )
489                 and not $pre_mod_item->$field
490                 and $item->{$field} )
491             {
492                 $item->{ $field . '_on' } =
493                   DateTime::Format::MySQL->format_datetime( dt_from_string() );
494             }
495         }
496     }
497
498     # If the field is defined but empty, we are removing and,
499     # and thus need to clear out the 'on' field as well
500     for my $field (@fields) {
501         if ( defined( $item->{$field} ) && !$item->{$field} ) {
502             $item->{ $field . '_on' } = undef;
503         }
504     }
505
506
507     _set_derived_columns_for_mod($item);
508     _do_column_fixes_for_mod($item);
509     # FIXME add checks
510     # duplicate barcode
511     # attempt to change itemnumber
512     # attempt to change biblionumber (if we want
513     # an API to relink an item to a different bib,
514     # it should be a separate function)
515
516     # update items table
517     _koha_modify_item($item);
518
519     # request that bib be reindexed so that searching on current
520     # item status is possible
521     ModZebra( $biblionumber, "specialUpdate", "biblioserver" );
522
523     _after_item_action_hooks({ action => 'modify', item_id => $itemnumber });
524
525     logaction( "CATALOGUING", "MODIFY", $itemnumber, "item " . Dumper($item) )
526       if $log_action && C4::Context->preference("CataloguingLog");
527 }
528
529 =head2 ModItemTransfer
530
531   ModItemTransfer($itemnumber, $frombranch, $tobranch, $trigger);
532
533 Marks an item as being transferred from one branch to another and records the trigger.
534
535 =cut
536
537 sub ModItemTransfer {
538     my ( $itemnumber, $frombranch, $tobranch, $trigger ) = @_;
539
540     my $dbh = C4::Context->dbh;
541     my $item = Koha::Items->find( $itemnumber );
542
543     # Remove the 'shelving cart' location status if it is being used.
544     CartToShelf( $itemnumber ) if $item->location && $item->location eq 'CART' && ( !$item->permanent_location || $item->permanent_location ne 'CART' );
545
546     $dbh->do("UPDATE branchtransfers SET datearrived = NOW(), comments = ? WHERE itemnumber = ? AND datearrived IS NULL", undef, "Canceled, new transfer from $frombranch to $tobranch created", $itemnumber);
547
548     #new entry in branchtransfers....
549     my $sth = $dbh->prepare(
550         "INSERT INTO branchtransfers (itemnumber, frombranch, datesent, tobranch, reason)
551         VALUES (?, ?, NOW(), ?, ?)");
552     $sth->execute($itemnumber, $frombranch, $tobranch, $trigger);
553
554     ModItem({ holdingbranch => $frombranch }, undef, $itemnumber, { log_action => 0 });
555     ModDateLastSeen($itemnumber);
556     return;
557 }
558
559 =head2 ModDateLastSeen
560
561 ModDateLastSeen( $itemnumber, $leave_item_lost );
562
563 Mark item as seen. Is called when an item is issued, returned or manually marked during inventory/stocktaking.
564 C<$itemnumber> is the item number
565 C<$leave_item_lost> determines if a lost item will be found or remain lost
566
567 =cut
568
569 sub ModDateLastSeen {
570     my ( $itemnumber, $leave_item_lost ) = @_;
571
572     my $today = output_pref({ dt => dt_from_string, dateformat => 'iso', dateonly => 1 });
573
574     my $params;
575     $params->{datelastseen} = $today;
576     $params->{itemlost} = 0 unless $leave_item_lost;
577
578     ModItem( $params, undef, $itemnumber, { log_action => 0 } );
579 }
580
581 =head2 DelItem
582
583   DelItem({ itemnumber => $itemnumber, [ biblionumber => $biblionumber ] } );
584
585 Exported function (core API) for deleting an item record in Koha.
586
587 =cut
588
589 sub DelItem {
590     my ( $params ) = @_;
591
592     my $itemnumber   = $params->{itemnumber};
593     my $biblionumber = $params->{biblionumber};
594
595     unless ($biblionumber) {
596         my $item = Koha::Items->find( $itemnumber );
597         $biblionumber = $item ? $item->biblio->biblionumber : undef;
598     }
599
600     # If there is no biblionumber for the given itemnumber, there is nothing to delete
601     return 0 unless $biblionumber;
602
603     # FIXME check the item has no current issues
604     my $deleted = _koha_delete_item( $itemnumber );
605
606     ModZebra( $biblionumber, "specialUpdate", "biblioserver" );
607
608     _after_item_action_hooks({ action => 'delete', item_id => $itemnumber });
609
610     #search item field code
611     logaction("CATALOGUING", "DELETE", $itemnumber, "item") if C4::Context->preference("CataloguingLog");
612     return $deleted;
613 }
614
615 =head2 CheckItemPreSave
616
617     my $item_ref = TransformMarcToKoha($marc, 'items');
618     # do stuff
619     my %errors = CheckItemPreSave($item_ref);
620     if (exists $errors{'duplicate_barcode'}) {
621         print "item has duplicate barcode: ", $errors{'duplicate_barcode'}, "\n";
622     } elsif (exists $errors{'invalid_homebranch'}) {
623         print "item has invalid home branch: ", $errors{'invalid_homebranch'}, "\n";
624     } elsif (exists $errors{'invalid_holdingbranch'}) {
625         print "item has invalid holding branch: ", $errors{'invalid_holdingbranch'}, "\n";
626     } else {
627         print "item is OK";
628     }
629
630 Given a hashref containing item fields, determine if it can be
631 inserted or updated in the database.  Specifically, checks for
632 database integrity issues, and returns a hash containing any
633 of the following keys, if applicable.
634
635 =over 2
636
637 =item duplicate_barcode
638
639 Barcode, if it duplicates one already found in the database.
640
641 =item invalid_homebranch
642
643 Home branch, if not defined in branches table.
644
645 =item invalid_holdingbranch
646
647 Holding branch, if not defined in branches table.
648
649 =back
650
651 This function does NOT implement any policy-related checks,
652 e.g., whether current operator is allowed to save an
653 item that has a given branch code.
654
655 =cut
656
657 sub CheckItemPreSave {
658     my $item_ref = shift;
659
660     my %errors = ();
661
662     # check for duplicate barcode
663     if (exists $item_ref->{'barcode'} and defined $item_ref->{'barcode'}) {
664         my $existing_item= Koha::Items->find({barcode => $item_ref->{'barcode'}});
665         if ($existing_item) {
666             if (!exists $item_ref->{'itemnumber'}                       # new item
667                 or $item_ref->{'itemnumber'} != $existing_item->itemnumber) { # existing item
668                 $errors{'duplicate_barcode'} = $item_ref->{'barcode'};
669             }
670         }
671     }
672
673     # check for valid home branch
674     if (exists $item_ref->{'homebranch'} and defined $item_ref->{'homebranch'}) {
675         my $home_library = Koha::Libraries->find( $item_ref->{homebranch} );
676         unless (defined $home_library) {
677             $errors{'invalid_homebranch'} = $item_ref->{'homebranch'};
678         }
679     }
680
681     # check for valid holding branch
682     if (exists $item_ref->{'holdingbranch'} and defined $item_ref->{'holdingbranch'}) {
683         my $holding_library = Koha::Libraries->find( $item_ref->{holdingbranch} );
684         unless (defined $holding_library) {
685             $errors{'invalid_holdingbranch'} = $item_ref->{'holdingbranch'};
686         }
687     }
688
689     return %errors;
690
691 }
692
693 =head1 EXPORTED SPECIAL ACCESSOR FUNCTIONS
694
695 The following functions provide various ways of 
696 getting an item record, a set of item records, or
697 lists of authorized values for certain item fields.
698
699 =cut
700
701 =head2 GetItemsForInventory
702
703 ($itemlist, $iTotalRecords) = GetItemsForInventory( {
704   minlocation  => $minlocation,
705   maxlocation  => $maxlocation,
706   location     => $location,
707   itemtype     => $itemtype,
708   ignoreissued => $ignoreissued,
709   datelastseen => $datelastseen,
710   branchcode   => $branchcode,
711   branch       => $branch,
712   offset       => $offset,
713   size         => $size,
714   statushash   => $statushash,
715 } );
716
717 Retrieve a list of title/authors/barcode/callnumber, for biblio inventory.
718
719 The sub returns a reference to a list of hashes, each containing
720 itemnumber, author, title, barcode, item callnumber, and date last
721 seen. It is ordered by callnumber then title.
722
723 The required minlocation & maxlocation parameters are used to specify a range of item callnumbers
724 the datelastseen can be used to specify that you want to see items not seen since a past date only.
725 offset & size can be used to retrieve only a part of the whole listing (defaut behaviour)
726 $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.
727
728 $iTotalRecords is the number of rows that would have been returned without the $offset, $size limit clause
729
730 =cut
731
732 sub GetItemsForInventory {
733     my ( $parameters ) = @_;
734     my $minlocation  = $parameters->{'minlocation'}  // '';
735     my $maxlocation  = $parameters->{'maxlocation'}  // '';
736     my $class_source = $parameters->{'class_source'}  // C4::Context->preference('DefaultClassificationSource');
737     my $location     = $parameters->{'location'}     // '';
738     my $itemtype     = $parameters->{'itemtype'}     // '';
739     my $ignoreissued = $parameters->{'ignoreissued'} // '';
740     my $datelastseen = $parameters->{'datelastseen'} // '';
741     my $branchcode   = $parameters->{'branchcode'}   // '';
742     my $branch       = $parameters->{'branch'}       // '';
743     my $offset       = $parameters->{'offset'}       // '';
744     my $size         = $parameters->{'size'}         // '';
745     my $statushash   = $parameters->{'statushash'}   // '';
746     my $ignore_waiting_holds = $parameters->{'ignore_waiting_holds'} // '';
747
748     my $dbh = C4::Context->dbh;
749     my ( @bind_params, @where_strings );
750
751     my $min_cnsort = GetClassSort($class_source,undef,$minlocation);
752     my $max_cnsort = GetClassSort($class_source,undef,$maxlocation);
753
754     my $select_columns = q{
755         SELECT DISTINCT(items.itemnumber), barcode, itemcallnumber, title, author, biblio.biblionumber, biblio.frameworkcode, datelastseen, homebranch, location, notforloan, damaged, itemlost, withdrawn, stocknumber, items.cn_sort
756     };
757     my $select_count = q{SELECT COUNT(DISTINCT(items.itemnumber))};
758     my $query = q{
759         FROM items
760         LEFT JOIN biblio ON items.biblionumber = biblio.biblionumber
761         LEFT JOIN biblioitems on items.biblionumber = biblioitems.biblionumber
762     };
763     if ($statushash){
764         for my $authvfield (keys %$statushash){
765             if ( scalar @{$statushash->{$authvfield}} > 0 ){
766                 my $joinedvals = join ',', @{$statushash->{$authvfield}};
767                 push @where_strings, "$authvfield in (" . $joinedvals . ")";
768             }
769         }
770     }
771
772     if ($minlocation) {
773         push @where_strings, 'items.cn_sort >= ?';
774         push @bind_params, $min_cnsort;
775     }
776
777     if ($maxlocation) {
778         push @where_strings, 'items.cn_sort <= ?';
779         push @bind_params, $max_cnsort;
780     }
781
782     if ($datelastseen) {
783         $datelastseen = output_pref({ str => $datelastseen, dateformat => 'iso', dateonly => 1 });
784         push @where_strings, '(datelastseen < ? OR datelastseen IS NULL)';
785         push @bind_params, $datelastseen;
786     }
787
788     if ( $location ) {
789         push @where_strings, 'items.location = ?';
790         push @bind_params, $location;
791     }
792
793     if ( $branchcode ) {
794         if($branch eq "homebranch"){
795         push @where_strings, 'items.homebranch = ?';
796         }else{
797             push @where_strings, 'items.holdingbranch = ?';
798         }
799         push @bind_params, $branchcode;
800     }
801
802     if ( $itemtype ) {
803         push @where_strings, 'biblioitems.itemtype = ?';
804         push @bind_params, $itemtype;
805     }
806
807     if ( $ignoreissued) {
808         $query .= "LEFT JOIN issues ON items.itemnumber = issues.itemnumber ";
809         push @where_strings, 'issues.date_due IS NULL';
810     }
811
812     if ( $ignore_waiting_holds ) {
813         $query .= "LEFT JOIN reserves ON items.itemnumber = reserves.itemnumber ";
814         push( @where_strings, q{(reserves.found != 'W' OR reserves.found IS NULL)} );
815     }
816
817     if ( @where_strings ) {
818         $query .= 'WHERE ';
819         $query .= join ' AND ', @where_strings;
820     }
821     my $count_query = $select_count . $query;
822     $query .= ' ORDER BY items.cn_sort, itemcallnumber, title';
823     $query .= " LIMIT $offset, $size" if ($offset and $size);
824     $query = $select_columns . $query;
825     my $sth = $dbh->prepare($query);
826     $sth->execute( @bind_params );
827
828     my @results = ();
829     my $tmpresults = $sth->fetchall_arrayref({});
830     $sth = $dbh->prepare( $count_query );
831     $sth->execute( @bind_params );
832     my ($iTotalRecords) = $sth->fetchrow_array();
833
834     my @avs = Koha::AuthorisedValues->search(
835         {   'marc_subfield_structures.kohafield' => { '>' => '' },
836             'me.authorised_value'                => { '>' => '' },
837         },
838         {   join     => { category => 'marc_subfield_structures' },
839             distinct => ['marc_subfield_structures.kohafield, me.category, frameworkcode, me.authorised_value'],
840             '+select' => [ 'marc_subfield_structures.kohafield', 'marc_subfield_structures.frameworkcode', 'me.authorised_value', 'me.lib' ],
841             '+as'     => [ 'kohafield',                          'frameworkcode',                          'authorised_value',    'lib' ],
842         }
843     );
844
845     my $avmapping = { map { $_->get_column('kohafield') . ',' . $_->get_column('frameworkcode') . ',' . $_->get_column('authorised_value') => $_->get_column('lib') } @avs };
846
847     foreach my $row (@$tmpresults) {
848
849         # Auth values
850         foreach (keys %$row) {
851             if (
852                 defined(
853                     $avmapping->{ "items.$_," . $row->{'frameworkcode'} . "," . ( $row->{$_} // q{} ) }
854                 )
855             ) {
856                 $row->{$_} = $avmapping->{"items.$_,".$row->{'frameworkcode'}.",".$row->{$_}};
857             }
858         }
859         push @results, $row;
860     }
861
862     return (\@results, $iTotalRecords);
863 }
864
865 =head2 GetItemsInfo
866
867   @results = GetItemsInfo($biblionumber);
868
869 Returns information about items with the given biblionumber.
870
871 C<GetItemsInfo> returns a list of references-to-hash. Each element
872 contains a number of keys. Most of them are attributes from the
873 C<biblio>, C<biblioitems>, C<items>, and C<itemtypes> tables in the
874 Koha database. Other keys include:
875
876 =over 2
877
878 =item C<$data-E<gt>{branchname}>
879
880 The name (not the code) of the branch to which the book belongs.
881
882 =item C<$data-E<gt>{datelastseen}>
883
884 This is simply C<items.datelastseen>, except that while the date is
885 stored in YYYY-MM-DD format in the database, here it is converted to
886 DD/MM/YYYY format. A NULL date is returned as C<//>.
887
888 =item C<$data-E<gt>{datedue}>
889
890 =item C<$data-E<gt>{class}>
891
892 This is the concatenation of C<biblioitems.classification>, the book's
893 Dewey code, and C<biblioitems.subclass>.
894
895 =item C<$data-E<gt>{ocount}>
896
897 I think this is the number of copies of the book available.
898
899 =item C<$data-E<gt>{order}>
900
901 If this is set, it is set to C<One Order>.
902
903 =back
904
905 =cut
906
907 sub GetItemsInfo {
908     my ( $biblionumber ) = @_;
909     my $dbh   = C4::Context->dbh;
910     require C4::Languages;
911     my $language = C4::Languages::getlanguage();
912     my $query = "
913     SELECT items.*,
914            biblio.*,
915            biblioitems.volume,
916            biblioitems.number,
917            biblioitems.itemtype,
918            biblioitems.isbn,
919            biblioitems.issn,
920            biblioitems.publicationyear,
921            biblioitems.publishercode,
922            biblioitems.volumedate,
923            biblioitems.volumedesc,
924            biblioitems.lccn,
925            biblioitems.url,
926            items.notforloan as itemnotforloan,
927            issues.borrowernumber,
928            issues.date_due as datedue,
929            issues.onsite_checkout,
930            borrowers.cardnumber,
931            borrowers.surname,
932            borrowers.firstname,
933            borrowers.branchcode as bcode,
934            serial.serialseq,
935            serial.publisheddate,
936            itemtypes.description,
937            COALESCE( localization.translation, itemtypes.description ) AS translated_description,
938            itemtypes.notforloan as notforloan_per_itemtype,
939            holding.branchurl,
940            holding.branchcode,
941            holding.branchname,
942            holding.opac_info as holding_branch_opac_info,
943            home.opac_info as home_branch_opac_info,
944            IF(tmp_holdsqueue.itemnumber,1,0) AS has_pending_hold
945      FROM items
946      LEFT JOIN branches AS holding ON items.holdingbranch = holding.branchcode
947      LEFT JOIN branches AS home ON items.homebranch=home.branchcode
948      LEFT JOIN biblio      ON      biblio.biblionumber     = items.biblionumber
949      LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber
950      LEFT JOIN issues USING (itemnumber)
951      LEFT JOIN borrowers USING (borrowernumber)
952      LEFT JOIN serialitems USING (itemnumber)
953      LEFT JOIN serial USING (serialid)
954      LEFT JOIN itemtypes   ON   itemtypes.itemtype         = "
955      . (C4::Context->preference('item-level_itypes') ? 'items.itype' : 'biblioitems.itemtype');
956     $query .= q|
957     LEFT JOIN tmp_holdsqueue USING (itemnumber)
958     LEFT JOIN localization ON itemtypes.itemtype = localization.code
959         AND localization.entity = 'itemtypes'
960         AND localization.lang = ?
961     |;
962
963     $query .= " WHERE items.biblionumber = ? ORDER BY home.branchname, items.enumchron, LPAD( items.copynumber, 8, '0' ), items.dateaccessioned DESC" ;
964     my $sth = $dbh->prepare($query);
965     $sth->execute($language, $biblionumber);
966     my $i = 0;
967     my @results;
968     my $serial;
969
970     my $userenv = C4::Context->userenv;
971     my $want_not_same_branch = C4::Context->preference("IndependentBranches") && !C4::Context->IsSuperLibrarian();
972     while ( my $data = $sth->fetchrow_hashref ) {
973         if ( $data->{borrowernumber} && $want_not_same_branch) {
974             $data->{'NOTSAMEBRANCH'} = $data->{'bcode'} ne $userenv->{branch};
975         }
976
977         $serial ||= $data->{'serial'};
978
979         my $descriptions;
980         # get notforloan complete status if applicable
981         $descriptions = Koha::AuthorisedValues->get_description_by_koha_field({frameworkcode => $data->{frameworkcode}, kohafield => 'items.notforloan', authorised_value => $data->{itemnotforloan} });
982         $data->{notforloanvalue}     = $descriptions->{lib} // '';
983         $data->{notforloanvalueopac} = $descriptions->{opac_description} // '';
984
985         # get restricted status and description if applicable
986         $descriptions = Koha::AuthorisedValues->get_description_by_koha_field({frameworkcode => $data->{frameworkcode}, kohafield => 'items.restricted', authorised_value => $data->{restricted} });
987         $data->{restrictedvalue}     = $descriptions->{lib} // '';
988         $data->{restrictedvalueopac} = $descriptions->{opac_description} // '';
989
990         # my stack procedures
991         $descriptions = Koha::AuthorisedValues->get_description_by_koha_field({frameworkcode => $data->{frameworkcode}, kohafield => 'items.stack', authorised_value => $data->{stack} });
992         $data->{stack}          = $descriptions->{lib} // '';
993
994         # Find the last 3 people who borrowed this item.
995         my $sth2 = $dbh->prepare("SELECT * FROM old_issues,borrowers
996                                     WHERE itemnumber = ?
997                                     AND old_issues.borrowernumber = borrowers.borrowernumber
998                                     ORDER BY returndate DESC
999                                     LIMIT 3");
1000         $sth2->execute($data->{'itemnumber'});
1001         my $ii = 0;
1002         while (my $data2 = $sth2->fetchrow_hashref()) {
1003             $data->{"timestamp$ii"} = $data2->{'timestamp'} if $data2->{'timestamp'};
1004             $data->{"card$ii"}      = $data2->{'cardnumber'} if $data2->{'cardnumber'};
1005             $data->{"borrower$ii"}  = $data2->{'borrowernumber'} if $data2->{'borrowernumber'};
1006             $ii++;
1007         }
1008
1009         $results[$i] = $data;
1010         $i++;
1011     }
1012
1013     return $serial
1014         ? sort { ($b->{'publisheddate'} || $b->{'enumchron'}) cmp ($a->{'publisheddate'} || $a->{'enumchron'}) } @results
1015         : @results;
1016 }
1017
1018 =head2 GetItemsLocationInfo
1019
1020   my @itemlocinfo = GetItemsLocationInfo($biblionumber);
1021
1022 Returns the branch names, shelving location and itemcallnumber for each item attached to the biblio in question
1023
1024 C<GetItemsInfo> returns a list of references-to-hash. Data returned:
1025
1026 =over 2
1027
1028 =item C<$data-E<gt>{homebranch}>
1029
1030 Branch Name of the item's homebranch
1031
1032 =item C<$data-E<gt>{holdingbranch}>
1033
1034 Branch Name of the item's holdingbranch
1035
1036 =item C<$data-E<gt>{location}>
1037
1038 Item's shelving location code
1039
1040 =item C<$data-E<gt>{location_intranet}>
1041
1042 The intranet description for the Shelving Location as set in authorised_values 'LOC'
1043
1044 =item C<$data-E<gt>{location_opac}>
1045
1046 The OPAC description for the Shelving Location as set in authorised_values 'LOC'.  Falls back to intranet description if no OPAC 
1047 description is set.
1048
1049 =item C<$data-E<gt>{itemcallnumber}>
1050
1051 Item's itemcallnumber
1052
1053 =item C<$data-E<gt>{cn_sort}>
1054
1055 Item's call number normalized for sorting
1056
1057 =back
1058   
1059 =cut
1060
1061 sub GetItemsLocationInfo {
1062         my $biblionumber = shift;
1063         my @results;
1064
1065         my $dbh = C4::Context->dbh;
1066         my $query = "SELECT a.branchname as homebranch, b.branchname as holdingbranch, 
1067                             location, itemcallnumber, cn_sort
1068                      FROM items, branches as a, branches as b
1069                      WHERE homebranch = a.branchcode AND holdingbranch = b.branchcode 
1070                      AND biblionumber = ?
1071                      ORDER BY cn_sort ASC";
1072         my $sth = $dbh->prepare($query);
1073         $sth->execute($biblionumber);
1074
1075         while ( my $data = $sth->fetchrow_hashref ) {
1076              my $av = Koha::AuthorisedValues->search({ category => 'LOC', authorised_value => $data->{location} });
1077              $av = $av->count ? $av->next : undef;
1078              $data->{location_intranet} = $av ? $av->lib : '';
1079              $data->{location_opac}     = $av ? $av->opac_description : '';
1080              push @results, $data;
1081         }
1082         return @results;
1083 }
1084
1085 =head2 GetHostItemsInfo
1086
1087     $hostiteminfo = GetHostItemsInfo($hostfield);
1088     Returns the iteminfo for items linked to records via a host field
1089
1090 =cut
1091
1092 sub GetHostItemsInfo {
1093     my ($record) = @_;
1094     my @returnitemsInfo;
1095
1096     if( !C4::Context->preference('EasyAnalyticalRecords') ) {
1097         return @returnitemsInfo;
1098     }
1099
1100     my @fields;
1101     if( C4::Context->preference('marcflavour') eq 'MARC21' ||
1102       C4::Context->preference('marcflavour') eq 'NORMARC') {
1103         @fields = $record->field('773');
1104     } elsif( C4::Context->preference('marcflavour') eq 'UNIMARC') {
1105         @fields = $record->field('461');
1106     }
1107
1108     foreach my $hostfield ( @fields ) {
1109         my $hostbiblionumber = $hostfield->subfield("0");
1110         my $linkeditemnumber = $hostfield->subfield("9");
1111         my @hostitemInfos = GetItemsInfo($hostbiblionumber);
1112         foreach my $hostitemInfo (@hostitemInfos) {
1113             if( $hostitemInfo->{itemnumber} eq $linkeditemnumber ) {
1114                 push @returnitemsInfo, $hostitemInfo;
1115                 last;
1116             }
1117         }
1118     }
1119     return @returnitemsInfo;
1120 }
1121
1122 =head2 get_hostitemnumbers_of
1123
1124   my @itemnumbers_of = get_hostitemnumbers_of($biblionumber);
1125
1126 Given a biblionumber, return the list of corresponding itemnumbers that are linked to it via host fields
1127
1128 Return a reference on a hash where key is a biblionumber and values are
1129 references on array of itemnumbers.
1130
1131 =cut
1132
1133
1134 sub get_hostitemnumbers_of {
1135     my ($biblionumber) = @_;
1136
1137     if( !C4::Context->preference('EasyAnalyticalRecords') ) {
1138         return ();
1139     }
1140
1141     my $marcrecord = C4::Biblio::GetMarcBiblio({ biblionumber => $biblionumber });
1142     return unless $marcrecord;
1143
1144     my ( @returnhostitemnumbers, $tag, $biblio_s, $item_s );
1145
1146     my $marcflavor = C4::Context->preference('marcflavour');
1147     if ( $marcflavor eq 'MARC21' || $marcflavor eq 'NORMARC' ) {
1148         $tag      = '773';
1149         $biblio_s = '0';
1150         $item_s   = '9';
1151     }
1152     elsif ( $marcflavor eq 'UNIMARC' ) {
1153         $tag      = '461';
1154         $biblio_s = '0';
1155         $item_s   = '9';
1156     }
1157
1158     foreach my $hostfield ( $marcrecord->field($tag) ) {
1159         my $hostbiblionumber = $hostfield->subfield($biblio_s);
1160         next unless $hostbiblionumber; # have tag, don't have $biblio_s subfield
1161         my $linkeditemnumber = $hostfield->subfield($item_s);
1162         if ( ! $linkeditemnumber ) {
1163             warn "ERROR biblionumber $biblionumber has 773^0, but doesn't have 9";
1164             next;
1165         }
1166         my $is_from_biblio = Koha::Items->search({ itemnumber => $linkeditemnumber, biblionumber => $hostbiblionumber });
1167         push @returnhostitemnumbers, $linkeditemnumber
1168           if $is_from_biblio;
1169     }
1170
1171     return @returnhostitemnumbers;
1172 }
1173
1174 =head2 GetHiddenItemnumbers
1175
1176     my @itemnumbers_to_hide = GetHiddenItemnumbers({ items => \@items, borcat => $category });
1177
1178 Given a list of items it checks which should be hidden from the OPAC given
1179 the current configuration. Returns a list of itemnumbers corresponding to
1180 those that should be hidden. Optionally takes a borcat parameter for certain borrower types
1181 to be excluded
1182
1183 =cut
1184
1185 sub GetHiddenItemnumbers {
1186     my $params = shift;
1187     my $items = $params->{items};
1188     if (my $exceptions = C4::Context->preference('OpacHiddenItemsExceptions') and $params->{'borcat'}){
1189         foreach my $except (split(/\|/, $exceptions)){
1190             if ($params->{'borcat'} eq $except){
1191                 return; # we don't hide anything for this borrower category
1192             }
1193         }
1194     }
1195     my @resultitems;
1196
1197     my $yaml = C4::Context->preference('OpacHiddenItems');
1198     return () if (! $yaml =~ /\S/ );
1199     $yaml = "$yaml\n\n"; # YAML is anal on ending \n. Surplus does not hurt
1200     my $hidingrules;
1201     eval {
1202         $hidingrules = YAML::Load($yaml);
1203     };
1204     if ($@) {
1205         warn "Unable to parse OpacHiddenItems syspref : $@";
1206         return ();
1207     }
1208     my $dbh = C4::Context->dbh;
1209
1210     # For each item
1211     foreach my $item (@$items) {
1212
1213         # We check each rule
1214         foreach my $field (keys %$hidingrules) {
1215             my $val;
1216             if (exists $item->{$field}) {
1217                 $val = $item->{$field};
1218             }
1219             else {
1220                 my $query = "SELECT $field from items where itemnumber = ?";
1221                 $val = $dbh->selectrow_array($query, undef, $item->{'itemnumber'});
1222             }
1223             $val = '' unless defined $val;
1224
1225             # If the results matches the values in the yaml file
1226             if (any { $val eq $_ } @{$hidingrules->{$field}}) {
1227
1228                 # We add the itemnumber to the list
1229                 push @resultitems, $item->{'itemnumber'};
1230
1231                 # If at least one rule matched for an item, no need to test the others
1232                 last;
1233             }
1234         }
1235     }
1236     return @resultitems;
1237 }
1238
1239 =head1 LIMITED USE FUNCTIONS
1240
1241 The following functions, while part of the public API,
1242 are not exported.  This is generally because they are
1243 meant to be used by only one script for a specific
1244 purpose, and should not be used in any other context
1245 without careful thought.
1246
1247 =cut
1248
1249 =head2 GetMarcItem
1250
1251   my $item_marc = GetMarcItem($biblionumber, $itemnumber);
1252
1253 Returns MARC::Record of the item passed in parameter.
1254 This function is meant for use only in C<cataloguing/additem.pl>,
1255 where it is needed to support that script's MARC-like
1256 editor.
1257
1258 =cut
1259
1260 sub GetMarcItem {
1261     my ( $biblionumber, $itemnumber ) = @_;
1262
1263     # GetMarcItem has been revised so that it does the following:
1264     #  1. Gets the item information from the items table.
1265     #  2. Converts it to a MARC field for storage in the bib record.
1266     #
1267     # The previous behavior was:
1268     #  1. Get the bib record.
1269     #  2. Return the MARC tag corresponding to the item record.
1270     #
1271     # The difference is that one treats the items row as authoritative,
1272     # while the other treats the MARC representation as authoritative
1273     # under certain circumstances.
1274
1275     my $item = Koha::Items->find($itemnumber) or return;
1276
1277     # Tack on 'items.' prefix to column names so that C4::Biblio::TransformKohaToMarc will work.
1278     # Also, don't emit a subfield if the underlying field is blank.
1279
1280     return Item2Marc($item->unblessed, $biblionumber);
1281
1282 }
1283 sub Item2Marc {
1284         my ($itemrecord,$biblionumber)=@_;
1285     my $mungeditem = { 
1286         map {  
1287             defined($itemrecord->{$_}) && $itemrecord->{$_} ne '' ? ("items.$_" => $itemrecord->{$_}) : ()  
1288         } keys %{ $itemrecord } 
1289     };
1290     my $framework = C4::Biblio::GetFrameworkCode( $biblionumber );
1291     my $itemmarc = C4::Biblio::TransformKohaToMarc( $mungeditem ); # Bug 21774: no_split parameter removed to allow cloned subfields
1292     my ( $itemtag, $itemsubfield ) = C4::Biblio::GetMarcFromKohaField(
1293         "items.itemnumber", $framework,
1294     );
1295
1296     my $unlinked_item_subfields = _parse_unlinked_item_subfields_from_xml($mungeditem->{'items.more_subfields_xml'});
1297     if (defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1) {
1298                 foreach my $field ($itemmarc->field($itemtag)){
1299             $field->add_subfields(@$unlinked_item_subfields);
1300         }
1301     }
1302         return $itemmarc;
1303 }
1304
1305 =head1 PRIVATE FUNCTIONS AND VARIABLES
1306
1307 The following functions are not meant to be called
1308 directly, but are documented in order to explain
1309 the inner workings of C<C4::Items>.
1310
1311 =cut
1312
1313 =head2 %derived_columns
1314
1315 This hash keeps track of item columns that
1316 are strictly derived from other columns in
1317 the item record and are not meant to be set
1318 independently.
1319
1320 Each key in the hash should be the name of a
1321 column (as named by TransformMarcToKoha).  Each
1322 value should be hashref whose keys are the
1323 columns on which the derived column depends.  The
1324 hashref should also contain a 'BUILDER' key
1325 that is a reference to a sub that calculates
1326 the derived value.
1327
1328 =cut
1329
1330 my %derived_columns = (
1331     'items.cn_sort' => {
1332         'itemcallnumber' => 1,
1333         'items.cn_source' => 1,
1334         'BUILDER' => \&_calc_items_cn_sort,
1335     }
1336 );
1337
1338 =head2 _set_derived_columns_for_add 
1339
1340   _set_derived_column_for_add($item);
1341
1342 Given an item hash representing a new item to be added,
1343 calculate any derived columns.  Currently the only
1344 such column is C<items.cn_sort>.
1345
1346 =cut
1347
1348 sub _set_derived_columns_for_add {
1349     my $item = shift;
1350
1351     foreach my $column (keys %derived_columns) {
1352         my $builder = $derived_columns{$column}->{'BUILDER'};
1353         my $source_values = {};
1354         foreach my $source_column (keys %{ $derived_columns{$column} }) {
1355             next if $source_column eq 'BUILDER';
1356             $source_values->{$source_column} = $item->{$source_column};
1357         }
1358         $builder->($item, $source_values);
1359     }
1360 }
1361
1362 =head2 _set_derived_columns_for_mod 
1363
1364   _set_derived_column_for_mod($item);
1365
1366 Given an item hash representing a new item to be modified.
1367 calculate any derived columns.  Currently the only
1368 such column is C<items.cn_sort>.
1369
1370 This routine differs from C<_set_derived_columns_for_add>
1371 in that it needs to handle partial item records.  In other
1372 words, the caller of C<ModItem> may have supplied only one
1373 or two columns to be changed, so this function needs to
1374 determine whether any of the columns to be changed affect
1375 any of the derived columns.  Also, if a derived column
1376 depends on more than one column, but the caller is not
1377 changing all of then, this routine retrieves the unchanged
1378 values from the database in order to ensure a correct
1379 calculation.
1380
1381 =cut
1382
1383 sub _set_derived_columns_for_mod {
1384     my $item = shift;
1385
1386     foreach my $column (keys %derived_columns) {
1387         my $builder = $derived_columns{$column}->{'BUILDER'};
1388         my $source_values = {};
1389         my %missing_sources = ();
1390         my $must_recalc = 0;
1391         foreach my $source_column (keys %{ $derived_columns{$column} }) {
1392             next if $source_column eq 'BUILDER';
1393             if (exists $item->{$source_column}) {
1394                 $must_recalc = 1;
1395                 $source_values->{$source_column} = $item->{$source_column};
1396             } else {
1397                 $missing_sources{$source_column} = 1;
1398             }
1399         }
1400         if ($must_recalc) {
1401             foreach my $source_column (keys %missing_sources) {
1402                 $source_values->{$source_column} = _get_single_item_column($source_column, $item->{'itemnumber'});
1403             }
1404             $builder->($item, $source_values);
1405         }
1406     }
1407 }
1408
1409 =head2 _do_column_fixes_for_mod
1410
1411   _do_column_fixes_for_mod($item);
1412
1413 Given an item hashref containing one or more
1414 columns to modify, fix up certain values.
1415 Specifically, set to 0 any passed value
1416 of C<notforloan>, C<damaged>, C<itemlost>, or
1417 C<withdrawn> that is either undefined or
1418 contains the empty string.
1419
1420 =cut
1421
1422 sub _do_column_fixes_for_mod {
1423     my $item = shift;
1424
1425     if (exists $item->{'notforloan'} and
1426         (not defined $item->{'notforloan'} or $item->{'notforloan'} eq '')) {
1427         $item->{'notforloan'} = 0;
1428     }
1429     if (exists $item->{'damaged'} and
1430         (not defined $item->{'damaged'} or $item->{'damaged'} eq '')) {
1431         $item->{'damaged'} = 0;
1432     }
1433     if (exists $item->{'itemlost'} and
1434         (not defined $item->{'itemlost'} or $item->{'itemlost'} eq '')) {
1435         $item->{'itemlost'} = 0;
1436     }
1437     if (exists $item->{'withdrawn'} and
1438         (not defined $item->{'withdrawn'} or $item->{'withdrawn'} eq '')) {
1439         $item->{'withdrawn'} = 0;
1440     }
1441     if (
1442         exists $item->{location}
1443         and ( !defined $item->{location}
1444             || ( $item->{location} ne 'CART' and $item->{location} ne 'PROC' ) )
1445         and not $item->{permanent_location}
1446       )
1447     {
1448         $item->{'permanent_location'} = $item->{'location'};
1449     }
1450     if (exists $item->{'timestamp'}) {
1451         delete $item->{'timestamp'};
1452     }
1453 }
1454
1455 =head2 _get_single_item_column
1456
1457   _get_single_item_column($column, $itemnumber);
1458
1459 Retrieves the value of a single column from an C<items>
1460 row specified by C<$itemnumber>.
1461
1462 =cut
1463
1464 sub _get_single_item_column {
1465     my $column = shift;
1466     my $itemnumber = shift;
1467     
1468     my $dbh = C4::Context->dbh;
1469     my $sth = $dbh->prepare("SELECT $column FROM items WHERE itemnumber = ?");
1470     $sth->execute($itemnumber);
1471     my ($value) = $sth->fetchrow();
1472     return $value; 
1473 }
1474
1475 =head2 _calc_items_cn_sort
1476
1477   _calc_items_cn_sort($item, $source_values);
1478
1479 Helper routine to calculate C<items.cn_sort>.
1480
1481 =cut
1482
1483 sub _calc_items_cn_sort {
1484     my $item = shift;
1485     my $source_values = shift;
1486
1487     $item->{'items.cn_sort'} = GetClassSort($source_values->{'items.cn_source'}, $source_values->{'itemcallnumber'}, "");
1488 }
1489
1490 =head2 _koha_new_item
1491
1492   my ($itemnumber,$error) = _koha_new_item( $item, $barcode );
1493
1494 Perform the actual insert into the C<items> table.
1495
1496 =cut
1497
1498 sub _koha_new_item {
1499     my ( $item, $barcode ) = @_;
1500     my $dbh=C4::Context->dbh;  
1501     my $error;
1502     $item->{permanent_location} //= $item->{location};
1503     _mod_item_dates( $item );
1504     my $query =
1505            "INSERT INTO items SET
1506             biblionumber        = ?,
1507             biblioitemnumber    = ?,
1508             barcode             = ?,
1509             dateaccessioned     = ?,
1510             booksellerid        = ?,
1511             homebranch          = ?,
1512             price               = ?,
1513             replacementprice    = ?,
1514             replacementpricedate = ?,
1515             datelastborrowed    = ?,
1516             datelastseen        = ?,
1517             stack               = ?,
1518             notforloan          = ?,
1519             damaged             = ?,
1520             itemlost            = ?,
1521             withdrawn           = ?,
1522             itemcallnumber      = ?,
1523             coded_location_qualifier = ?,
1524             restricted          = ?,
1525             itemnotes           = ?,
1526             itemnotes_nonpublic = ?,
1527             holdingbranch       = ?,
1528             location            = ?,
1529             permanent_location  = ?,
1530             onloan              = ?,
1531             issues              = ?,
1532             renewals            = ?,
1533             reserves            = ?,
1534             cn_source           = ?,
1535             cn_sort             = ?,
1536             ccode               = ?,
1537             itype               = ?,
1538             materials           = ?,
1539             uri                 = ?,
1540             enumchron           = ?,
1541             more_subfields_xml  = ?,
1542             copynumber          = ?,
1543             stocknumber         = ?,
1544             new_status          = ?
1545           ";
1546     my $sth = $dbh->prepare($query);
1547     my $today = output_pref({ dt => dt_from_string, dateformat => 'iso', dateonly => 1 });
1548    $sth->execute(
1549             $item->{'biblionumber'},
1550             $item->{'biblioitemnumber'},
1551             $barcode,
1552             $item->{'dateaccessioned'},
1553             $item->{'booksellerid'},
1554             $item->{'homebranch'},
1555             $item->{'price'},
1556             $item->{'replacementprice'},
1557             $item->{'replacementpricedate'} || $today,
1558             $item->{datelastborrowed},
1559             $item->{datelastseen} || $today,
1560             $item->{stack},
1561             $item->{'notforloan'},
1562             $item->{'damaged'},
1563             $item->{'itemlost'},
1564             $item->{'withdrawn'},
1565             $item->{'itemcallnumber'},
1566             $item->{'coded_location_qualifier'},
1567             $item->{'restricted'},
1568             $item->{'itemnotes'},
1569             $item->{'itemnotes_nonpublic'},
1570             $item->{'holdingbranch'},
1571             $item->{'location'},
1572             $item->{'permanent_location'},
1573             $item->{'onloan'},
1574             $item->{'issues'},
1575             $item->{'renewals'},
1576             $item->{'reserves'},
1577             $item->{'items.cn_source'},
1578             $item->{'items.cn_sort'},
1579             $item->{'ccode'},
1580             $item->{'itype'},
1581             $item->{'materials'},
1582             $item->{'uri'},
1583             $item->{'enumchron'},
1584             $item->{'more_subfields_xml'},
1585             $item->{'copynumber'},
1586             $item->{'stocknumber'},
1587             $item->{'new_status'},
1588     );
1589
1590     my $itemnumber;
1591     if ( defined $sth->errstr ) {
1592         $error.="ERROR in _koha_new_item $query".$sth->errstr;
1593     }
1594     else {
1595         $itemnumber = $dbh->{'mysql_insertid'};
1596     }
1597
1598     return ( $itemnumber, $error );
1599 }
1600
1601 =head2 MoveItemFromBiblio
1602
1603   MoveItemFromBiblio($itenumber, $frombiblio, $tobiblio);
1604
1605 Moves an item from a biblio to another
1606
1607 Returns undef if the move failed or the biblionumber of the destination record otherwise
1608
1609 =cut
1610
1611 sub MoveItemFromBiblio {
1612     my ($itemnumber, $frombiblio, $tobiblio) = @_;
1613     my $dbh = C4::Context->dbh;
1614     my ( $tobiblioitem ) = $dbh->selectrow_array(q|
1615         SELECT biblioitemnumber
1616         FROM biblioitems
1617         WHERE biblionumber = ?
1618     |, undef, $tobiblio );
1619     my $return = $dbh->do(q|
1620         UPDATE items
1621         SET biblioitemnumber = ?,
1622             biblionumber = ?
1623         WHERE itemnumber = ?
1624             AND biblionumber = ?
1625     |, undef, $tobiblioitem, $tobiblio, $itemnumber, $frombiblio );
1626     if ($return == 1) {
1627         ModZebra( $tobiblio, "specialUpdate", "biblioserver" );
1628         ModZebra( $frombiblio, "specialUpdate", "biblioserver" );
1629             # Checking if the item we want to move is in an order 
1630         require C4::Acquisition;
1631         my $order = C4::Acquisition::GetOrderFromItemnumber($itemnumber);
1632             if ($order) {
1633                     # Replacing the biblionumber within the order if necessary
1634                     $order->{'biblionumber'} = $tobiblio;
1635                 C4::Acquisition::ModOrder($order);
1636             }
1637
1638         # Update reserves, hold_fill_targets, tmp_holdsqueue and linktracker tables
1639         for my $table_name ( qw( reserves hold_fill_targets tmp_holdsqueue linktracker ) ) {
1640             $dbh->do( qq|
1641                 UPDATE $table_name
1642                 SET biblionumber = ?
1643                 WHERE itemnumber = ?
1644             |, undef, $tobiblio, $itemnumber );
1645         }
1646         return $tobiblio;
1647         }
1648     return;
1649 }
1650
1651 =head2 ItemSafeToDelete
1652
1653    ItemSafeToDelete( $biblionumber, $itemnumber);
1654
1655 Exported function (core API) for checking whether an item record is safe to delete.
1656
1657 returns 1 if the item is safe to delete,
1658
1659 "book_on_loan" if the item is checked out,
1660
1661 "not_same_branch" if the item is blocked by independent branches,
1662
1663 "book_reserved" if the there are holds aganst the item, or
1664
1665 "linked_analytics" if the item has linked analytic records.
1666
1667 =cut
1668
1669 sub ItemSafeToDelete {
1670     my ( $biblionumber, $itemnumber ) = @_;
1671     my $status;
1672     my $dbh = C4::Context->dbh;
1673
1674     my $error;
1675
1676     my $countanalytics = GetAnalyticsCount($itemnumber);
1677
1678     my $item = Koha::Items->find($itemnumber) or return;
1679
1680     if ($item->checkout) {
1681         $status = "book_on_loan";
1682     }
1683     elsif ( defined C4::Context->userenv
1684         and !C4::Context->IsSuperLibrarian()
1685         and C4::Context->preference("IndependentBranches")
1686         and ( C4::Context->userenv->{branch} ne $item->homebranch ) )
1687     {
1688         $status = "not_same_branch";
1689     }
1690     else {
1691         # check it doesn't have a waiting reserve
1692         my $sth = $dbh->prepare(
1693             q{
1694             SELECT COUNT(*) FROM reserves
1695             WHERE (found = 'W' OR found = 'T')
1696             AND itemnumber = ?
1697         }
1698         );
1699         $sth->execute($itemnumber);
1700         my ($reserve) = $sth->fetchrow;
1701         if ($reserve) {
1702             $status = "book_reserved";
1703         }
1704         elsif ( $countanalytics > 0 ) {
1705             $status = "linked_analytics";
1706         }
1707         else {
1708             $status = 1;
1709         }
1710     }
1711     return $status;
1712 }
1713
1714 =head2 DelItemCheck
1715
1716    DelItemCheck( $biblionumber, $itemnumber);
1717
1718 Exported function (core API) for deleting an item record in Koha if there no current issue.
1719
1720 DelItemCheck wraps ItemSafeToDelete around DelItem.
1721
1722 =cut
1723
1724 sub DelItemCheck {
1725     my ( $biblionumber, $itemnumber ) = @_;
1726     my $status = ItemSafeToDelete( $biblionumber, $itemnumber );
1727
1728     if ( $status == 1 ) {
1729         DelItem(
1730             {
1731                 biblionumber => $biblionumber,
1732                 itemnumber   => $itemnumber
1733             }
1734         );
1735     }
1736     return $status;
1737 }
1738
1739 =head2 _koha_modify_item
1740
1741   my ($itemnumber,$error) =_koha_modify_item( $item );
1742
1743 Perform the actual update of the C<items> row.  Note that this
1744 routine accepts a hashref specifying the columns to update.
1745
1746 =cut
1747
1748 sub _koha_modify_item {
1749     my ( $item ) = @_;
1750     my $dbh=C4::Context->dbh;  
1751     my $error;
1752
1753     my $query = "UPDATE items SET ";
1754     my @bind;
1755     _mod_item_dates( $item );
1756     for my $key ( keys %$item ) {
1757         next if ( $key eq 'itemnumber' );
1758         $query.="$key=?,";
1759         push @bind, $item->{$key};
1760     }
1761     $query =~ s/,$//;
1762     $query .= " WHERE itemnumber=?";
1763     push @bind, $item->{'itemnumber'};
1764     my $sth = $dbh->prepare($query);
1765     $sth->execute(@bind);
1766     if ( $sth->err ) {
1767         $error.="ERROR in _koha_modify_item $query: ".$sth->errstr;
1768         warn $error;
1769     }
1770     return ($item->{'itemnumber'},$error);
1771 }
1772
1773 sub _mod_item_dates { # date formatting for date fields in item hash
1774     my ( $item ) = @_;
1775     return if !$item || ref($item) ne 'HASH';
1776
1777     my @keys = grep
1778         { $_ =~ /^onloan$|^date|date$|datetime$/ }
1779         keys %$item;
1780     # Incl. dateaccessioned,replacementpricedate,datelastborrowed,datelastseen
1781     # NOTE: We do not (yet) have items fields ending with datetime
1782     # Fields with _on$ have been handled already
1783
1784     foreach my $key ( @keys ) {
1785         next if !defined $item->{$key}; # skip undefs
1786         my $dt = eval { dt_from_string( $item->{$key} ) };
1787             # eval: dt_from_string will die on us if we pass illegal dates
1788
1789         my $newstr;
1790         if( defined $dt  && ref($dt) eq 'DateTime' ) {
1791             if( $key =~ /datetime/ ) {
1792                 $newstr = DateTime::Format::MySQL->format_datetime($dt);
1793             } else {
1794                 $newstr = DateTime::Format::MySQL->format_date($dt);
1795             }
1796         }
1797         $item->{$key} = $newstr; # might be undef to clear garbage
1798     }
1799 }
1800
1801 =head2 _koha_delete_item
1802
1803   _koha_delete_item( $itemnum );
1804
1805 Internal function to delete an item record from the koha tables
1806
1807 =cut
1808
1809 sub _koha_delete_item {
1810     my ( $itemnum ) = @_;
1811
1812     my $dbh = C4::Context->dbh;
1813     # save the deleted item to deleteditems table
1814     my $sth = $dbh->prepare("SELECT * FROM items WHERE itemnumber=?");
1815     $sth->execute($itemnum);
1816     my $data = $sth->fetchrow_hashref();
1817
1818     # There is no item to delete
1819     return 0 unless $data;
1820
1821     my $query = "INSERT INTO deleteditems SET ";
1822     my @bind  = ();
1823     foreach my $key ( keys %$data ) {
1824         next if ( $key eq 'timestamp' ); # timestamp will be set by db
1825         $query .= "$key = ?,";
1826         push( @bind, $data->{$key} );
1827     }
1828     $query =~ s/\,$//;
1829     $sth = $dbh->prepare($query);
1830     $sth->execute(@bind);
1831
1832     # delete from items table
1833     $sth = $dbh->prepare("DELETE FROM items WHERE itemnumber=?");
1834     my $deleted = $sth->execute($itemnum);
1835     return ( $deleted == 1 ) ? 1 : 0;
1836 }
1837
1838 =head2 _marc_from_item_hash
1839
1840   my $item_marc = _marc_from_item_hash($item, $frameworkcode[, $unlinked_item_subfields]);
1841
1842 Given an item hash representing a complete item record,
1843 create a C<MARC::Record> object containing an embedded
1844 tag representing that item.
1845
1846 The third, optional parameter C<$unlinked_item_subfields> is
1847 an arrayref of subfields (not mapped to C<items> fields per the
1848 framework) to be added to the MARC representation
1849 of the item.
1850
1851 =cut
1852
1853 sub _marc_from_item_hash {
1854     my $item = shift;
1855     my $frameworkcode = shift;
1856     my $unlinked_item_subfields;
1857     if (@_) {
1858         $unlinked_item_subfields = shift;
1859     }
1860    
1861     # Tack on 'items.' prefix to column names so lookup from MARC frameworks will work
1862     # Also, don't emit a subfield if the underlying field is blank.
1863     my $mungeditem = { map {  (defined($item->{$_}) and $item->{$_} ne '') ? 
1864                                 (/^items\./ ? ($_ => $item->{$_}) : ("items.$_" => $item->{$_})) 
1865                                 : ()  } keys %{ $item } }; 
1866
1867     my $item_marc = MARC::Record->new();
1868     foreach my $item_field ( keys %{$mungeditem} ) {
1869         my ( $tag, $subfield ) = C4::Biblio::GetMarcFromKohaField( $item_field );
1870         next unless defined $tag and defined $subfield;    # skip if not mapped to MARC field
1871         my @values = split(/\s?\|\s?/, $mungeditem->{$item_field}, -1);
1872         foreach my $value (@values){
1873             if ( my $field = $item_marc->field($tag) ) {
1874                     $field->add_subfields( $subfield => $value );
1875             } else {
1876                 my $add_subfields = [];
1877                 if (defined $unlinked_item_subfields and ref($unlinked_item_subfields) eq 'ARRAY' and $#$unlinked_item_subfields > -1) {
1878                     $add_subfields = $unlinked_item_subfields;
1879             }
1880             $item_marc->add_fields( $tag, " ", " ", $subfield => $value, @$add_subfields );
1881             }
1882         }
1883     }
1884
1885     return $item_marc;
1886 }
1887
1888 =head2 _repack_item_errors
1889
1890 Add an error message hash generated by C<CheckItemPreSave>
1891 to a list of errors.
1892
1893 =cut
1894
1895 sub _repack_item_errors {
1896     my $item_sequence_num = shift;
1897     my $item_ref = shift;
1898     my $error_ref = shift;
1899
1900     my @repacked_errors = ();
1901
1902     foreach my $error_code (sort keys %{ $error_ref }) {
1903         my $repacked_error = {};
1904         $repacked_error->{'item_sequence'} = $item_sequence_num;
1905         $repacked_error->{'item_barcode'} = exists($item_ref->{'barcode'}) ? $item_ref->{'barcode'} : '';
1906         $repacked_error->{'error_code'} = $error_code;
1907         $repacked_error->{'error_information'} = $error_ref->{$error_code};
1908         push @repacked_errors, $repacked_error;
1909     } 
1910
1911     return @repacked_errors;
1912 }
1913
1914 =head2 _get_unlinked_item_subfields
1915
1916   my $unlinked_item_subfields = _get_unlinked_item_subfields($original_item_marc, $frameworkcode);
1917
1918 =cut
1919
1920 sub _get_unlinked_item_subfields {
1921     my $original_item_marc = shift;
1922     my $frameworkcode = shift;
1923
1924     my $marcstructure = C4::Biblio::GetMarcStructure(1, $frameworkcode, { unsafe => 1 });
1925
1926     # assume that this record has only one field, and that that
1927     # field contains only the item information
1928     my $subfields = [];
1929     my @fields = $original_item_marc->fields();
1930     if ($#fields > -1) {
1931         my $field = $fields[0];
1932             my $tag = $field->tag();
1933         foreach my $subfield ($field->subfields()) {
1934             if (defined $subfield->[1] and
1935                 $subfield->[1] ne '' and
1936                 !$marcstructure->{$tag}->{$subfield->[0]}->{'kohafield'}) {
1937                 push @$subfields, $subfield->[0] => $subfield->[1];
1938             }
1939         }
1940     }
1941     return $subfields;
1942 }
1943
1944 =head2 _get_unlinked_subfields_xml
1945
1946   my $unlinked_subfields_xml = _get_unlinked_subfields_xml($unlinked_item_subfields);
1947
1948 =cut
1949
1950 sub _get_unlinked_subfields_xml {
1951     my $unlinked_item_subfields = shift;
1952
1953     my $xml;
1954     if (defined $unlinked_item_subfields and ref($unlinked_item_subfields) eq 'ARRAY' and $#$unlinked_item_subfields > -1) {
1955         my $marc = MARC::Record->new();
1956         # use of tag 999 is arbitrary, and doesn't need to match the item tag
1957         # used in the framework
1958         $marc->append_fields(MARC::Field->new('999', ' ', ' ', @$unlinked_item_subfields));
1959         $marc->encoding("UTF-8");    
1960         $xml = $marc->as_xml("USMARC");
1961     }
1962
1963     return $xml;
1964 }
1965
1966 =head2 _parse_unlinked_item_subfields_from_xml
1967
1968   my $unlinked_item_subfields = _parse_unlinked_item_subfields_from_xml($whole_item->{'more_subfields_xml'}):
1969
1970 =cut
1971
1972 sub  _parse_unlinked_item_subfields_from_xml {
1973     my $xml = shift;
1974     require C4::Charset;
1975     return unless defined $xml and $xml ne "";
1976     my $marc = MARC::Record->new_from_xml(C4::Charset::StripNonXmlChars($xml),'UTF-8');
1977     my $unlinked_subfields = [];
1978     my @fields = $marc->fields();
1979     if ($#fields > -1) {
1980         foreach my $subfield ($fields[0]->subfields()) {
1981             push @$unlinked_subfields, $subfield->[0] => $subfield->[1];
1982         }
1983     }
1984     return $unlinked_subfields;
1985 }
1986
1987 =head2 GetAnalyticsCount
1988
1989   $count= &GetAnalyticsCount($itemnumber)
1990
1991 counts Usage of itemnumber in Analytical bibliorecords. 
1992
1993 =cut
1994
1995 sub GetAnalyticsCount {
1996     my ($itemnumber) = @_;
1997
1998     ### ZOOM search here
1999     my $query;
2000     $query= "hi=".$itemnumber;
2001     my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
2002     my ($err,$res,$result) = $searcher->simple_search_compat($query,0,10);
2003     return ($result);
2004 }
2005
2006 sub _SearchItems_build_where_fragment {
2007     my ($filter) = @_;
2008
2009     my $dbh = C4::Context->dbh;
2010
2011     my $where_fragment;
2012     if (exists($filter->{conjunction})) {
2013         my (@where_strs, @where_args);
2014         foreach my $f (@{ $filter->{filters} }) {
2015             my $fragment = _SearchItems_build_where_fragment($f);
2016             if ($fragment) {
2017                 push @where_strs, $fragment->{str};
2018                 push @where_args, @{ $fragment->{args} };
2019             }
2020         }
2021         my $where_str = '';
2022         if (@where_strs) {
2023             $where_str = '(' . join (' ' . $filter->{conjunction} . ' ', @where_strs) . ')';
2024             $where_fragment = {
2025                 str => $where_str,
2026                 args => \@where_args,
2027             };
2028         }
2029     } else {
2030         my @columns = Koha::Database->new()->schema()->resultset('Item')->result_source->columns;
2031         push @columns, Koha::Database->new()->schema()->resultset('Biblio')->result_source->columns;
2032         push @columns, Koha::Database->new()->schema()->resultset('Biblioitem')->result_source->columns;
2033         my @operators = qw(= != > < >= <= like);
2034         my $field = $filter->{field} // q{};
2035         if ( (0 < grep { $_ eq $field } @columns) or (substr($field, 0, 5) eq 'marc:') ) {
2036             my $op = $filter->{operator};
2037             my $query = $filter->{query};
2038
2039             if (!$op or (0 == grep { $_ eq $op } @operators)) {
2040                 $op = '='; # default operator
2041             }
2042
2043             my $column;
2044             if ($field =~ /^marc:(\d{3})(?:\$(\w))?$/) {
2045                 my $marcfield = $1;
2046                 my $marcsubfield = $2;
2047                 my ($kohafield) = $dbh->selectrow_array(q|
2048                     SELECT kohafield FROM marc_subfield_structure
2049                     WHERE tagfield=? AND tagsubfield=? AND frameworkcode=''
2050                 |, undef, $marcfield, $marcsubfield);
2051
2052                 if ($kohafield) {
2053                     $column = $kohafield;
2054                 } else {
2055                     # MARC field is not linked to a DB field so we need to use
2056                     # ExtractValue on marcxml from biblio_metadata or
2057                     # items.more_subfields_xml, depending on the MARC field.
2058                     my $xpath;
2059                     my $sqlfield;
2060                     my ($itemfield) = C4::Biblio::GetMarcFromKohaField('items.itemnumber');
2061                     if ($marcfield eq $itemfield) {
2062                         $sqlfield = 'more_subfields_xml';
2063                         $xpath = '//record/datafield/subfield[@code="' . $marcsubfield . '"]';
2064                     } else {
2065                         $sqlfield = 'metadata'; # From biblio_metadata
2066                         if ($marcfield < 10) {
2067                             $xpath = "//record/controlfield[\@tag=\"$marcfield\"]";
2068                         } else {
2069                             $xpath = "//record/datafield[\@tag=\"$marcfield\"]/subfield[\@code=\"$marcsubfield\"]";
2070                         }
2071                     }
2072                     $column = "ExtractValue($sqlfield, '$xpath')";
2073                 }
2074             } elsif ($field eq 'issues') {
2075                 # Consider NULL as 0 for issues count
2076                 $column = 'COALESCE(issues,0)';
2077             } else {
2078                 $column = $field;
2079             }
2080
2081             if (ref $query eq 'ARRAY') {
2082                 if ($op eq '=') {
2083                     $op = 'IN';
2084                 } elsif ($op eq '!=') {
2085                     $op = 'NOT IN';
2086                 }
2087                 $where_fragment = {
2088                     str => "$column $op (" . join (',', ('?') x @$query) . ")",
2089                     args => $query,
2090                 };
2091             } else {
2092                 $where_fragment = {
2093                     str => "$column $op ?",
2094                     args => [ $query ],
2095                 };
2096             }
2097         }
2098     }
2099
2100     return $where_fragment;
2101 }
2102
2103 =head2 SearchItems
2104
2105     my ($items, $total) = SearchItems($filter, $params);
2106
2107 Perform a search among items
2108
2109 $filter is a reference to a hash which can be a filter, or a combination of filters.
2110
2111 A filter has the following keys:
2112
2113 =over 2
2114
2115 =item * field: the name of a SQL column in table items
2116
2117 =item * query: the value to search in this column
2118
2119 =item * operator: comparison operator. Can be one of = != > < >= <= like
2120
2121 =back
2122
2123 A combination of filters hash the following keys:
2124
2125 =over 2
2126
2127 =item * conjunction: 'AND' or 'OR'
2128
2129 =item * filters: array ref of filters
2130
2131 =back
2132
2133 $params is a reference to a hash that can contain the following parameters:
2134
2135 =over 2
2136
2137 =item * rows: Number of items to return. 0 returns everything (default: 0)
2138
2139 =item * page: Page to return (return items from (page-1)*rows to (page*rows)-1)
2140                (default: 1)
2141
2142 =item * sortby: A SQL column name in items table to sort on
2143
2144 =item * sortorder: 'ASC' or 'DESC'
2145
2146 =back
2147
2148 =cut
2149
2150 sub SearchItems {
2151     my ($filter, $params) = @_;
2152
2153     $filter //= {};
2154     $params //= {};
2155     return unless ref $filter eq 'HASH';
2156     return unless ref $params eq 'HASH';
2157
2158     # Default parameters
2159     $params->{rows} ||= 0;
2160     $params->{page} ||= 1;
2161     $params->{sortby} ||= 'itemnumber';
2162     $params->{sortorder} ||= 'ASC';
2163
2164     my ($where_str, @where_args);
2165     my $where_fragment = _SearchItems_build_where_fragment($filter);
2166     if ($where_fragment) {
2167         $where_str = $where_fragment->{str};
2168         @where_args = @{ $where_fragment->{args} };
2169     }
2170
2171     my $dbh = C4::Context->dbh;
2172     my $query = q{
2173         SELECT SQL_CALC_FOUND_ROWS items.*
2174         FROM items
2175           LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
2176           LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber
2177           LEFT JOIN biblio_metadata ON biblio_metadata.biblionumber = biblio.biblionumber
2178           WHERE 1
2179     };
2180     if (defined $where_str and $where_str ne '') {
2181         $query .= qq{ AND $where_str };
2182     }
2183
2184     $query .= q{ AND biblio_metadata.format = 'marcxml' AND biblio_metadata.schema = ? };
2185     push @where_args, C4::Context->preference('marcflavour');
2186
2187     my @columns = Koha::Database->new()->schema()->resultset('Item')->result_source->columns;
2188     push @columns, Koha::Database->new()->schema()->resultset('Biblio')->result_source->columns;
2189     push @columns, Koha::Database->new()->schema()->resultset('Biblioitem')->result_source->columns;
2190     my $sortby = (0 < grep {$params->{sortby} eq $_} @columns)
2191         ? $params->{sortby} : 'itemnumber';
2192     my $sortorder = (uc($params->{sortorder}) eq 'ASC') ? 'ASC' : 'DESC';
2193     $query .= qq{ ORDER BY $sortby $sortorder };
2194
2195     my $rows = $params->{rows};
2196     my @limit_args;
2197     if ($rows > 0) {
2198         my $offset = $rows * ($params->{page}-1);
2199         $query .= qq { LIMIT ?, ? };
2200         push @limit_args, $offset, $rows;
2201     }
2202
2203     my $sth = $dbh->prepare($query);
2204     my $rv = $sth->execute(@where_args, @limit_args);
2205
2206     return unless ($rv);
2207     my ($total_rows) = $dbh->selectrow_array(q{ SELECT FOUND_ROWS() });
2208
2209     return ($sth->fetchall_arrayref({}), $total_rows);
2210 }
2211
2212
2213 =head1  OTHER FUNCTIONS
2214
2215 =head2 _find_value
2216
2217   ($indicators, $value) = _find_value($tag, $subfield, $record,$encoding);
2218
2219 Find the given $subfield in the given $tag in the given
2220 MARC::Record $record.  If the subfield is found, returns
2221 the (indicators, value) pair; otherwise, (undef, undef) is
2222 returned.
2223
2224 PROPOSITION :
2225 Such a function is used in addbiblio AND additem and serial-edit and maybe could be used in Authorities.
2226 I suggest we export it from this module.
2227
2228 =cut
2229
2230 sub _find_value {
2231     my ( $tagfield, $insubfield, $record, $encoding ) = @_;
2232     my @result;
2233     my $indicator;
2234     if ( $tagfield < 10 ) {
2235         if ( $record->field($tagfield) ) {
2236             push @result, $record->field($tagfield)->data();
2237         } else {
2238             push @result, "";
2239         }
2240     } else {
2241         foreach my $field ( $record->field($tagfield) ) {
2242             my @subfields = $field->subfields();
2243             foreach my $subfield (@subfields) {
2244                 if ( @$subfield[0] eq $insubfield ) {
2245                     push @result, @$subfield[1];
2246                     $indicator = $field->indicator(1) . $field->indicator(2);
2247                 }
2248             }
2249         }
2250     }
2251     return ( $indicator, @result );
2252 }
2253
2254
2255 =head2 PrepareItemrecordDisplay
2256
2257   PrepareItemrecordDisplay($itemrecord,$bibnum,$itemumber,$frameworkcode);
2258
2259 Returns a hash with all the fields for Display a given item data in a template
2260
2261 The $frameworkcode returns the item for the given frameworkcode, ONLY if bibnum is not provided
2262
2263 =cut
2264
2265 sub PrepareItemrecordDisplay {
2266
2267     my ( $bibnum, $itemnum, $defaultvalues, $frameworkcode ) = @_;
2268
2269     my $dbh = C4::Context->dbh;
2270     $frameworkcode = C4::Biblio::GetFrameworkCode($bibnum) if $bibnum;
2271     my ( $itemtagfield, $itemtagsubfield ) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
2272
2273     # Note: $tagslib obtained from GetMarcStructure() in 'unsafe' mode is
2274     # a shared data structure. No plugin (including custom ones) should change
2275     # its contents. See also GetMarcStructure.
2276     my $tagslib = GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } );
2277
2278     # return nothing if we don't have found an existing framework.
2279     return q{} unless $tagslib;
2280     my $itemrecord;
2281     if ($itemnum) {
2282         $itemrecord = C4::Items::GetMarcItem( $bibnum, $itemnum );
2283     }
2284     my @loop_data;
2285
2286     my $branch_limit = C4::Context->userenv ? C4::Context->userenv->{"branch"} : "";
2287     my $query = qq{
2288         SELECT authorised_value,lib FROM authorised_values
2289     };
2290     $query .= qq{
2291         LEFT JOIN authorised_values_branches ON ( id = av_id )
2292     } if $branch_limit;
2293     $query .= qq{
2294         WHERE category = ?
2295     };
2296     $query .= qq{ AND ( branchcode = ? OR branchcode IS NULL )} if $branch_limit;
2297     $query .= qq{ ORDER BY lib};
2298     my $authorised_values_sth = $dbh->prepare( $query );
2299     foreach my $tag ( sort keys %{$tagslib} ) {
2300         if ( $tag ne '' ) {
2301
2302             # loop through each subfield
2303             my $cntsubf;
2304             foreach my $subfield ( sort keys %{ $tagslib->{$tag} } ) {
2305                 next if IsMarcStructureInternal($tagslib->{$tag}{$subfield});
2306                 next unless ( $tagslib->{$tag}->{$subfield}->{'tab'} );
2307                 next if ( $tagslib->{$tag}->{$subfield}->{'tab'} ne "10" );
2308                 my %subfield_data;
2309                 $subfield_data{tag}           = $tag;
2310                 $subfield_data{subfield}      = $subfield;
2311                 $subfield_data{countsubfield} = $cntsubf++;
2312                 $subfield_data{kohafield}     = $tagslib->{$tag}->{$subfield}->{'kohafield'};
2313                 $subfield_data{id}            = "tag_".$tag."_subfield_".$subfield."_".int(rand(1000000));
2314
2315                 #        $subfield_data{marc_lib}=$tagslib->{$tag}->{$subfield}->{lib};
2316                 $subfield_data{marc_lib}   = $tagslib->{$tag}->{$subfield}->{lib};
2317                 $subfield_data{mandatory}  = $tagslib->{$tag}->{$subfield}->{mandatory};
2318                 $subfield_data{repeatable} = $tagslib->{$tag}->{$subfield}->{repeatable};
2319                 $subfield_data{hidden}     = "display:none"
2320                   if ( ( $tagslib->{$tag}->{$subfield}->{hidden} > 4 )
2321                     || ( $tagslib->{$tag}->{$subfield}->{hidden} < -4 ) );
2322                 my ( $x, $defaultvalue );
2323                 if ($itemrecord) {
2324                     ( $x, $defaultvalue ) = _find_value( $tag, $subfield, $itemrecord );
2325                 }
2326                 $defaultvalue = $tagslib->{$tag}->{$subfield}->{defaultvalue} unless $defaultvalue;
2327                 if ( !defined $defaultvalue ) {
2328                     $defaultvalue = q||;
2329                 } else {
2330                     $defaultvalue =~ s/"/&quot;/g;
2331                 }
2332
2333                 my $maxlength = $tagslib->{$tag}->{$subfield}->{maxlength};
2334
2335                 # search for itemcallnumber if applicable
2336                 if ( $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.itemcallnumber'
2337                     && C4::Context->preference('itemcallnumber') && $itemrecord) {
2338                     foreach my $itemcn_pref (split(/,/,C4::Context->preference('itemcallnumber'))){
2339                         my $CNtag      = substr( $itemcn_pref, 0, 3 );
2340                         next unless my $field = $itemrecord->field($CNtag);
2341                         my $CNsubfields = substr( $itemcn_pref, 3 );
2342                         $defaultvalue = $field->as_string( $CNsubfields, ' ');
2343                         last if $defaultvalue;
2344                     }
2345                 }
2346                 if (   $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.itemcallnumber'
2347                     && $defaultvalues
2348                     && $defaultvalues->{'callnumber'} ) {
2349                     if( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield) ){
2350                         # if the item record exists, only use default value if the item has no callnumber
2351                         $defaultvalue = $defaultvalues->{callnumber};
2352                     } elsif ( !$itemrecord and $defaultvalues ) {
2353                         # if the item record *doesn't* exists, always use the default value
2354                         $defaultvalue = $defaultvalues->{callnumber};
2355                     }
2356                 }
2357                 if (   ( $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.holdingbranch' || $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.homebranch' )
2358                     && $defaultvalues
2359                     && $defaultvalues->{'branchcode'} ) {
2360                     if ( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield) ) {
2361                         $defaultvalue = $defaultvalues->{branchcode};
2362                     }
2363                 }
2364                 if (   ( $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.location' )
2365                     && $defaultvalues
2366                     && $defaultvalues->{'location'} ) {
2367
2368                     if ( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield) ) {
2369                         # if the item record exists, only use default value if the item has no locationr
2370                         $defaultvalue = $defaultvalues->{location};
2371                     } elsif ( !$itemrecord and $defaultvalues ) {
2372                         # if the item record *doesn't* exists, always use the default value
2373                         $defaultvalue = $defaultvalues->{location};
2374                     }
2375                 }
2376                 if ( $tagslib->{$tag}->{$subfield}->{authorised_value} ) {
2377                     my @authorised_values;
2378                     my %authorised_lib;
2379
2380                     # builds list, depending on authorised value...
2381                     #---- branch
2382                     if ( $tagslib->{$tag}->{$subfield}->{'authorised_value'} eq "branches" ) {
2383                         if (   ( C4::Context->preference("IndependentBranches") )
2384                             && !C4::Context->IsSuperLibrarian() ) {
2385                             my $sth = $dbh->prepare( "SELECT branchcode,branchname FROM branches WHERE branchcode = ? ORDER BY branchname" );
2386                             $sth->execute( C4::Context->userenv->{branch} );
2387                             push @authorised_values, ""
2388                               unless ( $tagslib->{$tag}->{$subfield}->{mandatory} );
2389                             while ( my ( $branchcode, $branchname ) = $sth->fetchrow_array ) {
2390                                 push @authorised_values, $branchcode;
2391                                 $authorised_lib{$branchcode} = $branchname;
2392                             }
2393                         } else {
2394                             my $sth = $dbh->prepare( "SELECT branchcode,branchname FROM branches ORDER BY branchname" );
2395                             $sth->execute;
2396                             push @authorised_values, ""
2397                               unless ( $tagslib->{$tag}->{$subfield}->{mandatory} );
2398                             while ( my ( $branchcode, $branchname ) = $sth->fetchrow_array ) {
2399                                 push @authorised_values, $branchcode;
2400                                 $authorised_lib{$branchcode} = $branchname;
2401                             }
2402                         }
2403
2404                         $defaultvalue = C4::Context->userenv ? C4::Context->userenv->{branch} : undef;
2405                         if ( $defaultvalues and $defaultvalues->{branchcode} ) {
2406                             $defaultvalue = $defaultvalues->{branchcode};
2407                         }
2408
2409                         #----- itemtypes
2410                     } elsif ( $tagslib->{$tag}->{$subfield}->{authorised_value} eq "itemtypes" ) {
2411                         my $itemtypes = Koha::ItemTypes->search_with_localization;
2412                         push @authorised_values, ""
2413                           unless ( $tagslib->{$tag}->{$subfield}->{mandatory} );
2414                         while ( my $itemtype = $itemtypes->next ) {
2415                             push @authorised_values, $itemtype->itemtype;
2416                             $authorised_lib{$itemtype->itemtype} = $itemtype->translated_description;
2417                         }
2418                         if ($defaultvalues && $defaultvalues->{'itemtype'}) {
2419                             $defaultvalue = $defaultvalues->{'itemtype'};
2420                         }
2421
2422                         #---- class_sources
2423                     } elsif ( $tagslib->{$tag}->{$subfield}->{authorised_value} eq "cn_source" ) {
2424                         push @authorised_values, "" unless ( $tagslib->{$tag}->{$subfield}->{mandatory} );
2425
2426                         my $class_sources = GetClassSources();
2427                         my $default_source = $defaultvalue || C4::Context->preference("DefaultClassificationSource");
2428
2429                         foreach my $class_source (sort keys %$class_sources) {
2430                             next unless $class_sources->{$class_source}->{'used'} or
2431                                         ($class_source eq $default_source);
2432                             push @authorised_values, $class_source;
2433                             $authorised_lib{$class_source} = $class_sources->{$class_source}->{'description'};
2434                         }
2435
2436                         $defaultvalue = $default_source;
2437
2438                         #---- "true" authorised value
2439                     } else {
2440                         $authorised_values_sth->execute(
2441                             $tagslib->{$tag}->{$subfield}->{authorised_value},
2442                             $branch_limit ? $branch_limit : ()
2443                         );
2444                         push @authorised_values, ""
2445                           unless ( $tagslib->{$tag}->{$subfield}->{mandatory} );
2446                         while ( my ( $value, $lib ) = $authorised_values_sth->fetchrow_array ) {
2447                             push @authorised_values, $value;
2448                             $authorised_lib{$value} = $lib;
2449                         }
2450                     }
2451                     $subfield_data{marc_value} = {
2452                         type    => 'select',
2453                         values  => \@authorised_values,
2454                         default => $defaultvalue // q{},
2455                         labels  => \%authorised_lib,
2456                     };
2457                 } elsif ( $tagslib->{$tag}->{$subfield}->{value_builder} ) {
2458                 # it is a plugin
2459                     require Koha::FrameworkPlugin;
2460                     my $plugin = Koha::FrameworkPlugin->new({
2461                         name => $tagslib->{$tag}->{$subfield}->{value_builder},
2462                         item_style => 1,
2463                     });
2464                     my $pars = { dbh => $dbh, record => undef, tagslib =>$tagslib, id => $subfield_data{id}, tabloop => undef };
2465                     $plugin->build( $pars );
2466                     if ( $itemrecord and my $field = $itemrecord->field($tag) ) {
2467                         $defaultvalue = $field->subfield($subfield) || q{};
2468                     }
2469                     if( !$plugin->errstr ) {
2470                         #TODO Move html to template; see report 12176/13397
2471                         my $tab= $plugin->noclick? '-1': '';
2472                         my $class= $plugin->noclick? ' disabled': '';
2473                         my $title= $plugin->noclick? 'No popup': 'Tag editor';
2474                         $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;
2475                     } else {
2476                         warn $plugin->errstr;
2477                         $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
2478                     }
2479                 }
2480                 elsif ( $tag eq '' ) {       # it's an hidden field
2481                     $subfield_data{marc_value} = qq(<input type="hidden" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength" value="$defaultvalue" />);
2482                 }
2483                 elsif ( $tagslib->{$tag}->{$subfield}->{'hidden'} ) {   # FIXME: shouldn't input type be "hidden" ?
2484                     $subfield_data{marc_value} = qq(<input type="text" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength" value="$defaultvalue" />);
2485                 }
2486                 elsif ( length($defaultvalue) > 100
2487                             or (C4::Context->preference("marcflavour") eq "UNIMARC" and
2488                                   300 <= $tag && $tag < 400 && $subfield eq 'a' )
2489                             or (C4::Context->preference("marcflavour") eq "MARC21"  and
2490                                   500 <= $tag && $tag < 600                     )
2491                           ) {
2492                     # oversize field (textarea)
2493                     $subfield_data{marc_value} = qq(<textarea id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength">$defaultvalue</textarea>\n");
2494                 } else {
2495                     $subfield_data{marc_value} = qq(<input type="text" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength" value="$defaultvalue" />);
2496                 }
2497                 push( @loop_data, \%subfield_data );
2498             }
2499         }
2500     }
2501     my $itemnumber;
2502     if ( $itemrecord && $itemrecord->field($itemtagfield) ) {
2503         $itemnumber = $itemrecord->subfield( $itemtagfield, $itemtagsubfield );
2504     }
2505     return {
2506         'itemtagfield'    => $itemtagfield,
2507         'itemtagsubfield' => $itemtagsubfield,
2508         'itemnumber'      => $itemnumber,
2509         'iteminformation' => \@loop_data
2510     };
2511 }
2512
2513 sub ToggleNewStatus {
2514     my ( $params ) = @_;
2515     my @rules = @{ $params->{rules} };
2516     my $report_only = $params->{report_only};
2517
2518     my $dbh = C4::Context->dbh;
2519     my @errors;
2520     my @item_columns = map { "items.$_" } Koha::Items->columns;
2521     my @biblioitem_columns = map { "biblioitems.$_" } Koha::Biblioitems->columns;
2522     my $report;
2523     for my $rule ( @rules ) {
2524         my $age = $rule->{age};
2525         my $conditions = $rule->{conditions};
2526         my $substitutions = $rule->{substitutions};
2527         foreach ( @$substitutions ) {
2528             ( $_->{item_field} ) = ( $_->{field} =~ /items\.(.*)/ );
2529         }
2530         my @params;
2531
2532         my $query = q|
2533             SELECT items.*
2534             FROM items
2535             LEFT JOIN biblioitems ON biblioitems.biblionumber = items.biblionumber
2536             WHERE 1
2537         |;
2538         for my $condition ( @$conditions ) {
2539             if (
2540                  grep { $_ eq $condition->{field} } @item_columns
2541               or grep { $_ eq $condition->{field} } @biblioitem_columns
2542             ) {
2543                 if ( $condition->{value} =~ /\|/ ) {
2544                     my @values = split /\|/, $condition->{value};
2545                     $query .= qq| AND $condition->{field} IN (|
2546                         . join( ',', ('?') x scalar @values )
2547                         . q|)|;
2548                     push @params, @values;
2549                 } else {
2550                     $query .= qq| AND $condition->{field} = ?|;
2551                     push @params, $condition->{value};
2552                 }
2553             }
2554         }
2555         if ( defined $age ) {
2556             $query .= q| AND TO_DAYS(NOW()) - TO_DAYS(dateaccessioned) >= ? |;
2557             push @params, $age;
2558         }
2559         my $sth = $dbh->prepare($query);
2560         $sth->execute( @params );
2561         while ( my $values = $sth->fetchrow_hashref ) {
2562             my $biblionumber = $values->{biblionumber};
2563             my $itemnumber = $values->{itemnumber};
2564             for my $substitution ( @$substitutions ) {
2565                 next unless $substitution->{field};
2566                 next if ( defined $values->{ $substitution->{item_field} } and $values->{ $substitution->{item_field} } eq $substitution->{value} );
2567                 C4::Items::ModItem( { $substitution->{item_field} => $substitution->{value} }, $biblionumber, $itemnumber )
2568                     unless $report_only;
2569                 push @{ $report->{$itemnumber} }, $substitution;
2570             }
2571         }
2572     }
2573
2574     return $report;
2575 }
2576
2577 =head2 _after_item_action_hooks
2578
2579 Helper method that takes care of calling all plugin hooks
2580
2581 =cut
2582
2583 sub _after_item_action_hooks {
2584     my ( $args ) = @_;
2585
2586     my $item_id = $args->{item_id};
2587     my $action  = $args->{action};
2588
2589     if ( C4::Context->preference('UseKohaPlugins') && C4::Context->config("enable_plugins") ) {
2590
2591         my @plugins = Koha::Plugins->new->GetPlugins({
2592             method => 'after_item_action',
2593         });
2594
2595         if (@plugins) {
2596
2597             my $item = Koha::Items->find( $item_id );
2598
2599             foreach my $plugin ( @plugins ) {
2600                 try {
2601                     $plugin->after_item_action({ action => $action, item => $item, item_id => $item_id });
2602                 }
2603                 catch {
2604                     warn "$_";
2605                 };
2606             }
2607         }
2608     }
2609 }
2610
2611 1;