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