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