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