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