Bug 31212: Change datelastseen from date to datetime field
[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     my $today = output_pref({ dt => dt_from_string, dateformat => 'iso', dateonly => 0 });
407
408     my $item = Koha::Items->find($itemnumber);
409     $item->datelastseen(dt_from_string->ymd);
410     my $log = $item->itemlost && !$leave_item_lost ? 1 : 0; # If item was lost, record the change to the item
411     $item->itemlost(0) unless $leave_item_lost;
412     $item->store({ log_action => $log, skip_record_index => $params->{skip_record_index}, skip_holds_queue => $params->{skip_holds_queue} });
413 }
414
415 =head2 CheckItemPreSave
416
417     my $item_ref = TransformMarcToKoha({ record => $marc, limit_table => 'items' });
418     # do stuff
419     my %errors = CheckItemPreSave($item_ref);
420     if (exists $errors{'duplicate_barcode'}) {
421         print "item has duplicate barcode: ", $errors{'duplicate_barcode'}, "\n";
422     } elsif (exists $errors{'invalid_homebranch'}) {
423         print "item has invalid home branch: ", $errors{'invalid_homebranch'}, "\n";
424     } elsif (exists $errors{'invalid_holdingbranch'}) {
425         print "item has invalid holding branch: ", $errors{'invalid_holdingbranch'}, "\n";
426     } else {
427         print "item is OK";
428     }
429
430 Given a hashref containing item fields, determine if it can be
431 inserted or updated in the database.  Specifically, checks for
432 database integrity issues, and returns a hash containing any
433 of the following keys, if applicable.
434
435 =over 2
436
437 =item duplicate_barcode
438
439 Barcode, if it duplicates one already found in the database.
440
441 =item invalid_homebranch
442
443 Home branch, if not defined in branches table.
444
445 =item invalid_holdingbranch
446
447 Holding branch, if not defined in branches table.
448
449 =back
450
451 This function does NOT implement any policy-related checks,
452 e.g., whether current operator is allowed to save an
453 item that has a given branch code.
454
455 =cut
456
457 sub CheckItemPreSave {
458     my $item_ref = shift;
459
460     my %errors = ();
461
462     # check for duplicate barcode
463     if (exists $item_ref->{'barcode'} and defined $item_ref->{'barcode'}) {
464         my $existing_item= Koha::Items->find({barcode => $item_ref->{'barcode'}});
465         if ($existing_item) {
466             if (!exists $item_ref->{'itemnumber'}                       # new item
467                 or $item_ref->{'itemnumber'} != $existing_item->itemnumber) { # existing item
468                 $errors{'duplicate_barcode'} = $item_ref->{'barcode'};
469             }
470         }
471     }
472
473     # check for valid home branch
474     if (exists $item_ref->{'homebranch'} and defined $item_ref->{'homebranch'}) {
475         my $home_library = Koha::Libraries->find( $item_ref->{homebranch} );
476         unless (defined $home_library) {
477             $errors{'invalid_homebranch'} = $item_ref->{'homebranch'};
478         }
479     }
480
481     # check for valid holding branch
482     if (exists $item_ref->{'holdingbranch'} and defined $item_ref->{'holdingbranch'}) {
483         my $holding_library = Koha::Libraries->find( $item_ref->{holdingbranch} );
484         unless (defined $holding_library) {
485             $errors{'invalid_holdingbranch'} = $item_ref->{'holdingbranch'};
486         }
487     }
488
489     return %errors;
490
491 }
492
493 =head1 EXPORTED SPECIAL ACCESSOR FUNCTIONS
494
495 The following functions provide various ways of 
496 getting an item record, a set of item records, or
497 lists of authorized values for certain item fields.
498
499 =cut
500
501 =head2 GetItemsForInventory
502
503 ($itemlist, $iTotalRecords) = GetItemsForInventory( {
504   minlocation  => $minlocation,
505   maxlocation  => $maxlocation,
506   location     => $location,
507   ignoreissued => $ignoreissued,
508   datelastseen => $datelastseen,
509   branchcode   => $branchcode,
510   branch       => $branch,
511   offset       => $offset,
512   size         => $size,
513   statushash   => $statushash,
514   itemtypes    => \@itemsarray,
515 } );
516
517 Retrieve a list of title/authors/barcode/callnumber, for biblio inventory.
518
519 The sub returns a reference to a list of hashes, each containing
520 itemnumber, author, title, barcode, item callnumber, and date last
521 seen. It is ordered by callnumber then title.
522
523 The required minlocation & maxlocation parameters are used to specify a range of item callnumbers
524 the datelastseen can be used to specify that you want to see items not seen since a past date only.
525 offset & size can be used to retrieve only a part of the whole listing (defaut behaviour)
526 $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.
527
528 $iTotalRecords is the number of rows that would have been returned without the $offset, $size limit clause
529
530 =cut
531
532 sub GetItemsForInventory {
533     my ( $parameters ) = @_;
534     my $minlocation  = $parameters->{'minlocation'}  // '';
535     my $maxlocation  = $parameters->{'maxlocation'}  // '';
536     my $class_source = $parameters->{'class_source'}  // C4::Context->preference('DefaultClassificationSource');
537     my $location     = $parameters->{'location'}     // '';
538     my $itemtype     = $parameters->{'itemtype'}     // '';
539     my $ignoreissued = $parameters->{'ignoreissued'} // '';
540     my $datelastseen = $parameters->{'datelastseen'} // '';
541     my $branchcode   = $parameters->{'branchcode'}   // '';
542     my $branch       = $parameters->{'branch'}       // '';
543     my $offset       = $parameters->{'offset'}       // '';
544     my $size         = $parameters->{'size'}         // '';
545     my $statushash   = $parameters->{'statushash'}   // '';
546     my $ignore_waiting_holds = $parameters->{'ignore_waiting_holds'} // '';
547     my $itemtypes    = $parameters->{'itemtypes'}    || [];
548     my $ccode        = $parameters->{'ccode'}        // '';
549
550     my $dbh = C4::Context->dbh;
551     my ( @bind_params, @where_strings );
552
553     my $min_cnsort = GetClassSort($class_source,undef,$minlocation);
554     my $max_cnsort = GetClassSort($class_source,undef,$maxlocation);
555
556     my $select_columns = q{
557         SELECT DISTINCT(items.itemnumber), barcode, itemcallnumber, title, author, biblio.biblionumber, biblio.frameworkcode, datelastseen, homebranch, location, notforloan, damaged, itemlost, withdrawn, stocknumber, items.cn_sort, ccode
558
559     };
560     my $select_count = q{SELECT COUNT(DISTINCT(items.itemnumber))};
561     my $query = q{
562         FROM items
563         LEFT JOIN biblio ON items.biblionumber = biblio.biblionumber
564         LEFT JOIN biblioitems on items.biblionumber = biblioitems.biblionumber
565     };
566     if ($statushash){
567         for my $authvfield (keys %$statushash){
568             if ( scalar @{$statushash->{$authvfield}} > 0 ){
569                 my $joinedvals = join ',', @{$statushash->{$authvfield}};
570                 push @where_strings, "$authvfield in (" . $joinedvals . ")";
571             }
572         }
573     }
574
575     if ($ccode){
576         push @where_strings, 'ccode = ?';
577         push @bind_params, $ccode;
578     }
579
580     if ($minlocation) {
581         push @where_strings, 'items.cn_sort >= ?';
582         push @bind_params, $min_cnsort;
583     }
584
585     if ($maxlocation) {
586         push @where_strings, 'items.cn_sort <= ?';
587         push @bind_params, $max_cnsort;
588     }
589
590     if ($datelastseen) {
591         $datelastseen = output_pref({ str => $datelastseen, dateformat => 'iso', dateonly => 0 });
592         push @where_strings, '(datelastseen < ? OR datelastseen IS NULL)';
593         push @bind_params, $datelastseen;
594     }
595
596     if ( $location ) {
597         push @where_strings, 'items.location = ?';
598         push @bind_params, $location;
599     }
600
601     if ( $branchcode ) {
602         if($branch eq "homebranch"){
603         push @where_strings, 'items.homebranch = ?';
604         }else{
605             push @where_strings, 'items.holdingbranch = ?';
606         }
607         push @bind_params, $branchcode;
608     }
609
610     if ( $itemtype ) {
611         push @where_strings, 'biblioitems.itemtype = ?';
612         push @bind_params, $itemtype;
613     }
614     if ( $ignoreissued) {
615         $query .= "LEFT JOIN issues ON items.itemnumber = issues.itemnumber ";
616         push @where_strings, 'issues.date_due IS NULL';
617     }
618
619     if ( $ignore_waiting_holds ) {
620         $query .= "LEFT JOIN reserves ON items.itemnumber = reserves.itemnumber ";
621         push( @where_strings, q{(reserves.found != 'W' OR reserves.found IS NULL)} );
622     }
623
624     if ( @$itemtypes ) {
625         my $itemtypes_str = join ', ', @$itemtypes;
626         push @where_strings, "( biblioitems.itemtype IN (" . $itemtypes_str . ") OR items.itype IN (" . $itemtypes_str . ") )";
627     }
628
629     if ( @where_strings ) {
630         $query .= 'WHERE ';
631         $query .= join ' AND ', @where_strings;
632     }
633     my $count_query = $select_count . $query;
634     $query .= ' ORDER BY items.cn_sort, itemcallnumber, title';
635     $query .= " LIMIT $offset, $size" if ($offset and $size);
636     $query = $select_columns . $query;
637     my $sth = $dbh->prepare($query);
638     $sth->execute( @bind_params );
639
640     my @results = ();
641     my $tmpresults = $sth->fetchall_arrayref({});
642     $sth = $dbh->prepare( $count_query );
643     $sth->execute( @bind_params );
644     my ($iTotalRecords) = $sth->fetchrow_array();
645
646     my @avs = Koha::AuthorisedValues->search(
647         {   'marc_subfield_structures.kohafield' => { '>' => '' },
648             'me.authorised_value'                => { '>' => '' },
649         },
650         {   join     => { category => 'marc_subfield_structures' },
651             distinct => ['marc_subfield_structures.kohafield, me.category, frameworkcode, me.authorised_value'],
652             '+select' => [ 'marc_subfield_structures.kohafield', 'marc_subfield_structures.frameworkcode', 'me.authorised_value', 'me.lib' ],
653             '+as'     => [ 'kohafield',                          'frameworkcode',                          'authorised_value',    'lib' ],
654         }
655     )->as_list;
656
657     my $avmapping = { map { $_->get_column('kohafield') . ',' . $_->get_column('frameworkcode') . ',' . $_->get_column('authorised_value') => $_->get_column('lib') } @avs };
658
659     foreach my $row (@$tmpresults) {
660
661         # Auth values
662         foreach (keys %$row) {
663             if (
664                 defined(
665                     $avmapping->{ "items.$_," . $row->{'frameworkcode'} . "," . ( $row->{$_} // q{} ) }
666                 )
667             ) {
668                 $row->{$_} = $avmapping->{"items.$_,".$row->{'frameworkcode'}.",".$row->{$_}};
669             }
670         }
671         push @results, $row;
672     }
673
674     return (\@results, $iTotalRecords);
675 }
676
677 =head2 get_hostitemnumbers_of
678
679   my @itemnumbers_of = get_hostitemnumbers_of($biblionumber);
680
681 Given a biblionumber, return the list of corresponding itemnumbers that are linked to it via host fields
682
683 Return a reference on a hash where key is a biblionumber and values are
684 references on array of itemnumbers.
685
686 =cut
687
688
689 sub get_hostitemnumbers_of {
690     my ($biblionumber) = @_;
691
692     if( !C4::Context->preference('EasyAnalyticalRecords') ) {
693         return ();
694     }
695
696     my $biblio = Koha::Biblios->find($biblionumber);
697     my $marcrecord = $biblio->metadata->record;
698     return unless $marcrecord;
699
700     my ( @returnhostitemnumbers, $tag, $biblio_s, $item_s );
701
702     my $marcflavor = C4::Context->preference('marcflavour');
703     if ( $marcflavor eq 'MARC21' ) {
704         $tag      = '773';
705         $biblio_s = '0';
706         $item_s   = '9';
707     }
708     elsif ( $marcflavor eq 'UNIMARC' ) {
709         $tag      = '461';
710         $biblio_s = '0';
711         $item_s   = '9';
712     }
713
714     foreach my $hostfield ( $marcrecord->field($tag) ) {
715         my $hostbiblionumber = $hostfield->subfield($biblio_s);
716         next unless $hostbiblionumber; # have tag, don't have $biblio_s subfield
717         my $linkeditemnumber = $hostfield->subfield($item_s);
718         if ( ! $linkeditemnumber ) {
719             warn "ERROR biblionumber $biblionumber has 773^0, but doesn't have 9";
720             next;
721         }
722         my $is_from_biblio = Koha::Items->search({ itemnumber => $linkeditemnumber, biblionumber => $hostbiblionumber });
723         push @returnhostitemnumbers, $linkeditemnumber
724           if $is_from_biblio;
725     }
726
727     return @returnhostitemnumbers;
728 }
729
730 =head1 LIMITED USE FUNCTIONS
731
732 The following functions, while part of the public API,
733 are not exported.  This is generally because they are
734 meant to be used by only one script for a specific
735 purpose, and should not be used in any other context
736 without careful thought.
737
738 =cut
739
740 =head2 GetMarcItem
741
742   my $item_marc = GetMarcItem($biblionumber, $itemnumber);
743
744 Returns MARC::Record of the item passed in parameter.
745 This function is meant for use only in C<cataloguing/additem.pl>,
746 where it is needed to support that script's MARC-like
747 editor.
748
749 =cut
750
751 sub GetMarcItem {
752     my ( $biblionumber, $itemnumber ) = @_;
753
754     # GetMarcItem has been revised so that it does the following:
755     #  1. Gets the item information from the items table.
756     #  2. Converts it to a MARC field for storage in the bib record.
757     #
758     # The previous behavior was:
759     #  1. Get the bib record.
760     #  2. Return the MARC tag corresponding to the item record.
761     #
762     # The difference is that one treats the items row as authoritative,
763     # while the other treats the MARC representation as authoritative
764     # under certain circumstances.
765
766     my $item = Koha::Items->find($itemnumber) or return;
767
768     # Tack on 'items.' prefix to column names so that C4::Biblio::TransformKohaToMarc will work.
769     # Also, don't emit a subfield if the underlying field is blank.
770
771     return Item2Marc($item->unblessed, $biblionumber);
772
773 }
774 sub Item2Marc {
775         my ($itemrecord,$biblionumber)=@_;
776     my $mungeditem = { 
777         map {  
778             defined($itemrecord->{$_}) && $itemrecord->{$_} ne '' ? ("items.$_" => $itemrecord->{$_}) : ()  
779         } keys %{ $itemrecord } 
780     };
781     my $framework = C4::Biblio::GetFrameworkCode( $biblionumber );
782     my $itemmarc = C4::Biblio::TransformKohaToMarc( $mungeditem, { framework => $framework } );
783     my ( $itemtag, $itemsubfield ) = C4::Biblio::GetMarcFromKohaField(
784         "items.itemnumber", $framework,
785     );
786
787     my $unlinked_item_subfields = _parse_unlinked_item_subfields_from_xml($mungeditem->{'items.more_subfields_xml'});
788     if (defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1) {
789                 foreach my $field ($itemmarc->field($itemtag)){
790             $field->add_subfields(@$unlinked_item_subfields);
791         }
792     }
793         return $itemmarc;
794 }
795
796 =head1 PRIVATE FUNCTIONS AND VARIABLES
797
798 The following functions are not meant to be called
799 directly, but are documented in order to explain
800 the inner workings of C<C4::Items>.
801
802 =cut
803
804 =head2 _marc_from_item_hash
805
806   my $item_marc = _marc_from_item_hash($item, $frameworkcode[, $unlinked_item_subfields]);
807
808 Given an item hash representing a complete item record,
809 create a C<MARC::Record> object containing an embedded
810 tag representing that item.
811
812 The third, optional parameter C<$unlinked_item_subfields> is
813 an arrayref of subfields (not mapped to C<items> fields per the
814 framework) to be added to the MARC representation
815 of the item.
816
817 =cut
818
819 sub _marc_from_item_hash {
820     my $item = shift;
821     my $frameworkcode = shift;
822     my $unlinked_item_subfields;
823     if (@_) {
824         $unlinked_item_subfields = shift;
825     }
826    
827     # Tack on 'items.' prefix to column names so lookup from MARC frameworks will work
828     # Also, don't emit a subfield if the underlying field is blank.
829     my $mungeditem = { map {  (defined($item->{$_}) and $item->{$_} ne '') ? 
830                                 (/^items\./ ? ($_ => $item->{$_}) : ("items.$_" => $item->{$_})) 
831                                 : ()  } keys %{ $item } }; 
832
833     my $item_marc = MARC::Record->new();
834     foreach my $item_field ( keys %{$mungeditem} ) {
835         my ( $tag, $subfield ) = C4::Biblio::GetMarcFromKohaField( $item_field );
836         next unless defined $tag and defined $subfield;    # skip if not mapped to MARC field
837         my @values = split(/\s?\|\s?/, $mungeditem->{$item_field}, -1);
838         foreach my $value (@values){
839             if ( my $field = $item_marc->field($tag) ) {
840                     $field->add_subfields( $subfield => $value );
841             } else {
842                 my $add_subfields = [];
843                 if (defined $unlinked_item_subfields and ref($unlinked_item_subfields) eq 'ARRAY' and $#$unlinked_item_subfields > -1) {
844                     $add_subfields = $unlinked_item_subfields;
845             }
846             $item_marc->add_fields( $tag, " ", " ", $subfield => $value, @$add_subfields );
847             }
848         }
849     }
850
851     return $item_marc;
852 }
853
854 =head2 _repack_item_errors
855
856 Add an error message hash generated by C<CheckItemPreSave>
857 to a list of errors.
858
859 =cut
860
861 sub _repack_item_errors {
862     my $item_sequence_num = shift;
863     my $item_ref = shift;
864     my $error_ref = shift;
865
866     my @repacked_errors = ();
867
868     foreach my $error_code (sort keys %{ $error_ref }) {
869         my $repacked_error = {};
870         $repacked_error->{'item_sequence'} = $item_sequence_num;
871         $repacked_error->{'item_barcode'} = exists($item_ref->{'barcode'}) ? $item_ref->{'barcode'} : '';
872         $repacked_error->{'error_code'} = $error_code;
873         $repacked_error->{'error_information'} = $error_ref->{$error_code};
874         push @repacked_errors, $repacked_error;
875     } 
876
877     return @repacked_errors;
878 }
879
880 =head2 _get_unlinked_item_subfields
881
882   my $unlinked_item_subfields = _get_unlinked_item_subfields($original_item_marc, $frameworkcode);
883
884 =cut
885
886 sub _get_unlinked_item_subfields {
887     my $original_item_marc = shift;
888     my $frameworkcode = shift;
889
890     my $marcstructure = C4::Biblio::GetMarcStructure(1, $frameworkcode, { unsafe => 1 });
891
892     # assume that this record has only one field, and that that
893     # field contains only the item information
894     my $subfields = [];
895     my @fields = $original_item_marc->fields();
896     if ($#fields > -1) {
897         my $field = $fields[0];
898             my $tag = $field->tag();
899         foreach my $subfield ($field->subfields()) {
900             if (defined $subfield->[1] and
901                 $subfield->[1] ne '' and
902                 !$marcstructure->{$tag}->{$subfield->[0]}->{'kohafield'}) {
903                 push @$subfields, $subfield->[0] => $subfield->[1];
904             }
905         }
906     }
907     return $subfields;
908 }
909
910 =head2 _get_unlinked_subfields_xml
911
912   my $unlinked_subfields_xml = _get_unlinked_subfields_xml($unlinked_item_subfields);
913
914 =cut
915
916 sub _get_unlinked_subfields_xml {
917     my $unlinked_item_subfields = shift;
918
919     my $xml;
920     if (defined $unlinked_item_subfields and ref($unlinked_item_subfields) eq 'ARRAY' and $#$unlinked_item_subfields > -1) {
921         my $marc = MARC::Record->new();
922         # use of tag 999 is arbitrary, and doesn't need to match the item tag
923         # used in the framework
924         $marc->append_fields(MARC::Field->new('999', ' ', ' ', @$unlinked_item_subfields));
925         $marc->encoding("UTF-8");    
926         $xml = $marc->as_xml("USMARC");
927     }
928
929     return $xml;
930 }
931
932 =head2 _parse_unlinked_item_subfields_from_xml
933
934   my $unlinked_item_subfields = _parse_unlinked_item_subfields_from_xml($whole_item->{'more_subfields_xml'}):
935
936 =cut
937
938 sub  _parse_unlinked_item_subfields_from_xml {
939     my $xml = shift;
940     require C4::Charset;
941     return unless defined $xml and $xml ne "";
942     my $marc = MARC::Record->new_from_xml(C4::Charset::StripNonXmlChars($xml),'UTF-8');
943     my $unlinked_subfields = [];
944     my @fields = $marc->fields();
945     if ($#fields > -1) {
946         foreach my $subfield ($fields[0]->subfields()) {
947             push @$unlinked_subfields, $subfield->[0] => $subfield->[1];
948         }
949     }
950     return $unlinked_subfields;
951 }
952
953 =head2 GetAnalyticsCount
954
955   $count= &GetAnalyticsCount($itemnumber)
956
957 counts Usage of itemnumber in Analytical bibliorecords. 
958
959 =cut
960
961 sub GetAnalyticsCount {
962     my ($itemnumber) = @_;
963
964     if ( !C4::Context->preference('EasyAnalyticalRecords') ) {
965         return 0;
966     }
967
968     ### ZOOM search here
969     my $query;
970     $query= "hi=".$itemnumber;
971     my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
972     my ($err,$res,$result) = $searcher->simple_search_compat($query,0,10);
973     return ($result);
974 }
975
976 sub _SearchItems_build_where_fragment {
977     my ($filter) = @_;
978
979     my $dbh = C4::Context->dbh;
980
981     my $where_fragment;
982     if (exists($filter->{conjunction})) {
983         my (@where_strs, @where_args);
984         foreach my $f (@{ $filter->{filters} }) {
985             my $fragment = _SearchItems_build_where_fragment($f);
986             if ($fragment) {
987                 push @where_strs, $fragment->{str};
988                 push @where_args, @{ $fragment->{args} };
989             }
990         }
991         my $where_str = '';
992         if (@where_strs) {
993             $where_str = '(' . join (' ' . $filter->{conjunction} . ' ', @where_strs) . ')';
994             $where_fragment = {
995                 str => $where_str,
996                 args => \@where_args,
997             };
998         }
999     } else {
1000         my @columns = Koha::Database->new()->schema()->resultset('Item')->result_source->columns;
1001         push @columns, Koha::Database->new()->schema()->resultset('Biblio')->result_source->columns;
1002         push @columns, Koha::Database->new()->schema()->resultset('Biblioitem')->result_source->columns;
1003         push @columns, Koha::Database->new()->schema()->resultset('Issue')->result_source->columns;
1004         my @operators = qw(= != > < >= <= is like);
1005         push @operators, 'not like';
1006         my $field = $filter->{field} // q{};
1007         if ( (0 < grep { $_ eq $field } @columns) or (substr($field, 0, 5) eq 'marc:') ) {
1008             my $op = $filter->{operator};
1009             my $query = $filter->{query};
1010             my $ifnull = $filter->{ifnull};
1011
1012             if (!$op or (0 == grep { $_ eq $op } @operators)) {
1013                 $op = '='; # default operator
1014             }
1015
1016             my $column;
1017             if ($field =~ /^marc:(\d{3})(?:\$(\w))?$/) {
1018                 my $marcfield = $1;
1019                 my $marcsubfield = $2;
1020                 my ($kohafield) = $dbh->selectrow_array(q|
1021                     SELECT kohafield FROM marc_subfield_structure
1022                     WHERE tagfield=? AND tagsubfield=? AND frameworkcode=''
1023                 |, undef, $marcfield, $marcsubfield);
1024
1025                 if ($kohafield) {
1026                     $column = $kohafield;
1027                 } else {
1028                     # MARC field is not linked to a DB field so we need to use
1029                     # ExtractValue on marcxml from biblio_metadata or
1030                     # items.more_subfields_xml, depending on the MARC field.
1031                     my $xpath;
1032                     my $sqlfield;
1033                     my ($itemfield) = C4::Biblio::GetMarcFromKohaField('items.itemnumber');
1034                     if ($marcfield eq $itemfield) {
1035                         $sqlfield = 'more_subfields_xml';
1036                         $xpath = '//record/datafield/subfield[@code="' . $marcsubfield . '"]';
1037                     } else {
1038                         $sqlfield = 'metadata'; # From biblio_metadata
1039                         if ($marcfield < 10) {
1040                             $xpath = "//record/controlfield[\@tag=\"$marcfield\"]";
1041                         } else {
1042                             $xpath = "//record/datafield[\@tag=\"$marcfield\"]/subfield[\@code=\"$marcsubfield\"]";
1043                         }
1044                     }
1045                     $column = "ExtractValue($sqlfield, '$xpath')";
1046                 }
1047             }
1048             elsif ($field eq 'isbn') {
1049                 if ( C4::Context->preference("SearchWithISBNVariations") and $query ) {
1050                     my @isbns = C4::Koha::GetVariationsOfISBN( $query );
1051                     $query = [];
1052                     push @$query, @isbns;
1053                 }
1054                 $column = $field;
1055             }
1056             elsif ($field eq 'issn') {
1057                 if ( C4::Context->preference("SearchWithISSNVariations") and $query ) {
1058                     my @issns = C4::Koha::GetVariationsOfISSN( $query );
1059                     $query = [];
1060                     push @$query, @issns;
1061                 }
1062                 $column = $field;
1063             } else {
1064                 $column = $field;
1065             }
1066
1067             if ( defined $ifnull ) {
1068                 $column = "COALESCE($column, ?)";
1069             }
1070
1071             if (ref $query eq 'ARRAY') {
1072                 if ($op eq 'like') {
1073                     $where_fragment = {
1074                         str => "($column LIKE " . join (" OR $column LIKE ", ('?') x @$query ) . ")",
1075                         args => $query,
1076                     };
1077                 }
1078                 else {
1079                     if ($op eq '=') {
1080                         $op = 'IN';
1081                     } elsif ($op eq '!=') {
1082                         $op = 'NOT IN';
1083                     }
1084                     $where_fragment = {
1085                         str => "$column $op (" . join (',', ('?') x @$query) . ")",
1086                         args => $query,
1087                     };
1088                 }
1089             } elsif ( $op eq 'is' ) {
1090                 $where_fragment = {
1091                     str => "$column $op $query",
1092                     args => [],
1093                 };
1094             } else {
1095                 $where_fragment = {
1096                     str => "$column $op ?",
1097                     args => [ $query ],
1098                 };
1099             }
1100
1101             if ( defined $ifnull ) {
1102                 unshift @{ $where_fragment->{args} }, $ifnull;
1103             }
1104         }
1105     }
1106
1107     return $where_fragment;
1108 }
1109
1110 =head2 SearchItems
1111
1112     my ($items, $total) = SearchItems($filter, $params);
1113
1114 Perform a search among items
1115
1116 $filter is a reference to a hash which can be a filter, or a combination of filters.
1117
1118 A filter has the following keys:
1119
1120 =over 2
1121
1122 =item * field: the name of a SQL column in table items
1123
1124 =item * query: the value to search in this column
1125
1126 =item * operator: comparison operator. Can be one of = != > < >= <= like 'not like' is
1127
1128 =back
1129
1130 A combination of filters hash the following keys:
1131
1132 =over 2
1133
1134 =item * conjunction: 'AND' or 'OR'
1135
1136 =item * filters: array ref of filters
1137
1138 =back
1139
1140 $params is a reference to a hash that can contain the following parameters:
1141
1142 =over 2
1143
1144 =item * rows: Number of items to return. 0 returns everything (default: 0)
1145
1146 =item * page: Page to return (return items from (page-1)*rows to (page*rows)-1)
1147                (default: 1)
1148
1149 =item * sortby: A SQL column name in items table to sort on
1150
1151 =item * sortorder: 'ASC' or 'DESC'
1152
1153 =back
1154
1155 =cut
1156
1157 sub SearchItems {
1158     my ($filter, $params) = @_;
1159
1160     $filter //= {};
1161     $params //= {};
1162     return unless ref $filter eq 'HASH';
1163     return unless ref $params eq 'HASH';
1164
1165     # Default parameters
1166     $params->{rows} ||= 0;
1167     $params->{page} ||= 1;
1168     $params->{sortby} ||= 'itemnumber';
1169     $params->{sortorder} ||= 'ASC';
1170
1171     my ($where_str, @where_args);
1172     my $where_fragment = _SearchItems_build_where_fragment($filter);
1173     if ($where_fragment) {
1174         $where_str = $where_fragment->{str};
1175         @where_args = @{ $where_fragment->{args} };
1176     }
1177
1178     my $dbh = C4::Context->dbh;
1179     my $query = q{
1180         SELECT SQL_CALC_FOUND_ROWS items.*
1181         FROM items
1182           LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
1183           LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber
1184           LEFT JOIN biblio_metadata ON biblio_metadata.biblionumber = biblio.biblionumber
1185           LEFT JOIN issues ON issues.itemnumber = items.itemnumber
1186           WHERE 1
1187     };
1188     if (defined $where_str and $where_str ne '') {
1189         $query .= qq{ AND $where_str };
1190     }
1191
1192     $query .= q{ AND biblio_metadata.format = 'marcxml' AND biblio_metadata.schema = ? };
1193     push @where_args, C4::Context->preference('marcflavour');
1194
1195     my @columns = Koha::Database->new()->schema()->resultset('Item')->result_source->columns;
1196     push @columns, Koha::Database->new()->schema()->resultset('Biblio')->result_source->columns;
1197     push @columns, Koha::Database->new()->schema()->resultset('Biblioitem')->result_source->columns;
1198     push @columns, Koha::Database->new()->schema()->resultset('Issue')->result_source->columns;
1199
1200     if ( $params->{sortby} eq 'availability' ) {
1201         my $sortorder = (uc($params->{sortorder}) eq 'ASC') ? 'ASC' : 'DESC';
1202         $query .= qq{ ORDER BY onloan $sortorder };
1203     } else {
1204         my $sortby = (0 < grep {$params->{sortby} eq $_} @columns)
1205             ? $params->{sortby} : 'itemnumber';
1206         my $sortorder = (uc($params->{sortorder}) eq 'ASC') ? 'ASC' : 'DESC';
1207         $query .= qq{ ORDER BY $sortby $sortorder };
1208     }
1209
1210     my $rows = $params->{rows};
1211     my @limit_args;
1212     if ($rows > 0) {
1213         my $offset = $rows * ($params->{page}-1);
1214         $query .= qq { LIMIT ?, ? };
1215         push @limit_args, $offset, $rows;
1216     }
1217
1218     my $sth = $dbh->prepare($query);
1219     my $rv = $sth->execute(@where_args, @limit_args);
1220
1221     return unless ($rv);
1222     my ($total_rows) = $dbh->selectrow_array(q{ SELECT FOUND_ROWS() });
1223
1224     return ($sth->fetchall_arrayref({}), $total_rows);
1225 }
1226
1227
1228 =head1  OTHER FUNCTIONS
1229
1230 =head2 _find_value
1231
1232   ($indicators, $value) = _find_value($tag, $subfield, $record,$encoding);
1233
1234 Find the given $subfield in the given $tag in the given
1235 MARC::Record $record.  If the subfield is found, returns
1236 the (indicators, value) pair; otherwise, (undef, undef) is
1237 returned.
1238
1239 PROPOSITION :
1240 Such a function is used in addbiblio AND additem and serial-edit and maybe could be used in Authorities.
1241 I suggest we export it from this module.
1242
1243 =cut
1244
1245 sub _find_value {
1246     my ( $tagfield, $insubfield, $record, $encoding ) = @_;
1247     my @result;
1248     my $indicator;
1249     if ( $tagfield < 10 ) {
1250         if ( $record->field($tagfield) ) {
1251             push @result, $record->field($tagfield)->data();
1252         } else {
1253             push @result, "";
1254         }
1255     } else {
1256         foreach my $field ( $record->field($tagfield) ) {
1257             my @subfields = $field->subfields();
1258             foreach my $subfield (@subfields) {
1259                 if ( @$subfield[0] eq $insubfield ) {
1260                     push @result, @$subfield[1];
1261                     $indicator = $field->indicator(1) . $field->indicator(2);
1262                 }
1263             }
1264         }
1265     }
1266     return ( $indicator, @result );
1267 }
1268
1269
1270 =head2 PrepareItemrecordDisplay
1271
1272   PrepareItemrecordDisplay($bibnum,$itemumber,$defaultvalues,$frameworkcode);
1273
1274 Returns a hash with all the fields for Display a given item data in a template
1275
1276 $defaultvalues should either contain a hashref of values for the new item, or be undefined.
1277
1278 The $frameworkcode returns the item for the given frameworkcode, ONLY if bibnum is not provided
1279
1280 =cut
1281
1282 sub PrepareItemrecordDisplay {
1283
1284     my ( $bibnum, $itemnum, $defaultvalues, $frameworkcode ) = @_;
1285
1286     my $dbh = C4::Context->dbh;
1287     $frameworkcode = C4::Biblio::GetFrameworkCode($bibnum) if $bibnum;
1288     my ( $itemtagfield, $itemtagsubfield ) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
1289
1290     # Note: $tagslib obtained from GetMarcStructure() in 'unsafe' mode is
1291     # a shared data structure. No plugin (including custom ones) should change
1292     # its contents. See also GetMarcStructure.
1293     my $tagslib = GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } );
1294
1295     # Pick the default location from NewItemsDefaultLocation
1296     if ( C4::Context->preference('NewItemsDefaultLocation') ) {
1297         $defaultvalues //= {};
1298         $defaultvalues->{location} //= C4::Context->preference('NewItemsDefaultLocation');
1299     }
1300
1301     # return nothing if we don't have found an existing framework.
1302     return q{} unless $tagslib;
1303     my $itemrecord;
1304     if ($itemnum) {
1305         $itemrecord = C4::Items::GetMarcItem( $bibnum, $itemnum );
1306     }
1307     my @loop_data;
1308
1309     my $branch_limit = C4::Context->userenv ? C4::Context->userenv->{"branch"} : "";
1310     my $query = qq{
1311         SELECT authorised_value,lib FROM authorised_values
1312     };
1313     $query .= qq{
1314         LEFT JOIN authorised_values_branches ON ( id = av_id )
1315     } if $branch_limit;
1316     $query .= qq{
1317         WHERE category = ?
1318     };
1319     $query .= qq{ AND ( branchcode = ? OR branchcode IS NULL )} if $branch_limit;
1320     $query .= qq{ ORDER BY lib};
1321     my $authorised_values_sth = $dbh->prepare( $query );
1322     foreach my $tag ( sort keys %{$tagslib} ) {
1323         if ( $tag ne '' ) {
1324
1325             # loop through each subfield
1326             my $cntsubf;
1327             foreach my $subfield (
1328                 sort { $a->{display_order} <=> $b->{display_order} || $a->{subfield} cmp $b->{subfield} }
1329                 grep { ref($_) && %$_ } # Not a subfield (values for "important", "lib", "mandatory", etc.) or empty
1330                 values %{ $tagslib->{$tag} } )
1331             {
1332                 next unless ( $subfield->{'tab'} );
1333                 next if ( $subfield->{'tab'} ne "10" );
1334                 my %subfield_data;
1335                 $subfield_data{tag}           = $tag;
1336                 $subfield_data{subfield}      = $subfield->{subfield};
1337                 $subfield_data{countsubfield} = $cntsubf++;
1338                 $subfield_data{kohafield}     = $subfield->{kohafield};
1339                 $subfield_data{id}            = "tag_".$tag."_subfield_".$subfield->{subfield}."_".int(rand(1000000));
1340
1341                 #        $subfield_data{marc_lib}=$tagslib->{$tag}->{$subfield}->{lib};
1342                 $subfield_data{marc_lib}   = $subfield->{lib};
1343                 $subfield_data{mandatory}  = $subfield->{mandatory};
1344                 $subfield_data{repeatable} = $subfield->{repeatable};
1345                 $subfield_data{hidden}     = "display:none"
1346                   if ( ( $subfield->{hidden} > 4 )
1347                     || ( $subfield->{hidden} < -4 ) );
1348                 my ( $x, $defaultvalue );
1349                 if ($itemrecord) {
1350                     ( $x, $defaultvalue ) = _find_value( $tag, $subfield->{subfield}, $itemrecord );
1351                 }
1352                 $defaultvalue = $subfield->{defaultvalue} unless $defaultvalue;
1353                 if ( !defined $defaultvalue ) {
1354                     $defaultvalue = q||;
1355                 } else {
1356                     $defaultvalue =~ s/"/&quot;/g;
1357                     # get today date & replace <<YYYY>>, <<MM>>, <<DD>> if provided in the default value
1358                     my $today_dt = dt_from_string;
1359                     my $year     = $today_dt->strftime('%Y');
1360                     my $shortyear     = $today_dt->strftime('%y');
1361                     my $month    = $today_dt->strftime('%m');
1362                     my $day      = $today_dt->strftime('%d');
1363                     $defaultvalue =~ s/<<YYYY>>/$year/g;
1364                     $defaultvalue =~ s/<<YY>>/$shortyear/g;
1365                     $defaultvalue =~ s/<<MM>>/$month/g;
1366                     $defaultvalue =~ s/<<DD>>/$day/g;
1367
1368                     # And <<USER>> with surname (?)
1369                     my $username =
1370                       (   C4::Context->userenv
1371                         ? C4::Context->userenv->{'surname'}
1372                         : "superlibrarian" );
1373                     $defaultvalue =~ s/<<USER>>/$username/g;
1374                 }
1375
1376                 my $maxlength = $subfield->{maxlength};
1377
1378                 # search for itemcallnumber if applicable
1379                 if ( $subfield->{kohafield} eq 'items.itemcallnumber'
1380                     && C4::Context->preference('itemcallnumber') && $itemrecord) {
1381                     foreach my $itemcn_pref (split(/,/,C4::Context->preference('itemcallnumber'))){
1382                         my $CNtag      = substr( $itemcn_pref, 0, 3 );
1383                         next unless my $field = $itemrecord->field($CNtag);
1384                         my $CNsubfields = substr( $itemcn_pref, 3 );
1385                         $CNsubfields = undef if $CNsubfields eq '';
1386                         $defaultvalue = $field->as_string( $CNsubfields, ' ');
1387                         last if $defaultvalue;
1388                     }
1389                 }
1390                 if (   $subfield->{kohafield} eq 'items.itemcallnumber'
1391                     && $defaultvalues
1392                     && $defaultvalues->{'callnumber'} ) {
1393                     if( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield->{subfield}) ){
1394                         # if the item record exists, only use default value if the item has no callnumber
1395                         $defaultvalue = $defaultvalues->{callnumber};
1396                     } elsif ( !$itemrecord and $defaultvalues ) {
1397                         # if the item record *doesn't* exists, always use the default value
1398                         $defaultvalue = $defaultvalues->{callnumber};
1399                     }
1400                 }
1401                 if (   ( $subfield->{kohafield} eq 'items.holdingbranch' || $subfield->{kohafield} eq 'items.homebranch' )
1402                     && $defaultvalues
1403                     && $defaultvalues->{'branchcode'} ) {
1404                     if ( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield) ) {
1405                         $defaultvalue = $defaultvalues->{branchcode};
1406                     }
1407                 }
1408                 if (   ( $subfield->{kohafield} eq 'items.location' )
1409                     && $defaultvalues
1410                     && $defaultvalues->{'location'} ) {
1411
1412                     if ( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield->{subfield}) ) {
1413                         # if the item record exists, only use default value if the item has no locationr
1414                         $defaultvalue = $defaultvalues->{location};
1415                     } elsif ( !$itemrecord and $defaultvalues ) {
1416                         # if the item record *doesn't* exists, always use the default value
1417                         $defaultvalue = $defaultvalues->{location};
1418                     }
1419                 }
1420                 if (   ( $subfield->{kohafield} eq 'items.ccode' )
1421                     && $defaultvalues
1422                     && $defaultvalues->{'ccode'} ) {
1423
1424                     if ( !$itemrecord and $defaultvalues ) {
1425                         # if the item record *doesn't* exists, always use the default value
1426                         $defaultvalue = $defaultvalues->{ccode};
1427                     }
1428                 }
1429                 if ( $subfield->{authorised_value} ) {
1430                     my @authorised_values;
1431                     my %authorised_lib;
1432
1433                     # builds list, depending on authorised value...
1434                     #---- branch
1435                     if ( $subfield->{'authorised_value'} eq "branches" ) {
1436                         if (   ( C4::Context->preference("IndependentBranches") )
1437                             && !C4::Context->IsSuperLibrarian() ) {
1438                             my $sth = $dbh->prepare( "SELECT branchcode,branchname FROM branches WHERE branchcode = ? ORDER BY branchname" );
1439                             $sth->execute( C4::Context->userenv->{branch} );
1440                             push @authorised_values, ""
1441                               unless ( $subfield->{mandatory} );
1442                             while ( my ( $branchcode, $branchname ) = $sth->fetchrow_array ) {
1443                                 push @authorised_values, $branchcode;
1444                                 $authorised_lib{$branchcode} = $branchname;
1445                             }
1446                         } else {
1447                             my $sth = $dbh->prepare( "SELECT branchcode,branchname FROM branches ORDER BY branchname" );
1448                             $sth->execute;
1449                             push @authorised_values, ""
1450                               unless ( $subfield->{mandatory} );
1451                             while ( my ( $branchcode, $branchname ) = $sth->fetchrow_array ) {
1452                                 push @authorised_values, $branchcode;
1453                                 $authorised_lib{$branchcode} = $branchname;
1454                             }
1455                         }
1456
1457                         $defaultvalue = C4::Context->userenv ? C4::Context->userenv->{branch} : undef;
1458                         if ( $defaultvalues and $defaultvalues->{branchcode} ) {
1459                             $defaultvalue = $defaultvalues->{branchcode};
1460                         }
1461
1462                         #----- itemtypes
1463                     } elsif ( $subfield->{authorised_value} eq "itemtypes" ) {
1464                         my $itemtypes = Koha::ItemTypes->search_with_localization;
1465                         push @authorised_values, "";
1466                         while ( my $itemtype = $itemtypes->next ) {
1467                             push @authorised_values, $itemtype->itemtype;
1468                             $authorised_lib{$itemtype->itemtype} = $itemtype->translated_description;
1469                         }
1470                         if ($defaultvalues && $defaultvalues->{'itemtype'}) {
1471                             $defaultvalue = $defaultvalues->{'itemtype'};
1472                         }
1473
1474                         #---- class_sources
1475                     } elsif ( $subfield->{authorised_value} eq "cn_source" ) {
1476                         push @authorised_values, "";
1477
1478                         my $class_sources = GetClassSources();
1479                         my $default_source = $defaultvalue || C4::Context->preference("DefaultClassificationSource");
1480
1481                         foreach my $class_source (sort keys %$class_sources) {
1482                             next unless $class_sources->{$class_source}->{'used'} or
1483                                         ($class_source eq $default_source);
1484                             push @authorised_values, $class_source;
1485                             $authorised_lib{$class_source} = $class_sources->{$class_source}->{'description'};
1486                         }
1487
1488                         $defaultvalue = $default_source;
1489
1490                         #---- "true" authorised value
1491                     } else {
1492                         $authorised_values_sth->execute(
1493                             $subfield->{authorised_value},
1494                             $branch_limit ? $branch_limit : ()
1495                         );
1496                         push @authorised_values, "";
1497                         while ( my ( $value, $lib ) = $authorised_values_sth->fetchrow_array ) {
1498                             push @authorised_values, $value;
1499                             $authorised_lib{$value} = $lib;
1500                         }
1501                     }
1502                     $subfield_data{marc_value} = {
1503                         type    => 'select',
1504                         values  => \@authorised_values,
1505                         default => $defaultvalue // q{},
1506                         labels  => \%authorised_lib,
1507                     };
1508                 } elsif ( $subfield->{value_builder} ) {
1509                 # it is a plugin
1510                     require Koha::FrameworkPlugin;
1511                     my $plugin = Koha::FrameworkPlugin->new({
1512                         name => $subfield->{value_builder},
1513                         item_style => 1,
1514                     });
1515                     my $pars = { dbh => $dbh, record => undef, tagslib =>$tagslib, id => $subfield_data{id} };
1516                     $plugin->build( $pars );
1517                     if ( $itemrecord and my $field = $itemrecord->field($tag) ) {
1518                         $defaultvalue = $field->subfield($subfield->{subfield}) || q{};
1519                     }
1520                     if( !$plugin->errstr ) {
1521                         #TODO Move html to template; see report 12176/13397
1522                         my $tab= $plugin->noclick? '-1': '';
1523                         my $class= $plugin->noclick? ' disabled': '';
1524                         my $title= $plugin->noclick? 'No popup': 'Tag editor';
1525                         $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;
1526                     } else {
1527                         warn $plugin->errstr;
1528                         $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
1529                     }
1530                 }
1531                 elsif ( $tag eq '' ) {       # it's an hidden field
1532                     $subfield_data{marc_value} = qq(<input type="hidden" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength" value="$defaultvalue" />);
1533                 }
1534                 elsif ( $subfield->{'hidden'} ) {   # FIXME: shouldn't input type be "hidden" ?
1535                     $subfield_data{marc_value} = qq(<input type="text" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength" value="$defaultvalue" />);
1536                 }
1537                 elsif ( length($defaultvalue) > 100
1538                             or (C4::Context->preference("marcflavour") eq "UNIMARC" and
1539                                   300 <= $tag && $tag < 400 && $subfield->{subfield} eq 'a' )
1540                             or (C4::Context->preference("marcflavour") eq "MARC21"  and
1541                                   500 <= $tag && $tag < 600                     )
1542                           ) {
1543                     # oversize field (textarea)
1544                     $subfield_data{marc_value} = qq(<textarea id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength">$defaultvalue</textarea>\n");
1545                 } else {
1546                     $subfield_data{marc_value} = qq(<input type="text" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength" value="$defaultvalue" />);
1547                 }
1548                 push( @loop_data, \%subfield_data );
1549             }
1550         }
1551     }
1552     my $itemnumber;
1553     if ( $itemrecord && $itemrecord->field($itemtagfield) ) {
1554         $itemnumber = $itemrecord->subfield( $itemtagfield, $itemtagsubfield );
1555     }
1556     return {
1557         'itemtagfield'    => $itemtagfield,
1558         'itemtagsubfield' => $itemtagsubfield,
1559         'itemnumber'      => $itemnumber,
1560         'iteminformation' => \@loop_data
1561     };
1562 }
1563
1564 sub ToggleNewStatus {
1565     my ( $params ) = @_;
1566     my @rules = @{ $params->{rules} };
1567     my $report_only = $params->{report_only};
1568
1569     my $dbh = C4::Context->dbh;
1570     my @errors;
1571     my @item_columns = map { "items.$_" } Koha::Items->columns;
1572     my @biblioitem_columns = map { "biblioitems.$_" } Koha::Biblioitems->columns;
1573     my $report;
1574     for my $rule ( @rules ) {
1575         my $age = $rule->{age};
1576         # Default to using items.dateaccessioned if there's an old item modification rule
1577         # missing an agefield value
1578         my $agefield = $rule->{agefield} ? $rule->{agefield} : 'items.dateaccessioned';
1579         my $conditions = $rule->{conditions};
1580         my $substitutions = $rule->{substitutions};
1581         foreach ( @$substitutions ) {
1582             ( $_->{item_field} ) = ( $_->{field} =~ /items\.(.*)/ );
1583         }
1584         my @params;
1585
1586         my $query = q|
1587             SELECT items.*
1588             FROM items
1589             LEFT JOIN biblioitems ON biblioitems.biblionumber = items.biblionumber
1590             WHERE 1
1591         |;
1592         for my $condition ( @$conditions ) {
1593             next unless $condition->{field};
1594             if (
1595                  grep { $_ eq $condition->{field} } @item_columns
1596               or grep { $_ eq $condition->{field} } @biblioitem_columns
1597             ) {
1598                 if ( $condition->{value} =~ /\|/ ) {
1599                     my @values = split /\|/, $condition->{value};
1600                     $query .= qq| AND $condition->{field} IN (|
1601                         . join( ',', ('?') x scalar @values )
1602                         . q|)|;
1603                     push @params, @values;
1604                 } else {
1605                     $query .= qq| AND $condition->{field} = ?|;
1606                     push @params, $condition->{value};
1607                 }
1608             }
1609         }
1610         if ( defined $age ) {
1611             $query .= qq| AND TO_DAYS(NOW()) - TO_DAYS($agefield) >= ? |;
1612             push @params, $age;
1613         }
1614         my $sth = $dbh->prepare($query);
1615         $sth->execute( @params );
1616         while ( my $values = $sth->fetchrow_hashref ) {
1617             my $biblionumber = $values->{biblionumber};
1618             my $itemnumber = $values->{itemnumber};
1619             my $item = Koha::Items->find($itemnumber);
1620             for my $substitution ( @$substitutions ) {
1621                 my $field = $substitution->{item_field};
1622                 my $value = $substitution->{value};
1623                 next unless $substitution->{field};
1624                 next if ( defined $values->{ $substitution->{item_field} } and $values->{ $substitution->{item_field} } eq $substitution->{value} );
1625                 $item->$field($value);
1626                 push @{ $report->{$itemnumber} }, $substitution;
1627             }
1628             $item->store unless $report_only;
1629         }
1630     }
1631
1632     return $report;
1633 }
1634
1635 1;