Bug 26581: (follow-up) Skip merge when deleting authority
[koha.git] / C4 / Items.pm
blobb1e8de702994c2fd8fce70fa01f991a009be4d7d
1 package C4::Items;
3 # Copyright 2007 LibLime, Inc.
4 # Parts Copyright Biblibre 2010
6 # This file is part of Koha.
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.
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.
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>.
21 use Modern::Perl;
23 use vars qw(@ISA @EXPORT);
24 BEGIN {
25 require Exporter;
26 @ISA = qw(Exporter);
28 @EXPORT = qw(
29 AddItemFromMarc
30 AddItemBatchFromMarc
31 ModItemFromMarc
32 Item2Marc
33 ModDateLastSeen
34 ModItemTransfer
35 CheckItemPreSave
36 GetItemsForInventory
37 GetItemsInfo
38 GetItemsLocationInfo
39 GetHostItemsInfo
40 get_hostitemnumbers_of
41 GetHiddenItemnumbers
42 MoveItemFromBiblio
43 CartToShelf
44 GetAnalyticsCount
45 SearchItems
46 PrepareItemrecordDisplay
50 use Carp;
51 use Try::Tiny;
52 use C4::Context;
53 use C4::Koha;
54 use C4::Biblio;
55 use Koha::DateUtils;
56 use MARC::Record;
57 use C4::ClassSource;
58 use C4::Log;
59 use List::MoreUtils qw(any);
60 use YAML qw(Load);
61 use DateTime::Format::MySQL;
62 use Data::Dumper; # used as part of logging item record changes, not just for
63 # debugging; so please don't remove this
65 use Koha::AuthorisedValues;
66 use Koha::DateUtils qw(dt_from_string);
67 use Koha::Database;
69 use Koha::Biblioitems;
70 use Koha::Items;
71 use Koha::ItemTypes;
72 use Koha::SearchEngine;
73 use Koha::SearchEngine::Indexer;
74 use Koha::SearchEngine::Search;
75 use Koha::Libraries;
77 =head1 NAME
79 C4::Items - item management functions
81 =head1 DESCRIPTION
83 This module contains an API for manipulating item
84 records in Koha, and is used by cataloguing, circulation,
85 acquisitions, and serials management.
87 # FIXME This POD is not up-to-date
88 A Koha item record is stored in two places: the
89 items table and embedded in a MARC tag in the XML
90 version of the associated bib record in C<biblioitems.marcxml>.
91 This is done to allow the item information to be readily
92 indexed (e.g., by Zebra), but means that each item
93 modification transaction must keep the items table
94 and the MARC XML in sync at all times.
96 The items table will be considered authoritative. In other
97 words, if there is ever a discrepancy between the items
98 table and the MARC XML, the items table should be considered
99 accurate.
101 =head1 HISTORICAL NOTE
103 Most of the functions in C<C4::Items> were originally in
104 the C<C4::Biblio> module.
106 =head1 CORE EXPORTED FUNCTIONS
108 The following functions are meant for use by users
109 of C<C4::Items>
111 =cut
113 =head2 CartToShelf
115 CartToShelf($itemnumber);
117 Set the current shelving location of the item record
118 to its stored permanent shelving location. This is
119 primarily used to indicate when an item whose current
120 location is a special processing ('PROC') or shelving cart
121 ('CART') location is back in the stacks.
123 =cut
125 sub CartToShelf {
126 my ( $itemnumber ) = @_;
128 unless ( $itemnumber ) {
129 croak "FAILED CartToShelf() - no itemnumber supplied";
132 my $item = Koha::Items->find($itemnumber);
133 if ( $item->location eq 'CART' ) {
134 $item->location($item->permanent_location)->store;
138 =head2 AddItemFromMarc
140 my ($biblionumber, $biblioitemnumber, $itemnumber)
141 = AddItemFromMarc($source_item_marc, $biblionumber[, $params]);
143 Given a MARC::Record object containing an embedded item
144 record and a biblionumber, create a new item record.
146 The final optional parameter, C<$params>, expected to contain
147 'skip_record_index' key, which relayed down to Koha::Item/store,
148 there it prevents calling of index_records,
149 which takes most of the time in batch adds/deletes: index_records
150 to be called later in C<additem.pl> after the whole loop.
152 $params:
153 skip_record_index => 1|0
155 =cut
157 sub AddItemFromMarc {
158 my $source_item_marc = shift;
159 my $biblionumber = shift;
160 my $params = @_ ? shift : {};
162 my $dbh = C4::Context->dbh;
164 # parse item hash from MARC
165 my $frameworkcode = C4::Biblio::GetFrameworkCode($biblionumber);
166 my ( $itemtag, $itemsubfield ) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
168 my $localitemmarc = MARC::Record->new;
169 $localitemmarc->append_fields( $source_item_marc->field($itemtag) );
171 my $item_values = C4::Biblio::TransformMarcToKoha( $localitemmarc, $frameworkcode, 'items' );
172 my $unlinked_item_subfields = _get_unlinked_item_subfields( $localitemmarc, $frameworkcode );
173 $item_values->{more_subfields_xml} = _get_unlinked_subfields_xml($unlinked_item_subfields);
174 $item_values->{biblionumber} = $biblionumber;
175 $item_values->{cn_source} = delete $item_values->{'items.cn_source'}; # Because of C4::Biblio::_disambiguate
176 $item_values->{cn_sort} = delete $item_values->{'items.cn_sort'}; # Because of C4::Biblio::_disambiguate
177 my $item = Koha::Item->new( $item_values )->store({ skip_record_index => $params->{skip_record_index} });
178 return ( $item->biblionumber, $item->biblioitemnumber, $item->itemnumber );
181 =head2 AddItemBatchFromMarc
183 ($itemnumber_ref, $error_ref) = AddItemBatchFromMarc($record,
184 $biblionumber, $biblioitemnumber, $frameworkcode);
186 Efficiently create item records from a MARC biblio record with
187 embedded item fields. This routine is suitable for batch jobs.
189 This API assumes that the bib record has already been
190 saved to the C<biblio> and C<biblioitems> tables. It does
191 not expect that C<biblio_metadata.metadata> is populated, but it
192 will do so via a call to ModBibiloMarc.
194 The goal of this API is to have a similar effect to using AddBiblio
195 and AddItems in succession, but without inefficient repeated
196 parsing of the MARC XML bib record.
198 This function returns an arrayref of new itemsnumbers and an arrayref of item
199 errors encountered during the processing. Each entry in the errors
200 list is a hashref containing the following keys:
202 =over
204 =item item_sequence
206 Sequence number of original item tag in the MARC record.
208 =item item_barcode
210 Item barcode, provide to assist in the construction of
211 useful error messages.
213 =item error_code
215 Code representing the error condition. Can be 'duplicate_barcode',
216 'invalid_homebranch', or 'invalid_holdingbranch'.
218 =item error_information
220 Additional information appropriate to the error condition.
222 =back
224 =cut
226 sub AddItemBatchFromMarc {
227 my ($record, $biblionumber, $biblioitemnumber, $frameworkcode) = @_;
228 my @itemnumbers = ();
229 my @errors = ();
230 my $dbh = C4::Context->dbh;
232 # We modify the record, so lets work on a clone so we don't change the
233 # original.
234 $record = $record->clone();
235 # loop through the item tags and start creating items
236 my @bad_item_fields = ();
237 my ($itemtag, $itemsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
238 my $item_sequence_num = 0;
239 ITEMFIELD: foreach my $item_field ($record->field($itemtag)) {
240 $item_sequence_num++;
241 # we take the item field and stick it into a new
242 # MARC record -- this is required so far because (FIXME)
243 # TransformMarcToKoha requires a MARC::Record, not a MARC::Field
244 # and there is no TransformMarcFieldToKoha
245 my $temp_item_marc = MARC::Record->new();
246 $temp_item_marc->append_fields($item_field);
248 # add biblionumber and biblioitemnumber
249 my $item = TransformMarcToKoha( $temp_item_marc, $frameworkcode, 'items' );
250 my $unlinked_item_subfields = _get_unlinked_item_subfields($temp_item_marc, $frameworkcode);
251 $item->{'more_subfields_xml'} = _get_unlinked_subfields_xml($unlinked_item_subfields);
252 $item->{'biblionumber'} = $biblionumber;
253 $item->{'biblioitemnumber'} = $biblioitemnumber;
254 $item->{cn_source} = delete $item->{'items.cn_source'}; # Because of C4::Biblio::_disambiguate
255 $item->{cn_sort} = delete $item->{'items.cn_sort'}; # Because of C4::Biblio::_disambiguate
257 # check for duplicate barcode
258 my %item_errors = CheckItemPreSave($item);
259 if (%item_errors) {
260 push @errors, _repack_item_errors($item_sequence_num, $item, \%item_errors);
261 push @bad_item_fields, $item_field;
262 next ITEMFIELD;
265 my $item_object = Koha::Item->new($item)->store;
266 push @itemnumbers, $item_object->itemnumber; # FIXME not checking error
268 logaction("CATALOGUING", "ADD", $item_object->itemnumber, "item") if C4::Context->preference("CataloguingLog");
270 my $new_item_marc = _marc_from_item_hash($item_object->unblessed, $frameworkcode, $unlinked_item_subfields);
271 $item_field->replace_with($new_item_marc->field($itemtag));
274 # remove any MARC item fields for rejected items
275 foreach my $item_field (@bad_item_fields) {
276 $record->delete_field($item_field);
279 # update the MARC biblio
280 # $biblionumber = ModBiblioMarc( $record, $biblionumber, $frameworkcode );
282 return (\@itemnumbers, \@errors);
285 =head2 ModItemFromMarc
287 my $item = ModItemFromMarc($item_marc, $biblionumber, $itemnumber[, $params]);
289 The final optional parameter, C<$params>, expected to contain
290 'skip_record_index' key, which relayed down to Koha::Item/store,
291 there it prevents calling of index_records,
292 which takes most of the time in batch adds/deletes: index_records better
293 to be called later in C<additem.pl> after the whole loop.
295 $params:
296 skip_record_index => 1|0
298 =cut
300 sub ModItemFromMarc {
301 my ( $item_marc, $biblionumber, $itemnumber, $params ) = @_;
303 my $frameworkcode = C4::Biblio::GetFrameworkCode($biblionumber);
304 my ( $itemtag, $itemsubfield ) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
306 my $localitemmarc = MARC::Record->new;
307 $localitemmarc->append_fields( $item_marc->field($itemtag) );
308 my $item_object = Koha::Items->find($itemnumber);
309 my $item = TransformMarcToKoha( $localitemmarc, $frameworkcode, 'items' );
311 # Retrieving the values for the fields that are not linked
312 my @mapped_fields = Koha::MarcSubfieldStructures->search(
314 frameworkcode => $frameworkcode,
315 kohafield => { -like => "items.%" }
317 )->get_column('kohafield');
318 for my $c ( $item_object->_result->result_source->columns ) {
319 next if grep { "items.$c" eq $_ } @mapped_fields;
320 $item->{$c} = $item_object->$c;
323 $item->{cn_source} = delete $item->{'items.cn_source'}; # Because of C4::Biblio::_disambiguate
324 $item->{cn_sort} = delete $item->{'items.cn_sort'}; # Because of C4::Biblio::_disambiguate
325 $item->{itemnumber} = $itemnumber;
326 $item->{biblionumber} = $biblionumber;
327 $item_object = $item_object->set_or_blank($item);
328 my $unlinked_item_subfields = _get_unlinked_item_subfields( $localitemmarc, $frameworkcode );
329 $item_object->more_subfields_xml(_get_unlinked_subfields_xml($unlinked_item_subfields));
330 $item_object->store({ skip_record_index => $params->{skip_record_index} });
332 return $item_object->unblessed;
335 =head2 ModItemTransfer
337 ModItemTransfer($itemnumber, $frombranch, $tobranch, $trigger, [$params]);
339 Marks an item as being transferred from one branch to another and records the trigger.
341 The last optional parameter allows for passing skip_record_index through to the items store call.
343 =cut
345 sub ModItemTransfer {
346 my ( $itemnumber, $frombranch, $tobranch, $trigger, $params ) = @_;
348 my $dbh = C4::Context->dbh;
349 my $item = Koha::Items->find( $itemnumber );
351 # Remove the 'shelving cart' location status if it is being used.
352 CartToShelf( $itemnumber ) if $item->location && $item->location eq 'CART' && ( !$item->permanent_location || $item->permanent_location ne 'CART' );
354 $dbh->do("UPDATE branchtransfers SET datearrived = NOW(), comments = ? WHERE itemnumber = ? AND datearrived IS NULL", undef, "Canceled, new transfer from $frombranch to $tobranch created", $itemnumber);
356 #new entry in branchtransfers....
357 my $sth = $dbh->prepare(
358 "INSERT INTO branchtransfers (itemnumber, frombranch, datesent, tobranch, reason)
359 VALUES (?, ?, NOW(), ?, ?)");
360 $sth->execute($itemnumber, $frombranch, $tobranch, $trigger);
362 # FIXME we are fetching the item twice in the 2 next statements!
363 Koha::Items->find($itemnumber)->holdingbranch($frombranch)->store({ log_action => 0, skip_record_index => $params->{skip_record_index} });
364 ModDateLastSeen($itemnumber, undef, { skip_record_index => $params->{skip_record_index} });
365 return;
368 =head2 ModDateLastSeen
370 ModDateLastSeen( $itemnumber, $leave_item_lost, $params );
372 Mark item as seen. Is called when an item is issued, returned or manually marked during inventory/stocktaking.
373 C<$itemnumber> is the item number
374 C<$leave_item_lost> determines if a lost item will be found or remain lost
376 The last optional parameter allows for passing skip_record_index through to the items store call.
378 =cut
380 sub ModDateLastSeen {
381 my ( $itemnumber, $leave_item_lost, $params ) = @_;
383 my $today = output_pref({ dt => dt_from_string, dateformat => 'iso', dateonly => 1 });
385 my $item = Koha::Items->find($itemnumber);
386 $item->datelastseen($today);
387 $item->itemlost(0) unless $leave_item_lost;
388 $item->store({ log_action => 0, skip_record_index => $params->{skip_record_index} });
391 =head2 CheckItemPreSave
393 my $item_ref = TransformMarcToKoha($marc, 'items');
394 # do stuff
395 my %errors = CheckItemPreSave($item_ref);
396 if (exists $errors{'duplicate_barcode'}) {
397 print "item has duplicate barcode: ", $errors{'duplicate_barcode'}, "\n";
398 } elsif (exists $errors{'invalid_homebranch'}) {
399 print "item has invalid home branch: ", $errors{'invalid_homebranch'}, "\n";
400 } elsif (exists $errors{'invalid_holdingbranch'}) {
401 print "item has invalid holding branch: ", $errors{'invalid_holdingbranch'}, "\n";
402 } else {
403 print "item is OK";
406 Given a hashref containing item fields, determine if it can be
407 inserted or updated in the database. Specifically, checks for
408 database integrity issues, and returns a hash containing any
409 of the following keys, if applicable.
411 =over 2
413 =item duplicate_barcode
415 Barcode, if it duplicates one already found in the database.
417 =item invalid_homebranch
419 Home branch, if not defined in branches table.
421 =item invalid_holdingbranch
423 Holding branch, if not defined in branches table.
425 =back
427 This function does NOT implement any policy-related checks,
428 e.g., whether current operator is allowed to save an
429 item that has a given branch code.
431 =cut
433 sub CheckItemPreSave {
434 my $item_ref = shift;
436 my %errors = ();
438 # check for duplicate barcode
439 if (exists $item_ref->{'barcode'} and defined $item_ref->{'barcode'}) {
440 my $existing_item= Koha::Items->find({barcode => $item_ref->{'barcode'}});
441 if ($existing_item) {
442 if (!exists $item_ref->{'itemnumber'} # new item
443 or $item_ref->{'itemnumber'} != $existing_item->itemnumber) { # existing item
444 $errors{'duplicate_barcode'} = $item_ref->{'barcode'};
449 # check for valid home branch
450 if (exists $item_ref->{'homebranch'} and defined $item_ref->{'homebranch'}) {
451 my $home_library = Koha::Libraries->find( $item_ref->{homebranch} );
452 unless (defined $home_library) {
453 $errors{'invalid_homebranch'} = $item_ref->{'homebranch'};
457 # check for valid holding branch
458 if (exists $item_ref->{'holdingbranch'} and defined $item_ref->{'holdingbranch'}) {
459 my $holding_library = Koha::Libraries->find( $item_ref->{holdingbranch} );
460 unless (defined $holding_library) {
461 $errors{'invalid_holdingbranch'} = $item_ref->{'holdingbranch'};
465 return %errors;
469 =head1 EXPORTED SPECIAL ACCESSOR FUNCTIONS
471 The following functions provide various ways of
472 getting an item record, a set of item records, or
473 lists of authorized values for certain item fields.
475 =cut
477 =head2 GetItemsForInventory
479 ($itemlist, $iTotalRecords) = GetItemsForInventory( {
480 minlocation => $minlocation,
481 maxlocation => $maxlocation,
482 location => $location,
483 itemtype => $itemtype,
484 ignoreissued => $ignoreissued,
485 datelastseen => $datelastseen,
486 branchcode => $branchcode,
487 branch => $branch,
488 offset => $offset,
489 size => $size,
490 statushash => $statushash,
491 } );
493 Retrieve a list of title/authors/barcode/callnumber, for biblio inventory.
495 The sub returns a reference to a list of hashes, each containing
496 itemnumber, author, title, barcode, item callnumber, and date last
497 seen. It is ordered by callnumber then title.
499 The required minlocation & maxlocation parameters are used to specify a range of item callnumbers
500 the datelastseen can be used to specify that you want to see items not seen since a past date only.
501 offset & size can be used to retrieve only a part of the whole listing (defaut behaviour)
502 $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.
504 $iTotalRecords is the number of rows that would have been returned without the $offset, $size limit clause
506 =cut
508 sub GetItemsForInventory {
509 my ( $parameters ) = @_;
510 my $minlocation = $parameters->{'minlocation'} // '';
511 my $maxlocation = $parameters->{'maxlocation'} // '';
512 my $class_source = $parameters->{'class_source'} // C4::Context->preference('DefaultClassificationSource');
513 my $location = $parameters->{'location'} // '';
514 my $itemtype = $parameters->{'itemtype'} // '';
515 my $ignoreissued = $parameters->{'ignoreissued'} // '';
516 my $datelastseen = $parameters->{'datelastseen'} // '';
517 my $branchcode = $parameters->{'branchcode'} // '';
518 my $branch = $parameters->{'branch'} // '';
519 my $offset = $parameters->{'offset'} // '';
520 my $size = $parameters->{'size'} // '';
521 my $statushash = $parameters->{'statushash'} // '';
522 my $ignore_waiting_holds = $parameters->{'ignore_waiting_holds'} // '';
524 my $dbh = C4::Context->dbh;
525 my ( @bind_params, @where_strings );
527 my $min_cnsort = GetClassSort($class_source,undef,$minlocation);
528 my $max_cnsort = GetClassSort($class_source,undef,$maxlocation);
530 my $select_columns = q{
531 SELECT DISTINCT(items.itemnumber), barcode, itemcallnumber, title, author, biblio.biblionumber, biblio.frameworkcode, datelastseen, homebranch, location, notforloan, damaged, itemlost, withdrawn, stocknumber, items.cn_sort
533 my $select_count = q{SELECT COUNT(DISTINCT(items.itemnumber))};
534 my $query = q{
535 FROM items
536 LEFT JOIN biblio ON items.biblionumber = biblio.biblionumber
537 LEFT JOIN biblioitems on items.biblionumber = biblioitems.biblionumber
539 if ($statushash){
540 for my $authvfield (keys %$statushash){
541 if ( scalar @{$statushash->{$authvfield}} > 0 ){
542 my $joinedvals = join ',', @{$statushash->{$authvfield}};
543 push @where_strings, "$authvfield in (" . $joinedvals . ")";
548 if ($minlocation) {
549 push @where_strings, 'items.cn_sort >= ?';
550 push @bind_params, $min_cnsort;
553 if ($maxlocation) {
554 push @where_strings, 'items.cn_sort <= ?';
555 push @bind_params, $max_cnsort;
558 if ($datelastseen) {
559 $datelastseen = output_pref({ str => $datelastseen, dateformat => 'iso', dateonly => 1 });
560 push @where_strings, '(datelastseen < ? OR datelastseen IS NULL)';
561 push @bind_params, $datelastseen;
564 if ( $location ) {
565 push @where_strings, 'items.location = ?';
566 push @bind_params, $location;
569 if ( $branchcode ) {
570 if($branch eq "homebranch"){
571 push @where_strings, 'items.homebranch = ?';
572 }else{
573 push @where_strings, 'items.holdingbranch = ?';
575 push @bind_params, $branchcode;
578 if ( $itemtype ) {
579 push @where_strings, 'biblioitems.itemtype = ?';
580 push @bind_params, $itemtype;
583 if ( $ignoreissued) {
584 $query .= "LEFT JOIN issues ON items.itemnumber = issues.itemnumber ";
585 push @where_strings, 'issues.date_due IS NULL';
588 if ( $ignore_waiting_holds ) {
589 $query .= "LEFT JOIN reserves ON items.itemnumber = reserves.itemnumber ";
590 push( @where_strings, q{(reserves.found != 'W' OR reserves.found IS NULL)} );
593 if ( @where_strings ) {
594 $query .= 'WHERE ';
595 $query .= join ' AND ', @where_strings;
597 my $count_query = $select_count . $query;
598 $query .= ' ORDER BY items.cn_sort, itemcallnumber, title';
599 $query .= " LIMIT $offset, $size" if ($offset and $size);
600 $query = $select_columns . $query;
601 my $sth = $dbh->prepare($query);
602 $sth->execute( @bind_params );
604 my @results = ();
605 my $tmpresults = $sth->fetchall_arrayref({});
606 $sth = $dbh->prepare( $count_query );
607 $sth->execute( @bind_params );
608 my ($iTotalRecords) = $sth->fetchrow_array();
610 my @avs = Koha::AuthorisedValues->search(
611 { 'marc_subfield_structures.kohafield' => { '>' => '' },
612 'me.authorised_value' => { '>' => '' },
614 { join => { category => 'marc_subfield_structures' },
615 distinct => ['marc_subfield_structures.kohafield, me.category, frameworkcode, me.authorised_value'],
616 '+select' => [ 'marc_subfield_structures.kohafield', 'marc_subfield_structures.frameworkcode', 'me.authorised_value', 'me.lib' ],
617 '+as' => [ 'kohafield', 'frameworkcode', 'authorised_value', 'lib' ],
621 my $avmapping = { map { $_->get_column('kohafield') . ',' . $_->get_column('frameworkcode') . ',' . $_->get_column('authorised_value') => $_->get_column('lib') } @avs };
623 foreach my $row (@$tmpresults) {
625 # Auth values
626 foreach (keys %$row) {
627 if (
628 defined(
629 $avmapping->{ "items.$_," . $row->{'frameworkcode'} . "," . ( $row->{$_} // q{} ) }
632 $row->{$_} = $avmapping->{"items.$_,".$row->{'frameworkcode'}.",".$row->{$_}};
635 push @results, $row;
638 return (\@results, $iTotalRecords);
641 =head2 GetItemsInfo
643 @results = GetItemsInfo($biblionumber);
645 Returns information about items with the given biblionumber.
647 C<GetItemsInfo> returns a list of references-to-hash. Each element
648 contains a number of keys. Most of them are attributes from the
649 C<biblio>, C<biblioitems>, C<items>, and C<itemtypes> tables in the
650 Koha database. Other keys include:
652 =over 2
654 =item C<$data-E<gt>{branchname}>
656 The name (not the code) of the branch to which the book belongs.
658 =item C<$data-E<gt>{datelastseen}>
660 This is simply C<items.datelastseen>, except that while the date is
661 stored in YYYY-MM-DD format in the database, here it is converted to
662 DD/MM/YYYY format. A NULL date is returned as C<//>.
664 =item C<$data-E<gt>{datedue}>
666 =item C<$data-E<gt>{class}>
668 This is the concatenation of C<biblioitems.classification>, the book's
669 Dewey code, and C<biblioitems.subclass>.
671 =item C<$data-E<gt>{ocount}>
673 I think this is the number of copies of the book available.
675 =item C<$data-E<gt>{order}>
677 If this is set, it is set to C<One Order>.
679 =back
681 =cut
683 sub GetItemsInfo {
684 my ( $biblionumber ) = @_;
685 my $dbh = C4::Context->dbh;
686 require C4::Languages;
687 my $language = C4::Languages::getlanguage();
688 my $query = "
689 SELECT items.*,
690 biblio.*,
691 biblioitems.volume,
692 biblioitems.number,
693 biblioitems.itemtype,
694 biblioitems.isbn,
695 biblioitems.issn,
696 biblioitems.publicationyear,
697 biblioitems.publishercode,
698 biblioitems.volumedate,
699 biblioitems.volumedesc,
700 biblioitems.lccn,
701 biblioitems.url,
702 items.notforloan as itemnotforloan,
703 issues.borrowernumber,
704 issues.date_due as datedue,
705 issues.onsite_checkout,
706 borrowers.cardnumber,
707 borrowers.surname,
708 borrowers.firstname,
709 borrowers.branchcode as bcode,
710 serial.serialseq,
711 serial.publisheddate,
712 itemtypes.description,
713 COALESCE( localization.translation, itemtypes.description ) AS translated_description,
714 itemtypes.notforloan as notforloan_per_itemtype,
715 holding.branchurl,
716 holding.branchcode,
717 holding.branchname,
718 holding.opac_info as holding_branch_opac_info,
719 home.opac_info as home_branch_opac_info,
720 IF(tmp_holdsqueue.itemnumber,1,0) AS has_pending_hold
721 FROM items
722 LEFT JOIN branches AS holding ON items.holdingbranch = holding.branchcode
723 LEFT JOIN branches AS home ON items.homebranch=home.branchcode
724 LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
725 LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber
726 LEFT JOIN issues USING (itemnumber)
727 LEFT JOIN borrowers USING (borrowernumber)
728 LEFT JOIN serialitems USING (itemnumber)
729 LEFT JOIN serial USING (serialid)
730 LEFT JOIN itemtypes ON itemtypes.itemtype = "
731 . (C4::Context->preference('item-level_itypes') ? 'items.itype' : 'biblioitems.itemtype');
732 $query .= q|
733 LEFT JOIN tmp_holdsqueue USING (itemnumber)
734 LEFT JOIN localization ON itemtypes.itemtype = localization.code
735 AND localization.entity = 'itemtypes'
736 AND localization.lang = ?
739 $query .= " WHERE items.biblionumber = ? ORDER BY home.branchname, items.enumchron, LPAD( items.copynumber, 8, '0' ), items.dateaccessioned DESC" ;
740 my $sth = $dbh->prepare($query);
741 $sth->execute($language, $biblionumber);
742 my $i = 0;
743 my @results;
744 my $serial;
746 my $userenv = C4::Context->userenv;
747 my $want_not_same_branch = C4::Context->preference("IndependentBranches") && !C4::Context->IsSuperLibrarian();
748 while ( my $data = $sth->fetchrow_hashref ) {
749 if ( $data->{borrowernumber} && $want_not_same_branch) {
750 $data->{'NOTSAMEBRANCH'} = $data->{'bcode'} ne $userenv->{branch};
753 $serial ||= $data->{'serial'};
755 my $descriptions;
756 # get notforloan complete status if applicable
757 $descriptions = Koha::AuthorisedValues->get_description_by_koha_field({frameworkcode => $data->{frameworkcode}, kohafield => 'items.notforloan', authorised_value => $data->{itemnotforloan} });
758 $data->{notforloanvalue} = $descriptions->{lib} // '';
759 $data->{notforloanvalueopac} = $descriptions->{opac_description} // '';
761 # get restricted status and description if applicable
762 $descriptions = Koha::AuthorisedValues->get_description_by_koha_field({frameworkcode => $data->{frameworkcode}, kohafield => 'items.restricted', authorised_value => $data->{restricted} });
763 $data->{restrictedvalue} = $descriptions->{lib} // '';
764 $data->{restrictedvalueopac} = $descriptions->{opac_description} // '';
766 # my stack procedures
767 $descriptions = Koha::AuthorisedValues->get_description_by_koha_field({frameworkcode => $data->{frameworkcode}, kohafield => 'items.stack', authorised_value => $data->{stack} });
768 $data->{stack} = $descriptions->{lib} // '';
770 # Find the last 3 people who borrowed this item.
771 my $sth2 = $dbh->prepare("SELECT * FROM old_issues,borrowers
772 WHERE itemnumber = ?
773 AND old_issues.borrowernumber = borrowers.borrowernumber
774 ORDER BY returndate DESC
775 LIMIT 3");
776 $sth2->execute($data->{'itemnumber'});
777 my $ii = 0;
778 while (my $data2 = $sth2->fetchrow_hashref()) {
779 $data->{"timestamp$ii"} = $data2->{'timestamp'} if $data2->{'timestamp'};
780 $data->{"card$ii"} = $data2->{'cardnumber'} if $data2->{'cardnumber'};
781 $data->{"borrower$ii"} = $data2->{'borrowernumber'} if $data2->{'borrowernumber'};
782 $ii++;
785 $results[$i] = $data;
786 $i++;
789 return $serial
790 ? sort { ($b->{'publisheddate'} || $b->{'enumchron'}) cmp ($a->{'publisheddate'} || $a->{'enumchron'}) } @results
791 : @results;
794 =head2 GetItemsLocationInfo
796 my @itemlocinfo = GetItemsLocationInfo($biblionumber);
798 Returns the branch names, shelving location and itemcallnumber for each item attached to the biblio in question
800 C<GetItemsInfo> returns a list of references-to-hash. Data returned:
802 =over 2
804 =item C<$data-E<gt>{homebranch}>
806 Branch Name of the item's homebranch
808 =item C<$data-E<gt>{holdingbranch}>
810 Branch Name of the item's holdingbranch
812 =item C<$data-E<gt>{location}>
814 Item's shelving location code
816 =item C<$data-E<gt>{location_intranet}>
818 The intranet description for the Shelving Location as set in authorised_values 'LOC'
820 =item C<$data-E<gt>{location_opac}>
822 The OPAC description for the Shelving Location as set in authorised_values 'LOC'. Falls back to intranet description if no OPAC
823 description is set.
825 =item C<$data-E<gt>{itemcallnumber}>
827 Item's itemcallnumber
829 =item C<$data-E<gt>{cn_sort}>
831 Item's call number normalized for sorting
833 =back
835 =cut
837 sub GetItemsLocationInfo {
838 my $biblionumber = shift;
839 my @results;
841 my $dbh = C4::Context->dbh;
842 my $query = "SELECT a.branchname as homebranch, b.branchname as holdingbranch,
843 location, itemcallnumber, cn_sort
844 FROM items, branches as a, branches as b
845 WHERE homebranch = a.branchcode AND holdingbranch = b.branchcode
846 AND biblionumber = ?
847 ORDER BY cn_sort ASC";
848 my $sth = $dbh->prepare($query);
849 $sth->execute($biblionumber);
851 while ( my $data = $sth->fetchrow_hashref ) {
852 my $av = Koha::AuthorisedValues->search({ category => 'LOC', authorised_value => $data->{location} });
853 $av = $av->count ? $av->next : undef;
854 $data->{location_intranet} = $av ? $av->lib : '';
855 $data->{location_opac} = $av ? $av->opac_description : '';
856 push @results, $data;
858 return @results;
861 =head2 GetHostItemsInfo
863 $hostiteminfo = GetHostItemsInfo($hostfield);
864 Returns the iteminfo for items linked to records via a host field
866 =cut
868 sub GetHostItemsInfo {
869 my ($record) = @_;
870 my @returnitemsInfo;
872 if( !C4::Context->preference('EasyAnalyticalRecords') ) {
873 return @returnitemsInfo;
876 my @fields;
877 if( C4::Context->preference('marcflavour') eq 'MARC21' ||
878 C4::Context->preference('marcflavour') eq 'NORMARC') {
879 @fields = $record->field('773');
880 } elsif( C4::Context->preference('marcflavour') eq 'UNIMARC') {
881 @fields = $record->field('461');
884 foreach my $hostfield ( @fields ) {
885 my $hostbiblionumber = $hostfield->subfield("0");
886 my $linkeditemnumber = $hostfield->subfield("9");
887 my @hostitemInfos = GetItemsInfo($hostbiblionumber);
888 foreach my $hostitemInfo (@hostitemInfos) {
889 if( $hostitemInfo->{itemnumber} eq $linkeditemnumber ) {
890 push @returnitemsInfo, $hostitemInfo;
891 last;
895 return @returnitemsInfo;
898 =head2 get_hostitemnumbers_of
900 my @itemnumbers_of = get_hostitemnumbers_of($biblionumber);
902 Given a biblionumber, return the list of corresponding itemnumbers that are linked to it via host fields
904 Return a reference on a hash where key is a biblionumber and values are
905 references on array of itemnumbers.
907 =cut
910 sub get_hostitemnumbers_of {
911 my ($biblionumber) = @_;
913 if( !C4::Context->preference('EasyAnalyticalRecords') ) {
914 return ();
917 my $marcrecord = C4::Biblio::GetMarcBiblio({ biblionumber => $biblionumber });
918 return unless $marcrecord;
920 my ( @returnhostitemnumbers, $tag, $biblio_s, $item_s );
922 my $marcflavor = C4::Context->preference('marcflavour');
923 if ( $marcflavor eq 'MARC21' || $marcflavor eq 'NORMARC' ) {
924 $tag = '773';
925 $biblio_s = '0';
926 $item_s = '9';
928 elsif ( $marcflavor eq 'UNIMARC' ) {
929 $tag = '461';
930 $biblio_s = '0';
931 $item_s = '9';
934 foreach my $hostfield ( $marcrecord->field($tag) ) {
935 my $hostbiblionumber = $hostfield->subfield($biblio_s);
936 next unless $hostbiblionumber; # have tag, don't have $biblio_s subfield
937 my $linkeditemnumber = $hostfield->subfield($item_s);
938 if ( ! $linkeditemnumber ) {
939 warn "ERROR biblionumber $biblionumber has 773^0, but doesn't have 9";
940 next;
942 my $is_from_biblio = Koha::Items->search({ itemnumber => $linkeditemnumber, biblionumber => $hostbiblionumber });
943 push @returnhostitemnumbers, $linkeditemnumber
944 if $is_from_biblio;
947 return @returnhostitemnumbers;
950 =head2 GetHiddenItemnumbers
952 my @itemnumbers_to_hide = GetHiddenItemnumbers({ items => \@items, borcat => $category });
954 Given a list of items it checks which should be hidden from the OPAC given
955 the current configuration. Returns a list of itemnumbers corresponding to
956 those that should be hidden. Optionally takes a borcat parameter for certain borrower types
957 to be excluded
959 =cut
961 sub GetHiddenItemnumbers {
962 my $params = shift;
963 my $items = $params->{items};
964 if (my $exceptions = C4::Context->preference('OpacHiddenItemsExceptions') and $params->{'borcat'}){
965 foreach my $except (split(/\|/, $exceptions)){
966 if ($params->{'borcat'} eq $except){
967 return; # we don't hide anything for this borrower category
971 my @resultitems;
973 my $yaml = C4::Context->preference('OpacHiddenItems');
974 return () if (! $yaml =~ /\S/ );
975 $yaml = "$yaml\n\n"; # YAML is anal on ending \n. Surplus does not hurt
976 my $hidingrules;
977 eval {
978 $hidingrules = YAML::Load($yaml);
980 if ($@) {
981 warn "Unable to parse OpacHiddenItems syspref : $@";
982 return ();
984 my $dbh = C4::Context->dbh;
986 # For each item
987 foreach my $item (@$items) {
989 # We check each rule
990 foreach my $field (keys %$hidingrules) {
991 my $val;
992 if (exists $item->{$field}) {
993 $val = $item->{$field};
995 else {
996 my $query = "SELECT $field from items where itemnumber = ?";
997 $val = $dbh->selectrow_array($query, undef, $item->{'itemnumber'});
999 $val = '' unless defined $val;
1001 # If the results matches the values in the yaml file
1002 if (any { $val eq $_ } @{$hidingrules->{$field}}) {
1004 # We add the itemnumber to the list
1005 push @resultitems, $item->{'itemnumber'};
1007 # If at least one rule matched for an item, no need to test the others
1008 last;
1012 return @resultitems;
1015 =head1 LIMITED USE FUNCTIONS
1017 The following functions, while part of the public API,
1018 are not exported. This is generally because they are
1019 meant to be used by only one script for a specific
1020 purpose, and should not be used in any other context
1021 without careful thought.
1023 =cut
1025 =head2 GetMarcItem
1027 my $item_marc = GetMarcItem($biblionumber, $itemnumber);
1029 Returns MARC::Record of the item passed in parameter.
1030 This function is meant for use only in C<cataloguing/additem.pl>,
1031 where it is needed to support that script's MARC-like
1032 editor.
1034 =cut
1036 sub GetMarcItem {
1037 my ( $biblionumber, $itemnumber ) = @_;
1039 # GetMarcItem has been revised so that it does the following:
1040 # 1. Gets the item information from the items table.
1041 # 2. Converts it to a MARC field for storage in the bib record.
1043 # The previous behavior was:
1044 # 1. Get the bib record.
1045 # 2. Return the MARC tag corresponding to the item record.
1047 # The difference is that one treats the items row as authoritative,
1048 # while the other treats the MARC representation as authoritative
1049 # under certain circumstances.
1051 my $item = Koha::Items->find($itemnumber) or return;
1053 # Tack on 'items.' prefix to column names so that C4::Biblio::TransformKohaToMarc will work.
1054 # Also, don't emit a subfield if the underlying field is blank.
1056 return Item2Marc($item->unblessed, $biblionumber);
1059 sub Item2Marc {
1060 my ($itemrecord,$biblionumber)=@_;
1061 my $mungeditem = {
1062 map {
1063 defined($itemrecord->{$_}) && $itemrecord->{$_} ne '' ? ("items.$_" => $itemrecord->{$_}) : ()
1064 } keys %{ $itemrecord }
1066 my $framework = C4::Biblio::GetFrameworkCode( $biblionumber );
1067 my $itemmarc = C4::Biblio::TransformKohaToMarc( $mungeditem, { framework => $framework } );
1068 my ( $itemtag, $itemsubfield ) = C4::Biblio::GetMarcFromKohaField(
1069 "items.itemnumber", $framework,
1072 my $unlinked_item_subfields = _parse_unlinked_item_subfields_from_xml($mungeditem->{'items.more_subfields_xml'});
1073 if (defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1) {
1074 foreach my $field ($itemmarc->field($itemtag)){
1075 $field->add_subfields(@$unlinked_item_subfields);
1078 return $itemmarc;
1081 =head1 PRIVATE FUNCTIONS AND VARIABLES
1083 The following functions are not meant to be called
1084 directly, but are documented in order to explain
1085 the inner workings of C<C4::Items>.
1087 =cut
1089 =head2 MoveItemFromBiblio
1091 MoveItemFromBiblio($itenumber, $frombiblio, $tobiblio);
1093 Moves an item from a biblio to another
1095 Returns undef if the move failed or the biblionumber of the destination record otherwise
1097 =cut
1099 sub MoveItemFromBiblio {
1100 my ($itemnumber, $frombiblio, $tobiblio) = @_;
1101 my $dbh = C4::Context->dbh;
1102 my ( $tobiblioitem ) = $dbh->selectrow_array(q|
1103 SELECT biblioitemnumber
1104 FROM biblioitems
1105 WHERE biblionumber = ?
1106 |, undef, $tobiblio );
1107 my $return = $dbh->do(q|
1108 UPDATE items
1109 SET biblioitemnumber = ?,
1110 biblionumber = ?
1111 WHERE itemnumber = ?
1112 AND biblionumber = ?
1113 |, undef, $tobiblioitem, $tobiblio, $itemnumber, $frombiblio );
1114 if ($return == 1) {
1115 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1116 $indexer->index_records( $tobiblio, "specialUpdate", "biblioserver" );
1117 $indexer->index_records( $frombiblio, "specialUpdate", "biblioserver" );
1118 # Checking if the item we want to move is in an order
1119 require C4::Acquisition;
1120 my $order = C4::Acquisition::GetOrderFromItemnumber($itemnumber);
1121 if ($order) {
1122 # Replacing the biblionumber within the order if necessary
1123 $order->{'biblionumber'} = $tobiblio;
1124 C4::Acquisition::ModOrder($order);
1127 # Update reserves, hold_fill_targets, tmp_holdsqueue and linktracker tables
1128 for my $table_name ( qw( reserves hold_fill_targets tmp_holdsqueue linktracker ) ) {
1129 $dbh->do( qq|
1130 UPDATE $table_name
1131 SET biblionumber = ?
1132 WHERE itemnumber = ?
1133 |, undef, $tobiblio, $itemnumber );
1135 return $tobiblio;
1137 return;
1140 =head2 _marc_from_item_hash
1142 my $item_marc = _marc_from_item_hash($item, $frameworkcode[, $unlinked_item_subfields]);
1144 Given an item hash representing a complete item record,
1145 create a C<MARC::Record> object containing an embedded
1146 tag representing that item.
1148 The third, optional parameter C<$unlinked_item_subfields> is
1149 an arrayref of subfields (not mapped to C<items> fields per the
1150 framework) to be added to the MARC representation
1151 of the item.
1153 =cut
1155 sub _marc_from_item_hash {
1156 my $item = shift;
1157 my $frameworkcode = shift;
1158 my $unlinked_item_subfields;
1159 if (@_) {
1160 $unlinked_item_subfields = shift;
1163 # Tack on 'items.' prefix to column names so lookup from MARC frameworks will work
1164 # Also, don't emit a subfield if the underlying field is blank.
1165 my $mungeditem = { map { (defined($item->{$_}) and $item->{$_} ne '') ?
1166 (/^items\./ ? ($_ => $item->{$_}) : ("items.$_" => $item->{$_}))
1167 : () } keys %{ $item } };
1169 my $item_marc = MARC::Record->new();
1170 foreach my $item_field ( keys %{$mungeditem} ) {
1171 my ( $tag, $subfield ) = C4::Biblio::GetMarcFromKohaField( $item_field );
1172 next unless defined $tag and defined $subfield; # skip if not mapped to MARC field
1173 my @values = split(/\s?\|\s?/, $mungeditem->{$item_field}, -1);
1174 foreach my $value (@values){
1175 if ( my $field = $item_marc->field($tag) ) {
1176 $field->add_subfields( $subfield => $value );
1177 } else {
1178 my $add_subfields = [];
1179 if (defined $unlinked_item_subfields and ref($unlinked_item_subfields) eq 'ARRAY' and $#$unlinked_item_subfields > -1) {
1180 $add_subfields = $unlinked_item_subfields;
1182 $item_marc->add_fields( $tag, " ", " ", $subfield => $value, @$add_subfields );
1187 return $item_marc;
1190 =head2 _repack_item_errors
1192 Add an error message hash generated by C<CheckItemPreSave>
1193 to a list of errors.
1195 =cut
1197 sub _repack_item_errors {
1198 my $item_sequence_num = shift;
1199 my $item_ref = shift;
1200 my $error_ref = shift;
1202 my @repacked_errors = ();
1204 foreach my $error_code (sort keys %{ $error_ref }) {
1205 my $repacked_error = {};
1206 $repacked_error->{'item_sequence'} = $item_sequence_num;
1207 $repacked_error->{'item_barcode'} = exists($item_ref->{'barcode'}) ? $item_ref->{'barcode'} : '';
1208 $repacked_error->{'error_code'} = $error_code;
1209 $repacked_error->{'error_information'} = $error_ref->{$error_code};
1210 push @repacked_errors, $repacked_error;
1213 return @repacked_errors;
1216 =head2 _get_unlinked_item_subfields
1218 my $unlinked_item_subfields = _get_unlinked_item_subfields($original_item_marc, $frameworkcode);
1220 =cut
1222 sub _get_unlinked_item_subfields {
1223 my $original_item_marc = shift;
1224 my $frameworkcode = shift;
1226 my $marcstructure = C4::Biblio::GetMarcStructure(1, $frameworkcode, { unsafe => 1 });
1228 # assume that this record has only one field, and that that
1229 # field contains only the item information
1230 my $subfields = [];
1231 my @fields = $original_item_marc->fields();
1232 if ($#fields > -1) {
1233 my $field = $fields[0];
1234 my $tag = $field->tag();
1235 foreach my $subfield ($field->subfields()) {
1236 if (defined $subfield->[1] and
1237 $subfield->[1] ne '' and
1238 !$marcstructure->{$tag}->{$subfield->[0]}->{'kohafield'}) {
1239 push @$subfields, $subfield->[0] => $subfield->[1];
1243 return $subfields;
1246 =head2 _get_unlinked_subfields_xml
1248 my $unlinked_subfields_xml = _get_unlinked_subfields_xml($unlinked_item_subfields);
1250 =cut
1252 sub _get_unlinked_subfields_xml {
1253 my $unlinked_item_subfields = shift;
1255 my $xml;
1256 if (defined $unlinked_item_subfields and ref($unlinked_item_subfields) eq 'ARRAY' and $#$unlinked_item_subfields > -1) {
1257 my $marc = MARC::Record->new();
1258 # use of tag 999 is arbitrary, and doesn't need to match the item tag
1259 # used in the framework
1260 $marc->append_fields(MARC::Field->new('999', ' ', ' ', @$unlinked_item_subfields));
1261 $marc->encoding("UTF-8");
1262 $xml = $marc->as_xml("USMARC");
1265 return $xml;
1268 =head2 _parse_unlinked_item_subfields_from_xml
1270 my $unlinked_item_subfields = _parse_unlinked_item_subfields_from_xml($whole_item->{'more_subfields_xml'}):
1272 =cut
1274 sub _parse_unlinked_item_subfields_from_xml {
1275 my $xml = shift;
1276 require C4::Charset;
1277 return unless defined $xml and $xml ne "";
1278 my $marc = MARC::Record->new_from_xml(C4::Charset::StripNonXmlChars($xml),'UTF-8');
1279 my $unlinked_subfields = [];
1280 my @fields = $marc->fields();
1281 if ($#fields > -1) {
1282 foreach my $subfield ($fields[0]->subfields()) {
1283 push @$unlinked_subfields, $subfield->[0] => $subfield->[1];
1286 return $unlinked_subfields;
1289 =head2 GetAnalyticsCount
1291 $count= &GetAnalyticsCount($itemnumber)
1293 counts Usage of itemnumber in Analytical bibliorecords.
1295 =cut
1297 sub GetAnalyticsCount {
1298 my ($itemnumber) = @_;
1300 ### ZOOM search here
1301 my $query;
1302 $query= "hi=".$itemnumber;
1303 my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
1304 my ($err,$res,$result) = $searcher->simple_search_compat($query,0,10);
1305 return ($result);
1308 sub _SearchItems_build_where_fragment {
1309 my ($filter) = @_;
1311 my $dbh = C4::Context->dbh;
1313 my $where_fragment;
1314 if (exists($filter->{conjunction})) {
1315 my (@where_strs, @where_args);
1316 foreach my $f (@{ $filter->{filters} }) {
1317 my $fragment = _SearchItems_build_where_fragment($f);
1318 if ($fragment) {
1319 push @where_strs, $fragment->{str};
1320 push @where_args, @{ $fragment->{args} };
1323 my $where_str = '';
1324 if (@where_strs) {
1325 $where_str = '(' . join (' ' . $filter->{conjunction} . ' ', @where_strs) . ')';
1326 $where_fragment = {
1327 str => $where_str,
1328 args => \@where_args,
1331 } else {
1332 my @columns = Koha::Database->new()->schema()->resultset('Item')->result_source->columns;
1333 push @columns, Koha::Database->new()->schema()->resultset('Biblio')->result_source->columns;
1334 push @columns, Koha::Database->new()->schema()->resultset('Biblioitem')->result_source->columns;
1335 my @operators = qw(= != > < >= <= like);
1336 my $field = $filter->{field} // q{};
1337 if ( (0 < grep { $_ eq $field } @columns) or (substr($field, 0, 5) eq 'marc:') ) {
1338 my $op = $filter->{operator};
1339 my $query = $filter->{query};
1341 if (!$op or (0 == grep { $_ eq $op } @operators)) {
1342 $op = '='; # default operator
1345 my $column;
1346 if ($field =~ /^marc:(\d{3})(?:\$(\w))?$/) {
1347 my $marcfield = $1;
1348 my $marcsubfield = $2;
1349 my ($kohafield) = $dbh->selectrow_array(q|
1350 SELECT kohafield FROM marc_subfield_structure
1351 WHERE tagfield=? AND tagsubfield=? AND frameworkcode=''
1352 |, undef, $marcfield, $marcsubfield);
1354 if ($kohafield) {
1355 $column = $kohafield;
1356 } else {
1357 # MARC field is not linked to a DB field so we need to use
1358 # ExtractValue on marcxml from biblio_metadata or
1359 # items.more_subfields_xml, depending on the MARC field.
1360 my $xpath;
1361 my $sqlfield;
1362 my ($itemfield) = C4::Biblio::GetMarcFromKohaField('items.itemnumber');
1363 if ($marcfield eq $itemfield) {
1364 $sqlfield = 'more_subfields_xml';
1365 $xpath = '//record/datafield/subfield[@code="' . $marcsubfield . '"]';
1366 } else {
1367 $sqlfield = 'metadata'; # From biblio_metadata
1368 if ($marcfield < 10) {
1369 $xpath = "//record/controlfield[\@tag=\"$marcfield\"]";
1370 } else {
1371 $xpath = "//record/datafield[\@tag=\"$marcfield\"]/subfield[\@code=\"$marcsubfield\"]";
1374 $column = "ExtractValue($sqlfield, '$xpath')";
1376 } else {
1377 $column = $field;
1380 if (ref $query eq 'ARRAY') {
1381 if ($op eq '=') {
1382 $op = 'IN';
1383 } elsif ($op eq '!=') {
1384 $op = 'NOT IN';
1386 $where_fragment = {
1387 str => "$column $op (" . join (',', ('?') x @$query) . ")",
1388 args => $query,
1390 } else {
1391 $where_fragment = {
1392 str => "$column $op ?",
1393 args => [ $query ],
1399 return $where_fragment;
1402 =head2 SearchItems
1404 my ($items, $total) = SearchItems($filter, $params);
1406 Perform a search among items
1408 $filter is a reference to a hash which can be a filter, or a combination of filters.
1410 A filter has the following keys:
1412 =over 2
1414 =item * field: the name of a SQL column in table items
1416 =item * query: the value to search in this column
1418 =item * operator: comparison operator. Can be one of = != > < >= <= like
1420 =back
1422 A combination of filters hash the following keys:
1424 =over 2
1426 =item * conjunction: 'AND' or 'OR'
1428 =item * filters: array ref of filters
1430 =back
1432 $params is a reference to a hash that can contain the following parameters:
1434 =over 2
1436 =item * rows: Number of items to return. 0 returns everything (default: 0)
1438 =item * page: Page to return (return items from (page-1)*rows to (page*rows)-1)
1439 (default: 1)
1441 =item * sortby: A SQL column name in items table to sort on
1443 =item * sortorder: 'ASC' or 'DESC'
1445 =back
1447 =cut
1449 sub SearchItems {
1450 my ($filter, $params) = @_;
1452 $filter //= {};
1453 $params //= {};
1454 return unless ref $filter eq 'HASH';
1455 return unless ref $params eq 'HASH';
1457 # Default parameters
1458 $params->{rows} ||= 0;
1459 $params->{page} ||= 1;
1460 $params->{sortby} ||= 'itemnumber';
1461 $params->{sortorder} ||= 'ASC';
1463 my ($where_str, @where_args);
1464 my $where_fragment = _SearchItems_build_where_fragment($filter);
1465 if ($where_fragment) {
1466 $where_str = $where_fragment->{str};
1467 @where_args = @{ $where_fragment->{args} };
1470 my $dbh = C4::Context->dbh;
1471 my $query = q{
1472 SELECT SQL_CALC_FOUND_ROWS items.*
1473 FROM items
1474 LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
1475 LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber
1476 LEFT JOIN biblio_metadata ON biblio_metadata.biblionumber = biblio.biblionumber
1477 WHERE 1
1479 if (defined $where_str and $where_str ne '') {
1480 $query .= qq{ AND $where_str };
1483 $query .= q{ AND biblio_metadata.format = 'marcxml' AND biblio_metadata.schema = ? };
1484 push @where_args, C4::Context->preference('marcflavour');
1486 my @columns = Koha::Database->new()->schema()->resultset('Item')->result_source->columns;
1487 push @columns, Koha::Database->new()->schema()->resultset('Biblio')->result_source->columns;
1488 push @columns, Koha::Database->new()->schema()->resultset('Biblioitem')->result_source->columns;
1489 my $sortby = (0 < grep {$params->{sortby} eq $_} @columns)
1490 ? $params->{sortby} : 'itemnumber';
1491 my $sortorder = (uc($params->{sortorder}) eq 'ASC') ? 'ASC' : 'DESC';
1492 $query .= qq{ ORDER BY $sortby $sortorder };
1494 my $rows = $params->{rows};
1495 my @limit_args;
1496 if ($rows > 0) {
1497 my $offset = $rows * ($params->{page}-1);
1498 $query .= qq { LIMIT ?, ? };
1499 push @limit_args, $offset, $rows;
1502 my $sth = $dbh->prepare($query);
1503 my $rv = $sth->execute(@where_args, @limit_args);
1505 return unless ($rv);
1506 my ($total_rows) = $dbh->selectrow_array(q{ SELECT FOUND_ROWS() });
1508 return ($sth->fetchall_arrayref({}), $total_rows);
1512 =head1 OTHER FUNCTIONS
1514 =head2 _find_value
1516 ($indicators, $value) = _find_value($tag, $subfield, $record,$encoding);
1518 Find the given $subfield in the given $tag in the given
1519 MARC::Record $record. If the subfield is found, returns
1520 the (indicators, value) pair; otherwise, (undef, undef) is
1521 returned.
1523 PROPOSITION :
1524 Such a function is used in addbiblio AND additem and serial-edit and maybe could be used in Authorities.
1525 I suggest we export it from this module.
1527 =cut
1529 sub _find_value {
1530 my ( $tagfield, $insubfield, $record, $encoding ) = @_;
1531 my @result;
1532 my $indicator;
1533 if ( $tagfield < 10 ) {
1534 if ( $record->field($tagfield) ) {
1535 push @result, $record->field($tagfield)->data();
1536 } else {
1537 push @result, "";
1539 } else {
1540 foreach my $field ( $record->field($tagfield) ) {
1541 my @subfields = $field->subfields();
1542 foreach my $subfield (@subfields) {
1543 if ( @$subfield[0] eq $insubfield ) {
1544 push @result, @$subfield[1];
1545 $indicator = $field->indicator(1) . $field->indicator(2);
1550 return ( $indicator, @result );
1554 =head2 PrepareItemrecordDisplay
1556 PrepareItemrecordDisplay($itemrecord,$bibnum,$itemumber,$frameworkcode);
1558 Returns a hash with all the fields for Display a given item data in a template
1560 The $frameworkcode returns the item for the given frameworkcode, ONLY if bibnum is not provided
1562 =cut
1564 sub PrepareItemrecordDisplay {
1566 my ( $bibnum, $itemnum, $defaultvalues, $frameworkcode ) = @_;
1568 my $dbh = C4::Context->dbh;
1569 $frameworkcode = C4::Biblio::GetFrameworkCode($bibnum) if $bibnum;
1570 my ( $itemtagfield, $itemtagsubfield ) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
1572 # Note: $tagslib obtained from GetMarcStructure() in 'unsafe' mode is
1573 # a shared data structure. No plugin (including custom ones) should change
1574 # its contents. See also GetMarcStructure.
1575 my $tagslib = GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } );
1577 # return nothing if we don't have found an existing framework.
1578 return q{} unless $tagslib;
1579 my $itemrecord;
1580 if ($itemnum) {
1581 $itemrecord = C4::Items::GetMarcItem( $bibnum, $itemnum );
1583 my @loop_data;
1585 my $branch_limit = C4::Context->userenv ? C4::Context->userenv->{"branch"} : "";
1586 my $query = qq{
1587 SELECT authorised_value,lib FROM authorised_values
1589 $query .= qq{
1590 LEFT JOIN authorised_values_branches ON ( id = av_id )
1591 } if $branch_limit;
1592 $query .= qq{
1593 WHERE category = ?
1595 $query .= qq{ AND ( branchcode = ? OR branchcode IS NULL )} if $branch_limit;
1596 $query .= qq{ ORDER BY lib};
1597 my $authorised_values_sth = $dbh->prepare( $query );
1598 foreach my $tag ( sort keys %{$tagslib} ) {
1599 if ( $tag ne '' ) {
1601 # loop through each subfield
1602 my $cntsubf;
1603 foreach my $subfield ( sort keys %{ $tagslib->{$tag} } ) {
1604 next if IsMarcStructureInternal($tagslib->{$tag}{$subfield});
1605 next unless ( $tagslib->{$tag}->{$subfield}->{'tab'} );
1606 next if ( $tagslib->{$tag}->{$subfield}->{'tab'} ne "10" );
1607 my %subfield_data;
1608 $subfield_data{tag} = $tag;
1609 $subfield_data{subfield} = $subfield;
1610 $subfield_data{countsubfield} = $cntsubf++;
1611 $subfield_data{kohafield} = $tagslib->{$tag}->{$subfield}->{'kohafield'};
1612 $subfield_data{id} = "tag_".$tag."_subfield_".$subfield."_".int(rand(1000000));
1614 # $subfield_data{marc_lib}=$tagslib->{$tag}->{$subfield}->{lib};
1615 $subfield_data{marc_lib} = $tagslib->{$tag}->{$subfield}->{lib};
1616 $subfield_data{mandatory} = $tagslib->{$tag}->{$subfield}->{mandatory};
1617 $subfield_data{repeatable} = $tagslib->{$tag}->{$subfield}->{repeatable};
1618 $subfield_data{hidden} = "display:none"
1619 if ( ( $tagslib->{$tag}->{$subfield}->{hidden} > 4 )
1620 || ( $tagslib->{$tag}->{$subfield}->{hidden} < -4 ) );
1621 my ( $x, $defaultvalue );
1622 if ($itemrecord) {
1623 ( $x, $defaultvalue ) = _find_value( $tag, $subfield, $itemrecord );
1625 $defaultvalue = $tagslib->{$tag}->{$subfield}->{defaultvalue} unless $defaultvalue;
1626 if ( !defined $defaultvalue ) {
1627 $defaultvalue = q||;
1628 } else {
1629 $defaultvalue =~ s/"/&quot;/g;
1630 # get today date & replace <<YYYY>>, <<MM>>, <<DD>> if provided in the default value
1631 my $today_dt = dt_from_string;
1632 my $year = $today_dt->strftime('%Y');
1633 my $shortyear = $today_dt->strftime('%y');
1634 my $month = $today_dt->strftime('%m');
1635 my $day = $today_dt->strftime('%d');
1636 $defaultvalue =~ s/<<YYYY>>/$year/g;
1637 $defaultvalue =~ s/<<YY>>/$shortyear/g;
1638 $defaultvalue =~ s/<<MM>>/$month/g;
1639 $defaultvalue =~ s/<<DD>>/$day/g;
1641 # And <<USER>> with surname (?)
1642 my $username =
1643 ( C4::Context->userenv
1644 ? C4::Context->userenv->{'surname'}
1645 : "superlibrarian" );
1646 $defaultvalue =~ s/<<USER>>/$username/g;
1649 my $maxlength = $tagslib->{$tag}->{$subfield}->{maxlength};
1651 # search for itemcallnumber if applicable
1652 if ( $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.itemcallnumber'
1653 && C4::Context->preference('itemcallnumber') && $itemrecord) {
1654 foreach my $itemcn_pref (split(/,/,C4::Context->preference('itemcallnumber'))){
1655 my $CNtag = substr( $itemcn_pref, 0, 3 );
1656 next unless my $field = $itemrecord->field($CNtag);
1657 my $CNsubfields = substr( $itemcn_pref, 3 );
1658 $defaultvalue = $field->as_string( $CNsubfields, ' ');
1659 last if $defaultvalue;
1662 if ( $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.itemcallnumber'
1663 && $defaultvalues
1664 && $defaultvalues->{'callnumber'} ) {
1665 if( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield) ){
1666 # if the item record exists, only use default value if the item has no callnumber
1667 $defaultvalue = $defaultvalues->{callnumber};
1668 } elsif ( !$itemrecord and $defaultvalues ) {
1669 # if the item record *doesn't* exists, always use the default value
1670 $defaultvalue = $defaultvalues->{callnumber};
1673 if ( ( $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.holdingbranch' || $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.homebranch' )
1674 && $defaultvalues
1675 && $defaultvalues->{'branchcode'} ) {
1676 if ( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield) ) {
1677 $defaultvalue = $defaultvalues->{branchcode};
1680 if ( ( $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.location' )
1681 && $defaultvalues
1682 && $defaultvalues->{'location'} ) {
1684 if ( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield) ) {
1685 # if the item record exists, only use default value if the item has no locationr
1686 $defaultvalue = $defaultvalues->{location};
1687 } elsif ( !$itemrecord and $defaultvalues ) {
1688 # if the item record *doesn't* exists, always use the default value
1689 $defaultvalue = $defaultvalues->{location};
1692 if ( $tagslib->{$tag}->{$subfield}->{authorised_value} ) {
1693 my @authorised_values;
1694 my %authorised_lib;
1696 # builds list, depending on authorised value...
1697 #---- branch
1698 if ( $tagslib->{$tag}->{$subfield}->{'authorised_value'} eq "branches" ) {
1699 if ( ( C4::Context->preference("IndependentBranches") )
1700 && !C4::Context->IsSuperLibrarian() ) {
1701 my $sth = $dbh->prepare( "SELECT branchcode,branchname FROM branches WHERE branchcode = ? ORDER BY branchname" );
1702 $sth->execute( C4::Context->userenv->{branch} );
1703 push @authorised_values, ""
1704 unless ( $tagslib->{$tag}->{$subfield}->{mandatory} );
1705 while ( my ( $branchcode, $branchname ) = $sth->fetchrow_array ) {
1706 push @authorised_values, $branchcode;
1707 $authorised_lib{$branchcode} = $branchname;
1709 } else {
1710 my $sth = $dbh->prepare( "SELECT branchcode,branchname FROM branches ORDER BY branchname" );
1711 $sth->execute;
1712 push @authorised_values, ""
1713 unless ( $tagslib->{$tag}->{$subfield}->{mandatory} );
1714 while ( my ( $branchcode, $branchname ) = $sth->fetchrow_array ) {
1715 push @authorised_values, $branchcode;
1716 $authorised_lib{$branchcode} = $branchname;
1720 $defaultvalue = C4::Context->userenv ? C4::Context->userenv->{branch} : undef;
1721 if ( $defaultvalues and $defaultvalues->{branchcode} ) {
1722 $defaultvalue = $defaultvalues->{branchcode};
1725 #----- itemtypes
1726 } elsif ( $tagslib->{$tag}->{$subfield}->{authorised_value} eq "itemtypes" ) {
1727 my $itemtypes = Koha::ItemTypes->search_with_localization;
1728 push @authorised_values, "";
1729 while ( my $itemtype = $itemtypes->next ) {
1730 push @authorised_values, $itemtype->itemtype;
1731 $authorised_lib{$itemtype->itemtype} = $itemtype->translated_description;
1733 if ($defaultvalues && $defaultvalues->{'itemtype'}) {
1734 $defaultvalue = $defaultvalues->{'itemtype'};
1737 #---- class_sources
1738 } elsif ( $tagslib->{$tag}->{$subfield}->{authorised_value} eq "cn_source" ) {
1739 push @authorised_values, "";
1741 my $class_sources = GetClassSources();
1742 my $default_source = $defaultvalue || C4::Context->preference("DefaultClassificationSource");
1744 foreach my $class_source (sort keys %$class_sources) {
1745 next unless $class_sources->{$class_source}->{'used'} or
1746 ($class_source eq $default_source);
1747 push @authorised_values, $class_source;
1748 $authorised_lib{$class_source} = $class_sources->{$class_source}->{'description'};
1751 $defaultvalue = $default_source;
1753 #---- "true" authorised value
1754 } else {
1755 $authorised_values_sth->execute(
1756 $tagslib->{$tag}->{$subfield}->{authorised_value},
1757 $branch_limit ? $branch_limit : ()
1759 push @authorised_values, "";
1760 while ( my ( $value, $lib ) = $authorised_values_sth->fetchrow_array ) {
1761 push @authorised_values, $value;
1762 $authorised_lib{$value} = $lib;
1765 $subfield_data{marc_value} = {
1766 type => 'select',
1767 values => \@authorised_values,
1768 default => $defaultvalue // q{},
1769 labels => \%authorised_lib,
1771 } elsif ( $tagslib->{$tag}->{$subfield}->{value_builder} ) {
1772 # it is a plugin
1773 require Koha::FrameworkPlugin;
1774 my $plugin = Koha::FrameworkPlugin->new({
1775 name => $tagslib->{$tag}->{$subfield}->{value_builder},
1776 item_style => 1,
1778 my $pars = { dbh => $dbh, record => undef, tagslib =>$tagslib, id => $subfield_data{id}, tabloop => undef };
1779 $plugin->build( $pars );
1780 if ( $itemrecord and my $field = $itemrecord->field($tag) ) {
1781 $defaultvalue = $field->subfield($subfield) || q{};
1783 if( !$plugin->errstr ) {
1784 #TODO Move html to template; see report 12176/13397
1785 my $tab= $plugin->noclick? '-1': '';
1786 my $class= $plugin->noclick? ' disabled': '';
1787 my $title= $plugin->noclick? 'No popup': 'Tag editor';
1788 $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;
1789 } else {
1790 warn $plugin->errstr;
1791 $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
1794 elsif ( $tag eq '' ) { # it's an hidden field
1795 $subfield_data{marc_value} = qq(<input type="hidden" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength" value="$defaultvalue" />);
1797 elsif ( $tagslib->{$tag}->{$subfield}->{'hidden'} ) { # FIXME: shouldn't input type be "hidden" ?
1798 $subfield_data{marc_value} = qq(<input type="text" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength" value="$defaultvalue" />);
1800 elsif ( length($defaultvalue) > 100
1801 or (C4::Context->preference("marcflavour") eq "UNIMARC" and
1802 300 <= $tag && $tag < 400 && $subfield eq 'a' )
1803 or (C4::Context->preference("marcflavour") eq "MARC21" and
1804 500 <= $tag && $tag < 600 )
1806 # oversize field (textarea)
1807 $subfield_data{marc_value} = qq(<textarea id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength">$defaultvalue</textarea>\n");
1808 } else {
1809 $subfield_data{marc_value} = qq(<input type="text" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength" value="$defaultvalue" />);
1811 push( @loop_data, \%subfield_data );
1815 my $itemnumber;
1816 if ( $itemrecord && $itemrecord->field($itemtagfield) ) {
1817 $itemnumber = $itemrecord->subfield( $itemtagfield, $itemtagsubfield );
1819 return {
1820 'itemtagfield' => $itemtagfield,
1821 'itemtagsubfield' => $itemtagsubfield,
1822 'itemnumber' => $itemnumber,
1823 'iteminformation' => \@loop_data
1827 sub ToggleNewStatus {
1828 my ( $params ) = @_;
1829 my @rules = @{ $params->{rules} };
1830 my $report_only = $params->{report_only};
1832 my $dbh = C4::Context->dbh;
1833 my @errors;
1834 my @item_columns = map { "items.$_" } Koha::Items->columns;
1835 my @biblioitem_columns = map { "biblioitems.$_" } Koha::Biblioitems->columns;
1836 my $report;
1837 for my $rule ( @rules ) {
1838 my $age = $rule->{age};
1839 my $conditions = $rule->{conditions};
1840 my $substitutions = $rule->{substitutions};
1841 foreach ( @$substitutions ) {
1842 ( $_->{item_field} ) = ( $_->{field} =~ /items\.(.*)/ );
1844 my @params;
1846 my $query = q|
1847 SELECT items.*
1848 FROM items
1849 LEFT JOIN biblioitems ON biblioitems.biblionumber = items.biblionumber
1850 WHERE 1
1852 for my $condition ( @$conditions ) {
1853 if (
1854 grep { $_ eq $condition->{field} } @item_columns
1855 or grep { $_ eq $condition->{field} } @biblioitem_columns
1857 if ( $condition->{value} =~ /\|/ ) {
1858 my @values = split /\|/, $condition->{value};
1859 $query .= qq| AND $condition->{field} IN (|
1860 . join( ',', ('?') x scalar @values )
1861 . q|)|;
1862 push @params, @values;
1863 } else {
1864 $query .= qq| AND $condition->{field} = ?|;
1865 push @params, $condition->{value};
1869 if ( defined $age ) {
1870 $query .= q| AND TO_DAYS(NOW()) - TO_DAYS(dateaccessioned) >= ? |;
1871 push @params, $age;
1873 my $sth = $dbh->prepare($query);
1874 $sth->execute( @params );
1875 while ( my $values = $sth->fetchrow_hashref ) {
1876 my $biblionumber = $values->{biblionumber};
1877 my $itemnumber = $values->{itemnumber};
1878 my $item = Koha::Items->find($itemnumber);
1879 for my $substitution ( @$substitutions ) {
1880 my $field = $substitution->{item_field};
1881 my $value = $substitution->{value};
1882 next unless $substitution->{field};
1883 next if ( defined $values->{ $substitution->{item_field} } and $values->{ $substitution->{item_field} } eq $substitution->{value} );
1884 $item->$field($value);
1885 push @{ $report->{$itemnumber} }, $substitution;
1887 $item->store unless $report_only;
1891 return $report;