Bug 36207: (RM follow-up) CSRF correction
[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({ skip_holds_queue => 1 });
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 => 1, # avoid indexing duplication, let ->transit handle it
384             }
385         );
386         $transfer->transit({ skip_record_index => $params->{skip_record_index} });
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);
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('items.itemnumber');
782
783     my $unlinked_item_subfields = _parse_unlinked_item_subfields_from_xml($mungeditem->{'items.more_subfields_xml'});
784     if (defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1) {
785                 foreach my $field ($itemmarc->field($itemtag)){
786             $field->add_subfields(@$unlinked_item_subfields);
787         }
788     }
789         return $itemmarc;
790 }
791
792 =head1 PRIVATE FUNCTIONS AND VARIABLES
793
794 The following functions are not meant to be called
795 directly, but are documented in order to explain
796 the inner workings of C<C4::Items>.
797
798 =cut
799
800 =head2 _marc_from_item_hash
801
802   my $item_marc = _marc_from_item_hash($item, $frameworkcode[, $unlinked_item_subfields]);
803
804 Given an item hash representing a complete item record,
805 create a C<MARC::Record> object containing an embedded
806 tag representing that item.
807
808 The third, optional parameter C<$unlinked_item_subfields> is
809 an arrayref of subfields (not mapped to C<items> fields per the
810 framework) to be added to the MARC representation
811 of the item.
812
813 =cut
814
815 sub _marc_from_item_hash {
816     my $item = shift;
817     my $frameworkcode = shift;
818     my $unlinked_item_subfields;
819     if (@_) {
820         $unlinked_item_subfields = shift;
821     }
822    
823     # Tack on 'items.' prefix to column names so lookup from MARC frameworks will work
824     # Also, don't emit a subfield if the underlying field is blank.
825     my $mungeditem = { map {  (defined($item->{$_}) and $item->{$_} ne '') ? 
826                                 (/^items\./ ? ($_ => $item->{$_}) : ("items.$_" => $item->{$_})) 
827                                 : ()  } keys %{ $item } }; 
828
829     my $item_marc = MARC::Record->new();
830     foreach my $item_field ( keys %{$mungeditem} ) {
831         my ( $tag, $subfield ) = C4::Biblio::GetMarcFromKohaField( $item_field );
832         next unless defined $tag and defined $subfield;    # skip if not mapped to MARC field
833         my @values = split(/\s?\|\s?/, $mungeditem->{$item_field}, -1);
834         foreach my $value (@values){
835             if ( my $field = $item_marc->field($tag) ) {
836                     $field->add_subfields( $subfield => $value );
837             } else {
838                 my $add_subfields = [];
839                 if (defined $unlinked_item_subfields and ref($unlinked_item_subfields) eq 'ARRAY' and $#$unlinked_item_subfields > -1) {
840                     $add_subfields = $unlinked_item_subfields;
841             }
842             $item_marc->add_fields( $tag, " ", " ", $subfield => $value, @$add_subfields );
843             }
844         }
845     }
846
847     return $item_marc;
848 }
849
850 =head2 _repack_item_errors
851
852 Add an error message hash generated by C<CheckItemPreSave>
853 to a list of errors.
854
855 =cut
856
857 sub _repack_item_errors {
858     my $item_sequence_num = shift;
859     my $item_ref = shift;
860     my $error_ref = shift;
861
862     my @repacked_errors = ();
863
864     foreach my $error_code (sort keys %{ $error_ref }) {
865         my $repacked_error = {};
866         $repacked_error->{'item_sequence'} = $item_sequence_num;
867         $repacked_error->{'item_barcode'} = exists($item_ref->{'barcode'}) ? $item_ref->{'barcode'} : '';
868         $repacked_error->{'error_code'} = $error_code;
869         $repacked_error->{'error_information'} = $error_ref->{$error_code};
870         push @repacked_errors, $repacked_error;
871     } 
872
873     return @repacked_errors;
874 }
875
876 =head2 _get_unlinked_item_subfields
877
878   my $unlinked_item_subfields = _get_unlinked_item_subfields($original_item_marc, $frameworkcode);
879
880 =cut
881
882 sub _get_unlinked_item_subfields {
883     my $original_item_marc = shift;
884     my $frameworkcode = shift;
885
886     my $marcstructure = C4::Biblio::GetMarcStructure(1, $frameworkcode, { unsafe => 1 });
887
888     # assume that this record has only one field, and that that
889     # field contains only the item information
890     my $subfields = [];
891     my @fields = $original_item_marc->fields();
892     if ($#fields > -1) {
893         my $field = $fields[0];
894             my $tag = $field->tag();
895         foreach my $subfield ($field->subfields()) {
896             if (defined $subfield->[1] and
897                 $subfield->[1] ne '' and
898                 !$marcstructure->{$tag}->{$subfield->[0]}->{'kohafield'}) {
899                 push @$subfields, $subfield->[0] => $subfield->[1];
900             }
901         }
902     }
903     return $subfields;
904 }
905
906 =head2 _get_unlinked_subfields_xml
907
908   my $unlinked_subfields_xml = _get_unlinked_subfields_xml($unlinked_item_subfields);
909
910 =cut
911
912 sub _get_unlinked_subfields_xml {
913     my $unlinked_item_subfields = shift;
914
915     my $xml;
916     if (defined $unlinked_item_subfields and ref($unlinked_item_subfields) eq 'ARRAY' and $#$unlinked_item_subfields > -1) {
917         my $marc = MARC::Record->new();
918         # use of tag 999 is arbitrary, and doesn't need to match the item tag
919         # used in the framework
920         $marc->append_fields(MARC::Field->new('999', ' ', ' ', @$unlinked_item_subfields));
921         $marc->encoding("UTF-8");    
922         $xml = $marc->as_xml("USMARC");
923     }
924
925     return $xml;
926 }
927
928 =head2 _parse_unlinked_item_subfields_from_xml
929
930   my $unlinked_item_subfields = _parse_unlinked_item_subfields_from_xml($whole_item->{'more_subfields_xml'}):
931
932 =cut
933
934 sub  _parse_unlinked_item_subfields_from_xml {
935     my $xml = shift;
936     require C4::Charset;
937     return unless defined $xml and $xml ne "";
938     my $marc = MARC::Record->new_from_xml(C4::Charset::StripNonXmlChars($xml),'UTF-8');
939     my $unlinked_subfields = [];
940     my @fields = $marc->fields();
941     if ($#fields > -1) {
942         foreach my $subfield ($fields[0]->subfields()) {
943             push @$unlinked_subfields, $subfield->[0] => $subfield->[1];
944         }
945     }
946     return $unlinked_subfields;
947 }
948
949 =head2 GetAnalyticsCount
950
951   $count= &GetAnalyticsCount($itemnumber)
952
953 counts Usage of itemnumber in Analytical bibliorecords. 
954
955 =cut
956
957 sub GetAnalyticsCount {
958     my ($itemnumber) = @_;
959
960     if ( !C4::Context->preference('EasyAnalyticalRecords') ) {
961         return 0;
962     }
963
964     ### ZOOM search here
965     my $query;
966     $query= "hi=".$itemnumber;
967     my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
968     my ($err,$res,$result) = $searcher->simple_search_compat($query,0,10);
969     return ($result);
970 }
971
972 sub _SearchItems_build_where_fragment {
973     my ($filter) = @_;
974
975     my $dbh = C4::Context->dbh;
976
977     my $where_fragment;
978     if (exists($filter->{conjunction})) {
979         my (@where_strs, @where_args);
980         foreach my $f (@{ $filter->{filters} }) {
981             my $fragment = _SearchItems_build_where_fragment($f);
982             if ($fragment) {
983                 push @where_strs, $fragment->{str};
984                 push @where_args, @{ $fragment->{args} };
985             }
986         }
987         my $where_str = '';
988         if (@where_strs) {
989             $where_str = '(' . join (' ' . $filter->{conjunction} . ' ', @where_strs) . ')';
990             $where_fragment = {
991                 str => $where_str,
992                 args => \@where_args,
993             };
994         }
995     } else {
996         my @columns = Koha::Database->new()->schema()->resultset('Item')->result_source->columns;
997         push @columns, Koha::Database->new()->schema()->resultset('Biblio')->result_source->columns;
998         push @columns, Koha::Database->new()->schema()->resultset('Biblioitem')->result_source->columns;
999         push @columns, Koha::Database->new()->schema()->resultset('Issue')->result_source->columns;
1000         my @operators = qw(= != > < >= <= is like);
1001         push @operators, 'not like';
1002         my $field = $filter->{field} // q{};
1003         if ( (0 < grep { $_ eq $field } @columns) or (substr($field, 0, 5) eq 'marc:') ) {
1004             my $op = $filter->{operator};
1005             my $query = $filter->{query};
1006             my $ifnull = $filter->{ifnull};
1007
1008             if (!$op or (0 == grep { $_ eq $op } @operators)) {
1009                 $op = '='; # default operator
1010             }
1011
1012             my $column;
1013             if ($field =~ /^marc:(\d{3})(?:\$(\w))?$/) {
1014                 my $marcfield = $1;
1015                 my $marcsubfield = $2;
1016                 my ($kohafield) = $dbh->selectrow_array(q|
1017                     SELECT kohafield FROM marc_subfield_structure
1018                     WHERE tagfield=? AND tagsubfield=? AND frameworkcode=''
1019                 |, undef, $marcfield, $marcsubfield);
1020
1021                 if ($kohafield) {
1022                     $column = $kohafield;
1023                 } else {
1024                     # MARC field is not linked to a DB field so we need to use
1025                     # ExtractValue on marcxml from biblio_metadata or
1026                     # items.more_subfields_xml, depending on the MARC field.
1027                     my $xpath;
1028                     my $sqlfield;
1029                     my ($itemfield) = C4::Biblio::GetMarcFromKohaField('items.itemnumber');
1030                     if ($marcfield eq $itemfield) {
1031                         $sqlfield = 'more_subfields_xml';
1032                         $xpath = '//record/datafield/subfield[@code="' . $marcsubfield . '"]';
1033                     } else {
1034                         $sqlfield = 'metadata'; # From biblio_metadata
1035                         if ($marcfield < 10) {
1036                             $xpath = "//record/controlfield[\@tag=\"$marcfield\"]";
1037                         } else {
1038                             $xpath = "//record/datafield[\@tag=\"$marcfield\"]/subfield[\@code=\"$marcsubfield\"]";
1039                         }
1040                     }
1041                     $column = "ExtractValue($sqlfield, '$xpath')";
1042                 }
1043             }
1044             elsif ($field eq 'isbn') {
1045                 if ( C4::Context->preference("SearchWithISBNVariations") and $query ) {
1046                     my @isbns = C4::Koha::GetVariationsOfISBN( $query );
1047                     $query = [];
1048                     push @$query, @isbns;
1049                 }
1050                 $column = $field;
1051             }
1052             elsif ($field eq 'issn') {
1053                 if ( C4::Context->preference("SearchWithISSNVariations") and $query ) {
1054                     my @issns = C4::Koha::GetVariationsOfISSN( $query );
1055                     $query = [];
1056                     push @$query, @issns;
1057                 }
1058                 $column = $field;
1059             } else {
1060                 $column = $field;
1061             }
1062
1063             if ( defined $ifnull ) {
1064                 $column = "COALESCE($column, ?)";
1065             }
1066
1067             if (ref $query eq 'ARRAY') {
1068                 if ($op eq 'like') {
1069                     $where_fragment = {
1070                         str => "($column LIKE " . join (" OR $column LIKE ", ('?') x @$query ) . ")",
1071                         args => $query,
1072                     };
1073                 }
1074                 else {
1075                     if ($op eq '=') {
1076                         $op = 'IN';
1077                     } elsif ($op eq '!=') {
1078                         $op = 'NOT IN';
1079                     }
1080                     $where_fragment = {
1081                         str => "$column $op (" . join (',', ('?') x @$query) . ")",
1082                         args => $query,
1083                     };
1084                 }
1085             } elsif ( $op eq 'is' ) {
1086                 $where_fragment = {
1087                     str => "$column $op $query",
1088                     args => [],
1089                 };
1090             } else {
1091                 $where_fragment = {
1092                     str => "$column $op ?",
1093                     args => [ $query ],
1094                 };
1095             }
1096
1097             if ( defined $ifnull ) {
1098                 unshift @{ $where_fragment->{args} }, $ifnull;
1099             }
1100         }
1101     }
1102
1103     return $where_fragment;
1104 }
1105
1106 =head2 SearchItems
1107
1108     my ($items, $total) = SearchItems($filter, $params);
1109
1110 Perform a search among items
1111
1112 $filter is a reference to a hash which can be a filter, or a combination of filters.
1113
1114 A filter has the following keys:
1115
1116 =over 2
1117
1118 =item * field: the name of a SQL column in table items
1119
1120 =item * query: the value to search in this column
1121
1122 =item * operator: comparison operator. Can be one of = != > < >= <= like 'not like' is
1123
1124 =back
1125
1126 A combination of filters hash the following keys:
1127
1128 =over 2
1129
1130 =item * conjunction: 'AND' or 'OR'
1131
1132 =item * filters: array ref of filters
1133
1134 =back
1135
1136 $params is a reference to a hash that can contain the following parameters:
1137
1138 =over 2
1139
1140 =item * rows: Number of items to return. 0 returns everything (default: 0)
1141
1142 =item * page: Page to return (return items from (page-1)*rows to (page*rows)-1)
1143                (default: 1)
1144
1145 =item * sortby: A SQL column name in items table to sort on
1146
1147 =item * sortorder: 'ASC' or 'DESC'
1148
1149 =back
1150
1151 =cut
1152
1153 sub SearchItems {
1154     my ($filter, $params) = @_;
1155
1156     $filter //= {};
1157     $params //= {};
1158     return unless ref $filter eq 'HASH';
1159     return unless ref $params eq 'HASH';
1160
1161     # Default parameters
1162     $params->{rows} ||= 0;
1163     $params->{page} ||= 1;
1164     $params->{sortby} ||= 'itemnumber';
1165     $params->{sortorder} ||= 'ASC';
1166
1167     my ($where_str, @where_args);
1168     my $where_fragment = _SearchItems_build_where_fragment($filter);
1169     if ($where_fragment) {
1170         $where_str = $where_fragment->{str};
1171         @where_args = @{ $where_fragment->{args} };
1172     }
1173
1174     my $dbh = C4::Context->dbh;
1175     my $query = q{
1176         SELECT SQL_CALC_FOUND_ROWS items.*
1177         FROM items
1178           LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
1179           LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber
1180           LEFT JOIN biblio_metadata ON biblio_metadata.biblionumber = biblio.biblionumber
1181           LEFT JOIN issues ON issues.itemnumber = items.itemnumber
1182           WHERE 1
1183     };
1184     if (defined $where_str and $where_str ne '') {
1185         $query .= qq{ AND $where_str };
1186     }
1187
1188     $query .= q{ AND biblio_metadata.format = 'marcxml' AND biblio_metadata.schema = ? };
1189     push @where_args, C4::Context->preference('marcflavour');
1190
1191     my @columns = Koha::Database->new()->schema()->resultset('Item')->result_source->columns;
1192     push @columns, Koha::Database->new()->schema()->resultset('Biblio')->result_source->columns;
1193     push @columns, Koha::Database->new()->schema()->resultset('Biblioitem')->result_source->columns;
1194     push @columns, Koha::Database->new()->schema()->resultset('Issue')->result_source->columns;
1195
1196     if ( $params->{sortby} eq 'availability' ) {
1197         my $sortorder = (uc($params->{sortorder}) eq 'ASC') ? 'ASC' : 'DESC';
1198         $query .= qq{ ORDER BY onloan $sortorder };
1199     } else {
1200         my $sortby = (0 < grep {$params->{sortby} eq $_} @columns)
1201             ? $params->{sortby} : 'itemnumber';
1202         my $sortorder = (uc($params->{sortorder}) eq 'ASC') ? 'ASC' : 'DESC';
1203         $query .= qq{ ORDER BY $sortby $sortorder };
1204     }
1205
1206     my $rows = $params->{rows};
1207     my @limit_args;
1208     if ($rows > 0) {
1209         my $offset = $rows * ($params->{page}-1);
1210         $query .= qq { LIMIT ?, ? };
1211         push @limit_args, $offset, $rows;
1212     }
1213
1214     my $sth = $dbh->prepare($query);
1215     my $rv = $sth->execute(@where_args, @limit_args);
1216
1217     return unless ($rv);
1218     my ($total_rows) = $dbh->selectrow_array(q{ SELECT FOUND_ROWS() });
1219
1220     return ($sth->fetchall_arrayref({}), $total_rows);
1221 }
1222
1223
1224 =head1  OTHER FUNCTIONS
1225
1226 =head2 _find_value
1227
1228   ($indicators, $value) = _find_value($tag, $subfield, $record,$encoding);
1229
1230 Find the given $subfield in the given $tag in the given
1231 MARC::Record $record.  If the subfield is found, returns
1232 the (indicators, value) pair; otherwise, (undef, undef) is
1233 returned.
1234
1235 PROPOSITION :
1236 Such a function is used in addbiblio AND additem and serial-edit and maybe could be used in Authorities.
1237 I suggest we export it from this module.
1238
1239 =cut
1240
1241 sub _find_value {
1242     my ( $tagfield, $insubfield, $record, $encoding ) = @_;
1243     my @result;
1244     my $indicator;
1245     if ( $tagfield < 10 ) {
1246         if ( $record->field($tagfield) ) {
1247             push @result, $record->field($tagfield)->data();
1248         } else {
1249             push @result, "";
1250         }
1251     } else {
1252         foreach my $field ( $record->field($tagfield) ) {
1253             my @subfields = $field->subfields();
1254             foreach my $subfield (@subfields) {
1255                 if ( @$subfield[0] eq $insubfield ) {
1256                     push @result, @$subfield[1];
1257                     $indicator = $field->indicator(1) . $field->indicator(2);
1258                 }
1259             }
1260         }
1261     }
1262     return ( $indicator, @result );
1263 }
1264
1265
1266 =head2 PrepareItemrecordDisplay
1267
1268   PrepareItemrecordDisplay($bibnum,$itemumber,$defaultvalues,$frameworkcode);
1269
1270 Returns a hash with all the fields for Display a given item data in a template
1271
1272 $defaultvalues should either contain a hashref of values for the new item, or be undefined.
1273
1274 The $frameworkcode returns the item for the given frameworkcode, ONLY if bibnum is not provided
1275
1276 =cut
1277
1278 sub PrepareItemrecordDisplay {
1279
1280     my ( $bibnum, $itemnum, $defaultvalues, $frameworkcode ) = @_;
1281
1282     my $dbh = C4::Context->dbh;
1283     $frameworkcode = C4::Biblio::GetFrameworkCode($bibnum) if $bibnum;
1284     my ( $itemtagfield, $itemtagsubfield ) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
1285
1286     # Note: $tagslib obtained from GetMarcStructure() in 'unsafe' mode is
1287     # a shared data structure. No plugin (including custom ones) should change
1288     # its contents. See also GetMarcStructure.
1289     my $tagslib = GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } );
1290
1291     # Pick the default location from NewItemsDefaultLocation
1292     if ( C4::Context->preference('NewItemsDefaultLocation') ) {
1293         $defaultvalues //= {};
1294         $defaultvalues->{location} //= C4::Context->preference('NewItemsDefaultLocation');
1295     }
1296
1297     # return nothing if we don't have found an existing framework.
1298     return q{} unless $tagslib;
1299     my $itemrecord;
1300     if ($itemnum) {
1301         $itemrecord = C4::Items::GetMarcItem( $bibnum, $itemnum );
1302     }
1303     my @loop_data;
1304
1305     my $branch_limit = C4::Context->userenv ? C4::Context->userenv->{"branch"} : "";
1306     my $query = qq{
1307         SELECT authorised_value,lib FROM authorised_values
1308     };
1309     $query .= qq{
1310         LEFT JOIN authorised_values_branches ON ( id = av_id )
1311     } if $branch_limit;
1312     $query .= qq{
1313         WHERE category = ?
1314     };
1315     $query .= qq{ AND ( branchcode = ? OR branchcode IS NULL )} if $branch_limit;
1316     $query .= qq{ ORDER BY lib};
1317     my $authorised_values_sth = $dbh->prepare( $query );
1318     foreach my $tag ( sort keys %{$tagslib} ) {
1319         if ( $tag ne '' ) {
1320
1321             # loop through each subfield
1322             my $cntsubf;
1323             foreach my $subfield (
1324                 sort { $a->{display_order} <=> $b->{display_order} || $a->{subfield} cmp $b->{subfield} }
1325                 grep { ref($_) && %$_ } # Not a subfield (values for "important", "lib", "mandatory", etc.) or empty
1326                 values %{ $tagslib->{$tag} } )
1327             {
1328                 next unless ( $subfield->{'tab'} );
1329                 next if ( $subfield->{'tab'} ne "10" );
1330                 my %subfield_data;
1331                 $subfield_data{tag}           = $tag;
1332                 $subfield_data{subfield}      = $subfield->{subfield};
1333                 $subfield_data{countsubfield} = $cntsubf++;
1334                 $subfield_data{kohafield}     = $subfield->{kohafield};
1335                 $subfield_data{id}            = "tag_".$tag."_subfield_".$subfield->{subfield}."_".int(rand(1000000));
1336
1337                 #        $subfield_data{marc_lib}=$tagslib->{$tag}->{$subfield}->{lib};
1338                 $subfield_data{marc_lib}   = $subfield->{lib};
1339                 $subfield_data{mandatory}  = $subfield->{mandatory};
1340                 $subfield_data{repeatable} = $subfield->{repeatable};
1341                 $subfield_data{hidden}     = "display:none"
1342                   if ( ( $subfield->{hidden} > 4 )
1343                     || ( $subfield->{hidden} < -4 ) );
1344                 my ( $x, $defaultvalue );
1345                 if ($itemrecord) {
1346                     ( $x, $defaultvalue ) = _find_value( $tag, $subfield->{subfield}, $itemrecord );
1347                 }
1348                 $defaultvalue = $subfield->{defaultvalue} unless $defaultvalue;
1349                 if ( !defined $defaultvalue ) {
1350                     $defaultvalue = q||;
1351                 } else {
1352                     $defaultvalue =~ s/"/&quot;/g;
1353                     # get today date & replace <<YYYY>>, <<MM>>, <<DD>> if provided in the default value
1354                     my $today_dt = dt_from_string;
1355                     my $year     = $today_dt->strftime('%Y');
1356                     my $shortyear     = $today_dt->strftime('%y');
1357                     my $month    = $today_dt->strftime('%m');
1358                     my $day      = $today_dt->strftime('%d');
1359                     $defaultvalue =~ s/<<YYYY>>/$year/g;
1360                     $defaultvalue =~ s/<<YY>>/$shortyear/g;
1361                     $defaultvalue =~ s/<<MM>>/$month/g;
1362                     $defaultvalue =~ s/<<DD>>/$day/g;
1363
1364                     # And <<USER>> with surname (?)
1365                     my $username =
1366                       (   C4::Context->userenv
1367                         ? C4::Context->userenv->{'surname'}
1368                         : "superlibrarian" );
1369                     $defaultvalue =~ s/<<USER>>/$username/g;
1370                 }
1371
1372                 my $maxlength = $subfield->{maxlength};
1373
1374                 # search for itemcallnumber if applicable
1375                 if ( $subfield->{kohafield} eq 'items.itemcallnumber'
1376                     && C4::Context->preference('itemcallnumber') && $itemrecord) {
1377                     foreach my $itemcn_pref (split(/,/,C4::Context->preference('itemcallnumber'))){
1378                         my $CNtag      = substr( $itemcn_pref, 0, 3 );
1379                         next unless my $field = $itemrecord->field($CNtag);
1380                         my $CNsubfields = substr( $itemcn_pref, 3 );
1381                         $CNsubfields = undef if $CNsubfields eq '';
1382                         $defaultvalue = $field->as_string( $CNsubfields, ' ');
1383                         last if $defaultvalue;
1384                     }
1385                 }
1386                 if (   $subfield->{kohafield} eq 'items.itemcallnumber'
1387                     && $defaultvalues
1388                     && $defaultvalues->{'callnumber'} ) {
1389                     if( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield->{subfield}) ){
1390                         # if the item record exists, only use default value if the item has no callnumber
1391                         $defaultvalue = $defaultvalues->{callnumber};
1392                     } elsif ( !$itemrecord and $defaultvalues ) {
1393                         # if the item record *doesn't* exists, always use the default value
1394                         $defaultvalue = $defaultvalues->{callnumber};
1395                     }
1396                 }
1397                 if (   ( $subfield->{kohafield} eq 'items.holdingbranch' || $subfield->{kohafield} eq 'items.homebranch' )
1398                     && $defaultvalues
1399                     && $defaultvalues->{'branchcode'} ) {
1400                     if ( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield) ) {
1401                         $defaultvalue = $defaultvalues->{branchcode};
1402                     }
1403                 }
1404                 if (   ( $subfield->{kohafield} eq 'items.location' )
1405                     && $defaultvalues
1406                     && $defaultvalues->{'location'} ) {
1407
1408                     if ( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield->{subfield}) ) {
1409                         # if the item record exists, only use default value if the item has no locationr
1410                         $defaultvalue = $defaultvalues->{location};
1411                     } elsif ( !$itemrecord and $defaultvalues ) {
1412                         # if the item record *doesn't* exists, always use the default value
1413                         $defaultvalue = $defaultvalues->{location};
1414                     }
1415                 }
1416                 if (   ( $subfield->{kohafield} eq 'items.ccode' )
1417                     && $defaultvalues
1418                     && $defaultvalues->{'ccode'} ) {
1419
1420                     if ( !$itemrecord and $defaultvalues ) {
1421                         # if the item record *doesn't* exists, always use the default value
1422                         $defaultvalue = $defaultvalues->{ccode};
1423                     }
1424                 }
1425                 if ( $subfield->{authorised_value} ) {
1426                     my @authorised_values;
1427                     my %authorised_lib;
1428
1429                     # builds list, depending on authorised value...
1430                     #---- branch
1431                     if ( $subfield->{'authorised_value'} eq "branches" ) {
1432                         if (   ( C4::Context->preference("IndependentBranches") )
1433                             && !C4::Context->IsSuperLibrarian() ) {
1434                             my $sth = $dbh->prepare( "SELECT branchcode,branchname FROM branches WHERE branchcode = ? ORDER BY branchname" );
1435                             $sth->execute( C4::Context->userenv->{branch} );
1436                             push @authorised_values, ""
1437                               unless ( $subfield->{mandatory} );
1438                             while ( my ( $branchcode, $branchname ) = $sth->fetchrow_array ) {
1439                                 push @authorised_values, $branchcode;
1440                                 $authorised_lib{$branchcode} = $branchname;
1441                             }
1442                         } else {
1443                             my $sth = $dbh->prepare( "SELECT branchcode,branchname FROM branches ORDER BY branchname" );
1444                             $sth->execute;
1445                             push @authorised_values, ""
1446                               unless ( $subfield->{mandatory} );
1447                             while ( my ( $branchcode, $branchname ) = $sth->fetchrow_array ) {
1448                                 push @authorised_values, $branchcode;
1449                                 $authorised_lib{$branchcode} = $branchname;
1450                             }
1451                         }
1452
1453                         $defaultvalue = C4::Context->userenv ? C4::Context->userenv->{branch} : undef;
1454                         if ( $defaultvalues and $defaultvalues->{branchcode} ) {
1455                             $defaultvalue = $defaultvalues->{branchcode};
1456                         }
1457
1458                         #----- itemtypes
1459                     } elsif ( $subfield->{authorised_value} eq "itemtypes" ) {
1460                         my $itemtypes = Koha::ItemTypes->search_with_localization;
1461                         push @authorised_values, "";
1462                         while ( my $itemtype = $itemtypes->next ) {
1463                             push @authorised_values, $itemtype->itemtype;
1464                             $authorised_lib{$itemtype->itemtype} = $itemtype->translated_description;
1465                         }
1466                         if ($defaultvalues && $defaultvalues->{'itemtype'}) {
1467                             $defaultvalue = $defaultvalues->{'itemtype'};
1468                         }
1469
1470                         #---- class_sources
1471                     } elsif ( $subfield->{authorised_value} eq "cn_source" ) {
1472                         push @authorised_values, "";
1473
1474                         my $class_sources = GetClassSources();
1475                         my $default_source = $defaultvalue || C4::Context->preference("DefaultClassificationSource");
1476
1477                         foreach my $class_source (sort keys %$class_sources) {
1478                             next unless $class_sources->{$class_source}->{'used'} or
1479                                         ($class_source eq $default_source);
1480                             push @authorised_values, $class_source;
1481                             $authorised_lib{$class_source} = $class_sources->{$class_source}->{'description'};
1482                         }
1483
1484                         $defaultvalue = $default_source;
1485
1486                         #---- "true" authorised value
1487                     } else {
1488                         $authorised_values_sth->execute(
1489                             $subfield->{authorised_value},
1490                             $branch_limit ? $branch_limit : ()
1491                         );
1492                         push @authorised_values, "";
1493                         while ( my ( $value, $lib ) = $authorised_values_sth->fetchrow_array ) {
1494                             push @authorised_values, $value;
1495                             $authorised_lib{$value} = $lib;
1496                         }
1497                     }
1498                     $subfield_data{marc_value} = {
1499                         type    => 'select',
1500                         values  => \@authorised_values,
1501                         default => $defaultvalue // q{},
1502                         labels  => \%authorised_lib,
1503                     };
1504                 } elsif ( $subfield->{value_builder} ) {
1505                 # it is a plugin
1506                     require Koha::FrameworkPlugin;
1507                     my $plugin = Koha::FrameworkPlugin->new({
1508                         name => $subfield->{value_builder},
1509                         item_style => 1,
1510                     });
1511                     my $pars = { dbh => $dbh, record => undef, tagslib =>$tagslib, id => $subfield_data{id} };
1512                     $plugin->build( $pars );
1513                     if ( $itemrecord and my $field = $itemrecord->field($tag) ) {
1514                         $defaultvalue = $field->subfield($subfield->{subfield}) || q{};
1515                     }
1516                     if( !$plugin->errstr ) {
1517                         #TODO Move html to template; see report 12176/13397
1518                         my $tab= $plugin->noclick? '-1': '';
1519                         my $class= $plugin->noclick? ' disabled': '';
1520                         my $title= $plugin->noclick? 'No popup': 'Tag editor';
1521                         $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;
1522                     } else {
1523                         warn $plugin->errstr;
1524                         $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
1525                     }
1526                 }
1527                 elsif ( $tag eq '' ) {       # it's an hidden field
1528                     $subfield_data{marc_value} = qq(<input type="hidden" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength" value="$defaultvalue" />);
1529                 }
1530                 elsif ( $subfield->{'hidden'} ) {   # FIXME: shouldn't input type be "hidden" ?
1531                     $subfield_data{marc_value} = qq(<input type="text" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength" value="$defaultvalue" />);
1532                 }
1533                 elsif ( length($defaultvalue) > 100
1534                             or (C4::Context->preference("marcflavour") eq "UNIMARC" and
1535                                   300 <= $tag && $tag < 400 && $subfield->{subfield} eq 'a' )
1536                             or (C4::Context->preference("marcflavour") eq "MARC21"  and
1537                                   500 <= $tag && $tag < 600                     )
1538                           ) {
1539                     # oversize field (textarea)
1540                     $subfield_data{marc_value} = qq(<textarea id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength">$defaultvalue</textarea>\n");
1541                 } else {
1542                     $subfield_data{marc_value} = qq(<input type="text" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength" value="$defaultvalue" />);
1543                 }
1544                 push( @loop_data, \%subfield_data );
1545             }
1546         }
1547     }
1548     my $itemnumber;
1549     if ( $itemrecord && $itemrecord->field($itemtagfield) ) {
1550         $itemnumber = $itemrecord->subfield( $itemtagfield, $itemtagsubfield );
1551     }
1552     return {
1553         'itemtagfield'    => $itemtagfield,
1554         'itemtagsubfield' => $itemtagsubfield,
1555         'itemnumber'      => $itemnumber,
1556         'iteminformation' => \@loop_data
1557     };
1558 }
1559
1560 sub ToggleNewStatus {
1561     my ($params)    = @_;
1562     my @rules       = @{ $params->{rules} };
1563     my $report_only = $params->{report_only};
1564
1565     my $dbh = C4::Context->dbh;
1566     my @errors;
1567     my @item_columns       = map { "items.$_" } Koha::Items->columns;
1568     my @biblioitem_columns = map { "biblioitems.$_" } Koha::Biblioitems->columns;
1569     my @biblio_columns     = map { "biblio.$_" } Koha::Biblios->columns;
1570     my $report;
1571     for my $rule (@rules) {
1572         my $age = $rule->{age};
1573
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             LEFT JOIN biblio ON biblio.biblionumber = biblioitems.biblionumber
1589             WHERE 1
1590         |;
1591         for my $condition (@$conditions) {
1592             next unless $condition->{field};
1593             if (   grep { $_ eq $condition->{field} } @item_columns
1594                 or grep { $_ eq $condition->{field} } @biblioitem_columns
1595                 or grep { $_ eq $condition->{field} } @biblio_columns )
1596             {
1597                 if ( $condition->{value} =~ /\|/ ) {
1598                     my @values = split /\|/, $condition->{value};
1599                     $query .= qq| AND $condition->{field} IN (| . join( ',', ('?') x scalar @values ) . q|)|;
1600                     push @params, @values;
1601                 } else {
1602                     $query .= qq| AND $condition->{field} = ?|;
1603                     push @params, $condition->{value};
1604                 }
1605             }
1606         }
1607         if ( defined $age ) {
1608             $query .= qq| AND TO_DAYS(NOW()) - TO_DAYS($agefield) >= ? |;
1609             push @params, $age;
1610         }
1611         my $sth = $dbh->prepare($query);
1612         $sth->execute(@params);
1613         while ( my $values = $sth->fetchrow_hashref ) {
1614             my $biblionumber = $values->{biblionumber};
1615             my $itemnumber   = $values->{itemnumber};
1616             my $item         = Koha::Items->find($itemnumber);
1617             for my $substitution (@$substitutions) {
1618                 my $field = $substitution->{item_field};
1619                 my $value = $substitution->{value};
1620                 next unless $substitution->{field};
1621                 next
1622                     if ( defined $values->{ $substitution->{item_field} }
1623                     and $values->{ $substitution->{item_field} } eq $substitution->{value} );
1624                 $item->$field($value);
1625                 push @{ $report->{$itemnumber} }, $substitution;
1626             }
1627             $item->store unless $report_only;
1628         }
1629     }
1630
1631     return $report;
1632 }
1633
1634 1;