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