Bug 33051: Make 22600075.pl idempotent
[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 our (@ISA, @EXPORT_OK);
24 BEGIN {
25     require Exporter;
26     @ISA = qw(Exporter);
27
28     @EXPORT_OK = qw(
29         AddItemFromMarc
30         AddItemBatchFromMarc
31         ModItemFromMarc
32         Item2Marc
33         ModDateLastSeen
34         ModItemTransfer
35         CheckItemPreSave
36         GetItemsForInventory
37         get_hostitemnumbers_of
38         GetMarcItem
39         CartToShelf
40         GetAnalyticsCount
41         SearchItems
42         PrepareItemrecordDisplay
43         ToggleNewStatus
44     );
45 }
46
47 use Carp qw( croak );
48 use C4::Context;
49 use C4::Koha;
50 use C4::Biblio qw( GetMarcStructure TransformMarcToKoha );
51 use MARC::Record;
52 use C4::ClassSource qw( GetClassSort GetClassSources GetClassSource );
53 use C4::Log qw( logaction );
54 use List::MoreUtils qw( any );
55 use DateTime::Format::MySQL;
56                   # debugging; so please don't remove this
57
58 use Koha::AuthorisedValues;
59 use Koha::DateUtils qw( dt_from_string );
60 use Koha::Database;
61
62 use Koha::Biblios;
63 use Koha::Biblioitems;
64 use Koha::Items;
65 use Koha::ItemTypes;
66 use Koha::SearchEngine;
67 use Koha::SearchEngine::Indexer;
68 use Koha::SearchEngine::Search;
69 use Koha::Libraries;
70
71 =head1 NAME
72
73 C4::Items - item management functions
74
75 =head1 DESCRIPTION
76
77 This module contains an API for manipulating item 
78 records in Koha, and is used by cataloguing, circulation,
79 acquisitions, and serials management.
80
81 # FIXME This POD is not up-to-date
82 A Koha item record is stored in two places: the
83 items table and embedded in a MARC tag in the XML
84 version of the associated bib record in C<biblioitems.marcxml>.
85 This is done to allow the item information to be readily
86 indexed (e.g., by Zebra), but means that each item
87 modification transaction must keep the items table
88 and the MARC XML in sync at all times.
89
90 The items table will be considered authoritative.  In other
91 words, if there is ever a discrepancy between the items
92 table and the MARC XML, the items table should be considered
93 accurate.
94
95 =head1 HISTORICAL NOTE
96
97 Most of the functions in C<C4::Items> were originally in
98 the C<C4::Biblio> module.
99
100 =head1 CORE EXPORTED FUNCTIONS
101
102 The following functions are meant for use by users
103 of C<C4::Items>
104
105 =cut
106
107 =head2 CartToShelf
108
109   CartToShelf($itemnumber);
110
111 Set the current shelving location of the item record
112 to its stored permanent shelving location.  This is
113 primarily used to indicate when an item whose current
114 location is a special processing ('PROC') or shelving cart
115 ('CART') location is back in the stacks.
116
117 =cut
118
119 sub CartToShelf {
120     my ( $itemnumber ) = @_;
121
122     unless ( $itemnumber ) {
123         croak "FAILED CartToShelf() - no itemnumber supplied";
124     }
125
126     my $item = Koha::Items->find($itemnumber);
127     if ( $item->location eq 'CART' ) {
128         $item->location($item->permanent_location)->store;
129     }
130 }
131
132 =head2 AddItemFromMarc
133
134   my ($biblionumber, $biblioitemnumber, $itemnumber) 
135       = AddItemFromMarc($source_item_marc, $biblionumber[, $params]);
136
137 Given a MARC::Record object containing an embedded item
138 record and a biblionumber, create a new item record.
139
140 The final optional parameter, C<$params>, may contain
141 'skip_record_index' key, which relayed down to Koha::Item/store,
142 there it prevents calling of index_records,
143 which takes most of the time in batch adds/deletes: index_records
144 to be called later in C<additem.pl> after the whole loop.
145
146 You may also optionally pass biblioitemnumber in the params hash to
147 boost performance of inserts by preventing a lookup in Koha::Item.
148
149 $params:
150     skip_record_index => 1|0
151     biblioitemnumber => $biblioitemnumber
152
153 =cut
154
155 sub AddItemFromMarc {
156     my $source_item_marc = shift;
157     my $biblionumber     = shift;
158     my $params           = @_ ? shift : {};
159
160     my $dbh = C4::Context->dbh;
161
162     # parse item hash from MARC
163     my $frameworkcode = C4::Biblio::GetFrameworkCode($biblionumber);
164     my ( $itemtag, $itemsubfield ) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
165     my $localitemmarc = MARC::Record->new;
166     $localitemmarc->append_fields( $source_item_marc->field($itemtag) );
167
168     my $item_values = C4::Biblio::TransformMarcToKoha({ record => $localitemmarc, limit_table => 'items' });
169     my $unlinked_item_subfields = _get_unlinked_item_subfields( $localitemmarc, $frameworkcode );
170     $item_values->{more_subfields_xml} = _get_unlinked_subfields_xml($unlinked_item_subfields);
171     $item_values->{biblionumber} = $biblionumber;
172     $item_values->{biblioitemnumber} = $params->{biblioitemnumber};
173     $item_values->{cn_source} = delete $item_values->{'items.cn_source'}; # Because of C4::Biblio::_disambiguate
174     $item_values->{cn_sort}   = delete $item_values->{'items.cn_sort'};   # Because of C4::Biblio::_disambiguate
175     my $item = Koha::Item->new( $item_values )->store({ skip_record_index => $params->{skip_record_index} });
176     return ( $item->biblionumber, $item->biblioitemnumber, $item->itemnumber );
177 }
178
179 =head2 AddItemBatchFromMarc
180
181   ($itemnumber_ref, $error_ref) = AddItemBatchFromMarc($record, 
182              $biblionumber, $biblioitemnumber, $frameworkcode);
183
184 Efficiently create item records from a MARC biblio record with
185 embedded item fields.  This routine is suitable for batch jobs.
186
187 This API assumes that the bib record has already been
188 saved to the C<biblio> and C<biblioitems> tables.  It does
189 not expect that C<biblio_metadata.metadata> is populated, but it
190 will do so via a call to ModBibiloMarc.
191
192 The goal of this API is to have a similar effect to using AddBiblio
193 and AddItems in succession, but without inefficient repeated
194 parsing of the MARC XML bib record.
195
196 This function returns an arrayref of new itemsnumbers and an arrayref of item
197 errors encountered during the processing.  Each entry in the errors
198 list is a hashref containing the following keys:
199
200 =over
201
202 =item item_sequence
203
204 Sequence number of original item tag in the MARC record.
205
206 =item item_barcode
207
208 Item barcode, provide to assist in the construction of
209 useful error messages.
210
211 =item error_code
212
213 Code representing the error condition.  Can be 'duplicate_barcode',
214 'invalid_homebranch', or 'invalid_holdingbranch'.
215
216 =item error_information
217
218 Additional information appropriate to the error condition.
219
220 =back
221
222 =cut
223
224 sub AddItemBatchFromMarc {
225     my ($record, $biblionumber, $biblioitemnumber, $frameworkcode) = @_;
226     my @itemnumbers = ();
227     my @errors = ();
228     my $dbh = C4::Context->dbh;
229
230     # We modify the record, so lets work on a clone so we don't change the
231     # original.
232     $record = $record->clone();
233     # loop through the item tags and start creating items
234     my @bad_item_fields = ();
235     my ($itemtag, $itemsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
236     my $item_sequence_num = 0;
237     ITEMFIELD: foreach my $item_field ($record->field($itemtag)) {
238         $item_sequence_num++;
239         # we take the item field and stick it into a new
240         # MARC record -- this is required so far because (FIXME)
241         # TransformMarcToKoha requires a MARC::Record, not a MARC::Field
242         # and there is no TransformMarcFieldToKoha
243         my $temp_item_marc = MARC::Record->new();
244         $temp_item_marc->append_fields($item_field);
245     
246         # add biblionumber and biblioitemnumber
247         my $item = TransformMarcToKoha({ record => $temp_item_marc, limit_table => 'items' });
248         my $unlinked_item_subfields = _get_unlinked_item_subfields($temp_item_marc, $frameworkcode);
249         $item->{'more_subfields_xml'} = _get_unlinked_subfields_xml($unlinked_item_subfields);
250         $item->{'biblionumber'} = $biblionumber;
251         $item->{'biblioitemnumber'} = $biblioitemnumber;
252         $item->{cn_source} = delete $item->{'items.cn_source'}; # Because of C4::Biblio::_disambiguate
253         $item->{cn_sort}   = delete $item->{'items.cn_sort'};   # Because of C4::Biblio::_disambiguate
254
255         # check for duplicate barcode
256         my %item_errors = CheckItemPreSave($item);
257         if (%item_errors) {
258             push @errors, _repack_item_errors($item_sequence_num, $item, \%item_errors);
259             push @bad_item_fields, $item_field;
260             next ITEMFIELD;
261         }
262
263         my $item_object = Koha::Item->new($item)->store;
264         push @itemnumbers, $item_object->itemnumber; # FIXME not checking error
265
266         logaction("CATALOGUING", "ADD", $item_object->itemnumber, "item") if C4::Context->preference("CataloguingLog");
267
268         my $new_item_marc = _marc_from_item_hash($item_object->unblessed, $frameworkcode, $unlinked_item_subfields);
269         $item_field->replace_with($new_item_marc->field($itemtag));
270     }
271
272     # remove any MARC item fields for rejected items
273     foreach my $item_field (@bad_item_fields) {
274         $record->delete_field($item_field);
275     }
276
277     return (\@itemnumbers, \@errors);
278 }
279
280 =head2 ModItemFromMarc
281
282 my $item = ModItemFromMarc($item_marc, $biblionumber, $itemnumber[, $params]);
283
284 The final optional parameter, C<$params>, expected to contain
285 'skip_record_index' key, which relayed down to Koha::Item/store,
286 there it prevents calling of index_records,
287 which takes most of the time in batch adds/deletes: index_records better
288 to be called later in C<additem.pl> after the whole loop.
289
290 $params:
291     skip_record_index => 1|0
292
293 =cut
294
295 sub ModItemFromMarc {
296     my ( $item_marc, $biblionumber, $itemnumber, $params ) = @_;
297
298     my $frameworkcode = C4::Biblio::GetFrameworkCode($biblionumber);
299     my ( $itemtag, $itemsubfield ) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
300
301     my $localitemmarc = MARC::Record->new;
302     $localitemmarc->append_fields( $item_marc->field($itemtag) );
303     my $item_object = Koha::Items->find($itemnumber);
304     my $item = TransformMarcToKoha({ record => $localitemmarc, limit_table => 'items' });
305
306     # When importing items we blank this column, we need to set it to the existing value
307     # to prevent it being blanked by set_or_blank
308     $item->{onloan} = $item_object->onloan if( $item_object->onloan && !defined $item->{onloan} );
309
310     # When importing and replacing items we should not remove the dateacquired so we should set it
311     # to the existing value
312     $item->{dateaccessioned} = $item_object->dateaccessioned
313       if ( $item_object->dateaccessioned && !defined $item->{dateaccessioned} );
314
315     my ( $perm_loc_tag, $perm_loc_subfield ) = C4::Biblio::GetMarcFromKohaField( "items.permanent_location" );
316     my $has_permanent_location = defined $perm_loc_tag && defined $item_marc->subfield( $perm_loc_tag, $perm_loc_subfield );
317
318     # Retrieving the values for the fields that are not linked
319     my @mapped_fields = Koha::MarcSubfieldStructures->search(
320         {
321             frameworkcode => $frameworkcode,
322             kohafield     => { -like => "items.%" }
323         }
324     )->get_column('kohafield');
325     for my $c ( $item_object->_result->result_source->columns ) {
326         next if grep { "items.$c" eq $_ } @mapped_fields;
327         $item->{$c} = $item_object->$c;
328     }
329
330     $item->{cn_source} = delete $item->{'items.cn_source'}; # Because of C4::Biblio::_disambiguate
331     delete $item->{'items.cn_sort'};   # Because of C4::Biblio::_disambiguate
332     $item->{itemnumber} = $itemnumber;
333     $item->{biblionumber} = $biblionumber;
334
335     my $existing_cn_sort = $item_object->cn_sort; # set_or_blank will reset cn_sort to undef as we are not passing it
336                                                   # We rely on Koha::Item->store to modify it if itemcallnumber or cn_source is modified
337     $item_object = $item_object->set_or_blank($item);
338     $item_object->cn_sort($existing_cn_sort); # Resetting to the existing value
339
340     $item_object->make_column_dirty('permanent_location') if $has_permanent_location;
341
342     my $unlinked_item_subfields = _get_unlinked_item_subfields( $localitemmarc, $frameworkcode );
343     $item_object->more_subfields_xml(_get_unlinked_subfields_xml($unlinked_item_subfields));
344     $item_object->store({ skip_record_index => $params->{skip_record_index} });
345
346     return $item_object->unblessed;
347 }
348
349 =head2 ModItemTransfer
350
351   ModItemTransfer($itemnumber, $frombranch, $tobranch, $trigger, [$params]);
352
353 Marks an item as being transferred from one branch to another and records the trigger.
354
355 The last optional parameter allows for passing skip_record_index through to the items store call.
356
357 =cut
358
359 sub ModItemTransfer {
360     my ( $itemnumber, $frombranch, $tobranch, $trigger, $params ) = @_;
361
362     my $dbh = C4::Context->dbh;
363     my $item = Koha::Items->find( $itemnumber );
364
365     # NOTE: This retains the existing hard coded behaviour by ignoring transfer limits
366     # and always replacing any existing transfers. (In theory, calls to ModItemTransfer
367     # will have been preceded by a check of branch transfer limits)
368     my $to_library = Koha::Libraries->find($tobranch);
369     my $transfer = $item->request_transfer(
370         {
371             to            => $to_library,
372             reason        => $trigger,
373             ignore_limits => 1,
374             replace       => 1
375         }
376     );
377
378     # Immediately set the item to in transit if it is checked in
379     if ( !$item->checkout ) {
380         $item->holdingbranch($frombranch)->store(
381             {
382                 log_action        => 0,
383                 skip_record_index => $params->{skip_record_index}
384             }
385         );
386         $transfer->transit;
387     }
388
389     return;
390 }
391
392 =head2 ModDateLastSeen
393
394 ModDateLastSeen( $itemnumber, $leave_item_lost, $params );
395
396 Mark item as seen. Is called when an item is issued, returned or manually marked during inventory/stocktaking.
397 C<$itemnumber> is the item number
398 C<$leave_item_lost> determines if a lost item will be found or remain lost
399
400 The last optional parameter allows for passing skip_record_index through to the items store call.
401
402 =cut
403
404 sub ModDateLastSeen {
405     my ( $itemnumber, $leave_item_lost, $params ) = @_;
406
407     my $item = Koha::Items->find($itemnumber);
408     $item->datelastseen(dt_from_string->ymd);
409     my $log = $item->itemlost && !$leave_item_lost ? 1 : 0; # If item was lost, record the change to the item
410     $item->itemlost(0) unless $leave_item_lost;
411     $item->store({ log_action => $log, skip_record_index => $params->{skip_record_index}, skip_holds_queue => $params->{skip_holds_queue} });
412 }
413
414 =head2 CheckItemPreSave
415
416     my $item_ref = TransformMarcToKoha({ record => $marc, limit_table => 'items' });
417     # do stuff
418     my %errors = CheckItemPreSave($item_ref);
419     if (exists $errors{'duplicate_barcode'}) {
420         print "item has duplicate barcode: ", $errors{'duplicate_barcode'}, "\n";
421     } elsif (exists $errors{'invalid_homebranch'}) {
422         print "item has invalid home branch: ", $errors{'invalid_homebranch'}, "\n";
423     } elsif (exists $errors{'invalid_holdingbranch'}) {
424         print "item has invalid holding branch: ", $errors{'invalid_holdingbranch'}, "\n";
425     } else {
426         print "item is OK";
427     }
428
429 Given a hashref containing item fields, determine if it can be
430 inserted or updated in the database.  Specifically, checks for
431 database integrity issues, and returns a hash containing any
432 of the following keys, if applicable.
433
434 =over 2
435
436 =item duplicate_barcode
437
438 Barcode, if it duplicates one already found in the database.
439
440 =item invalid_homebranch
441
442 Home branch, if not defined in branches table.
443
444 =item invalid_holdingbranch
445
446 Holding branch, if not defined in branches table.
447
448 =back
449
450 This function does NOT implement any policy-related checks,
451 e.g., whether current operator is allowed to save an
452 item that has a given branch code.
453
454 =cut
455
456 sub CheckItemPreSave {
457     my $item_ref = shift;
458
459     my %errors = ();
460
461     # check for duplicate barcode
462     if (exists $item_ref->{'barcode'} and defined $item_ref->{'barcode'}) {
463         my $existing_item= Koha::Items->find({barcode => $item_ref->{'barcode'}});
464         if ($existing_item) {
465             if (!exists $item_ref->{'itemnumber'}                       # new item
466                 or $item_ref->{'itemnumber'} != $existing_item->itemnumber) { # existing item
467                 $errors{'duplicate_barcode'} = $item_ref->{'barcode'};
468             }
469         }
470     }
471
472     # check for valid home branch
473     if (exists $item_ref->{'homebranch'} and defined $item_ref->{'homebranch'}) {
474         my $home_library = Koha::Libraries->find( $item_ref->{homebranch} );
475         unless (defined $home_library) {
476             $errors{'invalid_homebranch'} = $item_ref->{'homebranch'};
477         }
478     }
479
480     # check for valid holding branch
481     if (exists $item_ref->{'holdingbranch'} and defined $item_ref->{'holdingbranch'}) {
482         my $holding_library = Koha::Libraries->find( $item_ref->{holdingbranch} );
483         unless (defined $holding_library) {
484             $errors{'invalid_holdingbranch'} = $item_ref->{'holdingbranch'};
485         }
486     }
487
488     return %errors;
489
490 }
491
492 =head1 EXPORTED SPECIAL ACCESSOR FUNCTIONS
493
494 The following functions provide various ways of 
495 getting an item record, a set of item records, or
496 lists of authorized values for certain item fields.
497
498 =cut
499
500 =head2 GetItemsForInventory
501
502 ($itemlist, $iTotalRecords) = GetItemsForInventory( {
503   minlocation  => $minlocation,
504   maxlocation  => $maxlocation,
505   location     => $location,
506   ignoreissued => $ignoreissued,
507   datelastseen => $datelastseen,
508   branchcode   => $branchcode,
509   branch       => $branch,
510   offset       => $offset,
511   size         => $size,
512   statushash   => $statushash,
513   itemtypes    => \@itemsarray,
514 } );
515
516 Retrieve a list of title/authors/barcode/callnumber, for biblio inventory.
517
518 The sub returns a reference to a list of hashes, each containing
519 itemnumber, author, title, barcode, item callnumber, and date last
520 seen. It is ordered by callnumber then title.
521
522 The required minlocation & maxlocation parameters are used to specify a range of item callnumbers
523 the datelastseen can be used to specify that you want to see items not seen since a past date only.
524 offset & size can be used to retrieve only a part of the whole listing (defaut behaviour)
525 $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.
526
527 $iTotalRecords is the number of rows that would have been returned without the $offset, $size limit clause
528
529 =cut
530
531 sub GetItemsForInventory {
532     my ( $parameters ) = @_;
533     my $minlocation  = $parameters->{'minlocation'}  // '';
534     my $maxlocation  = $parameters->{'maxlocation'}  // '';
535     my $class_source = $parameters->{'class_source'}  // C4::Context->preference('DefaultClassificationSource');
536     my $location     = $parameters->{'location'}     // '';
537     my $itemtype     = $parameters->{'itemtype'}     // '';
538     my $ignoreissued = $parameters->{'ignoreissued'} // '';
539     my $datelastseen = $parameters->{'datelastseen'} // '';
540     my $branchcode   = $parameters->{'branchcode'}   // '';
541     my $branch       = $parameters->{'branch'}       // '';
542     my $offset       = $parameters->{'offset'}       // '';
543     my $size         = $parameters->{'size'}         // '';
544     my $statushash   = $parameters->{'statushash'}   // '';
545     my $ignore_waiting_holds = $parameters->{'ignore_waiting_holds'} // '';
546     my $itemtypes    = $parameters->{'itemtypes'}    || [];
547     my $ccode        = $parameters->{'ccode'}        // '';
548
549     my $dbh = C4::Context->dbh;
550     my ( @bind_params, @where_strings );
551
552     my $min_cnsort = GetClassSort($class_source,undef,$minlocation);
553     my $max_cnsort = GetClassSort($class_source,undef,$maxlocation);
554
555     my $select_columns = q{
556         SELECT DISTINCT(items.itemnumber), barcode, itemcallnumber, title, author, biblio.biblionumber, biblio.frameworkcode, datelastseen, homebranch, location, notforloan, damaged, itemlost, withdrawn, stocknumber, items.cn_sort, ccode
557
558     };
559     my $select_count = q{SELECT COUNT(DISTINCT(items.itemnumber))};
560     my $query = q{
561         FROM items
562         LEFT JOIN biblio ON items.biblionumber = biblio.biblionumber
563         LEFT JOIN biblioitems on items.biblionumber = biblioitems.biblionumber
564     };
565     if ($statushash){
566         for my $authvfield (keys %$statushash){
567             if ( scalar @{$statushash->{$authvfield}} > 0 ){
568                 my $joinedvals = join ',', @{$statushash->{$authvfield}};
569                 push @where_strings, "$authvfield in (" . $joinedvals . ")";
570             }
571         }
572     }
573
574     if ($ccode){
575         push @where_strings, 'ccode = ?';
576         push @bind_params, $ccode;
577     }
578
579     if ($minlocation) {
580         push @where_strings, 'items.cn_sort >= ?';
581         push @bind_params, $min_cnsort;
582     }
583
584     if ($maxlocation) {
585         push @where_strings, 'items.cn_sort <= ?';
586         push @bind_params, $max_cnsort;
587     }
588
589     if ($datelastseen) {
590         push @where_strings, '(datelastseen < ? OR datelastseen IS NULL)';
591         push @bind_params, $datelastseen;
592     }
593
594     if ( $location ) {
595         push @where_strings, 'items.location = ?';
596         push @bind_params, $location;
597     }
598
599     if ( $branchcode ) {
600         if($branch eq "homebranch"){
601         push @where_strings, 'items.homebranch = ?';
602         }else{
603             push @where_strings, 'items.holdingbranch = ?';
604         }
605         push @bind_params, $branchcode;
606     }
607
608     if ( $itemtype ) {
609         push @where_strings, 'biblioitems.itemtype = ?';
610         push @bind_params, $itemtype;
611     }
612     if ( $ignoreissued) {
613         $query .= "LEFT JOIN issues ON items.itemnumber = issues.itemnumber ";
614         push @where_strings, 'issues.date_due IS NULL';
615     }
616
617     if ( $ignore_waiting_holds ) {
618         $query .= "LEFT JOIN reserves ON items.itemnumber = reserves.itemnumber ";
619         push( @where_strings, q{(reserves.found != 'W' OR reserves.found IS NULL)} );
620     }
621
622     if ( @$itemtypes ) {
623         my $itemtypes_str = join ', ', @$itemtypes;
624         push @where_strings, "( biblioitems.itemtype IN (" . $itemtypes_str . ") OR items.itype IN (" . $itemtypes_str . ") )";
625     }
626
627     if ( @where_strings ) {
628         $query .= 'WHERE ';
629         $query .= join ' AND ', @where_strings;
630     }
631     my $count_query = $select_count . $query;
632     $query .= ' ORDER BY items.cn_sort, itemcallnumber, title';
633     $query .= " LIMIT $offset, $size" if ($offset and $size);
634     $query = $select_columns . $query;
635     my $sth = $dbh->prepare($query);
636     $sth->execute( @bind_params );
637
638     my @results = ();
639     my $tmpresults = $sth->fetchall_arrayref({});
640     $sth = $dbh->prepare( $count_query );
641     $sth->execute( @bind_params );
642     my ($iTotalRecords) = $sth->fetchrow_array();
643
644     my @avs = Koha::AuthorisedValues->search(
645         {   'marc_subfield_structures.kohafield' => { '>' => '' },
646             'me.authorised_value'                => { '>' => '' },
647         },
648         {   join     => { category => 'marc_subfield_structures' },
649             distinct => ['marc_subfield_structures.kohafield, me.category, frameworkcode, me.authorised_value'],
650             '+select' => [ 'marc_subfield_structures.kohafield', 'marc_subfield_structures.frameworkcode', 'me.authorised_value', 'me.lib' ],
651             '+as'     => [ 'kohafield',                          'frameworkcode',                          'authorised_value',    'lib' ],
652         }
653     )->as_list;
654
655     my $avmapping = { map { $_->get_column('kohafield') . ',' . $_->get_column('frameworkcode') . ',' . $_->get_column('authorised_value') => $_->get_column('lib') } @avs };
656
657     foreach my $row (@$tmpresults) {
658
659         # Auth values
660         foreach (keys %$row) {
661             if (
662                 defined(
663                     $avmapping->{ "items.$_," . $row->{'frameworkcode'} . "," . ( $row->{$_} // q{} ) }
664                 )
665             ) {
666                 $row->{$_} = $avmapping->{"items.$_,".$row->{'frameworkcode'}.",".$row->{$_}};
667             }
668         }
669         push @results, $row;
670     }
671
672     return (\@results, $iTotalRecords);
673 }
674
675 =head2 get_hostitemnumbers_of
676
677   my @itemnumbers_of = get_hostitemnumbers_of($biblionumber);
678
679 Given a biblionumber, return the list of corresponding itemnumbers that are linked to it via host fields
680
681 Return a reference on a hash where key is a biblionumber and values are
682 references on array of itemnumbers.
683
684 =cut
685
686
687 sub get_hostitemnumbers_of {
688     my ($biblionumber) = @_;
689
690     if( !C4::Context->preference('EasyAnalyticalRecords') ) {
691         return ();
692     }
693
694     my $biblio = Koha::Biblios->find($biblionumber);
695     my $marcrecord = $biblio->metadata->record;
696     return unless $marcrecord;
697
698     my ( @returnhostitemnumbers, $tag, $biblio_s, $item_s );
699
700     my $marcflavor = C4::Context->preference('marcflavour');
701     if ( $marcflavor eq 'MARC21' ) {
702         $tag      = '773';
703         $biblio_s = '0';
704         $item_s   = '9';
705     }
706     elsif ( $marcflavor eq 'UNIMARC' ) {
707         $tag      = '461';
708         $biblio_s = '0';
709         $item_s   = '9';
710     }
711
712     foreach my $hostfield ( $marcrecord->field($tag) ) {
713         my $hostbiblionumber = $hostfield->subfield($biblio_s);
714         next unless $hostbiblionumber; # have tag, don't have $biblio_s subfield
715         my $linkeditemnumber = $hostfield->subfield($item_s);
716         if ( ! $linkeditemnumber ) {
717             warn "ERROR biblionumber $biblionumber has 773^0, but doesn't have 9";
718             next;
719         }
720         my $is_from_biblio = Koha::Items->search({ itemnumber => $linkeditemnumber, biblionumber => $hostbiblionumber });
721         push @returnhostitemnumbers, $linkeditemnumber
722           if $is_from_biblio;
723     }
724
725     return @returnhostitemnumbers;
726 }
727
728 =head1 LIMITED USE FUNCTIONS
729
730 The following functions, while part of the public API,
731 are not exported.  This is generally because they are
732 meant to be used by only one script for a specific
733 purpose, and should not be used in any other context
734 without careful thought.
735
736 =cut
737
738 =head2 GetMarcItem
739
740   my $item_marc = GetMarcItem($biblionumber, $itemnumber);
741
742 Returns MARC::Record of the item passed in parameter.
743 This function is meant for use only in C<cataloguing/additem.pl>,
744 where it is needed to support that script's MARC-like
745 editor.
746
747 =cut
748
749 sub GetMarcItem {
750     my ( $biblionumber, $itemnumber ) = @_;
751
752     # GetMarcItem has been revised so that it does the following:
753     #  1. Gets the item information from the items table.
754     #  2. Converts it to a MARC field for storage in the bib record.
755     #
756     # The previous behavior was:
757     #  1. Get the bib record.
758     #  2. Return the MARC tag corresponding to the item record.
759     #
760     # The difference is that one treats the items row as authoritative,
761     # while the other treats the MARC representation as authoritative
762     # under certain circumstances.
763
764     my $item = Koha::Items->find($itemnumber) or return;
765
766     # Tack on 'items.' prefix to column names so that C4::Biblio::TransformKohaToMarc will work.
767     # Also, don't emit a subfield if the underlying field is blank.
768
769     return Item2Marc($item->unblessed, $biblionumber);
770
771 }
772 sub Item2Marc {
773         my ($itemrecord,$biblionumber)=@_;
774     my $mungeditem = { 
775         map {  
776             defined($itemrecord->{$_}) && $itemrecord->{$_} ne '' ? ("items.$_" => $itemrecord->{$_}) : ()  
777         } keys %{ $itemrecord } 
778     };
779     my $framework = C4::Biblio::GetFrameworkCode( $biblionumber );
780     my $itemmarc = C4::Biblio::TransformKohaToMarc( $mungeditem, { framework => $framework } );
781     my ( $itemtag, $itemsubfield ) = C4::Biblio::GetMarcFromKohaField(
782         "items.itemnumber", $framework,
783     );
784
785     my $unlinked_item_subfields = _parse_unlinked_item_subfields_from_xml($mungeditem->{'items.more_subfields_xml'});
786     if (defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1) {
787                 foreach my $field ($itemmarc->field($itemtag)){
788             $field->add_subfields(@$unlinked_item_subfields);
789         }
790     }
791         return $itemmarc;
792 }
793
794 =head1 PRIVATE FUNCTIONS AND VARIABLES
795
796 The following functions are not meant to be called
797 directly, but are documented in order to explain
798 the inner workings of C<C4::Items>.
799
800 =cut
801
802 =head2 _marc_from_item_hash
803
804   my $item_marc = _marc_from_item_hash($item, $frameworkcode[, $unlinked_item_subfields]);
805
806 Given an item hash representing a complete item record,
807 create a C<MARC::Record> object containing an embedded
808 tag representing that item.
809
810 The third, optional parameter C<$unlinked_item_subfields> is
811 an arrayref of subfields (not mapped to C<items> fields per the
812 framework) to be added to the MARC representation
813 of the item.
814
815 =cut
816
817 sub _marc_from_item_hash {
818     my $item = shift;
819     my $frameworkcode = shift;
820     my $unlinked_item_subfields;
821     if (@_) {
822         $unlinked_item_subfields = shift;
823     }
824    
825     # Tack on 'items.' prefix to column names so lookup from MARC frameworks will work
826     # Also, don't emit a subfield if the underlying field is blank.
827     my $mungeditem = { map {  (defined($item->{$_}) and $item->{$_} ne '') ? 
828                                 (/^items\./ ? ($_ => $item->{$_}) : ("items.$_" => $item->{$_})) 
829                                 : ()  } keys %{ $item } }; 
830
831     my $item_marc = MARC::Record->new();
832     foreach my $item_field ( keys %{$mungeditem} ) {
833         my ( $tag, $subfield ) = C4::Biblio::GetMarcFromKohaField( $item_field );
834         next unless defined $tag and defined $subfield;    # skip if not mapped to MARC field
835         my @values = split(/\s?\|\s?/, $mungeditem->{$item_field}, -1);
836         foreach my $value (@values){
837             if ( my $field = $item_marc->field($tag) ) {
838                     $field->add_subfields( $subfield => $value );
839             } else {
840                 my $add_subfields = [];
841                 if (defined $unlinked_item_subfields and ref($unlinked_item_subfields) eq 'ARRAY' and $#$unlinked_item_subfields > -1) {
842                     $add_subfields = $unlinked_item_subfields;
843             }
844             $item_marc->add_fields( $tag, " ", " ", $subfield => $value, @$add_subfields );
845             }
846         }
847     }
848
849     return $item_marc;
850 }
851
852 =head2 _repack_item_errors
853
854 Add an error message hash generated by C<CheckItemPreSave>
855 to a list of errors.
856
857 =cut
858
859 sub _repack_item_errors {
860     my $item_sequence_num = shift;
861     my $item_ref = shift;
862     my $error_ref = shift;
863
864     my @repacked_errors = ();
865
866     foreach my $error_code (sort keys %{ $error_ref }) {
867         my $repacked_error = {};
868         $repacked_error->{'item_sequence'} = $item_sequence_num;
869         $repacked_error->{'item_barcode'} = exists($item_ref->{'barcode'}) ? $item_ref->{'barcode'} : '';
870         $repacked_error->{'error_code'} = $error_code;
871         $repacked_error->{'error_information'} = $error_ref->{$error_code};
872         push @repacked_errors, $repacked_error;
873     } 
874
875     return @repacked_errors;
876 }
877
878 =head2 _get_unlinked_item_subfields
879
880   my $unlinked_item_subfields = _get_unlinked_item_subfields($original_item_marc, $frameworkcode);
881
882 =cut
883
884 sub _get_unlinked_item_subfields {
885     my $original_item_marc = shift;
886     my $frameworkcode = shift;
887
888     my $marcstructure = C4::Biblio::GetMarcStructure(1, $frameworkcode, { unsafe => 1 });
889
890     # assume that this record has only one field, and that that
891     # field contains only the item information
892     my $subfields = [];
893     my @fields = $original_item_marc->fields();
894     if ($#fields > -1) {
895         my $field = $fields[0];
896             my $tag = $field->tag();
897         foreach my $subfield ($field->subfields()) {
898             if (defined $subfield->[1] and
899                 $subfield->[1] ne '' and
900                 !$marcstructure->{$tag}->{$subfield->[0]}->{'kohafield'}) {
901                 push @$subfields, $subfield->[0] => $subfield->[1];
902             }
903         }
904     }
905     return $subfields;
906 }
907
908 =head2 _get_unlinked_subfields_xml
909
910   my $unlinked_subfields_xml = _get_unlinked_subfields_xml($unlinked_item_subfields);
911
912 =cut
913
914 sub _get_unlinked_subfields_xml {
915     my $unlinked_item_subfields = shift;
916
917     my $xml;
918     if (defined $unlinked_item_subfields and ref($unlinked_item_subfields) eq 'ARRAY' and $#$unlinked_item_subfields > -1) {
919         my $marc = MARC::Record->new();
920         # use of tag 999 is arbitrary, and doesn't need to match the item tag
921         # used in the framework
922         $marc->append_fields(MARC::Field->new('999', ' ', ' ', @$unlinked_item_subfields));
923         $marc->encoding("UTF-8");    
924         $xml = $marc->as_xml("USMARC");
925     }
926
927     return $xml;
928 }
929
930 =head2 _parse_unlinked_item_subfields_from_xml
931
932   my $unlinked_item_subfields = _parse_unlinked_item_subfields_from_xml($whole_item->{'more_subfields_xml'}):
933
934 =cut
935
936 sub  _parse_unlinked_item_subfields_from_xml {
937     my $xml = shift;
938     require C4::Charset;
939     return unless defined $xml and $xml ne "";
940     my $marc = MARC::Record->new_from_xml(C4::Charset::StripNonXmlChars($xml),'UTF-8');
941     my $unlinked_subfields = [];
942     my @fields = $marc->fields();
943     if ($#fields > -1) {
944         foreach my $subfield ($fields[0]->subfields()) {
945             push @$unlinked_subfields, $subfield->[0] => $subfield->[1];
946         }
947     }
948     return $unlinked_subfields;
949 }
950
951 =head2 GetAnalyticsCount
952
953   $count= &GetAnalyticsCount($itemnumber)
954
955 counts Usage of itemnumber in Analytical bibliorecords. 
956
957 =cut
958
959 sub GetAnalyticsCount {
960     my ($itemnumber) = @_;
961
962     if ( !C4::Context->preference('EasyAnalyticalRecords') ) {
963         return 0;
964     }
965
966     ### ZOOM search here
967     my $query;
968     $query= "hi=".$itemnumber;
969     my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
970     my ($err,$res,$result) = $searcher->simple_search_compat($query,0,10);
971     return ($result);
972 }
973
974 sub _SearchItems_build_where_fragment {
975     my ($filter) = @_;
976
977     my $dbh = C4::Context->dbh;
978
979     my $where_fragment;
980     if (exists($filter->{conjunction})) {
981         my (@where_strs, @where_args);
982         foreach my $f (@{ $filter->{filters} }) {
983             my $fragment = _SearchItems_build_where_fragment($f);
984             if ($fragment) {
985                 push @where_strs, $fragment->{str};
986                 push @where_args, @{ $fragment->{args} };
987             }
988         }
989         my $where_str = '';
990         if (@where_strs) {
991             $where_str = '(' . join (' ' . $filter->{conjunction} . ' ', @where_strs) . ')';
992             $where_fragment = {
993                 str => $where_str,
994                 args => \@where_args,
995             };
996         }
997     } else {
998         my @columns = Koha::Database->new()->schema()->resultset('Item')->result_source->columns;
999         push @columns, Koha::Database->new()->schema()->resultset('Biblio')->result_source->columns;
1000         push @columns, Koha::Database->new()->schema()->resultset('Biblioitem')->result_source->columns;
1001         push @columns, Koha::Database->new()->schema()->resultset('Issue')->result_source->columns;
1002         my @operators = qw(= != > < >= <= is like);
1003         push @operators, 'not like';
1004         my $field = $filter->{field} // q{};
1005         if ( (0 < grep { $_ eq $field } @columns) or (substr($field, 0, 5) eq 'marc:') ) {
1006             my $op = $filter->{operator};
1007             my $query = $filter->{query};
1008             my $ifnull = $filter->{ifnull};
1009
1010             if (!$op or (0 == grep { $_ eq $op } @operators)) {
1011                 $op = '='; # default operator
1012             }
1013
1014             my $column;
1015             if ($field =~ /^marc:(\d{3})(?:\$(\w))?$/) {
1016                 my $marcfield = $1;
1017                 my $marcsubfield = $2;
1018                 my ($kohafield) = $dbh->selectrow_array(q|
1019                     SELECT kohafield FROM marc_subfield_structure
1020                     WHERE tagfield=? AND tagsubfield=? AND frameworkcode=''
1021                 |, undef, $marcfield, $marcsubfield);
1022
1023                 if ($kohafield) {
1024                     $column = $kohafield;
1025                 } else {
1026                     # MARC field is not linked to a DB field so we need to use
1027                     # ExtractValue on marcxml from biblio_metadata or
1028                     # items.more_subfields_xml, depending on the MARC field.
1029                     my $xpath;
1030                     my $sqlfield;
1031                     my ($itemfield) = C4::Biblio::GetMarcFromKohaField('items.itemnumber');
1032                     if ($marcfield eq $itemfield) {
1033                         $sqlfield = 'more_subfields_xml';
1034                         $xpath = '//record/datafield/subfield[@code="' . $marcsubfield . '"]';
1035                     } else {
1036                         $sqlfield = 'metadata'; # From biblio_metadata
1037                         if ($marcfield < 10) {
1038                             $xpath = "//record/controlfield[\@tag=\"$marcfield\"]";
1039                         } else {
1040                             $xpath = "//record/datafield[\@tag=\"$marcfield\"]/subfield[\@code=\"$marcsubfield\"]";
1041                         }
1042                     }
1043                     $column = "ExtractValue($sqlfield, '$xpath')";
1044                 }
1045             }
1046             elsif ($field eq 'isbn') {
1047                 if ( C4::Context->preference("SearchWithISBNVariations") and $query ) {
1048                     my @isbns = C4::Koha::GetVariationsOfISBN( $query );
1049                     $query = [];
1050                     push @$query, @isbns;
1051                 }
1052                 $column = $field;
1053             }
1054             elsif ($field eq 'issn') {
1055                 if ( C4::Context->preference("SearchWithISSNVariations") and $query ) {
1056                     my @issns = C4::Koha::GetVariationsOfISSN( $query );
1057                     $query = [];
1058                     push @$query, @issns;
1059                 }
1060                 $column = $field;
1061             } else {
1062                 $column = $field;
1063             }
1064
1065             if ( defined $ifnull ) {
1066                 $column = "COALESCE($column, ?)";
1067             }
1068
1069             if (ref $query eq 'ARRAY') {
1070                 if ($op eq 'like') {
1071                     $where_fragment = {
1072                         str => "($column LIKE " . join (" OR $column LIKE ", ('?') x @$query ) . ")",
1073                         args => $query,
1074                     };
1075                 }
1076                 else {
1077                     if ($op eq '=') {
1078                         $op = 'IN';
1079                     } elsif ($op eq '!=') {
1080                         $op = 'NOT IN';
1081                     }
1082                     $where_fragment = {
1083                         str => "$column $op (" . join (',', ('?') x @$query) . ")",
1084                         args => $query,
1085                     };
1086                 }
1087             } elsif ( $op eq 'is' ) {
1088                 $where_fragment = {
1089                     str => "$column $op $query",
1090                     args => [],
1091                 };
1092             } else {
1093                 $where_fragment = {
1094                     str => "$column $op ?",
1095                     args => [ $query ],
1096                 };
1097             }
1098
1099             if ( defined $ifnull ) {
1100                 unshift @{ $where_fragment->{args} }, $ifnull;
1101             }
1102         }
1103     }
1104
1105     return $where_fragment;
1106 }
1107
1108 =head2 SearchItems
1109
1110     my ($items, $total) = SearchItems($filter, $params);
1111
1112 Perform a search among items
1113
1114 $filter is a reference to a hash which can be a filter, or a combination of filters.
1115
1116 A filter has the following keys:
1117
1118 =over 2
1119
1120 =item * field: the name of a SQL column in table items
1121
1122 =item * query: the value to search in this column
1123
1124 =item * operator: comparison operator. Can be one of = != > < >= <= like 'not like' is
1125
1126 =back
1127
1128 A combination of filters hash the following keys:
1129
1130 =over 2
1131
1132 =item * conjunction: 'AND' or 'OR'
1133
1134 =item * filters: array ref of filters
1135
1136 =back
1137
1138 $params is a reference to a hash that can contain the following parameters:
1139
1140 =over 2
1141
1142 =item * rows: Number of items to return. 0 returns everything (default: 0)
1143
1144 =item * page: Page to return (return items from (page-1)*rows to (page*rows)-1)
1145                (default: 1)
1146
1147 =item * sortby: A SQL column name in items table to sort on
1148
1149 =item * sortorder: 'ASC' or 'DESC'
1150
1151 =back
1152
1153 =cut
1154
1155 sub SearchItems {
1156     my ($filter, $params) = @_;
1157
1158     $filter //= {};
1159     $params //= {};
1160     return unless ref $filter eq 'HASH';
1161     return unless ref $params eq 'HASH';
1162
1163     # Default parameters
1164     $params->{rows} ||= 0;
1165     $params->{page} ||= 1;
1166     $params->{sortby} ||= 'itemnumber';
1167     $params->{sortorder} ||= 'ASC';
1168
1169     my ($where_str, @where_args);
1170     my $where_fragment = _SearchItems_build_where_fragment($filter);
1171     if ($where_fragment) {
1172         $where_str = $where_fragment->{str};
1173         @where_args = @{ $where_fragment->{args} };
1174     }
1175
1176     my $dbh = C4::Context->dbh;
1177     my $query = q{
1178         SELECT SQL_CALC_FOUND_ROWS items.*
1179         FROM items
1180           LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
1181           LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber
1182           LEFT JOIN biblio_metadata ON biblio_metadata.biblionumber = biblio.biblionumber
1183           LEFT JOIN issues ON issues.itemnumber = items.itemnumber
1184           WHERE 1
1185     };
1186     if (defined $where_str and $where_str ne '') {
1187         $query .= qq{ AND $where_str };
1188     }
1189
1190     $query .= q{ AND biblio_metadata.format = 'marcxml' AND biblio_metadata.schema = ? };
1191     push @where_args, C4::Context->preference('marcflavour');
1192
1193     my @columns = Koha::Database->new()->schema()->resultset('Item')->result_source->columns;
1194     push @columns, Koha::Database->new()->schema()->resultset('Biblio')->result_source->columns;
1195     push @columns, Koha::Database->new()->schema()->resultset('Biblioitem')->result_source->columns;
1196     push @columns, Koha::Database->new()->schema()->resultset('Issue')->result_source->columns;
1197
1198     if ( $params->{sortby} eq 'availability' ) {
1199         my $sortorder = (uc($params->{sortorder}) eq 'ASC') ? 'ASC' : 'DESC';
1200         $query .= qq{ ORDER BY onloan $sortorder };
1201     } else {
1202         my $sortby = (0 < grep {$params->{sortby} eq $_} @columns)
1203             ? $params->{sortby} : 'itemnumber';
1204         my $sortorder = (uc($params->{sortorder}) eq 'ASC') ? 'ASC' : 'DESC';
1205         $query .= qq{ ORDER BY $sortby $sortorder };
1206     }
1207
1208     my $rows = $params->{rows};
1209     my @limit_args;
1210     if ($rows > 0) {
1211         my $offset = $rows * ($params->{page}-1);
1212         $query .= qq { LIMIT ?, ? };
1213         push @limit_args, $offset, $rows;
1214     }
1215
1216     my $sth = $dbh->prepare($query);
1217     my $rv = $sth->execute(@where_args, @limit_args);
1218
1219     return unless ($rv);
1220     my ($total_rows) = $dbh->selectrow_array(q{ SELECT FOUND_ROWS() });
1221
1222     return ($sth->fetchall_arrayref({}), $total_rows);
1223 }
1224
1225
1226 =head1  OTHER FUNCTIONS
1227
1228 =head2 _find_value
1229
1230   ($indicators, $value) = _find_value($tag, $subfield, $record,$encoding);
1231
1232 Find the given $subfield in the given $tag in the given
1233 MARC::Record $record.  If the subfield is found, returns
1234 the (indicators, value) pair; otherwise, (undef, undef) is
1235 returned.
1236
1237 PROPOSITION :
1238 Such a function is used in addbiblio AND additem and serial-edit and maybe could be used in Authorities.
1239 I suggest we export it from this module.
1240
1241 =cut
1242
1243 sub _find_value {
1244     my ( $tagfield, $insubfield, $record, $encoding ) = @_;
1245     my @result;
1246     my $indicator;
1247     if ( $tagfield < 10 ) {
1248         if ( $record->field($tagfield) ) {
1249             push @result, $record->field($tagfield)->data();
1250         } else {
1251             push @result, "";
1252         }
1253     } else {
1254         foreach my $field ( $record->field($tagfield) ) {
1255             my @subfields = $field->subfields();
1256             foreach my $subfield (@subfields) {
1257                 if ( @$subfield[0] eq $insubfield ) {
1258                     push @result, @$subfield[1];
1259                     $indicator = $field->indicator(1) . $field->indicator(2);
1260                 }
1261             }
1262         }
1263     }
1264     return ( $indicator, @result );
1265 }
1266
1267
1268 =head2 PrepareItemrecordDisplay
1269
1270   PrepareItemrecordDisplay($bibnum,$itemumber,$defaultvalues,$frameworkcode);
1271
1272 Returns a hash with all the fields for Display a given item data in a template
1273
1274 $defaultvalues should either contain a hashref of values for the new item, or be undefined.
1275
1276 The $frameworkcode returns the item for the given frameworkcode, ONLY if bibnum is not provided
1277
1278 =cut
1279
1280 sub PrepareItemrecordDisplay {
1281
1282     my ( $bibnum, $itemnum, $defaultvalues, $frameworkcode ) = @_;
1283
1284     my $dbh = C4::Context->dbh;
1285     $frameworkcode = C4::Biblio::GetFrameworkCode($bibnum) if $bibnum;
1286     my ( $itemtagfield, $itemtagsubfield ) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
1287
1288     # Note: $tagslib obtained from GetMarcStructure() in 'unsafe' mode is
1289     # a shared data structure. No plugin (including custom ones) should change
1290     # its contents. See also GetMarcStructure.
1291     my $tagslib = GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } );
1292
1293     # Pick the default location from NewItemsDefaultLocation
1294     if ( C4::Context->preference('NewItemsDefaultLocation') ) {
1295         $defaultvalues //= {};
1296         $defaultvalues->{location} //= C4::Context->preference('NewItemsDefaultLocation');
1297     }
1298
1299     # return nothing if we don't have found an existing framework.
1300     return q{} unless $tagslib;
1301     my $itemrecord;
1302     if ($itemnum) {
1303         $itemrecord = C4::Items::GetMarcItem( $bibnum, $itemnum );
1304     }
1305     my @loop_data;
1306
1307     my $branch_limit = C4::Context->userenv ? C4::Context->userenv->{"branch"} : "";
1308     my $query = qq{
1309         SELECT authorised_value,lib FROM authorised_values
1310     };
1311     $query .= qq{
1312         LEFT JOIN authorised_values_branches ON ( id = av_id )
1313     } if $branch_limit;
1314     $query .= qq{
1315         WHERE category = ?
1316     };
1317     $query .= qq{ AND ( branchcode = ? OR branchcode IS NULL )} if $branch_limit;
1318     $query .= qq{ ORDER BY lib};
1319     my $authorised_values_sth = $dbh->prepare( $query );
1320     foreach my $tag ( sort keys %{$tagslib} ) {
1321         if ( $tag ne '' ) {
1322
1323             # loop through each subfield
1324             my $cntsubf;
1325             foreach my $subfield (
1326                 sort { $a->{display_order} <=> $b->{display_order} || $a->{subfield} cmp $b->{subfield} }
1327                 grep { ref($_) && %$_ } # Not a subfield (values for "important", "lib", "mandatory", etc.) or empty
1328                 values %{ $tagslib->{$tag} } )
1329             {
1330                 next unless ( $subfield->{'tab'} );
1331                 next if ( $subfield->{'tab'} ne "10" );
1332                 my %subfield_data;
1333                 $subfield_data{tag}           = $tag;
1334                 $subfield_data{subfield}      = $subfield->{subfield};
1335                 $subfield_data{countsubfield} = $cntsubf++;
1336                 $subfield_data{kohafield}     = $subfield->{kohafield};
1337                 $subfield_data{id}            = "tag_".$tag."_subfield_".$subfield->{subfield}."_".int(rand(1000000));
1338
1339                 #        $subfield_data{marc_lib}=$tagslib->{$tag}->{$subfield}->{lib};
1340                 $subfield_data{marc_lib}   = $subfield->{lib};
1341                 $subfield_data{mandatory}  = $subfield->{mandatory};
1342                 $subfield_data{repeatable} = $subfield->{repeatable};
1343                 $subfield_data{hidden}     = "display:none"
1344                   if ( ( $subfield->{hidden} > 4 )
1345                     || ( $subfield->{hidden} < -4 ) );
1346                 my ( $x, $defaultvalue );
1347                 if ($itemrecord) {
1348                     ( $x, $defaultvalue ) = _find_value( $tag, $subfield->{subfield}, $itemrecord );
1349                 }
1350                 $defaultvalue = $subfield->{defaultvalue} unless $defaultvalue;
1351                 if ( !defined $defaultvalue ) {
1352                     $defaultvalue = q||;
1353                 } else {
1354                     $defaultvalue =~ s/"/&quot;/g;
1355                     # get today date & replace <<YYYY>>, <<MM>>, <<DD>> if provided in the default value
1356                     my $today_dt = dt_from_string;
1357                     my $year     = $today_dt->strftime('%Y');
1358                     my $shortyear     = $today_dt->strftime('%y');
1359                     my $month    = $today_dt->strftime('%m');
1360                     my $day      = $today_dt->strftime('%d');
1361                     $defaultvalue =~ s/<<YYYY>>/$year/g;
1362                     $defaultvalue =~ s/<<YY>>/$shortyear/g;
1363                     $defaultvalue =~ s/<<MM>>/$month/g;
1364                     $defaultvalue =~ s/<<DD>>/$day/g;
1365
1366                     # And <<USER>> with surname (?)
1367                     my $username =
1368                       (   C4::Context->userenv
1369                         ? C4::Context->userenv->{'surname'}
1370                         : "superlibrarian" );
1371                     $defaultvalue =~ s/<<USER>>/$username/g;
1372                 }
1373
1374                 my $maxlength = $subfield->{maxlength};
1375
1376                 # search for itemcallnumber if applicable
1377                 if ( $subfield->{kohafield} eq 'items.itemcallnumber'
1378                     && C4::Context->preference('itemcallnumber') && $itemrecord) {
1379                     foreach my $itemcn_pref (split(/,/,C4::Context->preference('itemcallnumber'))){
1380                         my $CNtag      = substr( $itemcn_pref, 0, 3 );
1381                         next unless my $field = $itemrecord->field($CNtag);
1382                         my $CNsubfields = substr( $itemcn_pref, 3 );
1383                         $CNsubfields = undef if $CNsubfields eq '';
1384                         $defaultvalue = $field->as_string( $CNsubfields, ' ');
1385                         last if $defaultvalue;
1386                     }
1387                 }
1388                 if (   $subfield->{kohafield} eq 'items.itemcallnumber'
1389                     && $defaultvalues
1390                     && $defaultvalues->{'callnumber'} ) {
1391                     if( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield->{subfield}) ){
1392                         # if the item record exists, only use default value if the item has no callnumber
1393                         $defaultvalue = $defaultvalues->{callnumber};
1394                     } elsif ( !$itemrecord and $defaultvalues ) {
1395                         # if the item record *doesn't* exists, always use the default value
1396                         $defaultvalue = $defaultvalues->{callnumber};
1397                     }
1398                 }
1399                 if (   ( $subfield->{kohafield} eq 'items.holdingbranch' || $subfield->{kohafield} eq 'items.homebranch' )
1400                     && $defaultvalues
1401                     && $defaultvalues->{'branchcode'} ) {
1402                     if ( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield) ) {
1403                         $defaultvalue = $defaultvalues->{branchcode};
1404                     }
1405                 }
1406                 if (   ( $subfield->{kohafield} eq 'items.location' )
1407                     && $defaultvalues
1408                     && $defaultvalues->{'location'} ) {
1409
1410                     if ( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield->{subfield}) ) {
1411                         # if the item record exists, only use default value if the item has no locationr
1412                         $defaultvalue = $defaultvalues->{location};
1413                     } elsif ( !$itemrecord and $defaultvalues ) {
1414                         # if the item record *doesn't* exists, always use the default value
1415                         $defaultvalue = $defaultvalues->{location};
1416                     }
1417                 }
1418                 if (   ( $subfield->{kohafield} eq 'items.ccode' )
1419                     && $defaultvalues
1420                     && $defaultvalues->{'ccode'} ) {
1421
1422                     if ( !$itemrecord and $defaultvalues ) {
1423                         # if the item record *doesn't* exists, always use the default value
1424                         $defaultvalue = $defaultvalues->{ccode};
1425                     }
1426                 }
1427                 if ( $subfield->{authorised_value} ) {
1428                     my @authorised_values;
1429                     my %authorised_lib;
1430
1431                     # builds list, depending on authorised value...
1432                     #---- branch
1433                     if ( $subfield->{'authorised_value'} eq "branches" ) {
1434                         if (   ( C4::Context->preference("IndependentBranches") )
1435                             && !C4::Context->IsSuperLibrarian() ) {
1436                             my $sth = $dbh->prepare( "SELECT branchcode,branchname FROM branches WHERE branchcode = ? ORDER BY branchname" );
1437                             $sth->execute( C4::Context->userenv->{branch} );
1438                             push @authorised_values, ""
1439                               unless ( $subfield->{mandatory} );
1440                             while ( my ( $branchcode, $branchname ) = $sth->fetchrow_array ) {
1441                                 push @authorised_values, $branchcode;
1442                                 $authorised_lib{$branchcode} = $branchname;
1443                             }
1444                         } else {
1445                             my $sth = $dbh->prepare( "SELECT branchcode,branchname FROM branches ORDER BY branchname" );
1446                             $sth->execute;
1447                             push @authorised_values, ""
1448                               unless ( $subfield->{mandatory} );
1449                             while ( my ( $branchcode, $branchname ) = $sth->fetchrow_array ) {
1450                                 push @authorised_values, $branchcode;
1451                                 $authorised_lib{$branchcode} = $branchname;
1452                             }
1453                         }
1454
1455                         $defaultvalue = C4::Context->userenv ? C4::Context->userenv->{branch} : undef;
1456                         if ( $defaultvalues and $defaultvalues->{branchcode} ) {
1457                             $defaultvalue = $defaultvalues->{branchcode};
1458                         }
1459
1460                         #----- itemtypes
1461                     } elsif ( $subfield->{authorised_value} eq "itemtypes" ) {
1462                         my $itemtypes = Koha::ItemTypes->search_with_localization;
1463                         push @authorised_values, "";
1464                         while ( my $itemtype = $itemtypes->next ) {
1465                             push @authorised_values, $itemtype->itemtype;
1466                             $authorised_lib{$itemtype->itemtype} = $itemtype->translated_description;
1467                         }
1468                         if ($defaultvalues && $defaultvalues->{'itemtype'}) {
1469                             $defaultvalue = $defaultvalues->{'itemtype'};
1470                         }
1471
1472                         #---- class_sources
1473                     } elsif ( $subfield->{authorised_value} eq "cn_source" ) {
1474                         push @authorised_values, "";
1475
1476                         my $class_sources = GetClassSources();
1477                         my $default_source = $defaultvalue || C4::Context->preference("DefaultClassificationSource");
1478
1479                         foreach my $class_source (sort keys %$class_sources) {
1480                             next unless $class_sources->{$class_source}->{'used'} or
1481                                         ($class_source eq $default_source);
1482                             push @authorised_values, $class_source;
1483                             $authorised_lib{$class_source} = $class_sources->{$class_source}->{'description'};
1484                         }
1485
1486                         $defaultvalue = $default_source;
1487
1488                         #---- "true" authorised value
1489                     } else {
1490                         $authorised_values_sth->execute(
1491                             $subfield->{authorised_value},
1492                             $branch_limit ? $branch_limit : ()
1493                         );
1494                         push @authorised_values, "";
1495                         while ( my ( $value, $lib ) = $authorised_values_sth->fetchrow_array ) {
1496                             push @authorised_values, $value;
1497                             $authorised_lib{$value} = $lib;
1498                         }
1499                     }
1500                     $subfield_data{marc_value} = {
1501                         type    => 'select',
1502                         values  => \@authorised_values,
1503                         default => $defaultvalue // q{},
1504                         labels  => \%authorised_lib,
1505                     };
1506                 } elsif ( $subfield->{value_builder} ) {
1507                 # it is a plugin
1508                     require Koha::FrameworkPlugin;
1509                     my $plugin = Koha::FrameworkPlugin->new({
1510                         name => $subfield->{value_builder},
1511                         item_style => 1,
1512                     });
1513                     my $pars = { dbh => $dbh, record => undef, tagslib =>$tagslib, id => $subfield_data{id} };
1514                     $plugin->build( $pars );
1515                     if ( $itemrecord and my $field = $itemrecord->field($tag) ) {
1516                         $defaultvalue = $field->subfield($subfield->{subfield}) || q{};
1517                     }
1518                     if( !$plugin->errstr ) {
1519                         #TODO Move html to template; see report 12176/13397
1520                         my $tab= $plugin->noclick? '-1': '';
1521                         my $class= $plugin->noclick? ' disabled': '';
1522                         my $title= $plugin->noclick? 'No popup': 'Tag editor';
1523                         $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;
1524                     } else {
1525                         warn $plugin->errstr;
1526                         $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
1527                     }
1528                 }
1529                 elsif ( $tag eq '' ) {       # it's an hidden field
1530                     $subfield_data{marc_value} = qq(<input type="hidden" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength" value="$defaultvalue" />);
1531                 }
1532                 elsif ( $subfield->{'hidden'} ) {   # FIXME: shouldn't input type be "hidden" ?
1533                     $subfield_data{marc_value} = qq(<input type="text" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength" value="$defaultvalue" />);
1534                 }
1535                 elsif ( length($defaultvalue) > 100
1536                             or (C4::Context->preference("marcflavour") eq "UNIMARC" and
1537                                   300 <= $tag && $tag < 400 && $subfield->{subfield} eq 'a' )
1538                             or (C4::Context->preference("marcflavour") eq "MARC21"  and
1539                                   500 <= $tag && $tag < 600                     )
1540                           ) {
1541                     # oversize field (textarea)
1542                     $subfield_data{marc_value} = qq(<textarea id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength">$defaultvalue</textarea>\n");
1543                 } else {
1544                     $subfield_data{marc_value} = qq(<input type="text" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength" value="$defaultvalue" />);
1545                 }
1546                 push( @loop_data, \%subfield_data );
1547             }
1548         }
1549     }
1550     my $itemnumber;
1551     if ( $itemrecord && $itemrecord->field($itemtagfield) ) {
1552         $itemnumber = $itemrecord->subfield( $itemtagfield, $itemtagsubfield );
1553     }
1554     return {
1555         'itemtagfield'    => $itemtagfield,
1556         'itemtagsubfield' => $itemtagsubfield,
1557         'itemnumber'      => $itemnumber,
1558         'iteminformation' => \@loop_data
1559     };
1560 }
1561
1562 sub ToggleNewStatus {
1563     my ( $params ) = @_;
1564     my @rules = @{ $params->{rules} };
1565     my $report_only = $params->{report_only};
1566
1567     my $dbh = C4::Context->dbh;
1568     my @errors;
1569     my @item_columns = map { "items.$_" } Koha::Items->columns;
1570     my @biblioitem_columns = map { "biblioitems.$_" } Koha::Biblioitems->columns;
1571     my $report;
1572     for my $rule ( @rules ) {
1573         my $age = $rule->{age};
1574         # Default to using items.dateaccessioned if there's an old item modification rule
1575         # missing an agefield value
1576         my $agefield = $rule->{agefield} ? $rule->{agefield} : 'items.dateaccessioned';
1577         my $conditions = $rule->{conditions};
1578         my $substitutions = $rule->{substitutions};
1579         foreach ( @$substitutions ) {
1580             ( $_->{item_field} ) = ( $_->{field} =~ /items\.(.*)/ );
1581         }
1582         my @params;
1583
1584         my $query = q|
1585             SELECT items.*
1586             FROM items
1587             LEFT JOIN biblioitems ON biblioitems.biblionumber = items.biblionumber
1588             WHERE 1
1589         |;
1590         for my $condition ( @$conditions ) {
1591             next unless $condition->{field};
1592             if (
1593                  grep { $_ eq $condition->{field} } @item_columns
1594               or grep { $_ eq $condition->{field} } @biblioitem_columns
1595             ) {
1596                 if ( $condition->{value} =~ /\|/ ) {
1597                     my @values = split /\|/, $condition->{value};
1598                     $query .= qq| AND $condition->{field} IN (|
1599                         . join( ',', ('?') x scalar @values )
1600                         . q|)|;
1601                     push @params, @values;
1602                 } else {
1603                     $query .= qq| AND $condition->{field} = ?|;
1604                     push @params, $condition->{value};
1605                 }
1606             }
1607         }
1608         if ( defined $age ) {
1609             $query .= qq| AND TO_DAYS(NOW()) - TO_DAYS($agefield) >= ? |;
1610             push @params, $age;
1611         }
1612         my $sth = $dbh->prepare($query);
1613         $sth->execute( @params );
1614         while ( my $values = $sth->fetchrow_hashref ) {
1615             my $biblionumber = $values->{biblionumber};
1616             my $itemnumber = $values->{itemnumber};
1617             my $item = Koha::Items->find($itemnumber);
1618             for my $substitution ( @$substitutions ) {
1619                 my $field = $substitution->{item_field};
1620                 my $value = $substitution->{value};
1621                 next unless $substitution->{field};
1622                 next if ( defined $values->{ $substitution->{item_field} } and $values->{ $substitution->{item_field} } eq $substitution->{value} );
1623                 $item->$field($value);
1624                 push @{ $report->{$itemnumber} }, $substitution;
1625             }
1626             $item->store unless $report_only;
1627         }
1628     }
1629
1630     return $report;
1631 }
1632
1633 1;