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